Team Ai
Apppublic

Lashtw/Vibecodingex

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
monsterUtils.js533 linesDownload Raw Back to utils
1/**
2 * Logic for generating Pixel Art Monsters
3 * Adapted from monster_preview.html
4 */
5
6export const MONSTER_STAGES = {
7    EGG: 0,
8    BASIC: 1,
9    EVOLVED: 2,
10    FINAL: 3
11};
12
13// Monster Definitions
14export const MONSTER_DEFS = [
15    { id: 'Egg', name: '🥚 像素蛋', type: 'neutral', stage: 0, arch: 'egg', fam: 'Origin' },
16    // Stage 1
17    { id: 'L1_C', name: '🔴 灰塵球 Dust-Ball', type: 'glitch', stage: 1, arch: 'dust', fam: 'Dust' },
18    { id: 'L1_B', name: '🟡 像素狗 Pixel-Pup', type: 'animal', stage: 1, arch: 'beast_pup', fam: 'Beast' },
19    { id: 'L1_A', name: '🔵 光之靈 Lumina', type: 'holy', stage: 1, arch: 'spirit_orb', fam: 'Spirit' },
20    // Stage 2
21    { id: 'L2_CC', name: '🔴 垃圾怪 Trash-Mob', type: 'glitch', stage: 2, arch: 'dust_trash', fam: 'Dust' },
22    { id: 'L2_CB', name: '🟡 史萊姆 Slime-Box', type: 'slime', stage: 2, arch: 'dust_slime', fam: 'Dust' },
23    { id: 'L2_CA', name: '🔵 駭客蟲 Hacker-Bug', type: 'tech', stage: 2, arch: 'dust_tech', fam: 'Dust' },
24    { id: 'L2_BC', name: '🔴 廢鐵狼 Rusty-Wolf', type: 'grunge', stage: 2, arch: 'beast_wolf', fam: 'Beast' },
25    { id: 'L2_BB', name: '🟡 勇者貓 Hero-Cat', type: 'animal', stage: 2, arch: 'beast_cat', fam: 'Beast' },
26    { id: 'L2_BA', name: '🔵 機甲獅 Mecha-Lion', type: 'mech', stage: 2, arch: 'beast_mech', fam: 'Beast' },
27    { id: 'L2_AC', name: '🔴 幽靈火 Ghost-Fire', type: 'spirit', stage: 2, arch: 'spirit_fire', fam: 'Spirit' },
28    { id: 'L2_AB', name: '🟡 天使鳥 Angel-Bird', type: 'holy', stage: 2, arch: 'spirit_wing', fam: 'Spirit' },
29    { id: 'L2_AA', name: '🔵 星雲龍 Cosmos-Dragon', type: 'cosmic', stage: 2, arch: 'spirit_dragon', fam: 'Spirit' },
30    // Stage 3
31    { id: 'L3_CCC', name: '🗑️ 崩潰垃圾山', type: 'glitch', stage: 3, arch: 'pile_big', fam: 'Glitch/Trash' },
32    { id: 'L3_CCB', name: '📦 寶箱怪', type: 'mimic', stage: 3, arch: 'box_teeth', fam: 'Glitch/Trash' },
33    { id: 'L3_CCA', name: '🦠 病毒王', type: 'virus', stage: 3, arch: 'corona', fam: 'Glitch/Trash' },
34    { id: 'L3_CBC', name: '💧 汙泥怪', type: 'grunge', stage: 3, arch: 'fluid', fam: 'Slime' },
35    { id: 'L3_CBB', name: '🧊 果凍騎士', type: 'slime', stage: 3, arch: 'knight_slime', fam: 'Slime' },
36    { id: 'L3_CBA', name: '💎 鑽石魔像', type: 'crystal', stage: 3, arch: 'golem', fam: 'Slime' },
37    { id: 'L3_CAC', name: '🕷️ 錯誤蜘蛛', type: 'glitch', stage: 3, arch: 'spider', fam: 'Hacker' },
38    { id: 'L3_CAB', name: '👾 程式遊俠', type: 'tech', stage: 3, arch: 'ranger', fam: 'Hacker' },
39    { id: 'L3_CAA', name: '🧠 量子主腦', type: 'tech', stage: 3, arch: 'brain', fam: 'Hacker' },
40    { id: 'L3_BAC', name: '🚜 重裝推土機', type: 'mech', stage: 3, arch: 'tank', fam: 'Mech Lion' },
41    { id: 'L3_BAB', name: '✈️ 變形戰機', type: 'mech', stage: 3, arch: 'jet', fam: 'Mech Lion' },
42    { id: 'L3_BAA', name: '🤖 究極鋼彈', type: 'mech', stage: 3, arch: 'gundam', fam: 'Mech Lion' },
43    { id: 'L3_ACC', name: '💀 骷髏法師', type: 'undead', stage: 3, arch: 'mage_skull', fam: 'Ghost' },
44    { id: 'L3_ACB', name: '🕯️ 南瓜燈杰克', type: 'spirit', stage: 3, arch: 'pumpkin', fam: 'Ghost' },
45    { id: 'L3_ACA', name: '👻 虛空死神', type: 'void', stage: 3, arch: 'reaper', fam: 'Ghost' },
46    // Stage 3 - REDESIGN TARGETS (Wolf V2 & Cat)
47    { id: 'L3_BCC', name: '🧟 喪屍犬', type: 'undead', stage: 3, arch: 'wolf_zombie_v2', fam: 'Wolf' },
48    { id: 'L3_BCB', name: '🐕 警備犬', type: 'animal', stage: 3, arch: 'wolf_guard_v2', fam: 'Wolf' },
49    { id: 'L3_BCA', name: '🐺 合金戰狼', type: 'mech', stage: 3, arch: 'wolf_alloy_v2', fam: 'Wolf' },
50    { id: 'L3_BBC', name: '😼 流浪劍客', type: 'warrior', stage: 3, arch: 'ronin', fam: 'Cat' },
51    { id: 'L3_BBB', name: '👑 貓咪國王', type: 'royal', stage: 3, arch: 'king_cat', fam: 'Cat' },
52    { id: 'L3_BBA', name: '🦁 雷霆獅王', type: 'elemental', stage: 3, arch: 'lion_mane', fam: 'Cat' },
53    // Stage 3 - NEW ADDITIONS (Angel & Cosmos)
54    { id: 'L3_ABC', name: '👁️ 座天使 Ophanim', type: 'holy', stage: 3, arch: 'biblical_angel', fam: 'Angel' },
55    { id: 'L3_ABB', name: '⚔️ 女武神 Valkyrie', type: 'warrior', stage: 3, arch: 'valkyrie', fam: 'Angel' },
56    { id: 'L3_ABA', name: '🔥 聖火鳳凰 Phoenix', type: 'divine', stage: 3, arch: 'phoenix', fam: 'Angel' },
57    { id: 'L3_AAC', name: '🛡️ 大地泰坦 Titan', type: 'cosmic', stage: 3, arch: 'terra_titan', fam: 'Cosmos' },
58    { id: 'L3_AAB', name: '🐋 星海鯨 Astro-Whale', type: 'cosmic', stage: 3, arch: 'cosmo_whale', fam: 'Cosmos' },
59    { id: 'L3_AAA', name: '🌌 創世龍 Genesis', type: 'god', stage: 3, arch: 'genesis_dragon', fam: 'Cosmos' },
60];
61
62const palettes = {
63    base: ['#94A3B8', '#64748B', '#475569', '#334155'],
64
65    // Standard
66    glitch: ['#18181b', '#27272a', '#3f3f46', '#ef4444'],
67    trash: ['#3f3f46', '#71717a', '#a1a1aa', '#ef4444'],
68    mimic: ['#854d0e', '#a16207', '#ca8a04', '#eab308'],
69    virus: ['#2e1065', '#581c87', '#7e22ce', '#bef264', '#84cc16'],
70    spider: ['#1e293b', '#0f172a', '#b91c1c', '#ef4444'],
71    slime: ['#059669', '#10b981', '#34d399', '#d1fae5'],
72    fluid: ['#3f3f46', '#52525b', '#4b5563', '#a1a1aa'],
73    knight: ['#0ea5e9', '#38bdf8', '#7dd3fc', '#ffffff'],
74    crypto: ['#0891b2', '#06b6d4', '#22d3ee', '#cffafe'],
75    tech: ['#22d3ee', '#06b6d4', '#0891b2', '#155e75'],
76    ranger: ['#3b82f6', '#2563eb', '#1d4ed8', '#fbbf24'],
77    brain: ['#d8b4fe', '#c084fc', '#a855f7', '#7e22ce', '#22d3ee'],
78    tank: ['#3f3f46', '#18181b', '#facc15', '#fbbf24'],
79    jet: ['#cbd5e1', '#94a3b8', '#ef4444', '#dc2626'],
80    gundam: ['#ffffff', '#2563eb', '#dc2626', '#facc15'],
81    mage: ['#4c1d95', '#a855f7', '#172554', '#fbbf24'],
82    pumpkin: ['#ea580c', '#c2410c', '#3f3f46', '#facc15'],
83    reaper: ['#000000', '#171717', '#7f1d1d', '#9ca3af'],
84
85    // Wolf V2
86    wolf: ['#c2410c', '#9a3412', '#7c2d12', '#ef4444'], // Rust/Browns
87    zombie: ['#44403c', '#78716c', '#991b1b', '#fecaca'],
88    guard: ['#172554', '#1e3a8a', '#fbbf24', '#f59e0b', '#3b82f6'],
89    alloy: ['#e2e8f0', '#94a3b8', '#0ea5e9', '#0284c7', '#38bdf8', '#ffffff'], // Added white/brights
90
91    // Cat V2
92    cat: ['#fb923c', '#ea580c', '#c2410c', '#fff7ed'],
93    ronin: ['#c2410c', '#9a3412', '#fde047', '#e2e8f0'],
94    king: ['#7e22ce', '#a855f7', '#fbbf24', '#ffffff'],
95    thunder: ['#facc15', '#eab308', '#fefce8', '#f59e0b', '#ffffff'],
96
97    // Base Fams
98    beast: ['#fbbf24', '#f59e0b', '#d97706', '#78350f'],
99    mech_lion: ['#60a5fa', '#3b82f6', '#1d4ed8', '#93c5fd'],
100    holy: ['#fef3c7', '#fde68a', '#f59e0b', '#ffffff'],
101    fire: ['#c084fc', '#a855f7', '#7e22ce', '#ffffff'],
102    space: ['#4f46e5', '#312e81', '#818cf8', '#f472b6', '#22d3ee'],
103
104    // Angel V3
105    biblical: ['#facc15', '#eab308', '#ffffff', '#ef4444', '#3b82f6'], // Gold + Eyes
106    valkyrie: ['#e2e8f0', '#94a3b8', '#facc15', '#38bdf8', '#fbbf24'], // Silver/Gold
107    seraphim: ['#dc2626', '#ef4444', '#fbbf24', '#facc15', '#ffffff'], // Fire/Gold
108
109    // Cosmos V3
110    meteor: ['#475569', '#334155', '#ea580c', '#f97316'], // Rock/Magma
111    whale: ['#1e1b4b', '#312e81', '#6366f1', '#c7d2fe', '#ffffff'], // Deep Space
112    genesis: ['#000000', '#171717', '#c084fc', '#22d3ee', '#f472b6', '#ffffff'] // Cosmic
113};
114
115function mulberry32(a) {
116    return function () {
117        var t = a += 0x6D2B79F5;
118        t = Math.imul(t ^ (t >>> 15), t | 1);
119        t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
120        return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
121    }
122}
123
124/**
125 * Generates an SVG string of the monster
126 * @param {Object} monster The monster object (from MONSTER_DEFS)
127 * @returns {string} SVG string
128 */
129export function generateMonsterSVG(monster) {
130    // If no monster provided, return empty
131    if (!monster) return '';
132
133    const W = 24; const H = 24; const C = W / 2;
134
135    let seed = 0;
136    // Use monster ID as seed source
137    for (let i = 0; i < monster.id.length; i++) seed += monster.id.charCodeAt(i);
138    const rng = mulberry32(seed);
139
140    let palette = palettes.base;
141    const t = monster.type;
142    const a = monster.arch;
143
144    // Palette Select (Simplified Logic from original)
145    if (a.includes('wolf')) {
146        if (a.includes('zombie')) palette = palettes.zombie;
147        else if (a.includes('guard')) palette = palettes.guard;
148        else if (a.includes('alloy')) palette = palettes.alloy;
149        else palette = palettes.wolf;
150    }
151    else if (a === 'ronin') palette = palettes.ronin;
152    else if (a === 'king_cat') palette = palettes.king;
153    else if (a === 'lion_mane') palette = palettes.thunder;
154    else if (a === 'tank') palette = palettes.tank;
155    else if (a === 'jet') palette = palettes.jet;
156    else if (a === 'gundam') palette = palettes.gundam;
157    else if (a === 'mage_skull') palette = palettes.mage;
158    else if (a === 'pumpkin') palette = palettes.pumpkin;
159    else if (a === 'reaper') palette = palettes.reaper;
160    else if (a === 'biblical_angel') palette = palettes.biblical;
161    else if (a === 'valkyrie') palette = palettes.valkyrie;
162    else if (a === 'phoenix') palette = palettes.seraphim;
163    else if (a === 'terra_titan') palette = palettes.meteor;
164    else if (a === 'cosmo_whale') palette = palettes.whale;
165    else if (a === 'genesis_dragon') palette = palettes.genesis;
166    else if (a === 'beast_pup') palette = palettes.beast;
167    else if (a === 'beast_cat') palette = palettes.cat;
168    else if (a === 'beast_mech') palette = palettes.mech_lion;
169    else if (a === 'spirit_orb') palette = palettes.holy;
170    else if (a === 'spirit_fire') palette = palettes.fire;
171    else if (a === 'spirit_wing') palette = palettes.holy;
172    else if (a === 'spirit_dragon') palette = palettes.space;
173
174    // Safety Fallbacks
175    else if (a.includes('dust')) {
176        if (a === 'dust_trash' || a === 'pile_big' || a === 'box_teeth' || a === 'corona') palette = palettes.glitch;
177        else if (a.includes('slime') || a === 'fluid' || a === 'knight_slime' || a === 'golem') palette = palettes.slime;
178        else if (a.includes('tech') || a === 'spider' || a === 'ranger' || a === 'brain') palette = palettes.tech;
179        else palette = palettes.trash; // Generic
180    }
181    // Specific Overrides
182    if (a === 'pile_big') palette = palettes.trash;
183    if (a === 'box_teeth') palette = palettes.mimic;
184    if (a === 'corona') palette = palettes.virus;
185    if (a === 'fluid') palette = palettes.fluid;
186    if (a === 'knight_slime') palette = palettes.knight;
187    if (a === 'golem') palette = palettes.crypto;
188    if (a === 'spider') palette = palettes.spider;
189    if (a === 'ranger') palette = palettes.ranger;
190    if (a === 'brain') palette = palettes.brain;
191
192    let grid = new Array(H).fill(0).map(() => new Array(W).fill(null));
193
194    const rect = (x, y, w, h, col) => {
195        for (let i = Math.max(0, Math.floor(x)); i < Math.min(W, x + w); i++)
196            for (let j = Math.max(0, Math.floor(y)); j < Math.min(H, y + h); j++)
197                grid[j][i] = col;
198    }
199    const circle = (cx, cy, r, col) => {
200        for (let y = 0; y < H; y++)
201            for (let x = 0; x < W; x++)
202                if (Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) < r) grid[y][x] = col;
203    }
204    const line = (x1, y1, x2, y2, col) => {
205        let dx = Math.abs(x2 - x1), sx = x1 < x2 ? 1 : -1;
206        let dy = -Math.abs(y2 - y1), sy = y1 < y2 ? 1 : -1;
207        let err = dx + dy, e2;
208        while (true) {
209            if (y1 >= 0 && y1 < H && x1 >= 0 && x1 < W) grid[y1][x1] = col;
210            if (x1 == x2 && y1 == y2) break;
211            const e2_val = 2 * err;
212            if (e2_val >= dy) { err += dy; x1 += sx; }
213            if (e2_val <= dx) { err += dx; y1 += sy; }
214        }
215    }
216
217    const mainCol = palette[Math.floor(rng() * (palette.length - 1))];
218    const secCol = palette[Math.floor(rng() * (palette.length - 2))] || palette[1];
219    const thirdCol = palette[palette.length - 1];
220
221    // --- DRAWING LOGIC ---
222    if (a === 'egg') { circle(C, C, 7, mainCol); circle(C - 2, C - 3, 2, '#ffffff'); rect(C + 1, C + 2, 2, 2, secCol); rect(C - 4, C + 1, 2, 2, secCol); }
223    else if (a === 'dust') { circle(C, C, 5, '#1e293b'); for (let i = 0; i < 30; i++) { let angle = rng() * Math.PI * 2; let r = 4 + rng() * 4; rect(C + Math.cos(angle) * r, C + Math.sin(angle) * r, 1, 1, '#334155'); } rect(C - 4, C - 2, 3, 3, '#ffffff'); rect(C + 1, C - 2, 3, 3, '#ffffff'); rect(C - 3, C - 1, 1, 1, '#000000'); rect(C + 2, C - 1, 1, 1, '#000000'); }
224    else if (a === 'dust_trash') { circle(C, C, 6, mainCol); for (let i = 0; i < 40; i++) { let s = 7 + rng() * 3; rect(C + (rng() - 0.5) * s * 1.5, C + (rng() - 0.5) * s, 2, 2, i % 2 == 0 ? secCol : thirdCol); } rect(C - 3, C - 3, 3, 3, '#ffffff'); rect(C - 2, C - 2, 1, 1, '#000000'); rect(C + 1, C - 1, 2, 2, '#ffffff'); rect(C + 1, C - 1, 1, 1, '#000000'); }
225    else if (a === 'dust_slime') { for (let i = 0; i < 8; i++) circle(C + (rng() - 0.5) * 10, C + 3 + rng() * 3, 3, mainCol); rect(C - 3, C - 3, 2, 2, '#ffffff'); rect(C - 3, C - 1, 2, 2, '#000000'); rect(C + 1, C - 1, 2, 2, '#000000'); }
226    else if (a === 'dust_tech') { rect(C - 1, C - 9, 2, 4, secCol); rect(C - 5, C - 7, 10, 2, secCol); rect(C - 6, C + 2, 2, 4, mainCol); rect(C + 4, C + 2, 2, 4, mainCol); rect(C - 4, C - 1, 3, 2, '#a5f3fc'); rect(C + 1, C - 1, 3, 2, '#a5f3fc'); }
227    else if (a === 'beast_pup') { rect(C - 5, C - 1, 10, 6, mainCol); rect(C - 6, C - 5, 3, 4, secCol); rect(C + 3, C - 5, 3, 4, secCol); rect(C - 2, C + 1, 4, 3, secCol); rect(C - 1, C + 1, 2, 1, '#000000'); }
228    else if (a === 'beast_wolf') { rect(C - 6, C - 2, 12, 6, mainCol); rect(C - 7, C - 6, 5, 5, mainCol); for (let i = 0; i < 10; i++) rect(C - 7 + rng() * 14, C - 4 + rng() * 6, 1, 1, secCol); rect(C - 7, C - 8, 2, 3, secCol); rect(C - 4, C - 8, 2, 3, secCol); rect(C + 6, C, 4, 2, secCol); rect(C - 6, C - 5, 1, 1, '#ef4444'); }
229    else if (a === 'beast_cat') { rect(C - 6, C - 2, 12, 6, mainCol); rect(C - 7, C - 6, 5, 5, mainCol); rect(C - 7, C - 8, 2, 2, mainCol); rect(C - 4, C - 8, 2, 2, mainCol); rect(C - 6, C - 3, 1, 1, '#f9a8d4'); rect(C - 2, C - 3, 6, 6, '#ef4444'); }
230    else if (a === 'beast_mech') { rect(C - 6, C - 2, 12, 6, secCol); rect(C - 7, C - 6, 5, 5, mainCol); rect(C - 4, C - 8, 2, 6, thirdCol); rect(C - 6, C - 5, 3, 1, '#3b82f6'); }
231    else if (a === 'spirit_orb') { circle(C, C, 6, mainCol); for (let t = 0; t < Math.PI * 2; t += 0.4) { let r = 8; rect(C + Math.cos(t) * r, C + Math.sin(t) * r, 1, 1, secCol); } rect(C - 8, C - 2, 3, 4, '#ffffff'); rect(C + 5, C - 2, 3, 4, '#ffffff'); }
232    else if (a === 'spirit_fire') { circle(C, C + 1, 5, '#ffffff'); for (let i = 0; i < 40; i++) { let r = 4 + rng() * 6; let t = rng() * Math.PI * 2; let py = C + Math.sin(t) * r - (rng() * 6); let px = C + Math.cos(t) * r; rect(px, py, 2, 2, i % 3 == 0 ? thirdCol : secCol); } rect(C - 3, C - 1, 2, 3, '#000000'); rect(C + 1, C - 1, 2, 3, '#000000'); }
233    else if (a === 'spirit_wing') { circle(C, C, 5, mainCol); rect(C - 7, C + 2, 14, 2, secCol); rect(C - 11, C - 4, 2, 2, '#ffffff'); rect(C - 10, C - 5, 2, 4, '#ffffff'); rect(C - 8, C - 3, 4, 4, '#ffffff'); rect(C + 9, C - 4, 2, 2, '#ffffff'); rect(C + 8, C - 5, 2, 4, '#ffffff'); rect(C + 4, C - 3, 4, 4, '#ffffff'); rect(C - 4, C - 9, 8, 1, '#fbbf24'); rect(C - 5, C - 8, 1, 1, '#fbbf24'); rect(C + 4, C - 8, 1, 1, '#fbbf24'); rect(C - 1, C, 2, 2, '#fbbf24'); rect(C - 3, C - 2, 1, 1, '#3b82f6'); rect(C + 2, C - 2, 1, 1, '#3b82f6'); }
234    else if (a === 'spirit_dragon') { circle(C, C, 7, '#1e1b4b'); for (let i = 0; i < 20; i++) { let px = C + (rng() - 0.5) * 12; let py = C + (rng() - 0.5) * 10; rect(px, py, 1, 1, i % 3 == 0 ? '#f472b6' : (i % 2 == 0 ? '#22d3ee' : '#ffffff')); } rect(C - 9, C - 5, 4, 4, secCol); rect(C - 8, C - 9, 2, 4, thirdCol); rect(C + 6, C - 2, 6, 2, secCol); rect(C - 4, C - 2, 2, 1, '#22d3ee'); }
235
236    // --- STAGE 3 ---
237    else if (a === 'pile_big') { circle(C, C, 8, '#000000'); for (let i = 0; i < 60; i++) rect(C + (rng() - 0.5) * 16, C + (rng() - 0.5) * 16, 2, 2, '#ef4444'); rect(C - 4, C - 2, 2, 2, '#ffffff'); rect(C - 3, C - 2, 1, 1, '#000000'); rect(C + 4, C, 2, 2, '#ffffff'); rect(C + 4, C, 1, 1, '#000000'); rect(C, C - 5, 3, 3, '#ffffff'); rect(C + 1, C - 5, 1, 1, '#000000'); }
238    else if (a === 'box_teeth') { rect(C - 8, C - 6, 16, 14, mainCol); rect(C - 8, C, 16, 2, '#000000'); rect(C - 1, C + 4, 2, 2, '#000000'); rect(C - 6, C + 2, 1, 2, '#ffffff'); rect(C - 2, C + 2, 1, 2, '#ffffff'); rect(C + 2, C + 2, 1, 2, '#ffffff'); rect(C + 5, C + 2, 1, 2, '#ffffff'); rect(C - 4, C - 3, 2, 1, '#ef4444'); rect(C + 2, C - 3, 2, 1, '#ef4444'); }
239    else if (a === 'corona') { circle(C, C, 6, mainCol); for (let t = 0; t < 6.28; t += 0.8) rect(C + Math.cos(t) * 10, C + Math.sin(t) * 10, 3, 3, secCol); rect(C - 4, C - 8, 8, 2, '#facc15'); rect(C - 4, C - 10, 2, 2, '#facc15'); rect(C + 2, C - 10, 2, 2, '#facc15'); rect(C - 3, C - 1, 2, 1, '#bef264'); rect(C + 1, C - 1, 2, 1, '#bef264'); rect(C - 2, C + 2, 4, 1, '#000000'); }
240    else if (a === 'fluid') { for (let i = 0; i < 4; i++) rect(C - 10 + i * 2, C + 4, 18 - i * 4, 4, mainCol); rect(C - 6, C, 2, 6, mainCol); rect(C + 4, C - 2, 3, 8, mainCol); rect(C - 5, C - 6, 3, 3, secCol); rect(C + 2, C - 5, 2, 2, secCol); rect(C - 2, C + 2, 4, 4, '#ffffff'); rect(C - 1, C + 2, 2, 2, '#000000'); }
241    else if (a === 'knight_slime') { rect(C - 3, C - 4, 6, 8, mainCol); rect(C - 4, C - 9, 8, 6, mainCol); rect(C - 2, C - 3, 4, 6, '#e0f2fe'); rect(C + 4, C, 8, 2, secCol); rect(C + 4, C - 2, 2, 6, secCol); rect(C - 7, C - 2, 4, 8, thirdCol); rect(C - 3, C - 7, 6, 1, '#000000'); }
242    else if (a === 'golem') { rect(C - 4, C - 4, 8, 8, mainCol); rect(C - 8, C - 8, 4, 4, secCol); rect(C + 4, C - 8, 4, 4, secCol); rect(C - 6, C + 4, 4, 6, secCol); rect(C + 2, C + 4, 4, 6, secCol); rect(C - 2, C - 10, 4, 4, thirdCol); rect(C - 3, C - 3, 2, 2, '#ffffff'); rect(C + 5, C - 7, 1, 1, '#ffffff'); rect(C - 1, C - 9, 2, 1, '#000000'); }
243    else if (a === 'spider') { rect(C - 4, C - 4, 8, 8, mainCol); rect(C - 5, C - 5, 10, 2, secCol); line(C - 4, C, C - 10, C - 4, secCol); line(C - 4, C + 2, C - 10, C + 4, secCol); line(C - 4, C + 4, C - 8, C + 8, secCol); line(C + 4, C, C + 10, C - 4, secCol); line(C + 4, C + 2, C + 10, C + 4, secCol); line(C + 4, C + 4, C + 8, C + 8, secCol); rect(C - 3, C - 2, 2, 2, '#ef4444'); rect(C + 1, C - 2, 2, 2, '#ef4444'); rect(C - 1, C + 1, 2, 2, '#ef4444'); }
244    else if (a === 'ranger') { rect(C - 4, C - 10, 8, 8, mainCol); rect(C - 3, C - 9, 2, 6, secCol); rect(C + 1, C - 9, 2, 6, secCol); rect(C - 1, C - 5, 2, 2, '#fbbf24'); rect(C - 4, C - 2, 8, 6, mainCol); rect(C + 4, C - 2, 6, 4, mainCol); rect(C + 8, C - 1, 2, 2, '#22d3ee'); rect(C - 4, C + 4, 3, 6, mainCol); rect(C + 1, C + 4, 3, 6, mainCol); }
245    else if (a === 'brain') { rect(C - 6, C - 6, 12, 12, '#a5f3fc'); rect(C - 6, C + 6, 12, 2, secCol); rect(C - 6, C - 8, 12, 2, secCol); rect(C - 4, C - 4, 8, 6, '#d8b4fe'); rect(C - 2, C - 2, 4, 1, '#ffffff'); line(C - 6, C, C - 10, C + 4, secCol); line(C + 6, C, C + 10, C + 4, secCol); }
246    else if (a === 'tank') {
247        rect(C - 8, C + 4, 16, 4, '#1e293b'); rect(C - 9, C + 5, 18, 2, '#000000');
248        rect(C - 6, C - 2, 12, 6, mainCol); rect(C - 4, C - 6, 8, 4, mainCol); rect(C, C - 5, 8, 2, '#3f3f46'); rect(C - 5, C, 10, 2, '#facc15');
249        rect(C - 3, C - 3, 2, 2, '#3f3f46'); rect(C - 1, C - 2, 2, 1, '#ef4444');
250    }
251    else if (a === 'jet') {
252        rect(C - 2, C - 10, 4, 6, '#ef4444'); rect(C - 4, C - 4, 8, 10, mainCol); rect(C - 10, C, 6, 8, secCol); rect(C + 4, C, 6, 8, secCol);
253        rect(C - 3, C - 6, 6, 2, '#38bdf8'); rect(C - 3, C + 6, 2, 4, '#ef4444'); rect(C + 1, C + 6, 2, 4, '#ef4444');
254    }
255    else if (a === 'gundam') {
256        rect(C - 4, C - 10, 8, 6, '#ffffff'); rect(C - 5, C - 8, 10, 2, '#ffffff'); rect(C - 1, C - 11, 2, 3, '#dc2626'); rect(C - 3, C - 9, 6, 1, '#facc15');
257        rect(C - 4, C - 4, 8, 8, '#2563eb'); rect(C - 2, C - 4, 4, 2, '#dc2626'); rect(C - 5, C - 4, 2, 4, '#ffffff'); rect(C + 3, C - 4, 2, 4, '#ffffff');
258        rect(C - 4, C + 4, 3, 6, '#ffffff'); rect(C + 1, C + 4, 3, 6, '#ffffff'); rect(C + 5, C, 2, 6, '#334155');
259    }
260    else if (a === 'mage_skull') {
261        rect(C - 4, C - 5, 8, 14, '#172554'); rect(C - 3, C - 9, 6, 5, '#e5e5e5'); rect(C - 2, C - 8, 1, 1, '#000000'); rect(C + 1, C - 8, 1, 1, '#000000');
262        rect(C - 6, C - 10, 12, 1, '#4c1d95'); rect(C - 3, C - 13, 6, 3, '#4c1d95'); rect(C + 5, C - 8, 1, 16, '#fbbf24'); circle(C + 5, C - 9, 2, '#a855f7');
263    }
264    else if (a === 'pumpkin') {
265        rect(C - 4, C - 2, 8, 10, '#3f3f46'); circle(C, C - 6, 6, '#ea580c'); rect(C - 2, C - 7, 1, 1, '#facc15'); rect(C + 1, C - 7, 1, 1, '#facc15');
266        rect(C - 2, C - 5, 4, 1, '#facc15'); rect(C, C - 13, 1, 2, '#166534'); rect(C - 6, C, 2, 2, '#ea580c'); rect(C + 4, C, 2, 2, '#ea580c');
267    }
268    else if (a === 'reaper') {
269        rect(C - 5, C - 8, 10, 16, '#000000'); rect(C - 3, C - 6, 6, 4, '#171717'); rect(C - 1, C - 5, 1, 1, '#ef4444'); rect(C, C - 5, 1, 1, '#ef4444');
270        rect(C + 6, C - 8, 1, 16, '#7f1d1d'); rect(C + 4, C - 10, 6, 2, '#9ca3af'); rect(C + 3, C - 10, 1, 6, '#9ca3af');
271    }
272
273    // --- REDESIGNED STAGE 3 (Wolf V2 & Cat V2 Targets) ---
274    else if (a === 'wolf_zombie_v2') {
275        rect(C - 6, C - 3, 13, 6, '#4b5563'); // Darker rot body
276        rect(C - 2, C - 2, 5, 4, '#991b1b'); // Ribcage
277        rect(C - 8, C - 7, 6, 6, '#4b5563'); // Head
278        rect(C - 6, C - 6, 2, 2, '#e7e5e4'); // Skull patch
279        rect(C - 5, C - 5, 1, 1, '#000000'); // Eye empty
280        rect(C - 7, C + 3, 2, 5, '#4b5563'); // Leg FL
281        rect(C + 4, C + 3, 2, 5, '#e7e5e4'); // Leg BR (Bone)
282        for (let i = 0; i < 4; i++) rect(C + (rng() - 0.5) * 14, C - 6 + (rng() - 0.5) * 8, 1, 1, '#bef264');
283    }
284    else if (a === 'wolf_guard_v2') {
285        rect(C - 7, C - 3, 14, 7, '#1e3a8a'); // Body Armor
286        rect(C - 3, C - 4, 6, 6, '#fbbf24'); // Heavy Plating
287        rect(C - 9, C - 8, 7, 7, '#172554'); // Helmet
288        rect(C - 9, C - 6, 7, 1, '#60a5fa'); // Visor
289        rect(C - 3, C - 6, 3, 3, '#1e40af'); // Pad
290        rect(C - 7, C + 4, 3, 5, '#3b82f6');
291        rect(C + 5, C + 4, 3, 5, '#3b82f6');
292    }
293    else if (a === 'wolf_alloy_v2') {
294        rect(C - 7, C - 3, 14, 5, '#e2e8f0'); // Silver Body
295        rect(C - 2, C - 8, 4, 6, '#94a3b8'); // Booster base
296        rect(C - 3, C - 9, 2, 4, '#0ea5e9'); // Thruster
297        rect(C - 9, C - 6, 7, 5, '#e2e8f0'); // Head
298        rect(C - 6, C - 6, 4, 1, '#0ea5e9'); // Visor
299        line(C + 7, C, C + 11, C - 4, '#0ea5e9'); // Saber tail
300        rect(C - 7, C + 5, 2, 3, '#94a3b8');
301        rect(C + 6, C + 5, 2, 3, '#94a3b8');
302    }
303    else if (a === 'ronin') {
304        rect(C - 3, C - 4, 6, 8, '#c2410c'); // Orange Gi
305        rect(C - 4, C - 10, 8, 4, '#fde047'); // Hat
306        rect(C - 3, C + 4, 2, 4, '#3f3f46'); // Hakama
307        rect(C + 1, C + 4, 2, 4, '#3f3f46');
308        line(C + 4, C - 4, C + 8, C - 8, '#e5e5e5'); // Blade handle
309        rect(C - 4, C - 3, 2, 2, '#ef4444'); // Scarf
310    }
311    else if (a === 'king_cat') {
312        rect(C - 4, C - 3, 8, 8, '#7e22ce'); // Robe
313        rect(C - 2, C - 3, 4, 8, '#ffffff'); // Fur
314        rect(C - 4, C - 10, 8, 3, '#fbbf24'); // Crown
315        rect(C - 1, C - 10, 2, 2, '#ef4444');
316        line(C + 6, C, C + 6, C + 6, '#fbbf24'); // Scepter
317        circle(C + 6, C - 1, 2, '#ef4444');
318    }
319    else if (a === 'lion_mane') {
320        rect(C - 4, C - 3, 8, 8, '#ffffff');
321        for (let t = 0; t < 6.3; t += 0.6) rect(C + Math.cos(t) * 11, C + Math.sin(t) * 11 - 5, 2, 2, '#facc15'); // Mane
322        rect(C - 2, C - 6, 4, 4, '#fff7ed'); // Face
323        rect(C - 1, C - 5, 2, 1, '#3b82f6'); // Eyes
324        line(C - 8, C, C - 4, C + 4, '#facc15'); // Lightning
325        line(C + 8, C, C + 4, C - 4, '#facc15');
326    }
327
328    // --- ANGEL FAMILY V3 ---
329    else if (a === 'biblical_angel') {
330        for (let t = 0; t < 6.3; t += 0.1) {
331            let r = 9;
332            let x1 = C + Math.cos(t) * r; let y1 = C + Math.sin(t) * r * 0.4;
333            rect(x1, y1, 1, 1, mainCol);
334            let x2 = C + Math.cos(t) * r * 0.4; let y2 = C + Math.sin(t) * r;
335            rect(x2, y2, 1, 1, secCol);
336        }
337        circle(C, C, 4, '#ffffff');
338        for (let i = 0; i < 8; i++) {
339            rect(C + Math.cos(i * 0.8) * 9, C + Math.sin(i * 0.8) * 3.5, 2, 2, '#ffffff');
340            rect(C + Math.cos(i * 0.8) * 9, C + Math.sin(i * 0.8) * 3.5, 1, 1, '#3b82f6');
341        }
342    }
343    else if (a === 'valkyrie') {
344        rect(C - 3, C - 5, 6, 8, '#e2e8f0'); rect(C - 3, C - 9, 6, 4, '#facc15');
345        rect(C - 4, C + 3, 3, 5, '#e2e8f0'); rect(C + 1, C + 3, 3, 5, '#e2e8f0');
346        rect(C - 8, C - 6, 4, 10, '#ffffff'); rect(C + 5, C - 6, 4, 10, '#ffffff');
347        line(C + 4, C + 8, C + 10, C - 8, '#fbbf24'); rect(C + 9, C - 9, 2, 4, '#38bdf8');
348    }
349    else if (a === 'phoenix') {
350        rect(C - 2, C - 4, 4, 8, mainCol); rect(C - 2, C - 8, 4, 4, mainCol);
351        rect(C, C - 7, 1, 1, '#fbbf24'); rect(C - 3, C - 6, 1, 2, '#fbbf24');
352        for (let i = 0; i < 8; i++) {
353            rect(C - 2 - i, C - 4 - i, 1, 2, '#ef4444');
354            rect(C + 1 + i, C - 4 - i, 1, 2, '#ef4444');
355            rect(C - 2 - i, C - 2 - i, 1, 4, secCol);
356            rect(C + 1 + i, C - 2 - i, 1, 4, secCol);
357        }
358        rect(C - 1, C + 4, 2, 6, '#fbbf24'); rect(C - 3, C + 6, 2, 4, '#ef4444'); rect(C + 1, C + 6, 2, 4, '#ef4444');
359    }
360
361    // --- COSMOS FAMILY V3 ---
362    else if (a === 'terra_titan') {
363        rect(C - 6, C - 6, 12, 10, mainCol); rect(C - 8, C - 8, 4, 6, secCol); rect(C + 4, C - 8, 4, 6, secCol);
364        rect(C - 3, C - 9, 6, 4, mainCol); rect(C - 2, C - 8, 4, 1, '#f97316');
365        rect(C - 8, C - 2, 3, 8, mainCol); rect(C + 5, C - 2, 3, 8, mainCol);
366        rect(C - 6, C + 4, 4, 6, secCol); rect(C + 2, C + 4, 4, 6, secCol);
367        circle(C, C - 2, 3, '#ea580c'); rect(C - 1, C - 3, 2, 2, '#f97316');
368    }
369    else if (a === 'cosmo_whale') {
370        rect(C - 10, C - 6, 14, 10, mainCol); rect(C + 4, C - 4, 6, 6, mainCol); rect(C + 10, C - 2, 4, 3, mainCol);
371        rect(C - 10, C + 2, 12, 2, '#c7d2fe'); rect(C + 12, C - 5, 2, 8, mainCol);
372        rect(C + 11, C - 6, 1, 2, mainCol); rect(C + 11, C + 4, 1, 2, mainCol);
373        rect(C - 5, C, 2, 1, '#ffffff');
374        for (let k = 0; k < 5; k++) rect(C - 6 + (rng() - 0.5) * 4, C - 10 + (rng() - 0.5) * 4, 1, 1, '#22d3ee');
375        rect(C - 8, C - 3, 1, 1, '#ffffff'); rect(C - 2, C - 4, 1, 1, '#ffffff'); rect(C + 6, C - 1, 1, 1, '#ffffff');
376    }
377    else if (a === 'genesis_dragon') {
378        let px = C, py = C;
379        for (let i = 0; i < 30; i++) {
380            rect(px, py, 3, 3, i % 2 == 0 ? '#000000' : secCol);
381            px += Math.cos(i * 0.4) * 2;
382            py += Math.sin(i * 0.4) * 2;
383        }
384        circle(C, C, 5, '#c084fc');
385        for (let k = 0; k < 20; k++) {
386            rect(C + (rng() - 0.5) * 20, C + (rng() - 0.5) * 20, 1, 1, k % 3 == 0 ? '#22d3ee' : '#f472b6');
387        }
388    }
389
390    let svg = '';
391    for (let y = 0; y < H; y++) {
392        for (let x = 0; x < W; x++) {
393            if (grid[y][x]) svg += `<rect x="${x}" y="${y}" width="1" height="1" fill="${grid[y][x]}"/>`;
394        }
395    }
396    return `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-full h-full drop-shadow-md shape-rendering-crispEdges">${svg}</svg>`;
397}
398
399/**
400 * Determines the next monster form based on parameters and lineage
401 * @param {number} currentStage - Current evolutionary stage (0-3)
402 * @param {number} likes - Student's total likes
403 * @param {number} classSize - Active class size
404 * @param {string} [currentMonsterId] - ID of the current monster to determine lineage
405 * @returns {Object} Selected Monster Definition
406 */
407/**
408 * Determines the next monster form based on parameters and lineage
409 * @param {number} currentStage - Current evolutionary stage (0-3)
410 * @param {number} stageLikes - Likes earned IN THIS STAGE
411 * @param {number} classSize - Active class size
412 * @param {string} [currentMonsterId] - ID of the current monster to determine lineage
413 * @param {number} [percentile] - Speed ranking percentile (0.0 - 1.0, lower is faster). Default 1.0
414 * @returns {Object} Selected Monster Definition
415 */
416export function getNextMonster(currentStage, stageLikes, classSize, currentMonsterId = null, percentile = 1.0) {
417    if (currentStage === 0) return MONSTER_DEFS.find(m => m.id === 'Egg');
418
419    // Find current monster family if we have an ID
420    let currentFam = null;
421    if (currentMonsterId) {
422        const current = MONSTER_DEFS.find(m => m.id === currentMonsterId);
423        if (current) currentFam = current.fam; // e.g., 'Beast', 'Dust', 'Spirit'
424    }
425
426    // Stage 1: Basic Families (Origin -> Family)
427    // If coming from Egg (Stage 0), we pick a base family based on performance.
428    if (currentStage === 1) {
429        // Simple distribution for Stage 1 (Egg -> Base)
430        // We can use the same Score Logic or keep simple likes.
431        // Let's use the Score Logic to be consistent with design.
432
433        let score = 0;
434        // Speed Score
435        if (percentile <= 0.3) score += 5;      // Top 30%
436        else if (percentile <= 0.6) score += 3; // Top 60%
437        else score += 1;                        // Rest
438
439        // Like Score
440        if (stageLikes >= 2) score += 2;
441        else if (stageLikes > 0) score += 1;
442
443        if (score >= 5) return MONSTER_DEFS.find(m => m.id === 'L1_A'); // High (Spirit)
444        if (score >= 3) return MONSTER_DEFS.find(m => m.id === 'L1_B'); // Mid (Beast)
445        return MONSTER_DEFS.find(m => m.id === 'L1_C'); // Low (Dust)
446    }
447
448    // Branching Logic for Stage 2 & 3
449    // Score Calculation
450    let score = 0;
451
452    // 1. Speed Score
453    if (percentile <= 0.3) score += 5;      // Top 30%
454    else if (percentile <= 0.6) score += 3; // Top 60%
455    else score += 1;                        // Rest
456
457    // 2. Like Score
458    if (stageLikes >= 2) score += 2;      // High Likes
459    else if (stageLikes > 0) score += 1;  // Any Likes
460
461    // Tier Logic
462    let tier = 'C';
463    if (score >= 5) tier = 'A';      // High Tier
464    else if (score >= 3) tier = 'B'; // Mid Tier
465
466    // Safety check for small classes (Boost if alone)
467    if (classSize <= 2) {
468        if (stageLikes >= 1) tier = 'A';
469        else tier = 'B';
470    }
471
472    // Filter potential monsters for this Stage
473    let candidates = MONSTER_DEFS.filter(m => m.stage === currentStage);
474
475    // 1. Strict Lineage by ID Pattern (L{stage}_{path})
476    // e.g. L1_A -> L2_A... / L2_AB -> L3_AB...
477    // This allows exact branching control as per design doc.
478    const isStandardId = currentMonsterId?.match(/^L\d+_[ABC]+$/);
479
480    if (isStandardId) {
481        // Extract path suffix (e.g. "A" from "L1_A", "AB" from "L2_AB")
482        const currentPath = currentMonsterId.split('_')[1];
483        // Filter candidates: Must start with L{targetStage}_{currentPath}
484        // currentStage IS the target stage (e.g. 3), so we look for L3_...
485        const strictMatches = candidates.filter(m => m.id.startsWith(`L${currentStage}_${currentPath}`));
486
487        if (strictMatches.length > 0) {
488            candidates = strictMatches;
489        }
490    }
491    // 2. Fallback: Family Tree Logic (for custom IDs or Egg)
492    else if (currentFam && currentFam !== 'Origin') {
493        // Define Family Tree Mapping
494        const FAMILY_TREE = {
495            // Stage 1 Root: Low Tier (Red Path)
496            'Dust': ['Dust', 'Glitch', 'Trash', 'Slime', 'Tech', 'Hacker', 'Virus'],
497            // Stage 1 Root: Mid Tier (Yellow Path)
498            'Beast': ['Beast', 'Wolf', 'Cat', 'Mech', 'Mech Lion', 'Animal', 'Grunge', 'Royal', 'Warrior', 'Undead', 'Elemental'],
499            // Stage 1 Root: High Tier (Blue Path)
500            'Spirit': ['Spirit', 'Ghost', 'Holy', 'Cosmos', 'Angel', 'Divin', 'Void', 'Undead']
501        };
502
503        // Find which root family the current family belongs to
504        let rootFamily = null;
505        for (const [root, children] of Object.entries(FAMILY_TREE)) {
506            if (root === currentFam || children.includes(currentFam) || children.some(c => currentFam.includes(c))) {
507                rootFamily = root;
508                break;
509            }
510        }
511
512        if (rootFamily) {
513            const allowedFamilies = FAMILY_TREE[rootFamily];
514            candidates = candidates.filter(m => {
515                return allowedFamilies.some(fam => m.fam.includes(fam) || fam.includes(m.fam));
516            });
517        }
518    }
519
520    // Filter by Tier (Suffix A/B/C)
521    // IDs are like L2_AA, L2_AB... 
522    // Last char is C(Low), B(Mid), A(High).
523    const targetSuffix = tier;
524
525    let match = candidates.find(m => m.id.endsWith(targetSuffix));
526
527    // Fallback: If specific tier not found in this family (shouldn't happen if definitions are complete),
528    // pick ANY from this family.
529    if (!match && candidates.length > 0) match = candidates[0];
530
531    return match || MONSTER_DEFS[0]; // Absolute Fallback
532}
533