overte-www/scripts/poker_cards.js

237 lines
8.2 KiB
JavaScript

(function() {
// poker_cards.js — attach to the card hand mesh entity
// userData: { "pokerID": "main", "tableID": "{uuid}" }
var constants = Script.require(Script.resolvePath("poker_constants.js"));
var API_BASE = constants.API_BASE;
var _entityID = null;
var _pokerID = null;
var _username = null;
var _token = null;
var _ws = null;
var _rendererID = null; // local web entity that generates the texture
var _materialID = null; // local material entity applied to this mesh
var _wsRetryTimer = null;
var _pendingCards = null;
// ── helpers ──────────────────────────────────────────────────
function getUserData(id) {
try {
return JSON.parse(Entities.getEntityProperties(id, ["userData"]).userData || "{}");
} catch(e) { return {}; }
}
// ── session ──────────────────────────────────────────────────
function ensureSession(cb) {
if (_token) { cb(null, _token); return; }
_username = AccountServices.username;
if (!_username) { cb("not logged in"); return; }
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;
cb(null, _token);
} catch(e) { cb("session parse error"); }
} else {
cb("session error " + xhr.status);
}
};
xhr.send(JSON.stringify({ username: _username }));
}
// ── WebSocket ────────────────────────────────────────────────
function connectWS() {
var wsURL = API_BASE.replace("https://", "wss://").replace("http://", "ws://")
.replace("/api", "/ws");
_ws = new WebSocket(wsURL);
_ws.onopen = function() {
print("[pokerCards] WS connected");
// Identify so server can route private messages
_ws.send(JSON.stringify({ type: "identify", username: _username }));
};
_ws.onmessage = function(event) {
try {
var msg = JSON.parse(event.data);
if (msg.type === "hand:dealt") {
onCardsDealt(msg.cards);
} else if (msg.type === "hand:end") {
hideCards();
}
} catch(e) {
print("[pokerCards] WS parse error: " + e);
}
};
_ws.onclose = function() {
print("[pokerCards] WS closed, retrying in 5s");
_wsRetryTimer = Script.setTimeout(connectWS, 5000);
};
_ws.onerror = function(e) {
print("[pokerCards] WS error: " + e);
};
}
// ── card rendering ───────────────────────────────────────────
function onCardsDealt(cards) {
// cards is ["As", "Kh"] format from server
if (!cards || cards.length < 2) return;
var card1 = parseCard(cards[0]);
var card2 = parseCard(cards[1]);
print("[pokerCards] dealt " + cards[0] + " " + cards[1]);
// Spawn hidden web entity to render card texture
_rendererID = Entities.addEntity({
type: "Web",
name: "card-renderer",
sourceUrl: "https://wizards.cyou/tablet/card-face.html",
position: MyAvatar.position, // doesn't matter, it's invisible
dimensions: { x: 0.01, y: 0.01, z: 0.01 },
visible: false,
}, "local");
// Wait for web entity to load then send card data
Script.setTimeout(function() {
Entities.emitScriptEvent(_rendererID, JSON.stringify({
type: "dealCards",
card1: card1,
card2: card2,
}));
}, 1500);
}
function onTextureReady(dataURI) {
// Clean up renderer
if (_rendererID) {
Entities.deleteEntity(_rendererID);
_rendererID = null;
}
// Remove old material if any
if (_materialID) {
Entities.deleteEntity(_materialID);
_materialID = null;
}
// Apply local material entity to card mesh
_materialID = Entities.addEntity({
type: "Material",
name: "card-face-material",
parentID: _entityID,
materialURL: "materialData",
materialData: JSON.stringify({
materials: {
model: "hifi_pbr",
albedoMap: dataURI,
roughness: 0.8,
metallic: 0.0,
}
}),
priority: 1,
}, "local");
print("[pokerCards] card texture applied");
}
function hideCards() {
if (_materialID) {
Entities.deleteEntity(_materialID);
_materialID = null;
}
print("[pokerCards] cards hidden");
}
function parseCard(str) {
// "As" → { rank: "A", suit: "♠" }
// "10h" → { rank: "10", suit: "♥" }
var suitMap = { s: '♠', h: '♥', d: '♦', c: '♣' };
var suit = suitMap[str.slice(-1)] || '♠';
var rank = str.slice(0, -1);
return { rank: rank, suit: suit };
}
function onCardsDealt(cards) {
if (!cards || cards.length < 2) return;
var card1 = parseCard(cards[0]);
var card2 = parseCard(cards[1]);
print("[pokerCards] dealt " + cards[0] + " " + cards[1]);
_pendingCards = { card1: card1, card2: card2 };
_rendererID = Entities.addEntity({
type: "Web",
name: "card-renderer",
sourceUrl: "https://wizards.cyou/tablet/card-face.html",
position: MyAvatar.position,
dimensions: { x: 0.01, y: 0.01, z: 0.01 },
visible: false,
}, "local");
}
function onWebEventReceived(id, data) {
if (id !== _rendererID) return;
try {
var msg = JSON.parse(data);
if (msg.type === 'rendererReady') {
// Web entity loaded — now send card data
Entities.emitScriptEvent(_rendererID, JSON.stringify({
type: 'dealCards',
card1: _pendingCards.card1,
card2: _pendingCards.card2,
}));
} else if (msg.type === 'cardTexture') {
onTextureReady(msg.dataURI);
}
} catch(e) {}
}
// ── web entity event listener ────────────────────────────────
function onWebEventReceived(id, data) {
if (id !== _rendererID) return;
try {
var msg = JSON.parse(data);
if (msg.type === "cardTexture") {
onTextureReady(msg.dataURI);
}
} catch(e) {}
}
// ── lifecycle ────────────────────────────────────────────────
this.preload = function(entityID) {
_entityID = entityID;
var ud = getUserData(entityID);
_pokerID = ud.pokerID || "main";
Entities.webEventReceived.connect(onWebEventReceived);
ensureSession(function(err) {
if (err) { print("[pokerCards] session error: " + err); return; }
connectWS();
});
};
this.unload = function() {
if (_wsRetryTimer) Script.clearTimeout(_wsRetryTimer);
if (_ws) _ws.close();
if (_materialID) Entities.deleteEntity(_materialID);
if (_rendererID) Entities.deleteEntity(_rendererID);
Entities.webEventReceived.disconnect(onWebEventReceived);
_token = null;
};
});