2026-03-18 05:50:44 +00:00
|
|
|
(function () {
|
|
|
|
|
// poker_sit.js — Poker Seat Entity Script
|
|
|
|
|
//
|
|
|
|
|
// Attach to each seat pad entity. userData must contain:
|
|
|
|
|
// { "tableID": "{uuid-of-table-mesh-entity}", "pokerID": "main", "seatIndex": 3 }
|
|
|
|
|
//
|
|
|
|
|
// tableID — Overte entity UUID of the table mesh, used for position calculations
|
|
|
|
|
// pokerID — poker table slug used for API calls (matches table config id)
|
|
|
|
|
//
|
|
|
|
|
// Seat layout is loaded from poker_constants.js so this script
|
|
|
|
|
// does not need to know anything about the table geometry.
|
|
|
|
|
|
|
|
|
|
var constants = Script.require(Script.resolvePath("poker_constants.js"));
|
|
|
|
|
var POKER_SEATS = constants.POKER_SEATS;
|
|
|
|
|
var API_BASE = constants.API_BASE;
|
|
|
|
|
var POKER_BASE = constants.POKER_BASE;
|
|
|
|
|
var JUMP_ACTION = 50;
|
|
|
|
|
|
|
|
|
|
var _entityID = null;
|
|
|
|
|
var _tableID = null; // Overte entity UUID of the table mesh
|
|
|
|
|
var _pokerID = null; // poker table slug for API calls
|
|
|
|
|
var _seatIndex = null;
|
|
|
|
|
var _seatData = null;
|
|
|
|
|
var _token = null;
|
|
|
|
|
var _username = null;
|
|
|
|
|
var _busy = false;
|
|
|
|
|
var isSeated = false;
|
|
|
|
|
|
|
|
|
|
// ── helpers ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function getUserData(entityID) {
|
|
|
|
|
try {
|
|
|
|
|
var props = Entities.getEntityProperties(entityID, ["userData"]);
|
|
|
|
|
return JSON.parse(props.userData || "{}");
|
|
|
|
|
} catch (e) {
|
|
|
|
|
print("[pokerSeat] userData parse error: " + e);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getTableSeatCount() {
|
|
|
|
|
var ud = getUserData(_tableID);
|
|
|
|
|
return ud.seatCount || 7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSeatPosition() {
|
|
|
|
|
var tableProps = Entities.getEntityProperties(_tableID, ["position", "rotation"]);
|
|
|
|
|
var worldOffset = Vec3.multiplyQbyV(tableProps.rotation, _seatData.offset);
|
|
|
|
|
return Vec3.sum(tableProps.position, worldOffset);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSeatRotation() {
|
|
|
|
|
return Quat.fromPitchYawRollDegrees(0, _seatData.yaw, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function notify(msg) {
|
|
|
|
|
Window.displayAnnouncement(msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── session ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function ensureSession(cb) {
|
|
|
|
|
if (_token) { cb(null, _token); return; }
|
|
|
|
|
_username = MyAvatar.displayName;
|
|
|
|
|
if (!_username) { cb("Not logged in to Overte"); return; }
|
|
|
|
|
|
|
|
|
|
print("[pokerSeat] requesting session from: " + API_BASE + "/session for " + _username);
|
|
|
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
|
xhr.open("POST", API_BASE + "/session", true);
|
|
|
|
|
xhr.setRequestHeader("Content-Type", "application/json");
|
|
|
|
|
xhr.onreadystatechange = function () {
|
|
|
|
|
if (xhr.readyState !== 4) return;
|
|
|
|
|
if (xhr.status === 200) {
|
|
|
|
|
try {
|
|
|
|
|
_token = JSON.parse(xhr.responseText).token;
|
|
|
|
|
Script.setTimeout(function () { _token = null; }, 5 * 60 * 1000);
|
|
|
|
|
cb(null, _token);
|
|
|
|
|
} catch (e) { cb("Session parse error"); }
|
|
|
|
|
} else if (xhr.status === 403) {
|
|
|
|
|
cb("Must be in-world to sit down");
|
|
|
|
|
} else {
|
|
|
|
|
cb("Session error (" + xhr.status + ")");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
xhr.send(JSON.stringify({ username: _username }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── sit / stand ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function onSit() {
|
|
|
|
|
if (isSeated || _busy) return;
|
|
|
|
|
_busy = true;
|
|
|
|
|
|
|
|
|
|
if (!_seatData) {
|
|
|
|
|
notify("Seat not configured (index " + _seatIndex + ")");
|
|
|
|
|
_busy = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensureSession(function (err) {
|
|
|
|
|
if (err) { notify(err); _busy = false; return; }
|
|
|
|
|
|
|
|
|
|
// POST to server — handles buy-in deduction and seat reservation.
|
|
|
|
|
// Server will reject if seat is taken or balance insufficient.
|
|
|
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
|
xhr.open("POST", POKER_BASE + "/tables/" + _pokerID + "/sit", true);
|
|
|
|
|
xhr.setRequestHeader("Content-Type", "application/json");
|
|
|
|
|
xhr.setRequestHeader("Authorization", "Bearer " + _token);
|
|
|
|
|
xhr.onreadystatechange = function () {
|
|
|
|
|
if (xhr.readyState !== 4) return;
|
2026-03-18 06:34:58 +00:00
|
|
|
print("[pokerSeat] sit response: " + xhr.status + " " + xhr.responseText);
|
2026-03-18 05:50:44 +00:00
|
|
|
if (xhr.status === 200) {
|
|
|
|
|
MyAvatar.beginSit(getSeatPosition(), getSeatRotation());
|
|
|
|
|
isSeated = true;
|
|
|
|
|
Entities.editEntity(_entityID, { visible: false });
|
|
|
|
|
Controller.actionEvent.connect(onActionEvent);
|
|
|
|
|
Messages.sendMessage("poker:seat", JSON.stringify({
|
2026-03-18 06:34:58 +00:00
|
|
|
event: "sit", tableID: _tableID, pokerID: _pokerID,
|
|
|
|
|
seatIndex: _seatIndex, username: _username,
|
2026-03-18 05:50:44 +00:00
|
|
|
}));
|
2026-03-18 06:34:58 +00:00
|
|
|
_busy = false;
|
2026-03-18 05:50:44 +00:00
|
|
|
} else {
|
|
|
|
|
try {
|
2026-03-18 06:34:58 +00:00
|
|
|
var resp = JSON.parse(xhr.responseText);
|
|
|
|
|
if (resp.error === "already seated") {
|
|
|
|
|
// Server thinks we're seated — stand first, then re-sit
|
|
|
|
|
print("[pokerSeat] recovering stale seat, standing first");
|
|
|
|
|
doStand(function() { onSit(); });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
notify(resp.error || "Could not sit down");
|
2026-03-18 05:50:44 +00:00
|
|
|
} catch (e) {
|
|
|
|
|
notify("Could not sit down (" + xhr.status + ")");
|
|
|
|
|
}
|
2026-03-18 06:34:58 +00:00
|
|
|
_busy = false;
|
2026-03-18 05:50:44 +00:00
|
|
|
}
|
|
|
|
|
};
|
2026-03-18 06:34:58 +00:00
|
|
|
var payload = JSON.stringify({ seatIndex: parseInt(_seatIndex) });
|
|
|
|
|
print("[pokerSeat] sit payload: " + payload + " token: " + _token + " url: " + POKER_BASE + "/tables/" + _pokerID + "/sit");
|
|
|
|
|
xhr.send(payload);
|
2026-03-18 05:50:44 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 06:34:58 +00:00
|
|
|
function doStand(cb) {
|
|
|
|
|
var tok = getSharedToken(); // or _token if keeping per-instance
|
2026-03-18 05:50:44 +00:00
|
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
|
xhr.open("POST", POKER_BASE + "/tables/" + _pokerID + "/stand", true);
|
|
|
|
|
xhr.setRequestHeader("Content-Type", "application/json");
|
2026-03-18 06:34:58 +00:00
|
|
|
xhr.setRequestHeader("Authorization", "Bearer " + tok);
|
2026-03-18 05:50:44 +00:00
|
|
|
xhr.onreadystatechange = function () {
|
|
|
|
|
if (xhr.readyState !== 4) return;
|
2026-03-18 06:34:58 +00:00
|
|
|
_token = null; // invalidate immediately on stand
|
|
|
|
|
if (isSeated) {
|
|
|
|
|
MyAvatar.endSit(getSeatPosition(), getSeatRotation());
|
|
|
|
|
isSeated = false;
|
|
|
|
|
Entities.editEntity(_entityID, { visible: true });
|
|
|
|
|
Controller.actionEvent.disconnect(onActionEvent);
|
|
|
|
|
Messages.sendMessage("poker:seat", JSON.stringify({
|
|
|
|
|
event: "stand", tableID: _tableID,
|
|
|
|
|
seatIndex: _seatIndex, username: _username,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
if (cb) cb();
|
2026-03-18 05:50:44 +00:00
|
|
|
};
|
|
|
|
|
xhr.send(JSON.stringify({ seatIndex: parseInt(_seatIndex) }));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 06:34:58 +00:00
|
|
|
function onStand() {
|
|
|
|
|
if (!isSeated) return;
|
|
|
|
|
doStand(null);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 05:50:44 +00:00
|
|
|
function onActionEvent(action, value) {
|
|
|
|
|
if (action === JUMP_ACTION && value > 0) {
|
|
|
|
|
onStand();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── lifecycle ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
this.preload = function (entityID) {
|
|
|
|
|
_entityID = entityID;
|
|
|
|
|
|
|
|
|
|
var ud = getUserData(entityID);
|
|
|
|
|
_tableID = ud.tableID || null;
|
|
|
|
|
_pokerID = ud.pokerID || null;
|
|
|
|
|
_seatIndex = ud.seatIndex !== undefined ? String(ud.seatIndex) : null;
|
|
|
|
|
|
|
|
|
|
if (!_tableID || !_pokerID || _seatIndex === null) {
|
|
|
|
|
print("[pokerSeat] missing tableID, pokerID, or seatIndex in userData on " + entityID);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load seat geometry from constants keyed by table's seat count
|
|
|
|
|
var seatCount = getTableSeatCount();
|
|
|
|
|
var layout = POKER_SEATS[seatCount];
|
|
|
|
|
if (!layout || !layout[parseInt(_seatIndex)]) {
|
|
|
|
|
print("[pokerSeat] no layout for seatCount=" + seatCount + " index=" + _seatIndex);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_seatData = layout[parseInt(_seatIndex)];
|
|
|
|
|
print("[pokerSeat] loaded seat " + _seatIndex + " at table " + _tableID);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Mouse + controller triggers
|
|
|
|
|
this.clickDownOnEntity = function () { onSit(); };
|
|
|
|
|
this.clickUpOnEntity = function () { _busy = false; };
|
|
|
|
|
this.startNearTrigger = function () { onSit(); };
|
|
|
|
|
this.stopNearTrigger = function () { _busy = false; };
|
|
|
|
|
this.startFarTrigger = function () { onSit(); };
|
|
|
|
|
this.stopFarTrigger = function () { _busy = false; };
|
|
|
|
|
|
|
|
|
|
this.unload = function () {
|
|
|
|
|
if (isSeated) {
|
|
|
|
|
Controller.actionEvent.disconnect(onActionEvent);
|
|
|
|
|
isSeated = false;
|
|
|
|
|
}
|
|
|
|
|
_token = null;
|
|
|
|
|
};
|
|
|
|
|
});
|