598 lines
25 KiB
JavaScript
598 lines
25 KiB
JavaScript
// Procedural reel-symbol painters for Slot Machines.
|
||
//
|
||
// Every symbol is painted once into a 200×200 baked texture
|
||
// ('slots-sym-{machineId}-{symbolId}') via a RenderTexture, so reels and
|
||
// tumbles animate plain Images. If the user has supplied drop-in art
|
||
// ('slots-{machineId}-{symbolId}', loaded by PreloadScene from
|
||
// slots-artwork.json), textureKeyFor() prefers it — zero code changes.
|
||
|
||
const S = 200; // texture size
|
||
const C = S / 2; // center
|
||
|
||
export function dropInKey(machineId, symbolId) { return `slots-${machineId}-${symbolId}`; }
|
||
export function bakedKey(machineId, symbolId) { return `slots-sym-${machineId}-${symbolId}`; }
|
||
|
||
export function textureKeyFor(scene, machineId, symbolId) {
|
||
const k = dropInKey(machineId, symbolId);
|
||
return scene.textures.exists(k) ? k : bakedKey(machineId, symbolId);
|
||
}
|
||
|
||
export function allSymbolIds(machine) {
|
||
const ids = new Set();
|
||
for (const strip of machine.strips) for (const s of strip) ids.add(s);
|
||
if (machine.wild) ids.add(machine.wild.symbol);
|
||
if (machine.features.rainbow) ids.add(machine.features.rainbow.symbol);
|
||
return [...ids];
|
||
}
|
||
|
||
// ── Drawing primitives ────────────────────────────────────────────────────────
|
||
const P = (arr) => arr.map(([x, y]) => ({ x, y }));
|
||
|
||
function starPts(cx, cy, spikes, outer, inner, rot = -Math.PI / 2) {
|
||
const out = [];
|
||
for (let i = 0; i < spikes * 2; i++) {
|
||
const r = i % 2 === 0 ? outer : inner;
|
||
const a = rot + (i * Math.PI) / spikes;
|
||
out.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r });
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function heartPath(g, cx, cy, size, color) {
|
||
g.fillStyle(color, 1);
|
||
g.fillCircle(cx - size * 0.35, cy - size * 0.25, size * 0.4);
|
||
g.fillCircle(cx + size * 0.35, cy - size * 0.25, size * 0.4);
|
||
g.fillPoints(P([
|
||
[cx - size * 0.72, cy - size * 0.08],
|
||
[cx + size * 0.72, cy - size * 0.08],
|
||
[cx, cy + size * 0.75],
|
||
]), true);
|
||
}
|
||
|
||
function barShape(g, cy, label, addText, color = 0x14100a, textColor = '#f6efe0') {
|
||
g.fillStyle(color, 1);
|
||
g.fillRoundedRect(C - 72, cy - 22, 144, 44, 10);
|
||
g.lineStyle(4, 0xc8a84b, 1);
|
||
g.strokeRoundedRect(C - 72, cy - 22, 144, 44, 10);
|
||
addText(label, { x: C, y: cy, size: 30, color: textColor, font: 'Righteous' });
|
||
}
|
||
|
||
// ── Painters ─────────────────────────────────────────────────────────────────
|
||
// Keyed '{machineId}/{symbolId}' with a '{symbolId}' fallback for shared art.
|
||
// Each receives ({ g, text, theme }).
|
||
|
||
const PAINTERS = {
|
||
// ··· shared card royals ···
|
||
ace: cardLetter('A'), king: cardLetter('K'), queen: cardLetter('Q'), jack: cardLetter('J'),
|
||
|
||
// ··· Liberty Belle ···
|
||
bell({ g }) {
|
||
g.fillStyle(0xd9a93b, 1);
|
||
g.fillCircle(C, 64, 16);
|
||
g.fillPoints(P([[C - 14, 64], [C + 14, 64], [C + 52, 138], [C - 52, 138]]), true);
|
||
g.fillCircle(C, 96, 44);
|
||
g.fillStyle(0xb07f1f, 1);
|
||
g.fillRoundedRect(C - 62, 132, 124, 16, 7);
|
||
g.fillStyle(0x7a5410, 1);
|
||
g.fillCircle(C, 158, 12);
|
||
g.fillStyle(0xf7e3a1, 0.8);
|
||
g.fillEllipse(C - 22, 86, 14, 34);
|
||
},
|
||
horseshoe({ g }) {
|
||
g.lineStyle(26, 0x9aa3ad, 1);
|
||
g.beginPath();
|
||
g.arc(C, 92, 56, Math.PI * 0.95, Math.PI * 2.05, false);
|
||
g.strokePath();
|
||
g.lineStyle(26, 0x9aa3ad, 1);
|
||
g.beginPath(); g.moveTo(C - 54, 76); g.lineTo(C - 60, 142); g.strokePath();
|
||
g.beginPath(); g.moveTo(C + 54, 76); g.lineTo(C + 60, 142); g.strokePath();
|
||
g.fillStyle(0x5d666f, 1);
|
||
for (const [x, y] of [[C - 58, 138], [C + 58, 138], [C - 50, 60], [C + 50, 60], [C, 36]]) g.fillCircle(x, y, 6);
|
||
},
|
||
star({ g }) {
|
||
g.fillStyle(0xe8b733, 1);
|
||
g.fillPoints(starPts(C, C + 6, 5, 74, 30), true);
|
||
g.fillStyle(0xfbe8a6, 0.85);
|
||
g.fillPoints(starPts(C, C + 6, 5, 36, 15), true);
|
||
},
|
||
heart({ g }) { heartPath(g, C, C + 4, 92, 0xc0392b); g.fillStyle(0xf2b8ad, 0.7); g.fillCircle(C - 30, 72, 13); },
|
||
diamond({ g }) {
|
||
g.fillStyle(0xd35400, 1);
|
||
g.fillPoints(P([[C, 26], [C + 64, C + 4], [C, 178], [C - 64, C + 4]]), true);
|
||
g.fillStyle(0xf8c471, 0.75);
|
||
g.fillPoints(P([[C, 52], [C + 34, C + 4], [C, 152], [C - 34, C + 4]]), true);
|
||
},
|
||
spade({ g }) {
|
||
heartPath(g, C, 84, 84, 0x232b36);
|
||
// flip illusion: heart drawn upward + stem
|
||
g.fillStyle(0x232b36, 1);
|
||
g.fillPoints(P([[C - 7, 120], [C + 7, 120], [C + 22, 168], [C - 22, 168]]), true);
|
||
},
|
||
|
||
// ··· Fruit Frenzy ···
|
||
cherry({ g }) {
|
||
g.lineStyle(8, 0x3f7a2a, 1);
|
||
g.beginPath(); g.moveTo(C - 26, 132); g.lineTo(C + 2, 44); g.strokePath();
|
||
g.beginPath(); g.moveTo(C + 38, 124); g.lineTo(C + 2, 44); g.strokePath();
|
||
g.fillStyle(0x4f9a34, 1);
|
||
g.fillEllipse(C + 26, 48, 44, 20);
|
||
g.fillStyle(0xc62b33, 1);
|
||
g.fillCircle(C - 30, 142, 30);
|
||
g.fillCircle(C + 42, 134, 30);
|
||
g.fillStyle(0xf0888d, 0.8);
|
||
g.fillCircle(C - 40, 132, 9); g.fillCircle(C + 32, 124, 9);
|
||
},
|
||
lemon({ g }) {
|
||
g.fillStyle(0xeed02a, 1);
|
||
g.fillEllipse(C, C + 8, 132, 92);
|
||
g.fillCircle(C - 70, C + 8, 14); g.fillCircle(C + 70, C + 8, 14);
|
||
g.fillStyle(0xfbf0a0, 0.7); g.fillEllipse(C - 26, C - 8, 38, 20);
|
||
},
|
||
orange({ g }) {
|
||
g.fillStyle(0xe87f1e, 1); g.fillCircle(C, C + 10, 62);
|
||
g.fillStyle(0x4f9a34, 1); g.fillEllipse(C + 24, 48, 40, 18);
|
||
g.fillStyle(0xf6b66e, 0.7); g.fillCircle(C - 22, C - 8, 16);
|
||
},
|
||
plum({ g }) {
|
||
g.fillStyle(0x6e3a8c, 1); g.fillCircle(C, C + 10, 60);
|
||
g.lineStyle(5, 0x4d2363, 1);
|
||
g.beginPath(); g.arc(C - 6, C + 10, 52, -Math.PI * 0.45, Math.PI * 0.45); g.strokePath();
|
||
g.fillStyle(0x4f9a34, 1); g.fillEllipse(C + 22, 46, 38, 16);
|
||
g.fillStyle(0xb88ed1, 0.7); g.fillCircle(C - 24, C - 6, 14);
|
||
},
|
||
melon({ g }) {
|
||
g.fillStyle(0x2f7a33, 1);
|
||
g.slice(C, 64, 92, 0, Math.PI, false); g.fillPath();
|
||
g.fillStyle(0xe8536a, 1);
|
||
g.slice(C, 64, 76, 0, Math.PI, false); g.fillPath();
|
||
g.fillStyle(0x21262b, 1);
|
||
for (const [x, y] of [[C - 36, 96], [C, 116], [C + 36, 96], [C - 14, 84], [C + 16, 88]]) {
|
||
g.fillEllipse(x, y, 8, 13);
|
||
}
|
||
},
|
||
bar({ g, text }) { barShape(g, C - 34, 'BAR', text); barShape(g, C + 34, 'BAR', text); },
|
||
|
||
// ··· Lucky Sevens ···
|
||
'lucky-sevens/wild': ({ g, text }) => {
|
||
g.fillStyle(0x101319, 1);
|
||
g.fillPoints(P([[C, 30], [C + 70, C], [C, 170], [C - 70, C]]), true);
|
||
g.lineStyle(6, 0x8fd8ff, 1);
|
||
g.strokePoints(P([[C, 42], [C + 56, C], [C, 158], [C - 56, C], [C, 42]]), true);
|
||
g.lineStyle(3, 0x8fd8ff, 0.7);
|
||
g.strokePoints(P([[C, 66], [C + 32, C], [C, 134], [C - 32, C], [C, 66]]), true);
|
||
text('WILD', { x: C, y: C, size: 26, color: '#8fd8ff', font: 'Righteous' });
|
||
},
|
||
'seven-red': seven('#d4313f'),
|
||
'seven-white': seven('#f4f1e8'),
|
||
'seven-blue': seven('#4f8fe0'),
|
||
bar1({ g, text }) { barShape(g, C, 'BAR', text); },
|
||
bar2({ g, text }) { barShape(g, C - 28, 'BAR', text); barShape(g, C + 28, 'BAR', text); },
|
||
bar3({ g, text }) { barShape(g, C - 52, 'BAR', text); barShape(g, C, 'BAR', text); barShape(g, C + 52, 'BAR', text); },
|
||
blank({ g }) { g.fillStyle(0x000000, 0.06); g.fillRoundedRect(C - 50, C - 6, 100, 12, 6); },
|
||
|
||
// ··· Pharaoh's Fortune ···
|
||
pharaoh({ g, text }) {
|
||
g.fillStyle(0xe6b94d, 1);
|
||
g.fillRoundedRect(C - 44, 40, 88, 96, 18);
|
||
g.fillPoints(P([[C - 44, 60], [C - 72, 130], [C - 44, 122]]), true);
|
||
g.fillPoints(P([[C + 44, 60], [C + 72, 130], [C + 44, 122]]), true);
|
||
g.fillStyle(0x1c2c4e, 1);
|
||
for (let i = 0; i < 3; i++) g.fillRect(C - 44 + 10, 48 + i * 16, 68, 7);
|
||
g.fillStyle(0x18120a, 1);
|
||
g.fillEllipse(C - 18, 104, 18, 9); g.fillEllipse(C + 18, 104, 18, 9);
|
||
g.fillStyle(0xe6b94d, 1);
|
||
g.fillRoundedRect(C - 12, 130, 24, 34, 8);
|
||
text('WILD', { x: C, y: 182, size: 22, color: '#ffd86b', font: 'Righteous' });
|
||
},
|
||
pyramid({ g, text }) {
|
||
g.fillStyle(0xc99b3f, 1);
|
||
g.fillPoints(P([[C, 34], [C + 78, 148], [C - 78, 148]]), true);
|
||
g.fillStyle(0xf7df9a, 1);
|
||
g.fillPoints(P([[C, 34], [C + 22, 66], [C - 22, 66]]), true);
|
||
g.lineStyle(3, 0x8a6722, 0.8);
|
||
for (let i = 1; i < 4; i++) {
|
||
const y = 66 + i * 22; const w = 22 + i * 19;
|
||
g.beginPath(); g.moveTo(C - w, y); g.lineTo(C + w, y); g.strokePath();
|
||
}
|
||
text('BONUS', { x: C, y: 174, size: 22, color: '#ffd86b', font: 'Righteous' });
|
||
},
|
||
sphinx({ g }) {
|
||
g.fillStyle(0xd2a648, 1);
|
||
g.fillPoints(P([[40, 150], [40, 120], [78, 112], [86, 70], [122, 70], [128, 112], [164, 118], [164, 150]]), true);
|
||
g.fillRoundedRect(78, 52, 48, 52, 10);
|
||
g.fillStyle(0x1c2c4e, 1);
|
||
g.fillRect(82, 60, 8, 36); g.fillRect(114, 60, 8, 36);
|
||
g.fillStyle(0x18120a, 1);
|
||
g.fillEllipse(96, 78, 7, 4); g.fillEllipse(110, 78, 7, 4);
|
||
},
|
||
ankh({ g }) {
|
||
g.lineStyle(20, 0xe6b94d, 1);
|
||
g.strokeEllipse(C, 64, 52, 64);
|
||
g.beginPath(); g.moveTo(C, 96); g.lineTo(C, 170); g.strokePath();
|
||
g.beginPath(); g.moveTo(C - 44, 118); g.lineTo(C + 44, 118); g.strokePath();
|
||
},
|
||
eye({ g }) {
|
||
g.lineStyle(11, 0x3f6fc4, 1);
|
||
g.beginPath(); g.arc(C, C - 4, 56, Math.PI * 1.15, Math.PI * 1.85); g.strokePath();
|
||
g.beginPath(); g.arc(C, C - 30, 60, Math.PI * 0.12, Math.PI * 0.88); g.strokePath();
|
||
g.fillStyle(0x18120a, 1); g.fillCircle(C, C - 8, 20);
|
||
g.fillStyle(0x3f6fc4, 1); g.fillCircle(C, C - 8, 11);
|
||
g.lineStyle(9, 0x3f6fc4, 1);
|
||
g.beginPath(); g.moveTo(C - 34, C + 28); g.lineTo(C - 48, C + 64); g.strokePath();
|
||
g.beginPath(); g.moveTo(C + 30, C + 26); g.lineTo(C + 34, C + 58); g.strokePath();
|
||
g.beginPath(); g.arc(C + 18, C + 58, 16, Math.PI * 1.5, Math.PI * 0.8); g.strokePath();
|
||
},
|
||
cat({ g }) {
|
||
g.fillStyle(0x1b1410, 1);
|
||
g.fillPoints(P([[C - 26, 56], [C - 32, 24], [C - 8, 44]]), true);
|
||
g.fillPoints(P([[C + 26, 56], [C + 32, 24], [C + 8, 44]]), true);
|
||
g.fillCircle(C, 64, 30);
|
||
g.fillEllipse(C, 130, 64, 76);
|
||
g.fillStyle(0xe6b94d, 1);
|
||
g.fillRoundedRect(C - 26, 88, 52, 10, 5);
|
||
g.fillStyle(0x57f287, 1);
|
||
g.fillEllipse(C - 12, 60, 8, 11); g.fillEllipse(C + 12, 60, 8, 11);
|
||
},
|
||
|
||
// ··· Abyssal Treasures ···
|
||
pearl({ g, text }) {
|
||
g.fillStyle(0x2b5e6e, 1);
|
||
g.slice(C, 128, 74, Math.PI, Math.PI * 2, false); g.fillPath();
|
||
g.lineStyle(5, 0x59e7ff, 0.8);
|
||
for (let i = -2; i <= 2; i++) {
|
||
g.beginPath(); g.moveTo(C, 128); g.lineTo(C + i * 30, 62); g.strokePath();
|
||
}
|
||
g.fillStyle(0xf2f6f4, 1); g.fillCircle(C, 108, 30);
|
||
g.fillStyle(0xffffff, 0.9); g.fillCircle(C - 10, 98, 9);
|
||
text('WILD', { x: C, y: 176, size: 22, color: '#59e7ff', font: 'Righteous' });
|
||
},
|
||
mermaid({ g }) {
|
||
g.fillStyle(0x2fae9b, 1);
|
||
g.fillPoints(P([[C - 8, 30], [C + 26, 78], [C + 10, 128], [C - 26, 96]]), true);
|
||
g.fillPoints(P([[C - 2, 110], [C - 44, 168], [C - 6, 156], [C + 6, 176], [C + 26, 142]]), true);
|
||
g.lineStyle(4, 0x9af0e2, 0.8);
|
||
for (let i = 0; i < 3; i++) {
|
||
g.beginPath(); g.arc(C - 2 + i * 4, 66 + i * 22, 16, Math.PI * 0.15, Math.PI * 0.85); g.strokePath();
|
||
}
|
||
},
|
||
octopus({ g }) {
|
||
g.fillStyle(0x8857b8, 1);
|
||
g.slice(C, 90, 50, Math.PI, Math.PI * 2, false); g.fillPath();
|
||
g.fillRect(C - 50, 88, 100, 14);
|
||
g.lineStyle(14, 0x8857b8, 1);
|
||
for (const [dx, sw] of [[-38, 1], [-13, -1], [13, 1], [38, -1]]) {
|
||
g.beginPath();
|
||
g.moveTo(C + dx, 100);
|
||
g.lineTo(C + dx + sw * 4, 130);
|
||
g.arc(C + dx + sw * 18, 134, 15, Math.PI, Math.PI * (sw > 0 ? 1.9 : 0.1), sw < 0);
|
||
g.strokePath();
|
||
}
|
||
g.fillStyle(0xf2f6f4, 1); g.fillCircle(C - 18, 76, 11); g.fillCircle(C + 18, 76, 11);
|
||
g.fillStyle(0x18120a, 1); g.fillCircle(C - 16, 78, 5); g.fillCircle(C + 20, 78, 5);
|
||
},
|
||
seahorse({ g }) {
|
||
g.lineStyle(22, 0xe8913e, 1);
|
||
g.beginPath(); g.arc(C + 6, 70, 30, Math.PI * 1.1, Math.PI * 2.2); g.strokePath();
|
||
g.beginPath(); g.arc(C - 4, 124, 28, Math.PI * 0.3, Math.PI * 1.35, true); g.strokePath();
|
||
g.lineStyle(14, 0xe8913e, 1);
|
||
g.beginPath(); g.arc(C + 18, 152, 18, Math.PI * 1.2, Math.PI * 0.5); g.strokePath();
|
||
g.fillStyle(0xe8913e, 1);
|
||
g.fillPoints(P([[C + 28, 46], [C + 58, 38], [C + 36, 62]]), true);
|
||
g.fillStyle(0x18120a, 1); g.fillCircle(C + 16, 56, 6);
|
||
},
|
||
angler({ g }) {
|
||
g.fillStyle(0x27445c, 1);
|
||
g.fillEllipse(C, 118, 120, 84);
|
||
g.fillPoints(P([[C + 52, 118], [C + 88, 92], [C + 88, 144]]), true);
|
||
g.fillStyle(0xf2f6f4, 1);
|
||
g.fillPoints(P([[C - 52, 112], [C - 34, 112], [C - 43, 130]]), true);
|
||
g.fillPoints(P([[C - 28, 114], [C - 10, 114], [C - 19, 132]]), true);
|
||
g.lineStyle(5, 0xbfe85a, 1);
|
||
g.beginPath(); g.moveTo(C - 30, 84); g.arc(C - 52, 62, 26, Math.PI * 0.3, Math.PI * 1.2, true); g.strokePath();
|
||
g.fillStyle(0xeaff6e, 1); g.fillCircle(C - 74, 52, 12);
|
||
g.fillStyle(0xeaff6e, 0.25); g.fillCircle(C - 74, 52, 24);
|
||
g.fillStyle(0xf2f6f4, 1); g.fillCircle(C + 6, 96, 10);
|
||
g.fillStyle(0x18120a, 1); g.fillCircle(C + 8, 98, 5);
|
||
},
|
||
starfish({ g }) {
|
||
g.fillStyle(0xee7d68, 1);
|
||
g.fillPoints(starPts(C, C, 5, 80, 34), true);
|
||
g.fillStyle(0xf8c5ae, 0.9);
|
||
for (const { x, y } of starPts(C, C, 5, 48, 48)) g.fillCircle(x, y, 6);
|
||
g.fillCircle(C, C, 8);
|
||
},
|
||
shell({ g }) {
|
||
g.fillStyle(0xefb9a2, 1);
|
||
g.slice(C, 150, 86, Math.PI * 1.08, Math.PI * 1.92, false); g.fillPath();
|
||
g.lineStyle(5, 0xc4815f, 1);
|
||
for (let i = -2; i <= 2; i++) {
|
||
g.beginPath(); g.moveTo(C, 150); g.lineTo(C + i * 32, 70 + Math.abs(i) * 12); g.strokePath();
|
||
}
|
||
g.fillStyle(0xc4815f, 1); g.fillRoundedRect(C - 18, 146, 36, 18, 8);
|
||
},
|
||
coral({ g }) {
|
||
g.lineStyle(16, 0xd64f8e, 1);
|
||
g.beginPath(); g.moveTo(C, 170); g.lineTo(C, 110); g.lineTo(C - 30, 70); g.strokePath();
|
||
g.beginPath(); g.moveTo(C, 122); g.lineTo(C + 36, 80); g.lineTo(C + 36, 48); g.strokePath();
|
||
g.beginPath(); g.moveTo(C - 30, 70); g.lineTo(C - 54, 44); g.strokePath();
|
||
g.beginPath(); g.moveTo(C - 30, 70); g.lineTo(C - 6, 38); g.strokePath();
|
||
g.fillStyle(0xf08ab6, 1);
|
||
for (const [x, y] of [[C - 54, 44], [C - 6, 38], [C + 36, 48]]) g.fillCircle(x, y, 10);
|
||
},
|
||
bubble({ g }) {
|
||
g.fillStyle(0x6fc4e8, 0.25); g.fillCircle(C, C, 62);
|
||
g.lineStyle(7, 0x9fdcf2, 1); g.strokeCircle(C, C, 62);
|
||
g.fillStyle(0xffffff, 0.85); g.fillEllipse(C - 24, C - 26, 18, 12);
|
||
g.fillStyle(0x6fc4e8, 0.4); g.fillCircle(C + 38, C + 44, 12);
|
||
},
|
||
|
||
// ··· Dragon's Hoard ···
|
||
dragon({ g, text }) {
|
||
g.fillStyle(0xb83232, 1);
|
||
g.fillPoints(P([[C - 70, 96], [C - 4, 58], [C + 64, 76], [C + 64, 108], [C - 2, 132], [C - 36, 120]]), true);
|
||
g.fillPoints(P([[C - 4, 58], [C + 10, 24], [C + 30, 60]]), true); // horn
|
||
g.fillPoints(P([[C + 28, 60], [C + 52, 36], [C + 56, 70]]), true); // horn 2
|
||
g.fillStyle(0xe8483f, 1);
|
||
g.fillPoints(P([[C - 70, 96], [C - 36, 120], [C - 58, 128]]), true); // jaw
|
||
g.fillStyle(0xffd34d, 1); g.fillEllipse(C + 18, 84, 18, 12);
|
||
g.fillStyle(0x18120a, 1); g.fillEllipse(C + 18, 84, 5, 10);
|
||
text('WILD', { x: C, y: 168, size: 24, color: '#ff9d4d', font: 'Righteous' });
|
||
},
|
||
coin({ g }) {
|
||
g.fillStyle(0x8a5e12, 1); g.fillCircle(C, C, 70);
|
||
g.fillStyle(0xf0c441, 1); g.fillCircle(C, C - 4, 66);
|
||
g.lineStyle(5, 0xb8860b, 1); g.strokeCircle(C, C - 4, 50);
|
||
g.fillStyle(0xb8860b, 1);
|
||
g.fillPoints(starPts(C, C - 4, 4, 34, 12, 0), true);
|
||
g.fillStyle(0xfbe8a6, 0.9); g.fillEllipse(C - 26, C - 30, 16, 9);
|
||
},
|
||
knight({ g }) {
|
||
g.fillStyle(0x8d99ae, 1);
|
||
g.slice(C, 96, 52, Math.PI, Math.PI * 2, false); g.fillPath();
|
||
g.fillRect(C - 52, 94, 104, 38);
|
||
g.fillStyle(0x18120a, 1);
|
||
g.fillRect(C - 40, 100, 80, 9);
|
||
for (let i = -1; i <= 1; i++) g.fillRect(C + i * 22 - 3, 112, 6, 16);
|
||
g.fillStyle(0xe8483f, 1);
|
||
g.fillPoints(P([[C - 4, 44], [C + 4, 44], [C + 18, 14], [C - 18, 14]]), true);
|
||
},
|
||
castle({ g }) {
|
||
g.fillStyle(0x9aa3ad, 1);
|
||
g.fillRect(C - 64, 84, 128, 80);
|
||
g.fillRect(C - 78, 64, 30, 100); g.fillRect(C + 48, 64, 30, 100);
|
||
for (const x of [C - 78, C - 64, C - 24, C + 8, C + 48, C + 64]) g.fillRect(x, 48, 16, 18);
|
||
g.fillStyle(0x3b3f46, 1);
|
||
g.slice(C, 164, 26, Math.PI, Math.PI * 2, false); g.fillPath();
|
||
g.fillRect(C - 26, 162, 52, 4);
|
||
g.fillStyle(0x18120a, 0.8);
|
||
g.fillRect(C - 66, 96, 10, 16); g.fillRect(C + 56, 96, 10, 16);
|
||
},
|
||
chest({ g }) {
|
||
g.fillStyle(0x7a4a22, 1);
|
||
g.fillRoundedRect(C - 64, 92, 128, 70, 10);
|
||
g.slice(C, 92, 64, Math.PI, Math.PI * 2, false); g.fillPath();
|
||
g.fillStyle(0xf0c441, 1);
|
||
g.fillRect(C - 64, 86, 128, 12);
|
||
g.fillRect(C - 10, 40, 20, 122);
|
||
g.fillRoundedRect(C - 16, 100, 32, 30, 6);
|
||
g.fillStyle(0x18120a, 1); g.fillCircle(C, 112, 7);
|
||
g.fillStyle(0xfbe8a6, 1);
|
||
for (const [x, y] of [[C - 40, 38], [C + 38, 42], [C - 6, 26]]) g.fillCircle(x, y, 7);
|
||
},
|
||
gem({ g }) {
|
||
g.fillStyle(0x2e9e63, 1);
|
||
g.fillPoints(P([[C - 56, 78], [C - 28, 48], [C + 28, 48], [C + 56, 78], [C, 156]]), true);
|
||
g.lineStyle(4, 0x9ff2c6, 0.9);
|
||
g.strokePoints(P([[C - 56, 78], [C + 56, 78]]), false);
|
||
g.strokePoints(P([[C - 28, 48], [C - 18, 78], [C, 156]]), false);
|
||
g.strokePoints(P([[C + 28, 48], [C + 18, 78], [C, 156]]), false);
|
||
},
|
||
|
||
// ··· Gold Rush Gulch ···
|
||
bandit({ g, text }) {
|
||
g.fillStyle(0x2b211a, 1);
|
||
g.fillEllipse(C, 70, 124, 20);
|
||
g.fillRoundedRect(C - 38, 30, 76, 44, 14);
|
||
g.fillStyle(0xd9b48a, 1);
|
||
g.slice(C, 76, 38, 0, Math.PI, false); g.fillPath();
|
||
g.fillStyle(0xb8392e, 1);
|
||
g.fillPoints(P([[C - 38, 92], [C + 38, 92], [C + 20, 146], [C - 20, 146]]), true);
|
||
g.fillStyle(0xf2e3c8, 1);
|
||
for (const [x, y] of [[C - 16, 104], [C + 14, 104], [C - 2, 122]]) g.fillCircle(x, y, 4);
|
||
g.fillStyle(0x18120a, 1);
|
||
g.fillEllipse(C - 15, 82, 12, 7); g.fillEllipse(C + 15, 82, 12, 7);
|
||
text('WILD', { x: C, y: 176, size: 24, color: '#ffe27a', font: 'Righteous' });
|
||
},
|
||
dynamite({ g, text }) {
|
||
g.fillStyle(0xb83232, 1);
|
||
for (const dx of [-30, 0, 30]) g.fillRoundedRect(C + dx - 13, 64, 26, 96, 9);
|
||
g.lineStyle(4, 0x7a1c1c, 1);
|
||
for (const dx of [-30, 0, 30]) g.strokeRoundedRect(C + dx - 13, 64, 26, 96, 9);
|
||
g.lineStyle(5, 0xc8a84b, 1);
|
||
g.beginPath(); g.moveTo(C, 64); g.arc(C + 22, 44, 22, Math.PI, Math.PI * 2.4); g.strokePath();
|
||
g.fillStyle(0xffe27a, 1);
|
||
g.fillPoints(starPts(C + 44, 36, 4, 16, 6, 0), true);
|
||
text('BONUS', { x: C, y: 182, size: 20, color: '#ffe27a', font: 'Righteous' });
|
||
},
|
||
sheriff({ g }) {
|
||
g.fillStyle(0xf0c441, 1);
|
||
g.fillPoints(starPts(C, C, 6, 78, 46), true);
|
||
for (const { x, y } of starPts(C, C, 6, 78, 78)) g.fillCircle(x, y, 9);
|
||
g.lineStyle(5, 0xa87f17, 1); g.strokeCircle(C, C, 34);
|
||
},
|
||
saloon({ g }) {
|
||
g.fillStyle(0x6e4c24, 1);
|
||
g.fillRect(C - 70, 44, 140, 22);
|
||
g.fillStyle(0x8a6132, 1);
|
||
for (const side of [-1, 1]) {
|
||
g.fillRoundedRect(C + (side === -1 ? -62 : 10), 78, 52, 84, 8);
|
||
}
|
||
g.fillStyle(0x55391a, 1);
|
||
for (const side of [-1, 1]) {
|
||
const x = C + (side === -1 ? -54 : 18);
|
||
g.fillRoundedRect(x, 88, 36, 28, 5);
|
||
g.fillRoundedRect(x, 124, 36, 28, 5);
|
||
}
|
||
g.lineStyle(6, 0x55391a, 1);
|
||
g.beginPath(); g.moveTo(C - 78, 66); g.lineTo(C - 78, 170); g.strokePath();
|
||
g.beginPath(); g.moveTo(C + 78, 66); g.lineTo(C + 78, 170); g.strokePath();
|
||
},
|
||
wagon({ g }) {
|
||
g.fillStyle(0x5d4324, 1);
|
||
g.fillPoints(P([[C - 68, 78], [C + 68, 78], [C + 46, 138], [C - 46, 138]]), true);
|
||
g.fillStyle(0xf0c441, 1);
|
||
for (const [x, y] of [[C - 30, 72], [C, 64], [C + 30, 72], [C - 14, 60], [C + 16, 58]]) g.fillCircle(x, y, 13);
|
||
g.fillStyle(0x2b211a, 1);
|
||
g.fillCircle(C - 34, 152, 16); g.fillCircle(C + 34, 152, 16);
|
||
g.fillStyle(0x9aa3ad, 1);
|
||
g.fillCircle(C - 34, 152, 6); g.fillCircle(C + 34, 152, 6);
|
||
},
|
||
pickaxe({ g }) {
|
||
g.lineStyle(14, 0x8a6132, 1);
|
||
g.beginPath(); g.moveTo(C - 36, 160); g.lineTo(C + 36, 48); g.strokePath();
|
||
g.lineStyle(18, 0x9aa3ad, 1);
|
||
g.beginPath(); g.arc(C + 12, 92, 62, Math.PI * 1.25, Math.PI * 1.95); g.strokePath();
|
||
},
|
||
|
||
// ··· Sugar Spin ···
|
||
rainbow({ g }) {
|
||
const cols = [0xe8483f, 0xf0a541, 0x57c46a, 0x4f8fe0, 0x8857b8];
|
||
cols.forEach((col, i) => {
|
||
g.lineStyle(13, col, 1);
|
||
g.beginPath(); g.arc(C, 128, 78 - i * 13, Math.PI, Math.PI * 2); g.strokePath();
|
||
});
|
||
g.fillStyle(0xffffff, 1);
|
||
g.fillPoints(starPts(C - 58, 146, 4, 16, 6, 0), true);
|
||
g.fillPoints(starPts(C + 58, 146, 4, 16, 6, 0), true);
|
||
},
|
||
chocolate({ g }) {
|
||
g.fillStyle(0x5d3a1e, 1);
|
||
g.fillRoundedRect(C - 58, C - 58, 116, 116, 14);
|
||
g.fillStyle(0x7a4f2a, 1);
|
||
for (const dx of [-29, 29]) for (const dy of [-29, 29]) {
|
||
g.fillRoundedRect(C + dx - 22, C + dy - 22, 44, 44, 8);
|
||
}
|
||
g.fillStyle(0xa5764a, 0.7); g.fillRoundedRect(C - 51, C - 51, 30, 14, 7);
|
||
},
|
||
'candy-cane': ({ g }) => {
|
||
g.lineStyle(30, 0xf2ece2, 1);
|
||
g.beginPath(); g.moveTo(C - 18, 168); g.lineTo(C - 18, 78); g.arc(C + 12, 78, 30, Math.PI, 0); g.lineTo(C + 42, 100); g.strokePath();
|
||
g.lineStyle(30, 0xd4313f, 1);
|
||
for (const [y1, y2] of [[150, 130], [110, 92]]) {
|
||
g.beginPath(); g.moveTo(C - 18, y1); g.lineTo(C - 18, y2); g.strokePath();
|
||
}
|
||
g.beginPath(); g.arc(C + 12, 78, 30, Math.PI * 1.4, Math.PI * 1.75); g.strokePath();
|
||
},
|
||
gummy({ g }) {
|
||
g.fillStyle(0x57c46a, 0.92);
|
||
g.fillCircle(C - 26, 70, 16); g.fillCircle(C + 26, 70, 16);
|
||
g.fillCircle(C, 84, 36);
|
||
g.fillEllipse(C, 138, 76, 64);
|
||
g.fillEllipse(C - 38, 124, 22, 36); g.fillEllipse(C + 38, 124, 22, 36);
|
||
g.fillStyle(0xa6e8b2, 0.8); g.fillCircle(C - 12, 74, 9);
|
||
g.fillStyle(0x2c7a3c, 1); g.fillCircle(C - 12, 86, 5); g.fillCircle(C + 12, 86, 5);
|
||
},
|
||
lollipop({ g }) {
|
||
g.fillStyle(0xf2ece2, 1); g.fillRoundedRect(C - 6, C, 12, 84, 6);
|
||
g.fillStyle(0xe06a9f, 1); g.fillCircle(C, 76, 56);
|
||
g.lineStyle(9, 0xf2ece2, 1);
|
||
g.beginPath(); g.arc(C, 76, 40, 0, Math.PI * 1.4); g.strokePath();
|
||
g.beginPath(); g.arc(C, 76, 22, Math.PI, Math.PI * 2.4); g.strokePath();
|
||
g.fillStyle(0xf2ece2, 1); g.fillCircle(C, 76, 7);
|
||
},
|
||
jelly({ g }) {
|
||
g.fillStyle(0xd4313f, 0.95);
|
||
g.slice(C, 132, 62, Math.PI, Math.PI * 2, false); g.fillPath();
|
||
g.fillEllipse(C, 132, 124, 26);
|
||
g.fillStyle(0xf2ece2, 1); g.fillEllipse(C, 148, 150, 18);
|
||
g.fillStyle(0xf6979f, 0.85); g.fillEllipse(C - 22, 96, 18, 12);
|
||
},
|
||
mint({ g }) {
|
||
for (let i = 0; i < 8; i++) {
|
||
g.fillStyle(i % 2 === 0 ? 0x4fb877 : 0xf2ece2, 1);
|
||
g.slice(C, C, 64, (i * Math.PI) / 4, ((i + 1) * Math.PI) / 4, false);
|
||
g.fillPath();
|
||
}
|
||
g.lineStyle(5, 0x2c7a3c, 1); g.strokeCircle(C, C, 64);
|
||
},
|
||
berry({ g }) {
|
||
g.fillStyle(0xb83a6e, 1);
|
||
const cells = [[0, -22], [-22, -8], [22, -8], [-30, 16], [0, 8], [30, 16], [-14, 38], [14, 38], [0, 58]];
|
||
for (const [dx, dy] of cells) g.fillCircle(C + dx, C + dy - 4, 17);
|
||
g.fillStyle(0xe87fa8, 0.8);
|
||
for (const [dx, dy] of cells.slice(0, 4)) g.fillCircle(C + dx - 4, C + dy - 8, 5);
|
||
g.fillStyle(0x4f9a34, 1);
|
||
g.fillEllipse(C - 14, C - 44, 30, 13); g.fillEllipse(C + 14, C - 44, 30, 13);
|
||
},
|
||
sugar({ g }) {
|
||
g.fillStyle(0xf6f3ea, 1);
|
||
g.fillPoints(P([[C - 44, 70], [C + 18, 56], [C + 52, 84], [C - 10, 100]]), true); // top
|
||
g.fillStyle(0xddd6c4, 1);
|
||
g.fillPoints(P([[C - 44, 70], [C - 10, 100], [C - 10, 158], [C - 44, 128]]), true); // left
|
||
g.fillStyle(0xc9c0aa, 1);
|
||
g.fillPoints(P([[C - 10, 100], [C + 52, 84], [C + 52, 142], [C - 10, 158]]), true); // right
|
||
g.fillStyle(0xffffff, 1);
|
||
g.fillPoints(starPts(C + 56, 52, 4, 14, 5, 0), true);
|
||
},
|
||
};
|
||
|
||
function cardLetter(letter) {
|
||
return ({ g, text, theme }) => {
|
||
g.lineStyle(6, theme.trim, 0.9);
|
||
g.strokeRoundedRect(C - 52, C - 64, 104, 128, 16);
|
||
text(letter, { x: C, y: C, size: 96, color: theme.accentHex, font: 'Righteous' });
|
||
};
|
||
}
|
||
|
||
function seven(colorHex) {
|
||
return ({ text }) => {
|
||
text('7', {
|
||
x: C, y: C, size: 150, color: colorHex, font: 'Righteous',
|
||
stroke: '#14100a', strokeThickness: 10,
|
||
});
|
||
};
|
||
}
|
||
|
||
// ── Baking ────────────────────────────────────────────────────────────────────
|
||
export function bakeMachineSymbols(scene, machine) {
|
||
for (const id of allSymbolIds(machine)) {
|
||
const key = bakedKey(machine.id, id);
|
||
if (scene.textures.exists(key)) continue;
|
||
const g = scene.make.graphics({ add: false });
|
||
const extras = [];
|
||
const text = (str, { x = C, y = C, size = 32, color = '#ffffff', font = 'Righteous', stroke, strokeThickness } = {}) => {
|
||
const style = { fontFamily: font, fontSize: `${size}px`, color };
|
||
if (stroke) { style.stroke = stroke; style.strokeThickness = strokeThickness ?? 6; }
|
||
const t = scene.make.text({ x, y, text: str, style, add: false }).setOrigin(0.5);
|
||
extras.push(t);
|
||
return t;
|
||
};
|
||
const painter = PAINTERS[`${machine.id}/${id}`] ?? PAINTERS[id] ?? cardLetter('?');
|
||
painter({ g, text, theme: machine.theme });
|
||
const rt = scene.make.renderTexture({ width: S, height: S, add: false });
|
||
rt.draw(g, 0, 0);
|
||
for (const t of extras) rt.draw(t, t.x, t.y);
|
||
rt.saveTexture(key);
|
||
g.destroy();
|
||
for (const t of extras) t.destroy();
|
||
}
|
||
}
|
||
|
||
// Small particle textures shared by all machines.
|
||
export function bakeParticleTextures(scene) {
|
||
if (!scene.textures.exists('slots-coin-p')) {
|
||
const g = scene.make.graphics({ add: false });
|
||
g.fillStyle(0x8a5e12, 1); g.fillCircle(14, 14, 13);
|
||
g.fillStyle(0xf0c441, 1); g.fillCircle(14, 13, 11);
|
||
g.fillStyle(0xfbe8a6, 0.95); g.fillEllipse(10, 9, 7, 4);
|
||
g.generateTexture('slots-coin-p', 28, 28);
|
||
g.destroy();
|
||
}
|
||
if (!scene.textures.exists('slots-spark')) {
|
||
const g = scene.make.graphics({ add: false });
|
||
g.fillStyle(0xffffff, 1); g.fillCircle(5, 5, 5);
|
||
g.generateTexture('slots-spark', 10, 10);
|
||
g.destroy();
|
||
}
|
||
}
|