import { Container, Graphics, Text, TextStyle } from "pixi.js"; import type { SymbolStyle } from "@spinza/game-core/client"; /** * Procedural symbol artwork. Every Spinza symbol is drawn from primitives — * no external assets — so each game keeps an original look while sharing * one renderer. `size` is the tile edge in px; art is centred on (0,0). */ function hex(c: string): number { return parseInt(c.replace("#", ""), 16); } function lighten(c: string, amt: number): number { const n = hex(c); const r = Math.min(255, ((n >> 16) & 255) + Math.round(255 * amt)); const g = Math.min(255, ((n >> 8) & 255) + Math.round(255 * amt)); const b = Math.min(255, (n & 255) + Math.round(255 * amt)); return (r << 16) | (g << 8) | b; } function darken(c: string, amt: number): number { const n = hex(c); const r = Math.max(0, ((n >> 16) & 255) - Math.round(255 * amt)); const g = Math.max(0, ((n >> 8) & 255) - Math.round(255 * amt)); const b = Math.max(0, (n & 255) - Math.round(255 * amt)); return (r << 16) | (g << 8) | b; } function polygon(g: Graphics, cx: number, cy: number, r: number, sides: number, rot = -Math.PI / 2): Graphics { const pts: number[] = []; for (let i = 0; i < sides; i++) { const a = rot + (i * Math.PI * 2) / sides; pts.push(cx + Math.cos(a) * r, cy + Math.sin(a) * r); } return g.poly(pts); } function star(g: Graphics, cx: number, cy: number, outer: number, inner: number, points = 5): Graphics { const pts: number[] = []; for (let i = 0; i < points * 2; i++) { const r = i % 2 === 0 ? outer : inner; const a = -Math.PI / 2 + (i * Math.PI) / points; pts.push(cx + Math.cos(a) * r, cy + Math.sin(a) * r); } return g.poly(pts); } export interface SymbolArtOptions { size: number; /** Tier drives frame richness. */ tier: "low" | "mid" | "high" | "premium" | "special"; kind: string; /** Game frame preset — drives the tile plate look so every game has its own tiles. */ frame?: "metal" | "glass" | "gold" | "stone" | "ice" | "carbon" | "neon" | "obsidian"; } /** Tile plate behind the icon, styled per game frame preset. */ function drawPlate(size: number, style: SymbolStyle, opts: SymbolArtOptions): Graphics { const plate = new Graphics(); const pad = size * 0.06; const x = -size / 2 + pad; const w = size - pad * 2; const premium = opts.tier === "premium" || opts.tier === "special"; const main = hex(style.color); const accent = style.accent ? hex(style.accent) : lighten(style.color, 0.35); const line = Math.max(1, size * 0.012); switch (opts.frame) { case "glass": plate.roundRect(x, x, w, w, size * 0.2).fill({ color: 0xffffff, alpha: 0.07 }); plate.roundRect(x, x, w, w, size * 0.2).stroke({ color: 0xffffff, alpha: premium ? 0.5 : 0.22, width: line }); plate.roundRect(x + w * 0.08, x + w * 0.06, w * 0.84, w * 0.28, size * 0.12).fill({ color: 0xffffff, alpha: 0.06 }); break; case "gold": plate.roundRect(x, x, w, w, size * 0.1).fill({ color: 0x14100a, alpha: 0.8 }); plate.roundRect(x, x, w, w, size * 0.1).stroke({ color: premium ? 0xf3e2ad : 0xa88a4a, alpha: premium ? 0.9 : 0.5, width: line * 1.6 }); plate.roundRect(x + line * 3, x + line * 3, w - line * 6, w - line * 6, size * 0.08).stroke({ color: 0xc9a961, alpha: 0.25, width: line }); for (const [cx, cy] of [[x + size * 0.09, x + size * 0.09], [x + w - size * 0.09, x + size * 0.09], [x + size * 0.09, x + w - size * 0.09], [x + w - size * 0.09, x + w - size * 0.09]]) plate.circle(cx, cy, size * 0.018).fill({ color: 0xf3e2ad, alpha: 0.8 }); break; case "stone": plate.roundRect(x, x, w, w, size * 0.06).fill({ color: 0x1a1712, alpha: 0.85 }); plate.roundRect(x, x, w, w, size * 0.06).stroke({ color: premium ? accent : 0x7a6f5a, alpha: premium ? 0.7 : 0.45, width: line * 1.4 }); plate.moveTo(x + w * 0.15, x).lineTo(x + w * 0.22, x + w * 0.12).stroke({ color: 0x000000, alpha: 0.5, width: line }); plate.moveTo(x + w, x + w * 0.7).lineTo(x + w * 0.85, x + w * 0.78).stroke({ color: 0x000000, alpha: 0.5, width: line }); break; case "ice": plate.roundRect(x, x, w, w, size * 0.18).fill({ color: 0x9fdcff, alpha: 0.1 }); plate.roundRect(x, x, w, w, size * 0.18).stroke({ color: 0xdff6ff, alpha: premium ? 0.8 : 0.35, width: line }); plate.moveTo(x + w * 0.1, x + w * 0.9).lineTo(x + w * 0.35, x + w * 0.62).stroke({ color: 0xffffff, alpha: 0.35, width: line }); plate.moveTo(x + w * 0.16, x + w * 0.9).lineTo(x + w * 0.4, x + w * 0.66).stroke({ color: 0xffffff, alpha: 0.2, width: line }); break; case "carbon": plate.roundRect(x, x, w, w, size * 0.08).fill({ color: 0x0a0b0e, alpha: 0.92 }); for (let i = 0; i < 6; i++) plate.moveTo(x, x + (i / 6) * w).lineTo(x + w, x + (i / 6) * w).stroke({ color: 0xffffff, alpha: 0.035, width: line }); plate.roundRect(x, x, w, w, size * 0.08).stroke({ color: premium ? accent : 0x5c6270, alpha: premium ? 0.8 : 0.45, width: line * 1.3 }); break; case "neon": plate.roundRect(x, x, w, w, size * 0.16).fill({ color: 0x05060c, alpha: 0.75 }); plate.roundRect(x, x, w, w, size * 0.16).stroke({ color: main, alpha: premium ? 0.9 : 0.45, width: line * 1.4 }); plate.roundRect(x - line * 2, x - line * 2, w + line * 4, w + line * 4, size * 0.17).stroke({ color: main, alpha: premium ? 0.25 : 0.1, width: line * 4 }); break; case "obsidian": plate.roundRect(x, x, w, w, size * 0.04).fill({ color: 0x000000, alpha: 0.9 }); plate.roundRect(x, x, w, w, size * 0.04).stroke({ color: premium ? 0xe8cf8f : 0xffffff, alpha: premium ? 0.7 : 0.12, width: line }); break; case "metal": default: plate.roundRect(x, x, w, w, size * 0.14).fill({ color: 0x0b0d14, alpha: 0.75 }); plate.roundRect(x, x, w, w, size * 0.14).stroke({ color: premium ? accent : 0x9aa3b5, alpha: premium ? 0.6 : 0.2, width: line }); plate.roundRect(x + line * 2, x + line * 2, w - line * 4, w * 0.5, size * 0.1).fill({ color: 0xffffff, alpha: 0.025 }); if (opts.frame === "metal") for (const [cx, cy] of [[x + size * 0.07, x + size * 0.07], [x + w - size * 0.07, x + size * 0.07], [x + size * 0.07, x + w - size * 0.07], [x + w - size * 0.07, x + w - size * 0.07]]) plate.circle(cx, cy, size * 0.014).fill({ color: 0xc8d0dc, alpha: 0.5 }); break; } return plate; } export function drawSymbol(style: SymbolStyle, opts: SymbolArtOptions): Container { const { size } = opts; const c = new Container(); const main = hex(style.color); const accent = style.accent ? hex(style.accent) : lighten(style.color, 0.35); const glow = style.glow ?? 0.3; const r = size * 0.36; // icon radius const g = new Graphics(); // Tile plate (per game frame preset). c.addChild(drawPlate(size, style, opts)); // Glow halo. if (glow > 0) { const halo = new Graphics(); halo.circle(0, 0, r * 1.25).fill({ color: main, alpha: 0.18 * glow }); halo.circle(0, 0, r * 1.0).fill({ color: main, alpha: 0.16 * glow }); c.addChild(halo); } switch (style.shape) { case "gem": { g.poly([0, -r, r * 0.85, -r * 0.25, r * 0.55, r, -r * 0.55, r, -r * 0.85, -r * 0.25]).fill(main); g.poly([0, -r, r * 0.85, -r * 0.25, 0, -r * 0.1, -r * 0.85, -r * 0.25]).fill({ color: accent, alpha: 0.85 }); g.poly([0, -r * 0.1, r * 0.85, -r * 0.25, r * 0.55, r]).fill({ color: darken(style.color, 0.15) }); g.poly([-r * 0.2, -r * 0.7, r * 0.15, -r * 0.55, -r * 0.35, -r * 0.35]).fill({ color: 0xffffff, alpha: 0.65 }); break; } case "hex": { polygon(g, 0, 0, r, 6).fill(main); polygon(g, 0, 0, r * 0.68, 6).fill({ color: darken(style.color, 0.25) }); polygon(g, 0, 0, r * 0.42, 6).fill(accent); if (style.label) c.addChild(label(style.label, size * 0.26, 0x0b0d14)); break; } case "star": { star(g, 0, 0, r, r * 0.45).fill(main); star(g, 0, 0, r * 0.55, r * 0.25).fill({ color: 0xffffff, alpha: 0.85 }); break; } case "circle": { g.circle(0, 0, r).fill(main); g.circle(-r * 0.3, -r * 0.3, r * 0.35).fill({ color: 0xffffff, alpha: 0.35 }); g.circle(0, 0, r).stroke({ color: accent, width: size * 0.02 }); break; } case "diamond": { g.poly([0, -r, r, 0, 0, r, -r, 0]).fill(main); g.poly([0, -r, r, 0, 0, 0]).fill({ color: accent, alpha: 0.8 }); g.poly([0, 0, 0, r, -r, 0]).fill({ color: darken(style.color, 0.2) }); break; } case "shield": { g.poly([0, -r, r * 0.85, -r * 0.65, r * 0.75, r * 0.15, 0, r, -r * 0.75, r * 0.15, -r * 0.85, -r * 0.65]).fill(main); g.poly([0, -r * 0.7, r * 0.5, -r * 0.5, r * 0.45, r * 0.05, 0, r * 0.6, -r * 0.45, r * 0.05, -r * 0.5, -r * 0.5]).fill({ color: darken(style.color, 0.28) }); g.rect(-r * 0.08, -r * 0.5, r * 0.16, r * 0.9).fill(accent); g.rect(-r * 0.38, -r * 0.25, r * 0.76, r * 0.16).fill(accent); break; } case "bolt": { g.poly([r * 0.15, -r, -r * 0.55, r * 0.1, -r * 0.05, r * 0.1, -r * 0.25, r, r * 0.6, -r * 0.15, r * 0.05, -r * 0.15]).fill(main); g.poly([r * 0.1, -r * 0.8, -r * 0.3, r * 0.0, r * 0.0, 0]).fill({ color: 0xffffff, alpha: 0.5 }); break; } case "ring": { g.circle(0, 0, r).fill(main); g.circle(0, 0, r * 0.62).cut(); g.circle(0, 0, r * 0.62).stroke({ color: accent, width: size * 0.02, alpha: 0.9 }); g.arc(0, 0, r * 0.81, -Math.PI * 0.9, -Math.PI * 0.3).stroke({ color: 0xffffff, width: size * 0.04, alpha: 0.55 }); break; } case "chip": { g.roundRect(-r, -r * 0.75, r * 2, r * 1.5, r * 0.25).fill(main); g.roundRect(-r * 0.75, -r * 0.5, r * 1.5, r, r * 0.15).fill({ color: darken(style.color, 0.35) }); for (let i = -2; i <= 2; i++) { g.rect(-r - r * 0.18, i * r * 0.28 - r * 0.06, r * 0.18, r * 0.12).fill(accent); g.rect(r, i * r * 0.28 - r * 0.06, r * 0.18, r * 0.12).fill(accent); } if (style.label) c.addChild(label(style.label, size * 0.22, accent)); break; } case "crystal": { g.poly([0, -r, r * 0.5, -r * 0.3, r * 0.35, r * 0.8, -r * 0.35, r * 0.8, -r * 0.5, -r * 0.3]).fill(main); g.poly([0, -r, r * 0.5, -r * 0.3, 0, r * 0.1]).fill({ color: accent, alpha: 0.75 }); g.poly([0, r * 0.1, r * 0.35, r * 0.8, -r * 0.35, r * 0.8]).fill({ color: darken(style.color, 0.2) }); break; } case "letter": { g.roundRect(-r * 0.95, -r * 0.95, r * 1.9, r * 1.9, r * 0.3).fill({ color: darken(style.color, 0.45), alpha: 0.9 }); g.roundRect(-r * 0.95, -r * 0.95, r * 1.9, r * 1.9, r * 0.3).stroke({ color: main, width: size * 0.025 }); c.addChild(label(style.label ?? "?", size * 0.4, main, true)); break; } case "vault": { g.circle(0, 0, r).fill(darken(style.color, 0.4)); g.circle(0, 0, r).stroke({ color: main, width: size * 0.03 }); g.circle(0, 0, r * 0.7).stroke({ color: main, width: size * 0.02, alpha: 0.7 }); for (let i = 0; i < 6; i++) { const a = (i * Math.PI) / 3; g.moveTo(Math.cos(a) * r * 0.25, Math.sin(a) * r * 0.25).lineTo(Math.cos(a) * r * 0.62, Math.sin(a) * r * 0.62).stroke({ color: accent, width: size * 0.035 }); } g.circle(0, 0, r * 0.22).fill(accent); break; } case "orb": { g.circle(0, 0, r).fill(main); g.circle(0, 0, r * 0.75).fill({ color: accent, alpha: 0.35 }); g.circle(-r * 0.25, -r * 0.3, r * 0.25).fill({ color: 0xffffff, alpha: 0.5 }); g.circle(0, 0, r).stroke({ color: accent, width: size * 0.02, alpha: 0.9 }); if (style.label) c.addChild(label(style.label, size * 0.36, accent, true)); break; } case "coin": { g.circle(0, 0, r).fill(main); g.circle(0, 0, r * 0.8).stroke({ color: darken(style.color, 0.25), width: size * 0.03 }); g.circle(0, 0, r).stroke({ color: accent, width: size * 0.02 }); c.addChild(label(style.label ?? "SC", size * 0.24, darken(style.color, 0.5), true)); break; } case "skull": { g.circle(0, -r * 0.15, r * 0.8).fill(main); g.roundRect(-r * 0.45, r * 0.3, r * 0.9, r * 0.55, r * 0.15).fill(main); g.circle(-r * 0.3, -r * 0.2, r * 0.22).fill(0x0b0d14); g.circle(r * 0.3, -r * 0.2, r * 0.22).fill(0x0b0d14); g.poly([0, r * 0.05, r * 0.12, r * 0.3, -r * 0.12, r * 0.3]).fill(0x0b0d14); break; } case "flame": { g.poly([0, -r, r * 0.55, -r * 0.2, r * 0.7, r * 0.45, r * 0.3, r, -r * 0.3, r, -r * 0.7, r * 0.45, -r * 0.55, -r * 0.2]).fill(main); g.poly([0, -r * 0.4, r * 0.3, r * 0.1, r * 0.25, r * 0.6, -r * 0.25, r * 0.6, -r * 0.3, r * 0.1]).fill(accent); break; } case "snow": { for (let i = 0; i < 3; i++) { const a = (i * Math.PI) / 3; g.moveTo(Math.cos(a) * r, Math.sin(a) * r).lineTo(-Math.cos(a) * r, -Math.sin(a) * r).stroke({ color: main, width: size * 0.035 }); for (const s of [1, -1]) { const bx = Math.cos(a) * r * 0.6 * s; const by = Math.sin(a) * r * 0.6 * s; g.moveTo(bx, by).lineTo(bx + Math.cos(a + 0.6) * r * 0.3 * s, by + Math.sin(a + 0.6) * r * 0.3 * s).stroke({ color: accent, width: size * 0.025 }); g.moveTo(bx, by).lineTo(bx + Math.cos(a - 0.6) * r * 0.3 * s, by + Math.sin(a - 0.6) * r * 0.3 * s).stroke({ color: accent, width: size * 0.025 }); } } g.circle(0, 0, r * 0.14).fill(0xffffff); break; } case "eye": { g.ellipse(0, 0, r, r * 0.6).fill(main); g.circle(0, 0, r * 0.42).fill(darken(style.color, 0.5)); g.circle(0, 0, r * 0.2).fill(accent); g.circle(r * 0.12, -r * 0.14, r * 0.07).fill(0xffffff); break; } case "moon": { g.circle(0, 0, r).fill(main); g.circle(r * 0.45, -r * 0.2, r * 0.78).fill({ color: 0x0b0d14, alpha: 0.85 }); g.circle(-r * 0.4, r * 0.3, r * 0.12).fill({ color: darken(style.color, 0.2) }); break; } case "leaf": { g.moveTo(0, r).bezierCurveTo(-r * 1.1, r * 0.2, -r * 0.6, -r * 0.9, 0, -r).bezierCurveTo(r * 0.6, -r * 0.9, r * 1.1, r * 0.2, 0, r).fill(main); g.moveTo(0, r * 0.9).lineTo(0, -r * 0.8).stroke({ color: accent, width: size * 0.02 }); break; } case "atom": { g.circle(0, 0, r * 0.2).fill(accent); for (let i = 0; i < 3; i++) { const e = new Graphics(); e.ellipse(0, 0, r, r * 0.38).stroke({ color: main, width: size * 0.025 }); e.rotation = (i * Math.PI) / 3; c.addChild(e); } break; } case "crown": { g.poly([-r, r * 0.6, -r, -r * 0.4, -r * 0.5, r * 0.05, 0, -r, r * 0.5, r * 0.05, r, -r * 0.4, r, r * 0.6]).fill(main); g.rect(-r, r * 0.4, r * 2, r * 0.25).fill(darken(style.color, 0.3)); g.circle(0, -r * 0.95, r * 0.12).fill(accent); g.circle(-r, -r * 0.4, r * 0.1).fill(accent); g.circle(r, -r * 0.4, r * 0.1).fill(accent); break; } case "wave": { for (let k = -1; k <= 1; k++) { const y = k * r * 0.5; g.moveTo(-r, y).bezierCurveTo(-r * 0.5, y - r * 0.4, -r * 0.2, y + r * 0.4, 0, y).bezierCurveTo(r * 0.2, y - r * 0.4, r * 0.5, y + r * 0.4, r, y).stroke({ color: k === 0 ? accent : main, width: size * 0.04, alpha: k === 0 ? 1 : 0.8 }); } break; } case "cube": { g.poly([0, -r, r * 0.87, -r * 0.5, r * 0.87, r * 0.5, 0, r, -r * 0.87, r * 0.5, -r * 0.87, -r * 0.5]).fill(main); g.poly([0, -r, r * 0.87, -r * 0.5, 0, 0, -r * 0.87, -r * 0.5]).fill(accent); g.poly([0, 0, r * 0.87, -r * 0.5, r * 0.87, r * 0.5, 0, r]).fill(darken(style.color, 0.3)); break; } /* ----------------------------------------------------------- themed set */ case "lock": { g.roundRect(-r * 0.75, -r * 0.1, r * 1.5, r * 1.1, r * 0.2).fill(main); g.arc(0, -r * 0.15, r * 0.5, Math.PI, 0).stroke({ color: accent, width: size * 0.05 }); g.circle(0, r * 0.35, r * 0.16).fill(0x0b0d14); g.rect(-r * 0.06, r * 0.35, r * 0.12, r * 0.35).fill(0x0b0d14); break; } case "key": { g.circle(-r * 0.45, -r * 0.35, r * 0.42).stroke({ color: main, width: size * 0.06 }); g.moveTo(-r * 0.15, -r * 0.05).lineTo(r * 0.85, r * 0.95).stroke({ color: main, width: size * 0.06 }); g.moveTo(r * 0.45, r * 0.55).lineTo(r * 0.7, r * 0.3).stroke({ color: accent, width: size * 0.05 }); g.moveTo(r * 0.65, r * 0.75).lineTo(r * 0.9, r * 0.5).stroke({ color: accent, width: size * 0.05 }); break; } case "code": { g.roundRect(-r, -r * 0.7, r * 2, r * 1.4, r * 0.15).fill({ color: darken(style.color, 0.45), alpha: 0.95 }); g.roundRect(-r, -r * 0.7, r * 2, r * 1.4, r * 0.15).stroke({ color: main, width: size * 0.02 }); c.addChild(label(style.label ?? "", size * 0.26, main, true)); break; } case "badge": { g.roundRect(-r * 0.7, -r, r * 1.4, r * 2, r * 0.18).fill(main); g.circle(0, -r * 0.4, r * 0.28).fill(0x0b0d14); g.roundRect(-r * 0.45, r * 0.05, r * 0.9, r * 0.14, r * 0.05).fill(accent); g.roundRect(-r * 0.45, r * 0.35, r * 0.6, r * 0.14, r * 0.05).fill(accent); break; } case "mask": { g.ellipse(0, 0, r, r * 0.6).fill(main); g.ellipse(-r * 0.4, -r * 0.05, r * 0.28, r * 0.18).fill(0x0b0d14); g.ellipse(r * 0.4, -r * 0.05, r * 0.28, r * 0.18).fill(0x0b0d14); g.moveTo(-r * 0.95, -r * 0.1).lineTo(-r * 1.3, -r * 0.6).stroke({ color: accent, width: size * 0.025 }); g.moveTo(r * 0.95, -r * 0.1).lineTo(r * 1.3, -r * 0.6).stroke({ color: accent, width: size * 0.025 }); break; } case "glove": { g.roundRect(-r * 0.55, -r * 0.2, r * 1.1, r * 1.1, r * 0.25).fill(main); for (let i = 0; i < 4; i++) g.roundRect(-r * 0.55 + i * r * 0.28, -r, r * 0.22, r * 0.9, r * 0.1).fill(main); g.roundRect(-r * 0.9, -r * 0.1, r * 0.35, r * 0.7, r * 0.15).fill(main); g.roundRect(-r * 0.55, r * 0.6, r * 1.1, r * 0.3, r * 0.08).fill(accent); break; } case "rope": { g.circle(0, 0, r * 0.75).stroke({ color: main, width: size * 0.09 }); g.circle(0, 0, r * 0.75).stroke({ color: accent, width: size * 0.02, alpha: 0.6 }); for (let i = 0; i < 10; i++) { const a = (i / 10) * Math.PI * 2; g.moveTo(Math.cos(a) * r * 0.62, Math.sin(a) * r * 0.62).lineTo(Math.cos(a + 0.3) * r * 0.88, Math.sin(a + 0.3) * r * 0.88).stroke({ color: darken(style.color, 0.3), width: size * 0.02 }); } break; } case "bag": { g.moveTo(-r * 0.75, r * 0.9).bezierCurveTo(-r * 1.1, -r * 0.3, -r * 0.3, -r * 0.6, 0, -r * 0.55).bezierCurveTo(r * 0.3, -r * 0.6, r * 1.1, -r * 0.3, r * 0.75, r * 0.9).lineTo(-r * 0.75, r * 0.9).fill(main); g.roundRect(-r * 0.3, -r * 0.95, r * 0.6, r * 0.35, r * 0.1).fill(accent); g.moveTo(-r * 0.35, r * 0.3).lineTo(r * 0.35, r * 0.3).stroke({ color: darken(style.color, 0.3), width: size * 0.03 }); break; } case "lantern": { g.roundRect(-r * 0.7, -r * 0.6, r * 1.4, r * 1.2, r * 0.45).fill(main); g.rect(-r * 0.35, -r * 0.9, r * 0.7, r * 0.2).fill(accent); g.rect(-r * 0.35, r * 0.6, r * 0.7, r * 0.2).fill(accent); g.moveTo(0, r * 0.8).lineTo(0, r).stroke({ color: accent, width: size * 0.03 }); for (let i = -1; i <= 1; i++) g.moveTo(i * r * 0.3, -r * 0.55).lineTo(i * r * 0.3, r * 0.55).stroke({ color: darken(style.color, 0.3), width: size * 0.015 }); break; } case "fan": { for (let i = -3; i <= 3; i++) { const a = -Math.PI / 2 + i * 0.28; g.moveTo(0, r * 0.7).lineTo(Math.cos(a) * r * 1.05, r * 0.7 + Math.sin(a) * r * 1.05).stroke({ color: i % 2 ? main : accent, width: size * 0.07 }); } g.circle(0, r * 0.7, r * 0.14).fill(0x0b0d14); break; } case "scroll": { g.roundRect(-r * 0.7, -r * 0.85, r * 1.4, r * 1.7, r * 0.1).fill({ color: accent, alpha: 0.9 }); g.roundRect(-r * 0.85, -r, r * 1.7, r * 0.3, r * 0.15).fill(main); g.roundRect(-r * 0.85, r * 0.7, r * 1.7, r * 0.3, r * 0.15).fill(main); for (let i = 0; i < 3; i++) g.rect(-r * 0.45, -r * 0.5 + i * r * 0.32, r * 0.9, r * 0.08).fill({ color: darken(style.color, 0.4), alpha: 0.7 }); break; } case "cherry": { g.moveTo(-r * 0.3, r * 0.3).bezierCurveTo(-r * 0.2, -r * 0.5, r * 0.2, -r * 0.9, r * 0.5, -r).stroke({ color: 0x3ddc97, width: size * 0.035 }); g.moveTo(r * 0.45, r * 0.3).bezierCurveTo(r * 0.5, -r * 0.4, r * 0.5, -r * 0.8, r * 0.5, -r).stroke({ color: 0x3ddc97, width: size * 0.035 }); g.circle(-r * 0.35, r * 0.45, r * 0.42).fill(main); g.circle(r * 0.45, r * 0.45, r * 0.42).fill(main); g.circle(-r * 0.5, r * 0.3, r * 0.12).fill({ color: 0xffffff, alpha: 0.6 }); g.circle(r * 0.3, r * 0.3, r * 0.12).fill({ color: 0xffffff, alpha: 0.6 }); break; } case "seven": { g.poly([-r * 0.8, -r, r * 0.8, -r, r * 0.8, -r * 0.55, -r * 0.05, r, -r * 0.55, r, r * 0.2, -r * 0.55, -r * 0.8, -r * 0.55]).fill(main); g.poly([-r * 0.8, -r, r * 0.8, -r, r * 0.8, -r * 0.55, -r * 0.05, r, -r * 0.55, r, r * 0.2, -r * 0.55, -r * 0.8, -r * 0.55]).stroke({ color: accent, width: size * 0.03 }); break; } case "bell": { g.moveTo(-r * 0.75, r * 0.5).bezierCurveTo(-r * 0.75, -r * 0.5, -r * 0.3, -r * 0.9, 0, -r * 0.9).bezierCurveTo(r * 0.3, -r * 0.9, r * 0.75, -r * 0.5, r * 0.75, r * 0.5).lineTo(-r * 0.75, r * 0.5).fill(main); g.roundRect(-r * 0.9, r * 0.45, r * 1.8, r * 0.18, r * 0.09).fill(accent); g.circle(0, r * 0.8, r * 0.15).fill(accent); g.circle(0, -r * 0.95, r * 0.1).fill(accent); break; } case "joystick": { g.roundRect(-r * 0.9, r * 0.3, r * 1.8, r * 0.6, r * 0.15).fill(darken(style.color, 0.4)); g.rect(-r * 0.08, -r * 0.4, r * 0.16, r * 0.8).fill(0x9aa3b5); g.circle(0, -r * 0.55, r * 0.35).fill(main); g.circle(-r * 0.12, -r * 0.65, r * 0.1).fill({ color: 0xffffff, alpha: 0.5 }); g.circle(r * 0.5, r * 0.6, r * 0.12).fill(accent); break; } case "torii": { g.rect(-r, -r * 0.85, r * 2, r * 0.18).fill(main); g.rect(-r * 0.8, -r * 0.5, r * 1.6, r * 0.12).fill(main); g.rect(-r * 0.6, -r * 0.5, r * 0.16, r * 1.5).fill(main); g.rect(r * 0.44, -r * 0.5, r * 0.16, r * 1.5).fill(main); g.rect(-r * 0.08, -r * 0.5, r * 0.16, r * 0.35).fill(accent); break; } case "katana": { g.moveTo(-r * 0.9, r * 0.9).lineTo(r * 0.7, -r * 0.7).stroke({ color: 0xdfe6f0, width: size * 0.05 }); g.moveTo(-r * 0.9, r * 0.9).lineTo(r * 0.7, -r * 0.7).stroke({ color: 0xffffff, width: size * 0.015, alpha: 0.7 }); g.moveTo(-r * 0.55, r * 0.35).lineTo(-r * 0.25, r * 0.65).stroke({ color: accent, width: size * 0.05 }); g.moveTo(-r * 0.9, r * 0.9).lineTo(-r * 0.6, r * 0.6).stroke({ color: main, width: size * 0.08 }); break; } case "cassette": { g.roundRect(-r, -r * 0.65, r * 2, r * 1.3, r * 0.12).fill(main); g.roundRect(-r * 0.75, -r * 0.4, r * 1.5, r * 0.55, r * 0.08).fill(darken(style.color, 0.45)); g.circle(-r * 0.4, -r * 0.12, r * 0.16).fill(0x0b0d14).circle(r * 0.4, -r * 0.12, r * 0.16).fill(0x0b0d14); g.rect(-r * 0.6, r * 0.3, r * 1.2, r * 0.14).fill(accent); break; } case "ankh": { g.ellipse(0, -r * 0.55, r * 0.35, r * 0.42).stroke({ color: main, width: size * 0.06 }); g.rect(-r * 0.08, -r * 0.1, r * 0.16, r * 1.1).fill(main); g.rect(-r * 0.7, -r * 0.1, r * 1.4, r * 0.16).fill(main); g.circle(0, -r * 0.55, r * 0.08).fill(accent); break; } case "scarab": { g.ellipse(0, r * 0.15, r * 0.6, r * 0.7).fill(main); g.circle(0, -r * 0.6, r * 0.28).fill(darken(style.color, 0.25)); g.moveTo(0, -r * 0.4).lineTo(0, r * 0.8).stroke({ color: accent, width: size * 0.02 }); for (const sgn of [-1, 1]) { g.moveTo(sgn * r * 0.55, -r * 0.1).lineTo(sgn * r * 0.95, -r * 0.5).stroke({ color: accent, width: size * 0.035 }); g.moveTo(sgn * r * 0.6, r * 0.3).lineTo(sgn * r * 1.0, r * 0.4).stroke({ color: accent, width: size * 0.035 }); } break; } case "pyramid": { g.poly([0, -r, r, r * 0.75, -r, r * 0.75]).fill(main); g.poly([0, -r, r, r * 0.75, 0, r * 0.75]).fill(darken(style.color, 0.25)); g.poly([0, -r, r * 0.25, -r * 0.55, -r * 0.25, -r * 0.55]).fill(accent); break; } case "warning": { g.poly([0, -r, r * 0.95, r * 0.75, -r * 0.95, r * 0.75]).fill(main); g.poly([0, -r * 0.6, r * 0.55, r * 0.55, -r * 0.55, r * 0.55]).fill(0x0b0d14); g.rect(-r * 0.08, -r * 0.3, r * 0.16, r * 0.5).fill(main); g.circle(0, r * 0.38, r * 0.09).fill(main); break; } case "valve": { g.circle(0, 0, r * 0.85).stroke({ color: main, width: size * 0.07 }); for (let i = 0; i < 4; i++) { const a = (i / 4) * Math.PI * 2; g.moveTo(0, 0).lineTo(Math.cos(a) * r * 0.85, Math.sin(a) * r * 0.85).stroke({ color: main, width: size * 0.05 }); } g.circle(0, 0, r * 0.25).fill(accent); break; } case "gauge": { g.arc(0, r * 0.2, r * 0.9, Math.PI, 0).stroke({ color: darken(style.color, 0.3), width: size * 0.09 }); g.arc(0, r * 0.2, r * 0.9, Math.PI, Math.PI * 1.6).stroke({ color: main, width: size * 0.09 }); g.moveTo(0, r * 0.2).lineTo(r * 0.45, -r * 0.5).stroke({ color: accent, width: size * 0.04 }); g.circle(0, r * 0.2, r * 0.12).fill(accent); break; } case "barrel": { g.roundRect(-r * 0.65, -r * 0.9, r * 1.3, r * 1.8, r * 0.2).fill(main); g.rect(-r * 0.65, -r * 0.45, r * 1.3, r * 0.12).fill(darken(style.color, 0.4)); g.rect(-r * 0.65, r * 0.33, r * 1.3, r * 0.12).fill(darken(style.color, 0.4)); polygon(g, 0, 0, r * 0.28, 3, -Math.PI / 2).fill(accent); break; } case "wheel": { g.circle(0, 0, r * 0.95).fill(0x0b0d14); g.circle(0, 0, r * 0.95).stroke({ color: main, width: size * 0.05 }); g.circle(0, 0, r * 0.6).stroke({ color: accent, width: size * 0.03 }); for (let i = 0; i < 5; i++) { const a = (i / 5) * Math.PI * 2; g.moveTo(0, 0).lineTo(Math.cos(a) * r * 0.6, Math.sin(a) * r * 0.6).stroke({ color: accent, width: size * 0.035 }); } g.circle(0, 0, r * 0.14).fill(main); break; } case "helmet": { g.arc(0, r * 0.1, r * 0.85, Math.PI, 0).fill(main); g.rect(-r * 0.85, r * 0.1, r * 1.7, r * 0.45).fill(main); g.roundRect(-r * 0.6, -r * 0.05, r * 1.4, r * 0.4, r * 0.1).fill(0x0b0d14); g.moveTo(-r * 0.3, -r * 0.7).lineTo(r * 0.3, -r * 0.7).stroke({ color: accent, width: size * 0.04 }); break; } case "flag": { g.rect(-r * 0.85, -r, r * 0.1, r * 2).fill(0x9aa3b5); for (let i = 0; i < 4; i++) for (let j = 0; j < 3; j++) g.rect(-r * 0.75 + i * r * 0.4, -r + j * r * 0.33, r * 0.4, r * 0.33).fill((i + j) % 2 ? main : accent); break; } case "trophy": { g.moveTo(-r * 0.7, -r).lineTo(r * 0.7, -r).lineTo(r * 0.45, r * 0.15).lineTo(-r * 0.45, r * 0.15).fill(main); g.arc(-r * 0.75, -r * 0.55, r * 0.3, Math.PI * 0.5, Math.PI * 1.5).stroke({ color: main, width: size * 0.04 }); g.arc(r * 0.75, -r * 0.55, r * 0.3, -Math.PI * 0.5, Math.PI * 0.5).stroke({ color: main, width: size * 0.04 }); g.rect(-r * 0.1, r * 0.15, r * 0.2, r * 0.4).fill(main); g.roundRect(-r * 0.5, r * 0.55, r, r * 0.3, r * 0.06).fill(accent); break; } case "idol": { g.roundRect(-r * 0.55, -r * 0.9, r * 1.1, r * 1.8, r * 0.15).fill(main); g.rect(-r * 0.4, -r * 0.55, r * 0.8, r * 0.14).fill(0x0b0d14); g.rect(-r * 0.4, -r * 0.1, r * 0.8, r * 0.14).fill(0x0b0d14); g.poly([-r * 0.3, r * 0.35, r * 0.3, r * 0.35, 0, r * 0.7]).fill(accent); break; } case "totem": { for (let i = 0; i < 3; i++) { g.roundRect(-r * 0.55, -r + i * r * 0.68, r * 1.1, r * 0.6, r * 0.08).fill(i % 2 ? darken(style.color, 0.25) : main); g.circle(-r * 0.22, -r * 0.7 + i * r * 0.68, r * 0.08).fill(accent); g.circle(r * 0.22, -r * 0.7 + i * r * 0.68, r * 0.08).fill(accent); } break; } case "snake": { g.moveTo(-r * 0.8, r * 0.8).bezierCurveTo(-r * 0.9, -r * 0.2, r * 0.9, r * 0.6, r * 0.6, -r * 0.5).bezierCurveTo(r * 0.4, -r * 1.0, -r * 0.2, -r * 0.6, 0, -r * 0.3).stroke({ color: main, width: size * 0.08 }); g.circle(0, -r * 0.3, r * 0.2).fill(main); g.circle(r * 0.06, -r * 0.36, r * 0.06).fill(accent); break; } case "rocket": { g.moveTo(0, -r).bezierCurveTo(r * 0.6, -r * 0.4, r * 0.5, r * 0.4, r * 0.35, r * 0.6).lineTo(-r * 0.35, r * 0.6).bezierCurveTo(-r * 0.5, r * 0.4, -r * 0.6, -r * 0.4, 0, -r).fill(main); g.poly([-r * 0.35, r * 0.2, -r * 0.75, r * 0.8, -r * 0.35, r * 0.6]).fill(accent); g.poly([r * 0.35, r * 0.2, r * 0.75, r * 0.8, r * 0.35, r * 0.6]).fill(accent); g.circle(0, -r * 0.25, r * 0.18).fill(0x0b0d14); g.poly([-r * 0.2, r * 0.6, r * 0.2, r * 0.6, 0, r]).fill(0xffb347); break; } case "anchor": { g.circle(0, -r * 0.75, r * 0.2).stroke({ color: main, width: size * 0.05 }); g.rect(-r * 0.07, -r * 0.55, r * 0.14, r * 1.4).fill(main); g.rect(-r * 0.55, -r * 0.35, r * 1.1, r * 0.12).fill(main); g.arc(0, r * 0.15, r * 0.75, Math.PI * 0.15, Math.PI * 0.85).stroke({ color: main, width: size * 0.07 }); g.circle(-r * 0.7, r * 0.4, r * 0.08).fill(accent).circle(r * 0.7, r * 0.4, r * 0.08).fill(accent); break; } case "chest": { g.roundRect(-r * 0.9, -r * 0.2, r * 1.8, r * 1.0, r * 0.1).fill(main); g.roundRect(-r * 0.9, -r * 0.75, r * 1.8, r * 0.6, r * 0.3).fill(darken(style.color, 0.2)); g.rect(-r * 0.9, -r * 0.2, r * 1.8, r * 0.12).fill(accent); g.roundRect(-r * 0.15, -r * 0.2, r * 0.3, r * 0.35, r * 0.05).fill(accent); break; } case "trident": { g.rect(-r * 0.07, -r * 0.4, r * 0.14, r * 1.4).fill(main); for (const dx of [-0.5, 0, 0.5]) { g.moveTo(dx * r, -r * 0.3).lineTo(dx * r, -r).stroke({ color: main, width: size * 0.05 }); g.poly([dx * r - r * 0.12, -r * 0.85, dx * r + r * 0.12, -r * 0.85, dx * r, -r * 1.1]).fill(accent); } g.rect(-r * 0.5, -r * 0.4, r, r * 0.12).fill(main); break; } case "shell": { g.arc(0, r * 0.4, r * 0.95, Math.PI, 0).fill(main); for (let i = -3; i <= 3; i++) g.moveTo(0, r * 0.4).lineTo(Math.cos(-Math.PI / 2 + i * 0.38) * r * 0.95, r * 0.4 + Math.sin(-Math.PI / 2 + i * 0.38) * r * 0.95).stroke({ color: darken(style.color, 0.25), width: size * 0.02 }); g.rect(-r * 0.95, r * 0.4, r * 1.9, r * 0.18).fill(accent); break; } case "planet": { g.circle(0, 0, r * 0.7).fill(main); g.ellipse(0, r * 0.05, r * 1.05, r * 0.28).stroke({ color: accent, width: size * 0.04 }); g.circle(-r * 0.25, -r * 0.25, r * 0.18).fill({ color: 0xffffff, alpha: 0.3 }); g.ellipse(r * 0.15, r * 0.15, r * 0.3, r * 0.12).fill({ color: darken(style.color, 0.25), alpha: 0.8 }); break; } case "comet": { g.moveTo(r * 0.5, -r * 0.5).lineTo(-r * 1.0, r * 0.15).lineTo(-r * 0.6, r * 0.6).lineTo(r * 0.5, -r * 0.5).fill({ color: accent, alpha: 0.5 }); g.moveTo(r * 0.5, -r * 0.5).lineTo(-r * 0.7, r * 0.7).stroke({ color: accent, alpha: 0.4, width: size * 0.05 }); g.circle(r * 0.5, -r * 0.5, r * 0.4).fill(main); g.circle(r * 0.4, -r * 0.6, r * 0.12).fill({ color: 0xffffff, alpha: 0.6 }); break; } case "satellite": { g.roundRect(-r * 0.3, -r * 0.3, r * 0.6, r * 0.6, r * 0.1).fill(main); g.rect(-r * 1.0, -r * 0.18, r * 0.6, r * 0.36).fill(accent); g.rect(r * 0.4, -r * 0.18, r * 0.6, r * 0.36).fill(accent); for (const x of [-0.8, -0.6, 0.6, 0.8]) g.moveTo(x * r, -r * 0.18).lineTo(x * r, r * 0.18).stroke({ color: darken(style.color, 0.4), width: size * 0.012 }); g.arc(0, -r * 0.55, r * 0.3, Math.PI, 0).stroke({ color: main, width: size * 0.03 }); break; } case "pickaxe": { g.moveTo(-r * 0.7, r * 0.9).lineTo(r * 0.5, -r * 0.4).stroke({ color: 0x8a6d2e, width: size * 0.06 }); g.moveTo(r * 0.0, -r * 0.9).bezierCurveTo(r * 0.7, -r * 0.9, r * 1.0, -r * 0.4, r * 0.95, r * 0.15).lineTo(r * 0.55, -r * 0.35).fill(main); g.moveTo(r * 0.0, -r * 0.9).lineTo(r * 0.55, -r * 0.35).stroke({ color: accent, width: size * 0.03 }); break; } case "ore": { polygon(g, 0, r * 0.1, r * 0.9, 7).fill(0x3a3f4a); g.poly([-r * 0.3, -r * 0.1, 0, -r * 0.5, r * 0.35, -r * 0.05, r * 0.1, r * 0.35, -r * 0.25, r * 0.3]).fill(main); g.poly([-r * 0.3, -r * 0.1, 0, -r * 0.5, 0, 0]).fill(accent); break; } case "drill": { g.rect(-r * 0.4, -r * 0.9, r * 0.8, r * 0.7).fill(main); g.poly([-r * 0.4, -r * 0.2, r * 0.4, -r * 0.2, 0, r]).fill(0xc8d0dc); for (let i = 0; i < 4; i++) g.moveTo(-r * 0.32 + i * r * 0.1, -r * 0.2 + i * r * 0.28).lineTo(r * 0.32 - i * r * 0.1, -r * 0.2 + i * r * 0.28).stroke({ color: 0x5c6270, width: size * 0.015 }); g.rect(-r * 0.15, -r * 0.6, r * 0.3, r * 0.15).fill(accent); break; } case "ingot": { g.poly([-r * 0.7, -r * 0.3, r * 0.7, -r * 0.3, r * 0.95, r * 0.5, -r * 0.95, r * 0.5]).fill(main); g.poly([-r * 0.7, -r * 0.3, r * 0.7, -r * 0.3, r * 0.5, 0, -r * 0.5, 0]).fill(accent); g.moveTo(-r * 0.5, r * 0.15).lineTo(r * 0.5, r * 0.15).stroke({ color: darken(style.color, 0.3), width: size * 0.02 }); break; } case "hourglass": { g.poly([-r * 0.7, -r, r * 0.7, -r, r * 0.1, 0, r * 0.7, r, -r * 0.7, r, -r * 0.1, 0]).stroke({ color: main, width: size * 0.04 }); g.poly([-r * 0.45, -r * 0.8, r * 0.45, -r * 0.8, 0, -r * 0.1]).fill({ color: accent, alpha: 0.8 }); g.poly([-r * 0.55, r * 0.9, r * 0.55, r * 0.9, 0, r * 0.35]).fill({ color: accent, alpha: 0.8 }); break; } case "igloo": { g.arc(0, r * 0.4, r * 0.95, Math.PI, 0).fill(main); g.rect(-r * 0.95, r * 0.4, r * 1.9, r * 0.2).fill(main); g.arc(-r * 0.3, r * 0.5, r * 0.35, Math.PI, 0).fill(0x0b0d14); for (let i = 0; i < 3; i++) g.arc(0, r * 0.4, r * (0.35 + i * 0.25), Math.PI, 0).stroke({ color: accent, width: size * 0.015, alpha: 0.6 }); break; } case "paw": { g.ellipse(0, r * 0.35, r * 0.55, r * 0.45).fill(main); for (const [dx, dy] of [[-0.6, -0.25], [-0.22, -0.6], [0.22, -0.6], [0.6, -0.25]]) g.circle(dx * r, dy * r, r * 0.2).fill(main); g.ellipse(0, r * 0.4, r * 0.3, r * 0.22).fill({ color: accent, alpha: 0.6 }); break; } case "fish": { g.ellipse(-r * 0.1, 0, r * 0.7, r * 0.42).fill(main); g.poly([r * 0.5, 0, r, -r * 0.45, r, r * 0.45]).fill(accent); g.circle(-r * 0.5, -r * 0.1, r * 0.08).fill(0x0b0d14); g.moveTo(-r * 0.2, -r * 0.4).bezierCurveTo(0, -r * 0.7, r * 0.2, -r * 0.5, r * 0.25, -r * 0.3).fill({ color: accent, alpha: 0.8 }); break; } case "meteor": { polygon(g, r * 0.2, r * 0.2, r * 0.6, 7, 0.3).fill(main); g.circle(r * 0.35, 0, r * 0.12).fill({ color: darken(style.color, 0.3) }); g.circle(0, r * 0.4, r * 0.1).fill({ color: darken(style.color, 0.3) }); g.moveTo(-r * 0.2, -r * 0.3).lineTo(-r * 1.0, -r * 1.0).stroke({ color: accent, alpha: 0.6, width: size * 0.06 }); g.moveTo(r * 0.1, -r * 0.55).lineTo(-r * 0.5, -r * 1.05).stroke({ color: accent, alpha: 0.4, width: size * 0.04 }); break; } } c.addChild(g); return c; } function label(text: string, fontSize: number, color: number, bold = true): Text { const t = new Text({ text, style: new TextStyle({ fontFamily: "Geist, Inter, system-ui, sans-serif", fontSize, fontWeight: bold ? "800" : "600", fill: color, letterSpacing: 1 }), }); t.anchor.set(0.5); return t; } export { hex as hexColor, lighten, darken };