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
|
// Place table in front of avatar at floor level
|
||||||
// Use only the yaw component of avatar rotation so table sits flat
|
// Use only the yaw component of avatar rotation so table sits flat
|
||||||
var yaw = Quat.safeEulerAngles(avatarRotation).y;
|
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
|
// 2.5m in front of avatar, snapped to floor
|
||||||
var forward = Vec3.multiplyQbyV(tableRot, { x: 0, y: 0, z: -2.5 });
|
var forward = Vec3.multiplyQbyV(tableRot, { x: 0, y: 0, z: -2.5 });
|
||||||
|
|
@ -87,6 +87,7 @@
|
||||||
locked: false,
|
locked: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Timeout to set locked true after displaying mesh in world
|
||||||
Script.setTimeout(function() {
|
Script.setTimeout(function() {
|
||||||
Entities.editEntity(tableID, { locked: true });
|
Entities.editEntity(tableID, { locked: true });
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
@ -119,8 +120,11 @@
|
||||||
pokerID: pokerID,
|
pokerID: pokerID,
|
||||||
seatIndex: i,
|
seatIndex: i,
|
||||||
}),
|
}),
|
||||||
|
grab: {
|
||||||
triggerable: true,
|
triggerable: true,
|
||||||
grabbable: false,
|
grabbable: false
|
||||||
|
},
|
||||||
|
//grabbable: false,
|
||||||
ignorePickIntersection: false,
|
ignorePickIntersection: false,
|
||||||
locked: true,
|
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