Compare commits
11 commits
f75abe564d
...
c259168f6f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c259168f6f | ||
|
|
d052c897e6 | ||
|
|
78ac272931 | ||
|
|
c5823c7133 | ||
|
|
86a866ee5d | ||
|
|
4a2f8387c0 | ||
|
|
296ed70797 | ||
|
|
96ed3f540b | ||
|
|
f6e29ecfbc | ||
|
|
39f0ed8d87 | ||
|
|
2b80a789d0 |
6 changed files with 785 additions and 4 deletions
BIN
images/sit.png
Normal file
BIN
images/sit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
183
scripts/poker-admin-tablet.js
Normal file
183
scripts/poker-admin-tablet.js
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
(function() {
|
||||||
|
var ADMIN_USER = "nak";
|
||||||
|
|
||||||
|
if (AccountServices.username !== ADMIN_USER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var APP_NAME = "POKER";
|
||||||
|
var APP_URL = "https://wizards.cyou/tablet/poker-admin.html";
|
||||||
|
var APP_ICON = "https://wizards.cyou/tablet/poker-admin-icon.svg";
|
||||||
|
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
||||||
|
var button = tablet.addButton({
|
||||||
|
text: APP_NAME,
|
||||||
|
icon: APP_ICON
|
||||||
|
});
|
||||||
|
|
||||||
|
var webEventHandler = function(data) {
|
||||||
|
try {
|
||||||
|
var msg = JSON.parse(data);
|
||||||
|
|
||||||
|
if (msg.type === "ready") {
|
||||||
|
tablet.emitScriptEvent(JSON.stringify({
|
||||||
|
type: "init",
|
||||||
|
username: AccountServices.username || "",
|
||||||
|
position: MyAvatar.position,
|
||||||
|
rotation: MyAvatar.orientation,
|
||||||
|
}));
|
||||||
|
|
||||||
|
} else if (msg.type === "getPosition") {
|
||||||
|
tablet.emitScriptEvent(JSON.stringify({
|
||||||
|
type: "position",
|
||||||
|
position: MyAvatar.position,
|
||||||
|
rotation: MyAvatar.orientation,
|
||||||
|
}));
|
||||||
|
|
||||||
|
} else if (msg.type === "spawnSeats") {
|
||||||
|
spawnTable(msg.pokerID, msg.seatCount, msg.position, msg.rotation);
|
||||||
|
|
||||||
|
} else if (msg.type === "deleteTable") {
|
||||||
|
deleteTableEntities(msg.pokerID);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
print("[pokerAdmin] web event error: " + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function spawnTable(pokerID, seatCount, avatarPosition, avatarRotation) {
|
||||||
|
var constants = Script.require(Script.resolvePath("poker_constants.js"));
|
||||||
|
var layout = constants.POKER_SEATS[seatCount];
|
||||||
|
if (!layout) {
|
||||||
|
print("[pokerAdmin] no layout for seatCount=" + seatCount);
|
||||||
|
tablet.emitScriptEvent(JSON.stringify({
|
||||||
|
type: "spawnError",
|
||||||
|
error: "No seat layout for seatCount=" + seatCount,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place table in front of avatar at floor level
|
||||||
|
// Use only the yaw component of avatar rotation so table sits flat
|
||||||
|
var yaw = Quat.safeEulerAngles(avatarRotation).y;
|
||||||
|
var tableRot = Quat.fromPitchYawRollDegrees(0, yaw - 90, 0);
|
||||||
|
|
||||||
|
// 2.5m in front of avatar, snapped to floor
|
||||||
|
var forward = Vec3.multiplyQbyV(tableRot, { x: 0, y: 0, z: -2.5 });
|
||||||
|
var tablePos = {
|
||||||
|
x: avatarPosition.x + forward.x,
|
||||||
|
y: -0.45,
|
||||||
|
z: avatarPosition.z + forward.z,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spawn table model — natural dimensions, not squished
|
||||||
|
var tableID = Entities.addEntity({
|
||||||
|
type: "Model",
|
||||||
|
name: "poker_table_" + pokerID,
|
||||||
|
modelURL: constants.POKER_TABLE_MODEL_URL,
|
||||||
|
position: tablePos,
|
||||||
|
rotation: tableRot,
|
||||||
|
naturalDimensions: true,
|
||||||
|
registrationPoint: { x: 0.5, y: 0, z: 0.5 },
|
||||||
|
userData: JSON.stringify({
|
||||||
|
pokerID: pokerID,
|
||||||
|
seatCount: seatCount,
|
||||||
|
}),
|
||||||
|
grabbable: false,
|
||||||
|
locked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Script.setTimeout(function() {
|
||||||
|
Entities.editEntity(tableID, { locked: true });
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
print("[pokerAdmin] spawned table entity " + tableID + " for " + pokerID);
|
||||||
|
|
||||||
|
// Spawn seat pads as children of the table entity
|
||||||
|
// Offsets are in table-local space so no world transform needed
|
||||||
|
for (var i = 0; i < seatCount; i++) {
|
||||||
|
var seat = layout[i];
|
||||||
|
var seatRot = Quat.fromPitchYawRollDegrees(-90, seat.yaw, 0);
|
||||||
|
var seatOffset = {
|
||||||
|
x: seat.offset.x,
|
||||||
|
y: seat.offset.y - 0.13,
|
||||||
|
z: seat.offset.z
|
||||||
|
};
|
||||||
|
|
||||||
|
Entities.addEntity({
|
||||||
|
type: "Image",
|
||||||
|
name: "poker_seat_" + pokerID + "_" + i,
|
||||||
|
imageURL: constants.POKER_SEAT_PAD.imageURL,
|
||||||
|
dimensions: constants.POKER_SEAT_PAD.dimensions,
|
||||||
|
alpha: constants.POKER_SEAT_PAD.alpha,
|
||||||
|
parentID: tableID,
|
||||||
|
localPosition: seatOffset,
|
||||||
|
localRotation: seatRot,
|
||||||
|
script: "https://wizards.cyou/scripts/poker_sit.js",
|
||||||
|
userData: JSON.stringify({
|
||||||
|
tableID: tableID,
|
||||||
|
pokerID: pokerID,
|
||||||
|
seatIndex: i,
|
||||||
|
}),
|
||||||
|
triggerable: true,
|
||||||
|
grabbable: false,
|
||||||
|
ignorePickIntersection: false,
|
||||||
|
locked: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
print("[pokerAdmin] spawned seat " + i + " parented to " + tableID);
|
||||||
|
}
|
||||||
|
|
||||||
|
tablet.emitScriptEvent(JSON.stringify({
|
||||||
|
type: "spawnComplete",
|
||||||
|
tableEntityID: tableID,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTableEntities(pokerID) {
|
||||||
|
// Find table entity by name
|
||||||
|
var results = Entities.findEntitiesByName(
|
||||||
|
"poker_table_" + pokerID, MyAvatar.position, 100, false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
print("[pokerAdmin] no entities found for table " + pokerID);
|
||||||
|
tablet.emitScriptEvent(JSON.stringify({
|
||||||
|
type: "deleteEntitiesComplete",
|
||||||
|
pokerID: pokerID,
|
||||||
|
found: false,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.forEach(function(tableEntityID) {
|
||||||
|
// Find and delete all children (seat pads)
|
||||||
|
var children = Entities.getChildrenIDs(tableEntityID);
|
||||||
|
children.forEach(function(childID) {
|
||||||
|
Entities.editEntity(childID, { locked: false });
|
||||||
|
Entities.deleteEntity(childID);
|
||||||
|
print("[pokerAdmin] deleted seat entity " + childID);
|
||||||
|
});
|
||||||
|
// Delete the table itself
|
||||||
|
Entities.editEntity(tableEntityID, { locked: false });
|
||||||
|
Entities.deleteEntity(tableEntityID);
|
||||||
|
print("[pokerAdmin] deleted table entity " + tableEntityID);
|
||||||
|
});
|
||||||
|
|
||||||
|
tablet.emitScriptEvent(JSON.stringify({
|
||||||
|
type: "deleteEntitiesComplete",
|
||||||
|
pokerID: pokerID,
|
||||||
|
found: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
button.clicked.connect(function() {
|
||||||
|
tablet.gotoWebScreen(APP_URL);
|
||||||
|
tablet.webEventReceived.connect(webEventHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
Script.scriptEnding.connect(function() {
|
||||||
|
tablet.removeButton(button);
|
||||||
|
tablet.webEventReceived.disconnect(webEventHandler);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
@ -23,13 +23,13 @@ module.exports = {
|
||||||
|
|
||||||
// Seat pad entity appearance
|
// Seat pad entity appearance
|
||||||
POKER_SEAT_PAD: {
|
POKER_SEAT_PAD: {
|
||||||
dimensions: { x: 0.6, y: 0.6, z: 0.01 },
|
dimensions: { x: 0.4, y: 0.4, z: 0.01 },
|
||||||
imageURL: "https://wizards.cyou/poker/sit.png",
|
imageURL: "https://wizards.cyou/images/sit.png",
|
||||||
alpha: 0.7,
|
alpha: 0.7,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Table mesh URL
|
// Table mesh URL
|
||||||
POKER_TABLE_MODEL_URL: "https://wizards.cyou/poker/table.glb",
|
POKER_TABLE_MODEL_URL: "https://wizards.cyou/models/poker_table.glb",
|
||||||
|
|
||||||
// API base URLs
|
// API base URLs
|
||||||
API_BASE: "https://wizards.cyou/api",
|
API_BASE: "https://wizards.cyou/api",
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSeatRotation() {
|
function getSeatRotation() {
|
||||||
return Quat.fromPitchYawRollDegrees(0, _seatData.yaw, 0);
|
var tableProps = Entities.getEntityProperties(_tableID, ["rotation"]);
|
||||||
|
var tableYaw = Quat.safeEulerAngles(tableProps.rotation).y;
|
||||||
|
return Quat.fromPitchYawRollDegrees(0, tableYaw + _seatData.yaw, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function notify(msg) {
|
function notify(msg) {
|
||||||
|
|
@ -97,7 +99,9 @@
|
||||||
if (isSeated) {
|
if (isSeated) {
|
||||||
MyAvatar.endSit(getSeatPosition(), getSeatRotation());
|
MyAvatar.endSit(getSeatPosition(), getSeatRotation());
|
||||||
isSeated = false;
|
isSeated = false;
|
||||||
|
Entities.editEntity(_entityID, { locked: false });
|
||||||
Entities.editEntity(_entityID, { visible: true });
|
Entities.editEntity(_entityID, { visible: true });
|
||||||
|
Entities.editEntity(_entityID, { locked: true });
|
||||||
Controller.actionEvent.disconnect(onActionEvent);
|
Controller.actionEvent.disconnect(onActionEvent);
|
||||||
Messages.sendMessage("poker:seat", JSON.stringify({
|
Messages.sendMessage("poker:seat", JSON.stringify({
|
||||||
event: "stand",
|
event: "stand",
|
||||||
|
|
@ -136,7 +140,9 @@
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
MyAvatar.beginSit(getSeatPosition(), getSeatRotation());
|
MyAvatar.beginSit(getSeatPosition(), getSeatRotation());
|
||||||
isSeated = true;
|
isSeated = true;
|
||||||
|
Entities.editEntity(_entityID, { locked: false });
|
||||||
Entities.editEntity(_entityID, { visible: false });
|
Entities.editEntity(_entityID, { visible: false });
|
||||||
|
Entities.editEntity(_entityID, { locked: true });
|
||||||
Controller.actionEvent.connect(onActionEvent);
|
Controller.actionEvent.connect(onActionEvent);
|
||||||
Messages.sendMessage("poker:seat", JSON.stringify({
|
Messages.sendMessage("poker:seat", JSON.stringify({
|
||||||
event: "sit",
|
event: "sit",
|
||||||
|
|
|
||||||
32
tablet/poker-admin-icon.svg
Normal file
32
tablet/poker-admin-icon.svg
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#FFFFFF" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 463.644 463.644" xml:space="preserve">
|
||||||
|
<path id="XMLID_1_" d="M463.164,146.031l-77.369,288.746c-1.677,6.26-7.362,10.4-13.556,10.401c-1.198,0-2.414-0.155-3.625-0.479
|
||||||
|
l-189.261-50.712c-7.472-2.003-11.922-9.711-9.919-17.183l2.041-7.616c1.287-4.801,6.222-7.647,11.023-6.363
|
||||||
|
c4.801,1.287,7.65,6.222,6.363,11.023l-1.013,3.78l181.587,48.656l75.314-281.076l-77.031-20.64
|
||||||
|
c-4.801-1.287-7.651-6.222-6.364-11.023s6.225-7.648,11.022-6.364l80.869,21.668C460.718,130.853,465.167,138.56,463.164,146.031z
|
||||||
|
M166.128,56.029c-4.971,0-9,4.029-9,9v8.565c0,4.971,4.029,9,9,9s9-4.029,9-9v-8.565C175.128,60.058,171.099,56.029,166.128,56.029
|
||||||
|
z M280.889,176.762c2.202,3.114,2.202,7.278,0,10.393l-41.716,58.996c-1.687,2.385-4.427,3.804-7.349,3.804
|
||||||
|
c-2.921,0-5.662-1.418-7.348-3.804l-41.718-58.996c-2.202-3.114-2.202-7.278,0-10.393l41.718-58.996
|
||||||
|
c1.687-2.385,4.427-3.804,7.348-3.804c2.922,0,5.662,1.418,7.349,3.804L280.889,176.762z M262.518,181.958l-30.694-43.408
|
||||||
|
l-30.694,43.408l30.694,43.407L262.518,181.958z M343.016,380.764l-2.216,8.273c-1.286,4.801,1.563,9.736,6.365,11.022
|
||||||
|
c0.78,0.209,1.563,0.309,2.334,0.309c3.974,0,7.611-2.653,8.688-6.674l2.216-8.273c1.286-4.801-1.563-9.736-6.365-11.022
|
||||||
|
C349.237,373.111,344.302,375.963,343.016,380.764z M112.375,215.913c2.577-0.69,5.056-1.089,7.454-1.195V32.492
|
||||||
|
c0-7.736,6.293-14.029,14.028-14.029h195.935c7.736,0,14.03,6.293,14.03,14.029v182.225c2.396,0.106,4.875,0.505,7.45,1.195
|
||||||
|
c16.511,4.424,26.346,21.457,21.922,37.968c-4.28,15.974-17.951,28.108-29.372,36.404v41.139c0,7.736-6.294,14.03-14.03,14.03
|
||||||
|
H133.857c-7.735,0-14.028-6.294-14.028-14.03v-41.137c-11.422-8.295-25.093-20.428-29.376-36.405
|
||||||
|
c-2.143-7.996-1.042-16.35,3.1-23.523C97.695,223.186,104.38,218.055,112.375,215.913z M343.821,267.05
|
||||||
|
c6.531-6.172,10.424-12,11.985-17.828c1.855-6.924-2.27-14.067-9.194-15.923c-1.047-0.281-1.97-0.451-2.791-0.538V267.05z
|
||||||
|
M137.829,327.454h187.992v-41.7c-0.001-0.08-0.001-0.161,0-0.241v-59.907c-0.003-0.13-0.003-0.261,0-0.391V36.463H137.829v188.755
|
||||||
|
c0.003,0.13,0.003,0.261,0,0.392v59.898c0.001,0.084,0.001,0.168,0,0.252V327.454z M107.84,249.222
|
||||||
|
c1.563,5.83,5.457,11.66,11.989,17.832v-34.292c-0.822,0.086-1.746,0.256-2.794,0.537c-3.353,0.898-6.156,3.051-7.894,6.061
|
||||||
|
C107.404,242.369,106.942,245.871,107.84,249.222z M173.576,405.019l-79.363,21.265L18.897,145.209l77.031-20.641
|
||||||
|
c4.801-1.287,7.651-6.222,6.364-11.023c-1.287-4.801-6.225-7.65-11.022-6.364L10.402,128.85c-3.614,0.968-6.637,3.29-8.512,6.538
|
||||||
|
c-1.876,3.249-2.376,7.029-1.407,10.644l77.37,288.743c0.968,3.616,3.29,6.641,6.54,8.518c2.166,1.25,4.567,1.89,7,1.89
|
||||||
|
c1.216,0,2.439-0.16,3.644-0.482l83.199-22.293c4.801-1.287,7.651-6.222,6.364-11.022
|
||||||
|
C183.312,406.581,178.377,403.734,173.576,405.019z M51.298,156.782c-4.801,1.287-7.65,6.222-6.364,11.023l2.217,8.274
|
||||||
|
c1.078,4.021,4.714,6.673,8.688,6.673c0.771,0,1.555-0.1,2.335-0.309c4.801-1.287,7.65-6.222,6.364-11.023l-2.217-8.274
|
||||||
|
C61.034,158.344,56.101,155.496,51.298,156.782z M297.52,281.322c-4.971,0-9,4.029-9,9v8.565c0,4.971,4.029,9,9,9s9-4.029,9-9
|
||||||
|
v-8.565C306.52,285.352,302.491,281.322,297.52,281.322z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
560
tablet/poker-admin.html
Normal file
560
tablet/poker-admin.html
Normal file
|
|
@ -0,0 +1,560 @@
|
||||||
|
<!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) {
|
||||||
|
ensureSession(function(err) {
|
||||||
|
if (err) { setStatus(err, "error"); return; }
|
||||||
|
setStatus("Deleting " + id + "...");
|
||||||
|
|
||||||
|
// Delete server-side first
|
||||||
|
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() {
|
||||||
|
// Then clean up in-world entities
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({
|
||||||
|
type: "deleteTable",
|
||||||
|
pokerID: id,
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.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;
|
||||||
|
} else if (msg.type === "deleteEntitiesComplete") {
|
||||||
|
var detail = msg.found ? "Table and entities deleted" : "Table deleted (no entities found)";
|
||||||
|
setStatus(detail, "ok");
|
||||||
|
loadTables();
|
||||||
|
}
|
||||||
|
} 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>
|
||||||
Loading…
Add table
Reference in a new issue