// 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(); } }