553 lines
16 KiB
HTML
553 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>POKER ADMIN</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Cinzel:wght@400;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg: #0e0e1a;
|
||
--panel: #13132a;
|
||
--border: #2a2a4a;
|
||
--gold: #ffd700;
|
||
--gold-dim: #a08800;
|
||
--text: #c8c8e0;
|
||
--muted: #555570;
|
||
--red: #ff4444;
|
||
--green: #44cc88;
|
||
--mono: 'Share Tech Mono', monospace;
|
||
--serif: 'Cinzel', serif;
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-family: var(--mono);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
body::before {
|
||
content: '';
|
||
position: fixed;
|
||
inset: 0;
|
||
background-image:
|
||
linear-gradient(var(--border) 1px, transparent 1px),
|
||
linear-gradient(90deg, var(--border) 1px, transparent 1px);
|
||
background-size: 40px 40px;
|
||
opacity: 0.25;
|
||
pointer-events: none;
|
||
}
|
||
|
||
header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
background: var(--bg);
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 12px 16px 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.app-title {
|
||
font-family: var(--serif);
|
||
font-size: 13px;
|
||
letter-spacing: 4px;
|
||
color: var(--gold);
|
||
flex: 1;
|
||
}
|
||
|
||
.user-badge {
|
||
font-size: 9px;
|
||
color: var(--muted);
|
||
letter-spacing: 2px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
#status-bar {
|
||
font-size: 9px;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
padding: 4px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
letter-spacing: 1px;
|
||
min-height: 20px;
|
||
transition: color 0.3s;
|
||
}
|
||
#status-bar.error { color: var(--red); }
|
||
#status-bar.ok { color: var(--green); }
|
||
|
||
/* ── tabs ── */
|
||
.tabs {
|
||
display: flex;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.tab {
|
||
flex: 1;
|
||
background: transparent;
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
color: var(--muted);
|
||
font-family: var(--mono);
|
||
font-size: 9px;
|
||
letter-spacing: 2px;
|
||
text-transform: uppercase;
|
||
padding: 10px;
|
||
cursor: pointer;
|
||
transition: color 0.15s, border-color 0.15s;
|
||
}
|
||
|
||
.tab:hover { color: var(--text); }
|
||
.tab.active {
|
||
color: var(--gold);
|
||
border-bottom-color: var(--gold);
|
||
}
|
||
|
||
/* ── panels ── */
|
||
.panel {
|
||
display: none;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
padding: 16px;
|
||
gap: 12px;
|
||
}
|
||
.panel.active { display: flex; }
|
||
|
||
/* ── form elements ── */
|
||
.field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.field label {
|
||
font-size: 9px;
|
||
color: var(--muted);
|
||
letter-spacing: 2px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.field input {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
color: var(--text);
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
padding: 8px 10px;
|
||
outline: none;
|
||
transition: border-color 0.15s;
|
||
}
|
||
|
||
.field input:focus { border-color: var(--gold-dim); }
|
||
|
||
.field-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
}
|
||
|
||
.btn {
|
||
background: transparent;
|
||
border: 1px solid var(--gold-dim);
|
||
color: var(--gold);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 2px;
|
||
text-transform: uppercase;
|
||
padding: 10px;
|
||
cursor: pointer;
|
||
transition: background 0.15s, border-color 0.15s;
|
||
width: 100%;
|
||
}
|
||
|
||
.btn:hover { background: rgba(255,215,0,0.08); border-color: var(--gold); }
|
||
.btn.danger {
|
||
border-color: #662222;
|
||
color: var(--red);
|
||
}
|
||
.btn.danger:hover { background: rgba(255,68,68,0.08); border-color: var(--red); }
|
||
.btn:disabled { opacity: 0.4; cursor: default; }
|
||
|
||
/* ── table list ── */
|
||
.table-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.table-card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
padding: 10px 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.table-card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.table-name {
|
||
font-family: var(--serif);
|
||
font-size: 11px;
|
||
color: #e8e8ff;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.table-id {
|
||
font-size: 8px;
|
||
color: var(--muted);
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.table-meta {
|
||
font-size: 9px;
|
||
color: var(--muted);
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.table-meta span { color: var(--gold-dim); }
|
||
|
||
.table-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.table-actions .btn {
|
||
font-size: 8px;
|
||
padding: 6px 8px;
|
||
}
|
||
|
||
.section-header {
|
||
font-family: var(--serif);
|
||
font-size: 9px;
|
||
letter-spacing: 3px;
|
||
color: var(--muted);
|
||
text-transform: uppercase;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.divider {
|
||
height: 1px;
|
||
background: var(--border);
|
||
}
|
||
|
||
#no-tables {
|
||
font-size: 10px;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
padding: 24px;
|
||
letter-spacing: 1px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div class="app-title">POKER ADMIN</div>
|
||
<div class="user-badge" id="user-badge">—</div>
|
||
</header>
|
||
|
||
<div id="status-bar">Initialising...</div>
|
||
|
||
<div class="tabs">
|
||
<button class="tab active" onclick="switchTab('create')">Create</button>
|
||
<button class="tab" onclick="switchTab('manage')">Manage</button>
|
||
</div>
|
||
|
||
<!-- ── Create Tab ── -->
|
||
<div class="panel active" id="tab-create">
|
||
<div class="section-header">New Table</div>
|
||
|
||
<div class="field">
|
||
<label>Table ID (slug)</label>
|
||
<input type="text" id="f-id" placeholder="main" value="main">
|
||
</div>
|
||
<div class="field">
|
||
<label>Display Name</label>
|
||
<input type="text" id="f-name" placeholder="Main Table">
|
||
</div>
|
||
<div class="field-row">
|
||
<div class="field">
|
||
<label>Seat Count</label>
|
||
<input type="number" id="f-seats" value="7" min="2" max="9">
|
||
</div>
|
||
<div class="field">
|
||
<label>Turn Timer (s)</label>
|
||
<input type="number" id="f-timer" value="30" min="10" max="120">
|
||
</div>
|
||
</div>
|
||
<div class="field-row">
|
||
<div class="field">
|
||
<label>Small Blind</label>
|
||
<input type="number" id="f-sb" value="5" min="1">
|
||
</div>
|
||
<div class="field">
|
||
<label>Big Blind</label>
|
||
<input type="number" id="f-bb" value="10" min="2">
|
||
</div>
|
||
</div>
|
||
<div class="field-row">
|
||
<div class="field">
|
||
<label>Min Buy-in</label>
|
||
<input type="number" id="f-minbuy" value="100" min="1">
|
||
</div>
|
||
<div class="field">
|
||
<label>Max Buy-in</label>
|
||
<input type="number" id="f-maxbuy" value="1000" min="1">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<button class="btn" id="btn-create" onclick="createTable()">Spawn Table at Avatar Position</button>
|
||
</div>
|
||
|
||
<!-- ── Manage Tab ── -->
|
||
<div class="panel" id="tab-manage">
|
||
<div class="section-header">Tables</div>
|
||
<div class="table-list" id="table-list">
|
||
<div id="no-tables">Loading...</div>
|
||
</div>
|
||
<button class="btn" onclick="loadTables()" style="margin-top:auto">Refresh</button>
|
||
</div>
|
||
|
||
<script>
|
||
var API_BASE = "https://wizards.cyou/api";
|
||
var POKER_BASE = "https://wizards.cyou/poker";
|
||
|
||
var state = {
|
||
username: null,
|
||
token: null,
|
||
position: null,
|
||
rotation: null,
|
||
};
|
||
|
||
// ── tabs ─────────────────────────────────────────────────────
|
||
|
||
function switchTab(name) {
|
||
document.querySelectorAll(".tab").forEach(function(t) { t.classList.remove("active"); });
|
||
document.querySelectorAll(".panel").forEach(function(p) { p.classList.remove("active"); });
|
||
document.querySelector("[onclick=\"switchTab('" + name + "')\"]").classList.add("active");
|
||
document.getElementById("tab-" + name).classList.add("active");
|
||
if (name === "manage") loadTables();
|
||
}
|
||
|
||
// ── status ───────────────────────────────────────────────────
|
||
|
||
function setStatus(msg, cls) {
|
||
var el = document.getElementById("status-bar");
|
||
el.textContent = msg;
|
||
el.className = cls || "";
|
||
}
|
||
|
||
// ── session ──────────────────────────────────────────────────
|
||
|
||
function ensureSession(cb) {
|
||
if (state.token) { cb(null, state.token); return; }
|
||
fetch(API_BASE + "/session", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ username: state.username }),
|
||
})
|
||
.then(function(r) {
|
||
if (!r.ok) throw new Error("Session failed (" + r.status + ")");
|
||
return r.json();
|
||
})
|
||
.then(function(d) {
|
||
state.token = d.token;
|
||
cb(null, state.token);
|
||
})
|
||
.catch(function(e) { cb(e.message); });
|
||
}
|
||
|
||
// ── create table ─────────────────────────────────────────────
|
||
|
||
function createTable() {
|
||
var id = document.getElementById("f-id").value.trim();
|
||
var name = document.getElementById("f-name").value.trim();
|
||
var seats = parseInt(document.getElementById("f-seats").value);
|
||
var timer = parseInt(document.getElementById("f-timer").value);
|
||
var sb = parseInt(document.getElementById("f-sb").value);
|
||
var bb = parseInt(document.getElementById("f-bb").value);
|
||
var minbuy = parseInt(document.getElementById("f-minbuy").value);
|
||
var maxbuy = parseInt(document.getElementById("f-maxbuy").value);
|
||
|
||
if (!id || !name) { setStatus("ID and Name are required", "error"); return; }
|
||
if (bb !== sb * 2) { setStatus("Big blind must be exactly 2x small blind", "error"); return; }
|
||
if (minbuy < bb * 10) { setStatus("Min buy-in must be at least 10x big blind (" + (bb*10) + ")", "error"); return; }
|
||
if (maxbuy < minbuy) { setStatus("Max buy-in must be >= min buy-in", "error"); return; }
|
||
|
||
var btn = document.getElementById("btn-create");
|
||
btn.disabled = true;
|
||
setStatus("Getting session...");
|
||
|
||
ensureSession(function(err) {
|
||
if (err) { setStatus(err, "error"); btn.disabled = false; return; }
|
||
|
||
// Request a fresh position snapshot from the tablet script
|
||
setStatus("Fetching avatar position...");
|
||
EventBridge.emitWebEvent(JSON.stringify({ type: "getPosition" }));
|
||
|
||
// Store config to use once position arrives
|
||
state.pendingCreate = {
|
||
id: id, name: name, seatCount: seats, turnTimer: timer,
|
||
smallBlind: sb, bigBlind: bb, minBuyin: minbuy, maxBuyin: maxbuy,
|
||
};
|
||
btn.disabled = false;
|
||
});
|
||
}
|
||
|
||
function doCreateTable(cfg, position, rotation) {
|
||
var btn = document.getElementById("btn-create");
|
||
btn.disabled = true;
|
||
setStatus("Creating table...");
|
||
|
||
fetch(POKER_BASE + "/admin/tables", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
"Authorization": "Bearer " + state.token,
|
||
},
|
||
body: JSON.stringify(cfg),
|
||
})
|
||
.then(function(r) {
|
||
if (!r.ok) return r.json().then(function(e) { throw new Error(e.error || r.status); });
|
||
return r.json();
|
||
})
|
||
.then(function(d) {
|
||
setStatus("Table created! Spawning entities...", "ok");
|
||
// Tell the tablet script to spawn the in-world entities
|
||
EventBridge.emitWebEvent(JSON.stringify({
|
||
type: "spawnSeats",
|
||
pokerID: cfg.id,
|
||
seatCount: cfg.seatCount,
|
||
position: position,
|
||
rotation: rotation,
|
||
}));
|
||
})
|
||
.catch(function(e) {
|
||
setStatus(e.message, "error");
|
||
btn.disabled = false;
|
||
});
|
||
}
|
||
|
||
// ── manage tables ─────────────────────────────────────────────
|
||
|
||
function loadTables() {
|
||
setStatus("Loading tables...");
|
||
fetch(POKER_BASE + "/tables")
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
renderTables(d.tables || []);
|
||
setStatus(d.tables.length + " table(s) loaded", "ok");
|
||
})
|
||
.catch(function() { setStatus("Could not load tables", "error"); });
|
||
}
|
||
|
||
function renderTables(tables) {
|
||
var list = document.getElementById("table-list");
|
||
if (tables.length === 0) {
|
||
list.innerHTML = '<div id="no-tables">No tables yet</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = "";
|
||
tables.forEach(function(t) {
|
||
var cfg = t.config || t;
|
||
var card = document.createElement("div");
|
||
card.className = "table-card";
|
||
card.innerHTML =
|
||
'<div class="table-card-header">' +
|
||
'<div class="table-name">' + cfg.name + '</div>' +
|
||
'<div class="table-id">' + cfg.id + '</div>' +
|
||
'</div>' +
|
||
'<div class="table-meta">' +
|
||
'<span>' + cfg.seatCount + '</span> seats · ' +
|
||
'blinds <span>' + cfg.smallBlind + '/' + cfg.bigBlind + '</span> · ' +
|
||
'buy-in <span>' + cfg.minBuyin + '–' + cfg.maxBuyin + '</span>' +
|
||
'</div>' +
|
||
'<div class="table-actions">' +
|
||
'<button class="btn danger" onclick="deleteTable(\'' + cfg.id + '\')">Delete</button>' +
|
||
'</div>';
|
||
list.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function deleteTable(id) {
|
||
if (!confirm("Delete table " + id + "? Seated players will be refunded.")) return;
|
||
|
||
ensureSession(function(err) {
|
||
if (err) { setStatus(err, "error"); return; }
|
||
setStatus("Deleting " + id + "...");
|
||
fetch(POKER_BASE + "/admin/tables/" + id, {
|
||
method: "DELETE",
|
||
headers: { "Authorization": "Bearer " + state.token },
|
||
})
|
||
.then(function(r) {
|
||
if (!r.ok) return r.json().then(function(e) { throw new Error(e.error || r.status); });
|
||
return r.json();
|
||
})
|
||
.then(function() {
|
||
setStatus("Table " + id + " deleted", "ok");
|
||
loadTables();
|
||
})
|
||
.catch(function(e) { setStatus(e.message, "error"); });
|
||
});
|
||
}
|
||
|
||
// ── EventBridge ──────────────────────────────────────────────
|
||
|
||
function init() {
|
||
if (typeof EventBridge !== "undefined") {
|
||
EventBridge.scriptEventReceived.connect(function(data) {
|
||
try {
|
||
var msg = JSON.parse(data);
|
||
if (msg.type === "init") {
|
||
state.username = msg.username;
|
||
state.position = msg.position;
|
||
state.rotation = msg.rotation;
|
||
document.getElementById("user-badge").textContent = msg.username || "unknown";
|
||
setStatus("Ready");
|
||
if (msg.username !== "nak") {
|
||
setStatus("Admin access required", "error");
|
||
document.getElementById("btn-create").disabled = true;
|
||
}
|
||
} else if (msg.type === "position") {
|
||
state.position = msg.position;
|
||
state.rotation = msg.rotation;
|
||
if (state.pendingCreate) {
|
||
var cfg = state.pendingCreate;
|
||
state.pendingCreate = null;
|
||
doCreateTable(cfg, state.position, state.rotation);
|
||
}
|
||
} else if (msg.type === "spawnComplete") {
|
||
setStatus("Table and seats spawned!", "ok");
|
||
document.getElementById("btn-create").disabled = false;
|
||
}
|
||
} catch(e) {
|
||
print("poker-admin event error: " + e);
|
||
}
|
||
});
|
||
EventBridge.emitWebEvent(JSON.stringify({ type: "ready" }));
|
||
} else {
|
||
setStatus("EventBridge not available (dev mode)", "error");
|
||
}
|
||
}
|
||
|
||
window.onload = init;
|
||
</script>
|
||
</body>
|
||
</html>
|