Add in world hole card implementation
This commit is contained in:
parent
c259168f6f
commit
af50d821ef
4 changed files with 366 additions and 3 deletions
BIN
models/poker_hole_cards.glb
Normal file
BIN
models/poker_hole_cards.glb
Normal file
Binary file not shown.
|
|
@ -60,7 +60,7 @@
|
|||
// 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);
|
||||
var tableRot = Quat.fromPitchYawRollDegrees(0, yaw, 0);
|
||||
|
||||
// 2.5m in front of avatar, snapped to floor
|
||||
var forward = Vec3.multiplyQbyV(tableRot, { x: 0, y: 0, z: -2.5 });
|
||||
|
|
@ -87,6 +87,7 @@
|
|||
locked: false,
|
||||
});
|
||||
|
||||
// Timeout to set locked true after displaying mesh in world
|
||||
Script.setTimeout(function() {
|
||||
Entities.editEntity(tableID, { locked: true });
|
||||
}, 2000);
|
||||
|
|
@ -119,8 +120,11 @@
|
|||
pokerID: pokerID,
|
||||
seatIndex: i,
|
||||
}),
|
||||
grab: {
|
||||
triggerable: true,
|
||||
grabbable: false,
|
||||
grabbable: false
|
||||
},
|
||||
//grabbable: false,
|
||||
ignorePickIntersection: false,
|
||||
locked: true,
|
||||
});
|
||||
|
|
|
|||
237
scripts/poker_cards.js
Normal file
237
scripts/poker_cards.js
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
(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;
|
||||
};
|
||||
});
|
||||
122
tablet/card-face.html
Normal file
122
tablet/card-face.html
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; }
|
||||
body { background: transparent; overflow: hidden; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="c"></canvas>
|
||||
<script>
|
||||
var canvas = document.getElementById('c');
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
var TEX_SIZE = 512;
|
||||
canvas.width = TEX_SIZE;
|
||||
canvas.height = TEX_SIZE;
|
||||
|
||||
// UV islands span Y: 0.15 to 0.85 (centered, 70% of texture height)
|
||||
var CARD_TOP = Math.round(TEX_SIZE * 0.15); // 77px
|
||||
var CARD_BOTTOM = Math.round(TEX_SIZE * 0.85); // 435px
|
||||
var CARD_H = CARD_BOTTOM - CARD_TOP; // 358px
|
||||
var CARD_W = TEX_SIZE / 2; // 256px each
|
||||
|
||||
var SUITS = { '♠': '#1a1a2e', '♣': '#1a1a2e', '♥': '#cc2222', '♦': '#cc2222' };
|
||||
|
||||
function drawCard(xOffset, rank, suit) {
|
||||
var col = SUITS[suit] || '#1a1a2e';
|
||||
var w = CARD_W;
|
||||
var h = CARD_H;
|
||||
var r = 10;
|
||||
var p = 12;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(xOffset, CARD_TOP);
|
||||
|
||||
// Card background
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(r, 0);
|
||||
ctx.lineTo(w - r, 0);
|
||||
ctx.quadraticCurveTo(w, 0, w, r);
|
||||
ctx.lineTo(w, h - r);
|
||||
ctx.quadraticCurveTo(w, h, w - r, h);
|
||||
ctx.lineTo(r, h);
|
||||
ctx.quadraticCurveTo(0, h, 0, h - r);
|
||||
ctx.lineTo(0, r);
|
||||
ctx.quadraticCurveTo(0, 0, r, 0);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#d0d0d0';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = col;
|
||||
|
||||
// Top-left rank
|
||||
ctx.font = 'bold 36px Georgia, serif';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(rank, p, p);
|
||||
|
||||
// Suit below rank top-left
|
||||
ctx.font = '26px Georgia, serif';
|
||||
ctx.fillText(suit, p, p + 40);
|
||||
|
||||
// Bottom-right flipped
|
||||
ctx.save();
|
||||
ctx.translate(w, h);
|
||||
ctx.rotate(Math.PI);
|
||||
ctx.font = 'bold 36px Georgia, serif';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(rank, p, p);
|
||||
ctx.font = '26px Georgia, serif';
|
||||
ctx.fillText(suit, p, p + 40);
|
||||
ctx.restore();
|
||||
|
||||
// Center suit
|
||||
ctx.font = '110px Georgia, serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(suit, w / 2, h / 2);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function render(card1, card2) {
|
||||
// Clear with transparent background
|
||||
ctx.clearRect(0, 0, TEX_SIZE, TEX_SIZE);
|
||||
|
||||
drawCard(0, card1.rank, card1.suit);
|
||||
drawCard(CARD_W, card2.rank, card2.suit);
|
||||
|
||||
var dataURI = canvas.toDataURL('image/png');
|
||||
if (typeof EventBridge !== 'undefined') {
|
||||
EventBridge.emitWebEvent(JSON.stringify({
|
||||
type: 'cardTexture',
|
||||
dataURI: dataURI,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof EventBridge !== 'undefined') {
|
||||
EventBridge.scriptEventReceived.connect(function(data) {
|
||||
try {
|
||||
var msg = JSON.parse(data);
|
||||
if (msg.type === 'dealCards') {
|
||||
render(msg.card1, msg.card2);
|
||||
} else if (msg.type === 'ready') {
|
||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'rendererReady' }));
|
||||
}
|
||||
} catch(e) {}
|
||||
});
|
||||
// Signal ready on load
|
||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'rendererReady' }));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue