(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 print("[pokerCards] WS connected, identifying as: " + _username); _ws.send(JSON.stringify({ type: "identify", username: _username })); }; _ws.onmessage = function(event) { print("[pokerCards] WS message received: " + event.data.substring(0, 100)); try { var msg = JSON.parse(event.data); if (msg.type === "hand:dealt") { onCardsDealt(msg.cards); } else if (msg.type === "hand:end") { hideCards(); } else if (msg.type === "player:disconnect:stand") { if (msg.username === _username) { print("[pokerCards] deferred stand complete, chips returned: " + msg.stack); Messages.sendLocalMessage("poker:standComplete", JSON.stringify({ username: msg.username, stack: msg.stack, })); } } } 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) { 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"); print("[pokerCards] spawned renderer entity: " + _rendererID); } 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) { print("[pokerCards] webEvent from " + id + " renderer is " + _rendererID); if (id !== _rendererID) return; try { var msg = JSON.parse(data); print("[pokerCards] renderer msg type: " + msg.type); if (msg.type === 'rendererReady') { print("[pokerCards] renderer ready, sending cards"); Entities.emitScriptEvent(_rendererID, JSON.stringify({ type: 'dealCards', card1: _pendingCards.card1, card2: _pendingCards.card2, })); } else if (msg.type === 'cardTexture') { print("[pokerCards] texture received, length: " + msg.dataURI.length); onTextureReady(msg.dataURI); } } catch(e) { print("[pokerCards] webEvent parse error: " + 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; }; });