Lashtw commited on
Commit
21778f6
·
verified ·
1 Parent(s): f256bb2

Upload 10 files

Browse files
monster_preview.html ADDED
@@ -0,0 +1,586 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-TW">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>VIBECODING Monster Gallery - Final Check</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <style>
10
+ body {
11
+ background-color: #0F172A;
12
+ color: white;
13
+ font-family: 'Courier New', monospace;
14
+ }
15
+
16
+ @keyframes breathe {
17
+ 0% {
18
+ transform: scaleY(1) translateY(0);
19
+ }
20
+
21
+ 50% {
22
+ transform: scaleY(0.92) translateY(4px);
23
+ }
24
+
25
+ 100% {
26
+ transform: scaleY(1) translateY(0);
27
+ }
28
+ }
29
+
30
+ .pixel-art {
31
+ image-rendering: pixelated;
32
+ width: 144px;
33
+ height: 144px;
34
+ background: rgba(0, 0, 0, 0.3);
35
+ border-radius: 12px;
36
+ animation: breathe 2.5s infinite ease-in-out;
37
+ transform-origin: bottom center;
38
+ }
39
+
40
+ .pixel-art:hover {
41
+ animation-play-state: paused;
42
+ transform: scale(1.1);
43
+ z-index: 10;
44
+ box-shadow: 0 0 25px rgba(0, 0, 0, 0.6);
45
+ }
46
+
47
+ .card {
48
+ background: rgba(30, 41, 59, 0.5);
49
+ border: 1px solid rgba(148, 163, 184, 0.1);
50
+ border-radius: 16px;
51
+ padding: 15px;
52
+ text-align: center;
53
+ position: relative;
54
+ }
55
+
56
+ .grid-container {
57
+ display: grid;
58
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
59
+ gap: 24px;
60
+ padding: 20px;
61
+ }
62
+
63
+ h2 {
64
+ border-bottom: 2px solid #334155;
65
+ padding-bottom: 10px;
66
+ margin-top: 40px;
67
+ margin-bottom: 20px;
68
+ color: #60A5FA;
69
+ font-size: 1.5em;
70
+ font-weight: bold;
71
+ }
72
+
73
+ .badge {
74
+ display: inline-block;
75
+ padding: 2px 8px;
76
+ border-radius: 999px;
77
+ font-size: 0.7rem;
78
+ font-weight: bold;
79
+ margin-top: 5px;
80
+ background: #374151;
81
+ color: #9CA3AF;
82
+ }
83
+
84
+ .done-badge {
85
+ background: #059669;
86
+ color: #ccfbf1;
87
+ }
88
+
89
+ .new-badge {
90
+ background: #2563eb;
91
+ color: #dbeafe;
92
+ }
93
+
94
+ .fam-label {
95
+ position: absolute;
96
+ top: -10px;
97
+ left: 50%;
98
+ transform: translateX(-50%);
99
+ background: #1e293b;
100
+ color: #94a3b8;
101
+ font-size: 0.6rem;
102
+ padding: 2px 6px;
103
+ border-radius: 4px;
104
+ border: 1px solid #334155;
105
+ white-space: nowrap;
106
+ }
107
+ </style>
108
+ </head>
109
+
110
+ <body>
111
+ <div class="container mx-auto max-w-7xl p-8">
112
+ <h1
113
+ class="text-4xl font-extrabold mb-2 text-center text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-purple-500 to-pink-500">
114
+ 👾 VIBECODING 怪獸全圖鑑 👾</h1>
115
+ <p class="text-center text-slate-400 mb-12 font-mono">Restored & Updated: Wolf & Cat Redesigned</p>
116
+
117
+ <div id="gallery"></div>
118
+ </div>
119
+
120
+ <script>
121
+ const gallery = document.getElementById('gallery');
122
+ const W = 24; const H = 24; const C = W / 2;
123
+
124
+ function renderSVG(grid) {
125
+ let svg = '';
126
+ for (let y = 0; y < H; y++) {
127
+ for (let x = 0; x < W; x++) {
128
+ if (grid[y][x]) svg += `<rect x="${x}" y="${y}" width="1" height="1" fill="${grid[y][x]}"/>`;
129
+ }
130
+ }
131
+ return `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-full h-full drop-shadow-md">${svg}</svg>`;
132
+ }
133
+
134
+ const monsters = [
135
+ { id: 'Egg', name: '🥚 像素蛋', type: 'neutral', stage: 0, arch: 'egg', status: 'done', fam: 'Origin' },
136
+ // Stage 1
137
+ { id: 'L1_C', name: '🔴 灰塵球 Dust-Ball', type: 'glitch', stage: 1, arch: 'dust', status: 'done', fam: 'Dust' },
138
+ { id: 'L1_B', name: '🟡 像素狗 Pixel-Pup', type: 'animal', stage: 1, arch: 'beast_pup', status: 'done', fam: 'Beast' },
139
+ { id: 'L1_A', name: '🔵 光之靈 Lumina', type: 'holy', stage: 1, arch: 'spirit_orb', status: 'done', fam: 'Spirit' },
140
+ // Stage 2
141
+ { id: 'L2_CC', name: '🔴 垃圾怪 Trash-Mob', type: 'glitch', stage: 2, arch: 'dust_trash', status: 'done', fam: 'Dust' },
142
+ { id: 'L2_CB', name: '🟡 史萊姆 Slime-Box', type: 'slime', stage: 2, arch: 'dust_slime', status: 'done', fam: 'Dust' },
143
+ { id: 'L2_CA', name: '🔵 駭客蟲 Hacker-Bug', type: 'tech', stage: 2, arch: 'dust_tech', status: 'done', fam: 'Dust' },
144
+ { id: 'L2_BC', name: '🔴 廢鐵狼 Rusty-Wolf', type: 'grunge', stage: 2, arch: 'beast_wolf', status: 'done', fam: 'Beast' },
145
+ { id: 'L2_BB', name: '🟡 勇者貓 Hero-Cat', type: 'animal', stage: 2, arch: 'beast_cat', status: 'done', fam: 'Beast' },
146
+ { id: 'L2_BA', name: '🔵 機甲獅 Mecha-Lion', type: 'mech', stage: 2, arch: 'beast_mech', status: 'done', fam: 'Beast' },
147
+ { id: 'L2_AC', name: '🔴 幽靈火 Ghost-Fire', type: 'spirit', stage: 2, arch: 'spirit_fire', status: 'done', fam: 'Spirit' },
148
+ { id: 'L2_AB', name: '🟡 天使鳥 Angel-Bird', type: 'holy', stage: 2, arch: 'spirit_wing', status: 'done', fam: 'Spirit' },
149
+ { id: 'L2_AA', name: '🔵 星雲龍 Cosmos-Dragon', type: 'cosmic', stage: 2, arch: 'spirit_dragon', status: 'done', fam: 'Spirit' },
150
+ // Stage 3 - ESTABLISHED
151
+ { id: 'L3_CCC', name: '🗑️ 崩潰垃圾山', type: 'glitch', stage: 3, arch: 'pile_big', status: 'done', fam: 'Glitch/Trash' },
152
+ { id: 'L3_CCB', name: '📦 寶箱怪', type: 'mimic', stage: 3, arch: 'box_teeth', status: 'done', fam: 'Glitch/Trash' },
153
+ { id: 'L3_CCA', name: '🦠 病毒王', type: 'virus', stage: 3, arch: 'corona', status: 'done', fam: 'Glitch/Trash' },
154
+ { id: 'L3_CBC', name: '💧 汙泥怪', type: 'grunge', stage: 3, arch: 'fluid', status: 'done', fam: 'Slime' },
155
+ { id: 'L3_CBB', name: '🧊 果凍騎士', type: 'slime', stage: 3, arch: 'knight_slime', status: 'done', fam: 'Slime' },
156
+ { id: 'L3_CBA', name: '💎 鑽石魔像', type: 'crystal', stage: 3, arch: 'golem', status: 'done', fam: 'Slime' },
157
+ { id: 'L3_CAC', name: '🕷️ 錯誤蜘蛛', type: 'glitch', stage: 3, arch: 'spider', status: 'done', fam: 'Hacker' },
158
+ { id: 'L3_CAB', name: '👾 程式遊俠', type: 'tech', stage: 3, arch: 'ranger', status: 'done', fam: 'Hacker' },
159
+ { id: 'L3_CAA', name: '🧠 量子主腦', type: 'tech', stage: 3, arch: 'brain', status: 'done', fam: 'Hacker' },
160
+ { id: 'L3_BAC', name: '🚜 重裝推土機', type: 'mech', stage: 3, arch: 'tank', status: 'done', fam: 'Mech Lion' },
161
+ { id: 'L3_BAB', name: '✈️ 變形戰機', type: 'mech', stage: 3, arch: 'jet', status: 'done', fam: 'Mech Lion' },
162
+ { id: 'L3_BAA', name: '🤖 究極鋼彈', type: 'mech', stage: 3, arch: 'gundam', status: 'done', fam: 'Mech Lion' },
163
+ { id: 'L3_ACC', name: '💀 骷髏法師', type: 'undead', stage: 3, arch: 'mage_skull', status: 'done', fam: 'Ghost' },
164
+ { id: 'L3_ACB', name: '🕯️ 南瓜燈杰克', type: 'spirit', stage: 3, arch: 'pumpkin', status: 'done', fam: 'Ghost' },
165
+ { id: 'L3_ACA', name: '👻 虛空死神', type: 'void', stage: 3, arch: 'reaper', status: 'done', fam: 'Ghost' },
166
+
167
+ // Stage 3 - REDESIGN TARGETS (Wolf V2 & Cat)
168
+ { id: 'L3_BCC', name: '🧟 喪屍犬', type: 'undead', stage: 3, arch: 'wolf_zombie_v2', status: 'new', fam: 'Wolf' },
169
+ { id: 'L3_BCB', name: '🐕 警備犬', type: 'animal', stage: 3, arch: 'wolf_guard_v2', status: 'new', fam: 'Wolf' },
170
+ { id: 'L3_BCA', name: '🐺 合金戰狼', type: 'mech', stage: 3, arch: 'wolf_alloy_v2', status: 'new', fam: 'Wolf' },
171
+ { id: 'L3_BBC', name: '😼 流浪劍客', type: 'warrior', stage: 3, arch: 'ronin', status: 'new', fam: 'Cat' },
172
+ { id: 'L3_BBB', name: '👑 貓咪國王', type: 'royal', stage: 3, arch: 'king_cat', status: 'new', fam: 'Cat' },
173
+ { id: 'L3_BBA', name: '🦁 雷霆獅王', type: 'elemental', stage: 3, arch: 'lion_mane', status: 'new', fam: 'Cat' },
174
+ { id: 'L3_BBA', name: '🦁 雷霆獅王', type: 'elemental', stage: 3, arch: 'lion_mane', status: 'new', fam: 'Cat' },
175
+
176
+ // Stage 3 - NEW ADDITIONS (Angel & Cosmos)
177
+ { id: 'L3_ABC', name: '👁️ 座天使 Ophanim', type: 'holy', stage: 3, arch: 'biblical_angel', status: 'new', fam: 'Angel' },
178
+ { id: 'L3_ABB', name: '⚔️ 女武神 Valkyrie', type: 'warrior', stage: 3, arch: 'valkyrie', status: 'new', fam: 'Angel' },
179
+ { id: 'L3_ABA', name: '🔥 聖火鳳凰 Phoenix', type: 'divine', stage: 3, arch: 'phoenix', status: 'new', fam: 'Angel' },
180
+ { id: 'L3_AAC', name: '🛡️ 大地泰坦 Titan', type: 'cosmic', stage: 3, arch: 'terra_titan', status: 'new', fam: 'Cosmos' },
181
+ { id: 'L3_AAB', name: '🐋 星海鯨 Astro-Whale', type: 'cosmic', stage: 3, arch: 'cosmo_whale', status: 'new', fam: 'Cosmos' },
182
+ { id: 'L3_AAA', name: '🌌 創世龍 Genesis', type: 'god', stage: 3, arch: 'genesis_dragon', status: 'new', fam: 'Cosmos' },
183
+ ];
184
+
185
+ function mulberry32(a) {
186
+ return function () {
187
+ var t = a += 0x6D2B79F5;
188
+ t = Math.imul(t ^ (t >>> 15), t | 1);
189
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
190
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
191
+ }
192
+ }
193
+
194
+ const palettes = {
195
+ base: ['#94A3B8', '#64748B', '#475569', '#334155'],
196
+
197
+ // Standard
198
+ glitch: ['#18181b', '#27272a', '#3f3f46', '#ef4444'],
199
+ trash: ['#3f3f46', '#71717a', '#a1a1aa', '#ef4444'],
200
+ mimic: ['#854d0e', '#a16207', '#ca8a04', '#eab308'],
201
+ virus: ['#2e1065', '#581c87', '#7e22ce', '#bef264', '#84cc16'],
202
+ spider: ['#1e293b', '#0f172a', '#b91c1c', '#ef4444'],
203
+ slime: ['#059669', '#10b981', '#34d399', '#d1fae5'],
204
+ fluid: ['#3f3f46', '#52525b', '#4b5563', '#a1a1aa'],
205
+ knight: ['#0ea5e9', '#38bdf8', '#7dd3fc', '#ffffff'],
206
+ crypto: ['#0891b2', '#06b6d4', '#22d3ee', '#cffafe'],
207
+ tech: ['#22d3ee', '#06b6d4', '#0891b2', '#155e75'],
208
+ ranger: ['#3b82f6', '#2563eb', '#1d4ed8', '#fbbf24'],
209
+ brain: ['#d8b4fe', '#c084fc', '#a855f7', '#7e22ce', '#22d3ee'],
210
+ tank: ['#3f3f46', '#18181b', '#facc15', '#fbbf24'],
211
+ jet: ['#cbd5e1', '#94a3b8', '#ef4444', '#dc2626'],
212
+ gundam: ['#ffffff', '#2563eb', '#dc2626', '#facc15'],
213
+ mage: ['#4c1d95', '#a855f7', '#172554', '#fbbf24'],
214
+ pumpkin: ['#ea580c', '#c2410c', '#3f3f46', '#facc15'],
215
+ reaper: ['#000000', '#171717', '#7f1d1d', '#9ca3af'],
216
+
217
+ // Wolf V2
218
+ wolf: ['#c2410c', '#9a3412', '#7c2d12', '#ef4444'], // Rust/Browns
219
+ zombie: ['#44403c', '#78716c', '#991b1b', '#fecaca'],
220
+ guard: ['#172554', '#1e3a8a', '#fbbf24', '#f59e0b', '#3b82f6'],
221
+ alloy: ['#e2e8f0', '#94a3b8', '#0ea5e9', '#0284c7', '#38bdf8', '#ffffff'], // Added white/brights
222
+
223
+ // Cat V2
224
+ cat: ['#fb923c', '#ea580c', '#c2410c', '#fff7ed'],
225
+ ronin: ['#c2410c', '#9a3412', '#fde047', '#e2e8f0'],
226
+ king: ['#7e22ce', '#a855f7', '#fbbf24', '#ffffff'],
227
+ thunder: ['#facc15', '#eab308', '#fefce8', '#f59e0b', '#ffffff'],
228
+
229
+ // Base Fams
230
+ beast: ['#fbbf24', '#f59e0b', '#d97706', '#78350f'],
231
+ mech_lion: ['#60a5fa', '#3b82f6', '#1d4ed8', '#93c5fd'],
232
+ holy: ['#fef3c7', '#fde68a', '#f59e0b', '#ffffff'],
233
+ fire: ['#c084fc', '#a855f7', '#7e22ce', '#ffffff'],
234
+ space: ['#4f46e5', '#312e81', '#818cf8', '#f472b6', '#22d3ee'],
235
+
236
+ // Angel V3
237
+ biblical: ['#facc15', '#eab308', '#ffffff', '#ef4444', '#3b82f6'], // Gold + Eyes
238
+ valkyrie: ['#e2e8f0', '#94a3b8', '#facc15', '#38bdf8', '#fbbf24'], // Silver/Gold
239
+ seraphim: ['#dc2626', '#ef4444', '#fbbf24', '#facc15', '#ffffff'], // Fire/Gold
240
+
241
+ // Cosmos V3
242
+ meteor: ['#475569', '#334155', '#ea580c', '#f97316'], // Rock/Magma
243
+ whale: ['#1e1b4b', '#312e81', '#6366f1', '#c7d2fe', '#ffffff'], // Deep Space
244
+ genesis: ['#000000', '#171717', '#c084fc', '#22d3ee', '#f472b6', '#ffffff'] // Cosmic
245
+ };
246
+
247
+ function generateMonsterSVG(monster) {
248
+ let seed = 0;
249
+ for (let i = 0; i < monster.id.length; i++) seed += monster.id.charCodeAt(i);
250
+ const rng = mulberry32(seed);
251
+
252
+ let palette = palettes.base;
253
+ const t = monster.type;
254
+ const a = monster.arch;
255
+
256
+ // Palette Select
257
+ if (a.includes('wolf')) {
258
+ if (a.includes('zombie')) palette = palettes.zombie;
259
+ else if (a.includes('guard')) palette = palettes.guard;
260
+ else if (a.includes('alloy')) palette = palettes.alloy;
261
+ else palette = palettes.wolf;
262
+ }
263
+ else if (a === 'ronin') palette = palettes.ronin;
264
+ else if (a === 'king_cat') palette = palettes.king;
265
+ else if (a === 'lion_mane') palette = palettes.thunder;
266
+ else if (a === 'tank') palette = palettes.tank;
267
+ else if (a === 'jet') palette = palettes.jet;
268
+ else if (a === 'gundam') palette = palettes.gundam;
269
+ else if (a === 'mage_skull') palette = palettes.mage;
270
+ else if (a === 'pumpkin') palette = palettes.pumpkin;
271
+ else if (a === 'reaper') palette = palettes.reaper;
272
+
273
+ // Angel & Cosmos V3
274
+ else if (a === 'biblical_angel') palette = palettes.biblical;
275
+ else if (a === 'valkyrie') palette = palettes.valkyrie;
276
+ else if (a === 'phoenix') palette = palettes.seraphim; // Reuse Fire/Gold palette
277
+ else if (a === 'terra_titan') palette = palettes.meteor; // Reuse Rock palette
278
+ else if (a === 'cosmo_whale') palette = palettes.whale;
279
+ else if (a === 'genesis_dragon') palette = palettes.genesis;
280
+
281
+ // Beast Family
282
+ else if (a === 'beast_pup') palette = palettes.beast;
283
+ else if (a === 'beast_cat') palette = palettes.cat;
284
+ else if (a === 'beast_mech') palette = palettes.mech_lion;
285
+
286
+ // Spirit Family
287
+ else if (a === 'spirit_orb') palette = palettes.holy;
288
+ else if (a === 'spirit_fire') palette = palettes.fire;
289
+ else if (a === 'spirit_wing') palette = palettes.holy;
290
+ else if (a === 'spirit_dragon') palette = palettes.space;
291
+ // ... (Other palettes handled by fallbacks correctly as per previous logic)
292
+ // Safety fallbacks
293
+ if (a.includes('dust')) {
294
+ if (a === 'dust_trash' || a === 'pile_big' || a === 'box_teeth' || a === 'corona') palette = palettes.glitch; // Approx
295
+ if (a.includes('slime') || a === 'fluid' || a === 'knight_slime' || a === 'golem') palette = palettes.slime;
296
+ if (a.includes('tech') || a === 'spider' || a === 'ranger' || a === 'brain') palette = palettes.tech;
297
+ }
298
+ // Better-specific overrides
299
+ if (a === 'pile_big') palette = palettes.trash;
300
+ if (a === 'box_teeth') palette = palettes.mimic;
301
+ if (a === 'corona') palette = palettes.virus;
302
+ if (a === 'fluid') palette = palettes.fluid;
303
+ if (a === 'knight_slime') palette = palettes.knight;
304
+ if (a === 'golem') palette = palettes.crypto;
305
+ if (a === 'spider') palette = palettes.spider;
306
+ if (a === 'ranger') palette = palettes.ranger;
307
+ if (a === 'brain') palette = palettes.brain;
308
+
309
+ let grid = new Array(H).fill(0).map(() => new Array(W).fill(null));
310
+
311
+ const rect = (x, y, w, h, col) => {
312
+ for (let i = Math.max(0, Math.floor(x)); i < Math.min(W, x + w); i++)
313
+ for (let j = Math.max(0, Math.floor(y)); j < Math.min(H, y + h); j++)
314
+ grid[j][i] = col;
315
+ }
316
+ const circle = (cx, cy, r, col) => {
317
+ for (let y = 0; y < H; y++)
318
+ for (let x = 0; x < W; x++)
319
+ if (Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) < r) grid[y][x] = col;
320
+ }
321
+ const line = (x1, y1, x2, y2, col) => {
322
+ let dx = Math.abs(x2 - x1), sx = x1 < x2 ? 1 : -1;
323
+ let dy = -Math.abs(y2 - y1), sy = y1 < y2 ? 1 : -1;
324
+ let err = dx + dy, e2;
325
+ while (true) {
326
+ if (y1 >= 0 && y1 < H && x1 >= 0 && x1 < W) grid[y1][x1] = col;
327
+ if (x1 == x2 && y1 == y2) break;
328
+ const e2_val = 2 * err;
329
+ if (e2_val >= dy) { err += dy; x1 += sx; }
330
+ if (e2_val <= dx) { err += dx; y1 += sy; }
331
+ }
332
+ }
333
+
334
+ const mainCol = palette[Math.floor(rng() * (palette.length - 1))];
335
+ const secCol = palette[Math.floor(rng() * (palette.length - 2))] || palette[1];
336
+ const thirdCol = palette[palette.length - 1];
337
+
338
+ // --- STAGE 0-2 (Explicit Restoration) ---
339
+ 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); }
340
+ 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'); }
341
+ 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'); }
342
+ 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'); }
343
+ 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'); }
344
+ 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'); }
345
+ 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'); }
346
+ 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'); }
347
+ 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'); }
348
+ 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'); }
349
+ 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'); }
350
+ 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'); }
351
+ 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'); }
352
+
353
+ // --- STAGE 3 (Previously Done & APPROVED) ---
354
+ 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'); }
355
+ 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'); }
356
+ 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'); }
357
+ 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'); }
358
+ 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'); }
359
+ 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'); }
360
+ 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'); }
361
+ 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); }
362
+ 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); }
363
+ else if (a === 'tank') {
364
+ rect(C - 8, C + 4, 16, 4, '#1e293b'); rect(C - 9, C + 5, 18, 2, '#000000');
365
+ 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');
366
+ rect(C - 3, C - 3, 2, 2, '#3f3f46'); rect(C - 1, C - 2, 2, 1, '#ef4444');
367
+ }
368
+ else if (a === 'jet') {
369
+ 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);
370
+ rect(C - 3, C - 6, 6, 2, '#38bdf8'); rect(C - 3, C + 6, 2, 4, '#ef4444'); rect(C + 1, C + 6, 2, 4, '#ef4444');
371
+ }
372
+ else if (a === 'gundam') {
373
+ 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');
374
+ 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');
375
+ rect(C - 4, C + 4, 3, 6, '#ffffff'); rect(C + 1, C + 4, 3, 6, '#ffffff'); rect(C + 5, C, 2, 6, '#334155');
376
+ }
377
+ else if (a === 'mage_skull') {
378
+ 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');
379
+ 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');
380
+ }
381
+ else if (a === 'pumpkin') {
382
+ 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');
383
+ 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');
384
+ }
385
+ else if (a === 'reaper') {
386
+ 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');
387
+ rect(C + 6, C - 8, 1, 16, '#7f1d1d'); rect(C + 4, C - 10, 6, 2, '#9ca3af'); rect(C + 3, C - 10, 1, 6, '#9ca3af');
388
+ }
389
+
390
+ // --- REDESIGNED STAGE 3 (Wolf V2 & Cat V2 Targets) ---
391
+ else if (a === 'wolf_zombie_v2') {
392
+ rect(C - 6, C - 3, 13, 6, '#4b5563'); // Darker rot body
393
+ rect(C - 2, C - 2, 5, 4, '#991b1b'); // Ribcage
394
+ rect(C - 8, C - 7, 6, 6, '#4b5563'); // Head
395
+ rect(C - 6, C - 6, 2, 2, '#e7e5e4'); // Skull patch
396
+ rect(C - 5, C - 5, 1, 1, '#000000'); // Eye empty
397
+ rect(C - 7, C + 3, 2, 5, '#4b5563'); // Leg FL
398
+ rect(C + 4, C + 3, 2, 5, '#e7e5e4'); // Leg BR (Bone)
399
+ for (let i = 0; i < 4; i++) rect(C + (rng() - 0.5) * 14, C - 6 + (rng() - 0.5) * 8, 1, 1, '#bef264');
400
+ }
401
+ else if (a === 'wolf_guard_v2') {
402
+ rect(C - 7, C - 3, 14, 7, '#1e3a8a'); // Body Armor
403
+ rect(C - 3, C - 4, 6, 6, '#fbbf24'); // Heavy Plating
404
+ rect(C - 9, C - 8, 7, 7, '#172554'); // Helmet
405
+ rect(C - 9, C - 6, 7, 1, '#60a5fa'); // Visor
406
+ rect(C - 3, C - 6, 3, 3, '#1e40af'); // Pad
407
+ rect(C - 7, C + 4, 3, 5, '#3b82f6');
408
+ rect(C + 5, C + 4, 3, 5, '#3b82f6');
409
+ }
410
+ else if (a === 'wolf_alloy_v2') {
411
+ rect(C - 7, C - 3, 14, 5, '#e2e8f0'); // Silver Body
412
+ rect(C - 2, C - 8, 4, 6, '#94a3b8'); // Booster base
413
+ rect(C - 3, C - 9, 2, 4, '#0ea5e9'); // Thruster
414
+ rect(C - 9, C - 6, 7, 5, '#e2e8f0'); // Head
415
+ rect(C - 6, C - 6, 4, 1, '#0ea5e9'); // Visor
416
+ line(C + 7, C, C + 11, C - 4, '#0ea5e9'); // Saber tail
417
+ rect(C - 7, C + 5, 2, 3, '#94a3b8');
418
+ rect(C + 6, C + 5, 2, 3, '#94a3b8');
419
+ }
420
+ else if (a === 'ronin') {
421
+ rect(C - 3, C - 4, 6, 8, '#c2410c'); // Orange Gi
422
+ rect(C - 4, C - 10, 8, 4, '#fde047'); // Hat
423
+ rect(C - 3, C + 4, 2, 4, '#3f3f46'); // Hakama
424
+ rect(C + 1, C + 4, 2, 4, '#3f3f46');
425
+ line(C + 4, C - 4, C + 8, C - 8, '#e5e5e5'); // Blade handle
426
+ rect(C - 4, C - 3, 2, 2, '#ef4444'); // Scarf
427
+ }
428
+ else if (a === 'king_cat') {
429
+ rect(C - 4, C - 3, 8, 8, '#7e22ce'); // Robe
430
+ rect(C - 2, C - 3, 4, 8, '#ffffff'); // Fur
431
+ rect(C - 4, C - 10, 8, 3, '#fbbf24'); // Crown
432
+ rect(C - 1, C - 10, 2, 2, '#ef4444');
433
+ line(C + 6, C, C + 6, C + 6, '#fbbf24'); // Scepter
434
+ circle(C + 6, C - 1, 2, '#ef4444');
435
+ }
436
+ else if (a === 'lion_mane') {
437
+ rect(C - 4, C - 3, 8, 8, '#ffffff');
438
+ 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
439
+ rect(C - 2, C - 6, 4, 4, '#fff7ed'); // Face
440
+ rect(C - 1, C - 5, 2, 1, '#3b82f6'); // Eyes
441
+ line(C - 8, C, C - 4, C + 4, '#facc15'); // Lightning
442
+ line(C + 8, C, C + 4, C - 4, '#facc15');
443
+ }
444
+
445
+ // --- ANGEL FAMILY V3 ---
446
+ else if (a === 'biblical_angel') {
447
+ // Ophanim: Interlocking rings
448
+ for (let t = 0; t < 6.3; t += 0.1) {
449
+ let r = 9;
450
+ let x1 = C + Math.cos(t) * r; let y1 = C + Math.sin(t) * r * 0.4;
451
+ rect(x1, y1, 1, 1, mainCol); // Ring 1
452
+ let x2 = C + Math.cos(t) * r * 0.4; let y2 = C + Math.sin(t) * r;
453
+ rect(x2, y2, 1, 1, secCol); // Ring 2
454
+ }
455
+ circle(C, C, 4, '#ffffff'); // Core
456
+ // Eyes on rings
457
+ for (let i = 0; i < 8; i++) {
458
+ rect(C + Math.cos(i * 0.8) * 9, C + Math.sin(i * 0.8) * 3.5, 2, 2, '#ffffff');
459
+ rect(C + Math.cos(i * 0.8) * 9, C + Math.sin(i * 0.8) * 3.5, 1, 1, '#3b82f6');
460
+ }
461
+ }
462
+ else if (a === 'valkyrie') {
463
+ rect(C - 3, C - 5, 6, 8, '#e2e8f0'); // Armor Body
464
+ rect(C - 3, C - 9, 6, 4, '#facc15'); // Helm
465
+ rect(C - 4, C + 3, 3, 5, '#e2e8f0'); // Leg L
466
+ rect(C + 1, C + 3, 3, 5, '#e2e8f0'); // Leg R
467
+ rect(C - 8, C - 6, 4, 10, '#ffffff'); // Wing L
468
+ rect(C + 5, C - 6, 4, 10, '#ffffff'); // Wing R
469
+ line(C + 4, C + 8, C + 10, C - 8, '#fbbf24'); // Spear
470
+ rect(C + 9, C - 9, 2, 4, '#38bdf8'); // Spear Tip
471
+ }
472
+ else if (a === 'phoenix') {
473
+ // Body
474
+ rect(C - 2, C - 4, 4, 8, mainCol);
475
+ // Head
476
+ rect(C - 2, C - 8, 4, 4, mainCol);
477
+ rect(C, C - 7, 1, 1, '#fbbf24'); // Eye
478
+ rect(C - 3, C - 6, 1, 2, '#fbbf24'); // Beak
479
+
480
+ // Wings (Spread Y shape)
481
+ for (let i = 0; i < 8; i++) {
482
+ rect(C - 2 - i, C - 4 - i, 1, 2, '#ef4444');
483
+ rect(C + 1 + i, C - 4 - i, 1, 2, '#ef4444');
484
+ rect(C - 2 - i, C - 2 - i, 1, 4, secCol); // Feathers
485
+ rect(C + 1 + i, C - 2 - i, 1, 4, secCol);
486
+ }
487
+
488
+ // Tail
489
+ rect(C - 1, C + 4, 2, 6, '#fbbf24');
490
+ rect(C - 3, C + 6, 2, 4, '#ef4444');
491
+ rect(C + 1, C + 6, 2, 4, '#ef4444');
492
+ }
493
+
494
+ // --- COSMOS FAMILY V3 ---
495
+ else if (a === 'terra_titan') {
496
+ // Heavy Mech/Golem body
497
+ rect(C - 6, C - 6, 12, 10, mainCol); // Chest
498
+ rect(C - 8, C - 8, 4, 6, secCol); // Shoulder L
499
+ rect(C + 4, C - 8, 4, 6, secCol); // Shoulder R
500
+
501
+ // Head
502
+ rect(C - 3, C - 9, 6, 4, mainCol);
503
+ rect(C - 2, C - 8, 4, 1, '#f97316'); // Visor
504
+
505
+ // Limbs
506
+ rect(C - 8, C - 2, 3, 8, mainCol); // Arm L
507
+ rect(C + 5, C - 2, 3, 8, mainCol); // Arm R
508
+ rect(C - 6, C + 4, 4, 6, secCol); // Leg L
509
+ rect(C + 2, C + 4, 4, 6, secCol); // Leg R
510
+
511
+ // Core
512
+ circle(C, C - 2, 3, '#ea580c');
513
+ rect(C - 1, C - 3, 2, 2, '#f97316');
514
+ }
515
+
516
+ else if (a === 'cosmo_whale') {
517
+ // Massive Body (Sperm whale shape)
518
+ rect(C - 10, C - 6, 14, 10, mainCol); // Big Head/Front
519
+ rect(C + 4, C - 4, 6, 6, mainCol); // Mid
520
+ rect(C + 10, C - 2, 4, 3, mainCol); // Tail fuse
521
+
522
+ // Underbelly
523
+ rect(C - 10, C + 2, 12, 2, '#c7d2fe');
524
+
525
+ // Tail Fluke
526
+ rect(C + 12, C - 5, 2, 8, mainCol);
527
+ rect(C + 11, C - 6, 1, 2, mainCol);
528
+ rect(C + 11, C + 4, 1, 2, mainCol);
529
+
530
+ // Eye
531
+ rect(C - 5, C, 2, 1, '#ffffff');
532
+
533
+ // Blowhole Spray
534
+ for (let k = 0; k < 5; k++) rect(C - 6 + (rng() - 0.5) * 4, C - 10 + (rng() - 0.5) * 4, 1, 1, '#22d3ee');
535
+
536
+ // Stars/Spots
537
+ rect(C - 8, C - 3, 1, 1, '#ffffff');
538
+ rect(C - 2, C - 4, 1, 1, '#ffffff');
539
+ rect(C + 6, C - 1, 1, 1, '#ffffff');
540
+ }
541
+ else if (a === 'genesis_dragon') {
542
+ // Sinuous body
543
+ let px = C, py = C;
544
+ for (let i = 0; i < 30; i++) {
545
+ rect(px, py, 3, 3, i % 2 == 0 ? '#000000' : secCol);
546
+ px += Math.cos(i * 0.4) * 2;
547
+ py += Math.sin(i * 0.4) * 2;
548
+ }
549
+ circle(C, C, 5, '#c084fc'); // Void Core
550
+ // Cosmic Particles
551
+ for (let k = 0; k < 20; k++) {
552
+ rect(C + (rng() - 0.5) * 20, C + (rng() - 0.5) * 20, 1, 1, k % 3 == 0 ? '#22d3ee' : '#f472b6');
553
+ }
554
+ }
555
+
556
+ return renderSVG(grid);
557
+ }
558
+
559
+ const stages = [0, 1, 2, 3];
560
+ stages.forEach(stage => {
561
+ const section = document.createElement('div');
562
+ section.innerHTML = `<h2>Stage ${stage}</h2><div class="grid-container" id="stage-${stage}"></div>`;
563
+ gallery.appendChild(section);
564
+
565
+ const container = section.querySelector('.grid-container');
566
+
567
+ monsters.filter(m => m.stage === stage && (m.status === 'done' || m.status === 'new')).sort((a, b) => {
568
+ const famOrder = { 'Origin': 0, 'Dust': 1, 'Beast': 2, 'Spirit': 3, 'Glitch/Trash': 4, 'Slime': 5, 'Hacker': 6, 'Wolf': 7, 'Cat': 8, 'Mech Lion': 9, 'Ghost': 10, 'Angel': 11, 'Cosmos': 12 };
569
+ return (famOrder[a.fam] || 99) - (famOrder[b.fam] || 99);
570
+ }).forEach(m => {
571
+ const card = document.createElement('div');
572
+ card.className = 'card';
573
+ let badgeClass = m.status === 'new' ? 'new-badge' : 'done-badge';
574
+ card.innerHTML = `
575
+ <div class="fam-label">${m.fam || 'Base'}</div>
576
+ <div class="pixel-art mx-auto bg-slate-800 p-2">${generateMonsterSVG(m)}</div>
577
+ <div class="mt-4 font-bold text-sm text-slate-200" style="min-height: 48px; display: flex; align-items: center; justify-content: center;">${m.name.replace(' ', '<br>')}</div>
578
+ <span class="badge ${badgeClass}">${m.status.toUpperCase()}</span>
579
+ `;
580
+ container.appendChild(card);
581
+ });
582
+ });
583
+ </script>
584
+ </body>
585
+
586
+ </html>
src/services/classroom.js CHANGED
@@ -12,7 +12,8 @@ import {
12
  getDocs,
13
  orderBy,
14
  deleteDoc,
15
- updateDoc
 
16
  } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
17
 
18
  // Collection references
@@ -442,3 +443,40 @@ export async function markNotificationRead(notificationId) {
442
  read: true
443
  });
444
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  getDocs,
13
  orderBy,
14
  deleteDoc,
15
+ updateDoc,
16
+ getCountFromServer
17
  } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
18
 
19
  // Collection references
 
443
  read: true
444
  });
445
  }
446
+
447
+ /**
448
+ * Gets the number of users in a room
449
+ * @param {string} roomCode
450
+ * @returns {Promise<number>}
451
+ */
452
+ export async function getClassSize(roomCode) {
453
+ const q = query(
454
+ collection(db, USERS_COLLECTION),
455
+ where("current_room", "==", roomCode)
456
+ );
457
+ const snapshot = await getCountFromServer(q);
458
+ return snapshot.data().count;
459
+ }
460
+
461
+ /**
462
+ * Updates a student's monster stage
463
+ * @param {string} userId
464
+ * @param {number} newStage
465
+ */
466
+ export async function updateUserStage(userId, newStage) {
467
+ const userRef = doc(db, USERS_COLLECTION, userId);
468
+ await updateDoc(userRef, {
469
+ monster_stage: newStage
470
+ });
471
+ }
472
+
473
+ /**
474
+ * Gets user profile data
475
+ * @param {string} userId
476
+ * @returns {Promise<Object>}
477
+ */
478
+ export async function getUser(userId) {
479
+ const userRef = doc(db, USERS_COLLECTION, userId);
480
+ const snap = await getDoc(userRef);
481
+ return snap.exists() ? snap.data() : null;
482
+ }
src/utils/monsterUtils.js ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Logic for generating Pixel Art Monsters
3
+ * Adapted from monster_preview.html
4
+ */
5
+
6
+ export const MONSTER_STAGES = {
7
+ EGG: 0,
8
+ BASIC: 1,
9
+ EVOLVED: 2,
10
+ FINAL: 3
11
+ };
12
+
13
+ // Monster Definitions
14
+ export 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
+
62
+ const 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
+
115
+ function 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
+ */
129
+ export 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
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} [currentFam] - Current family hint (optional, for persistent lineage)
405
+ * @returns {Object} Selected Monster Definition
406
+ */
407
+ export function getNextMonster(currentStage, likes, classSize, currentFam = null) {
408
+ if (currentStage === 0) return MONSTER_DEFS.find(m => m.id === 'Egg');
409
+
410
+ // Stage 1: Basic Families
411
+ if (currentStage === 1) {
412
+ // Simple logic for Stage 1? Or Random?
413
+ // Let's use simple hash of likes or random to distribute if there's no clear indicator?
414
+ // Requirements say "After beginner 5 tasks, Stage 1 unlocks".
415
+ // Let's default to Dust or something based on likes?
416
+ // Actually the prompt says:
417
+ // "Stage 1 (Basic): Unlocks after Beginner... Three basic families: Dust, Pup, Spirit"
418
+ // Let's pick based on (Likes % 3) for variety or just return one?
419
+ // Let's return Spirit if Likes are high, Dust if low?
420
+
421
+ // Simple distribution for now:
422
+ if (likes > 5) return MONSTER_DEFS.find(m => m.id === 'L1_A'); // Spirit
423
+ if (likes > 2) return MONSTER_DEFS.find(m => m.id === 'L1_B'); // Pup
424
+ return MONSTER_DEFS.find(m => m.id === 'L1_C'); // Dust
425
+ }
426
+
427
+ // Branching Logic for Stage 2 & 3
428
+ // A: High Likes ( > classSize / 2 )
429
+ // B: Mid Likes ( 1 < likes <= classSize / 2 )
430
+ // C: Low Likes ( <= 1 )
431
+
432
+ const ratio = classSize > 0 ? likes / classSize : 0;
433
+
434
+ // Tier Logic
435
+ let tier = 'C';
436
+ if (likes > classSize / 2) tier = 'A';
437
+ else if (likes > 1) tier = 'B';
438
+
439
+ // Safety check for small classes
440
+ if (classSize <= 2 && likes >= 1) tier = 'A'; // Boost for testing pairs
441
+
442
+ // Filter potential monsters for this Stage
443
+ const candidates = MONSTER_DEFS.filter(m => m.stage === currentStage);
444
+
445
+ // Filter by Tier (Suffix A/B/C)
446
+ // IDs are like L2_AA, L2_AB... allow flexible matching.
447
+ // Last char of ID corresponds to Tier? (A=High, B=Mid, C=Low)
448
+ // Yes: L3_AAA (High), L3_CAA (High? No wait, logic in readme says:
449
+ // Tier 1 (Low) -> C route.
450
+ // Tier 3 (High) -> A route.
451
+
452
+ const targetSuffix = tier;
453
+
454
+ // If we have a current family, try to stick to it?
455
+ // Implementation Plan didn't specify strict lineage tracking yet, but usually we want to.
456
+
457
+ // Let's filter candidates ending in targetSuffix
458
+ let match = candidates.find(m => m.id.endsWith(targetSuffix) && (!currentFam || m.fam === currentFam));
459
+
460
+ // If no match (e.g. family change allowed or not found), just pick any of that Tier
461
+ if (!match) match = candidates.find(m => m.id.endsWith(targetSuffix));
462
+
463
+ return match || MONSTER_DEFS[0]; // Fallback
464
+ }
src/views/AdminView.js CHANGED
@@ -107,19 +107,62 @@ async function loadChallenges() {
107
  const list = document.getElementById('challenges-list');
108
  const challenges = await getChallenges();
109
 
110
- list.innerHTML = challenges.map(c => `
111
- <div class="bg-gray-800 p-4 rounded-lg border border-gray-700 flex justify-between items-center group hover:border-gray-500 transition-colors">
112
- <div>
113
- <span class="inline-block px-2 py-1 text-xs rounded bg-gray-700 text-gray-300 mr-2">${c.level}</span>
114
- <span class="font-bold text-white text-lg">${c.title}</span>
115
- <p class="text-gray-400 text-sm mt-1 truncate max-w-md">${c.description}</p>
116
- </div>
117
- <div class="flex space-x-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity">
118
- <button onclick="window.editChallenge('${c.id}')" class="bg-cyan-600/20 text-cyan-400 px-3 py-1 rounded hover:bg-cyan-600 hover:text-white transition-colors">編輯</button>
119
- <button onclick="window.deleteChallenge('${c.id}')" class="bg-red-600/20 text-red-400 px-3 py-1 rounded hover:bg-red-600 hover:text-white transition-colors">刪除</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  </div>
121
- </div>
122
- `).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  // Expose helpers globally for onclick
125
  window.editChallenge = (id) => {
@@ -134,14 +177,16 @@ async function loadChallenges() {
134
  };
135
  }
136
 
137
- function openModal(challenge = null) {
 
138
  const modal = document.getElementById('challenge-modal');
139
  const title = document.getElementById('modal-title');
140
 
141
  // Reset or Fill
142
  document.getElementById('edit-id').value = challenge ? challenge.id : '';
143
  document.getElementById('edit-title').value = challenge ? challenge.title : '';
144
- document.getElementById('edit-level').value = challenge ? challenge.level : 'beginner';
 
145
  document.getElementById('edit-desc').value = challenge ? challenge.description : '';
146
  document.getElementById('edit-link').value = challenge ? challenge.link : '';
147
  document.getElementById('edit-order').value = challenge ? challenge.order : '1';
 
107
  const list = document.getElementById('challenges-list');
108
  const challenges = await getChallenges();
109
 
110
+ const levels = ['beginner', 'intermediate', 'advanced'];
111
+ const levelNames = {
112
+ beginner: "初級 (Beginner)",
113
+ intermediate: "中級 (Intermediate)",
114
+ advanced: "高級 (Advanced)"
115
+ };
116
+
117
+ // Group challenges
118
+ const groups = { beginner: [], intermediate: [], advanced: [] };
119
+ challenges.forEach(c => {
120
+ if (groups[c.level]) groups[c.level].push(c);
121
+ else groups.beginner.push(c); // Fallback
122
+ });
123
+
124
+ list.innerHTML = levels.map(level => {
125
+ const groupItems = groups[level] || [];
126
+ const isOpen = level === 'beginner' ? 'open' : ''; // Open first by default
127
+
128
+ const itemsHtml = groupItems.map(c => `
129
+ <div class="bg-gray-800 p-4 rounded-lg border border-gray-700 flex justify-between items-center group hover:border-gray-500 transition-colors mb-2">
130
+ <div class="flex items-center space-x-3">
131
+ <span class="text-gray-500 font-mono text-sm">#${c.order}</span>
132
+ <div>
133
+ <span class="font-bold text-white text-lg">${c.title}</span>
134
+ <p class="text-gray-400 text-sm mt-1 truncate max-w-md">${c.description}</p>
135
+ </div>
136
+ </div>
137
+ <div class="flex space-x-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity">
138
+ <button onclick="window.editChallenge('${c.id}')" class="bg-cyan-600/20 text-cyan-400 px-3 py-1 rounded hover:bg-cyan-600 hover:text-white transition-colors">編輯</button>
139
+ <button onclick="window.deleteChallenge('${c.id}')" class="bg-red-600/20 text-red-400 px-3 py-1 rounded hover:bg-red-600 hover:text-white transition-colors">刪除</button>
140
+ </div>
141
  </div>
142
+ `).join('');
143
+
144
+ return `
145
+ <details class="group border border-gray-700 rounded-xl bg-gray-800/30 overflow-hidden mb-4" ${isOpen}>
146
+ <summary class="flex items-center justify-between p-4 cursor-pointer bg-gray-800 hover:bg-gray-700 transition-colors select-none">
147
+ <div class="flex items-center space-x-3">
148
+ <h3 class="text-xl font-bold text-cyan-400">${levelNames[level]}</h3>
149
+ <span class="text-xs text-gray-400 bg-gray-900 px-2 py-1 rounded-full">${groupItems.length} 題</span>
150
+ </div>
151
+ <div class="flex items-center space-x-4">
152
+ <button onclick="event.preventDefault(); window.openModal(null, '${level}')" class="bg-green-600 hover:bg-green-500 text-white text-xs font-bold py-1 px-3 rounded transition-colors shadow-lg">
153
+ + 新增至此區
154
+ </button>
155
+ <svg class="w-5 h-5 text-gray-400 transform group-open:rotate-180 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
156
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
157
+ </svg>
158
+ </div>
159
+ </summary>
160
+ <div class="p-4 pt-4 border-t border-gray-700/50">
161
+ ${itemsHtml || '<div class="text-gray-500 text-center italic">尚無題目,請新增</div>'}
162
+ </div>
163
+ </details>
164
+ `;
165
+ }).join('');
166
 
167
  // Expose helpers globally for onclick
168
  window.editChallenge = (id) => {
 
177
  };
178
  }
179
 
180
+ // Global expose for the "Add to Section" button
181
+ window.openModal = function (challenge = null, defaultLevel = 'beginner') {
182
  const modal = document.getElementById('challenge-modal');
183
  const title = document.getElementById('modal-title');
184
 
185
  // Reset or Fill
186
  document.getElementById('edit-id').value = challenge ? challenge.id : '';
187
  document.getElementById('edit-title').value = challenge ? challenge.title : '';
188
+ // Use challenge level if editing, otherwise use defaultLevel passed from section button
189
+ document.getElementById('edit-level').value = challenge ? challenge.level : defaultLevel;
190
  document.getElementById('edit-desc').value = challenge ? challenge.description : '';
191
  document.getElementById('edit-link').value = challenge ? challenge.link : '';
192
  document.getElementById('edit-order').value = challenge ? challenge.order : '1';
src/views/InstructorView.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createRoom, subscribeToRoom, getChallenges } from "../services/classroom.js";
2
 
3
  let cachedChallenges = [];
4
 
@@ -40,6 +40,9 @@ export async function renderInstructorView() {
40
  <button id="btn-show-stage" class="bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 px-8 rounded-xl flex items-center space-x-2 text-lg">
41
  <span>🖥️ 投放到大螢幕 (本機)</span>
42
  </button>
 
 
 
43
  <!-- Future Feature: Send to Students -->
44
  <!--
45
  <button id="btn-broadcast-all" class="bg-purple-600 hover:bg-purple-500 text-white font-bold py-3 px-8 rounded-xl flex items-center space-x-2 text-lg opacity-50 cursor-not-allowed" title="此功能開發中">
@@ -229,6 +232,30 @@ export function setupInstructorEvents() {
229
  const author = document.getElementById('broadcast-author').textContent;
230
  window.openStage(prompt, author);
231
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  }
233
 
234
  /**
@@ -296,7 +323,7 @@ function renderTransposedHeatmap(students) {
296
  statusClass = 'bg-green-500/20 border-green-500/50 hover:bg-green-500/40 cursor-pointer shadow-[0_0_10px_rgba(34,197,94,0.1)]';
297
  content = '✅';
298
  const safePrompt = p.prompt.replace(/"/g, '&quot;').replace(/'/g, "\\'");
299
- action = `onclick="window.showBroadcastModal('${student.nickname}', '${c.title}', '${safePrompt}')"`;
300
  } else if (p.status === 'started') {
301
  // Check stuck
302
  const startedAt = p.timestamp ? p.timestamp.toDate() : new Date();
@@ -342,7 +369,7 @@ function renderTransposedHeatmap(students) {
342
  }
343
 
344
  // Global scope for HTML access
345
- window.showBroadcastModal = (nickname, title, prompt) => {
346
  const modal = document.getElementById('broadcast-modal');
347
  const content = document.getElementById('broadcast-content');
348
 
@@ -351,6 +378,10 @@ window.showBroadcastModal = (nickname, title, prompt) => {
351
  document.getElementById('broadcast-challenge').textContent = title;
352
  document.getElementById('broadcast-prompt').textContent = prompt;
353
 
 
 
 
 
354
  modal.classList.remove('hidden');
355
  // Animation trigger
356
  setTimeout(() => {
 
1
+ import { createRoom, subscribeToRoom, getChallenges, resetProgress } from "../services/classroom.js";
2
 
3
  let cachedChallenges = [];
4
 
 
40
  <button id="btn-show-stage" class="bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 px-8 rounded-xl flex items-center space-x-2 text-lg">
41
  <span>🖥️ 投放到大螢幕 (本機)</span>
42
  </button>
43
+ <button id="btn-reject-task" class="bg-red-600 hover:bg-red-500 text-white font-bold py-3 px-8 rounded-xl flex items-center space-x-2 text-lg shadow-lg">
44
+ <span>🛑 退回重做 (Reject)</span>
45
+ </button>
46
  <!-- Future Feature: Send to Students -->
47
  <!--
48
  <button id="btn-broadcast-all" class="bg-purple-600 hover:bg-purple-500 text-white font-bold py-3 px-8 rounded-xl flex items-center space-x-2 text-lg opacity-50 cursor-not-allowed" title="此功能開發中">
 
232
  const author = document.getElementById('broadcast-author').textContent;
233
  window.openStage(prompt, author);
234
  });
235
+
236
+ // Reject Logic
237
+ document.getElementById('btn-reject-task').addEventListener('click', async () => {
238
+ if (!confirm('確定要退回此題目讓學員重做嗎?')) return;
239
+
240
+ // We need student ID (userId) and Challenge ID.
241
+ // Currently showBroadcastModal only receives nickname, title, prompt.
242
+ // We need to attach data-userid and data-challengeid to the modal.
243
+ const modal = document.getElementById('broadcast-modal');
244
+ const userId = modal.dataset.userId;
245
+ const challengeId = modal.dataset.challengeId;
246
+ const roomCode = localStorage.getItem('vibecoding_instructor_room');
247
+
248
+ if (userId && challengeId && roomCode) {
249
+ try {
250
+ await resetProgress(userId, roomCode, challengeId);
251
+ // Close modal
252
+ window.closeBroadcast();
253
+ } catch (e) {
254
+ console.error(e);
255
+ alert('退回失敗');
256
+ }
257
+ }
258
+ });
259
  }
260
 
261
  /**
 
323
  statusClass = 'bg-green-500/20 border-green-500/50 hover:bg-green-500/40 cursor-pointer shadow-[0_0_10px_rgba(34,197,94,0.1)]';
324
  content = '✅';
325
  const safePrompt = p.prompt.replace(/"/g, '&quot;').replace(/'/g, "\\'");
326
+ action = `onclick="window.showBroadcastModal('${student.id}', '${c.id}', '${student.nickname}', '${c.title}', '${safePrompt}')"`;
327
  } else if (p.status === 'started') {
328
  // Check stuck
329
  const startedAt = p.timestamp ? p.timestamp.toDate() : new Date();
 
369
  }
370
 
371
  // Global scope for HTML access
372
+ window.showBroadcastModal = (userId, challengeId, nickname, title, prompt) => {
373
  const modal = document.getElementById('broadcast-modal');
374
  const content = document.getElementById('broadcast-content');
375
 
 
378
  document.getElementById('broadcast-challenge').textContent = title;
379
  document.getElementById('broadcast-prompt').textContent = prompt;
380
 
381
+ // Store IDs for actions
382
+ modal.dataset.userId = userId;
383
+ modal.dataset.challengeId = challengeId;
384
+
385
  modal.classList.remove('hidden');
386
  // Animation trigger
387
  setTimeout(() => {
src/views/StudentView.js CHANGED
@@ -1,4 +1,6 @@
1
- import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPrompts, resetProgress, toggleLike, subscribeToNotifications, markNotificationRead } from "../services/classroom.js";
 
 
2
 
3
  // Cache challenges locally
4
  let cachedChallenges = [];
@@ -128,20 +130,103 @@ export async function renderStudentView() {
128
  advanced: "高級 (Advanced)"
129
  };
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  // Accordion Layout
132
  return `
133
- <div class="min-h-screen p-4 pb-32 max-w-md mx-auto sm:max-w-4xl">
134
- <header class="flex justify-between items-center mb-6 sticky top-0 bg-slate-900/95 backdrop-blur z-20 py-4 px-2 -mx-2 border-b border-gray-800">
135
- <div class="flex flex-col">
 
136
  <div class="flex items-center space-x-2">
137
  <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
138
  <span class="text-gray-400 text-sm truncate max-w-[150px]">${nickname}</span>
139
  </div>
140
  <div class="text-xs text-gray-500 mt-1">教室: <span class="font-mono text-cyan-400 font-bold">${roomCode}</span></div>
141
  </div>
142
- <div>
 
143
  <h1 class="text-xl font-bold italic text-white tracking-widest">VIBECODING</h1>
144
- </div>
145
  </header>
146
 
147
  <div class="space-y-4">
@@ -432,3 +517,20 @@ window.handleLike = async (progressId, targetUserId) => {
432
  loadPeerPrompts(select.value);
433
  }
434
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPrompts, resetProgress, toggleLike, subscribeToNotifications, markNotificationRead, getClassSize, updateUserStage, getUser } from "../services/classroom.js";
2
+ import { generateMonsterSVG, getNextMonster, MONSTER_STAGES } from "../utils/monsterUtils.js";
3
+ import { generateMonsterSVG, getNextMonster, MONSTER_STAGES } from "../utils/monsterUtils.js";
4
 
5
  // Cache challenges locally
6
  let cachedChallenges = [];
 
130
  advanced: "高級 (Advanced)"
131
  };
132
 
133
+ // --- Monster Evolution Logic ---
134
+ let classSize = 1;
135
+ let userProfile = {};
136
+ try {
137
+ classSize = await getClassSize(roomCode);
138
+ userProfile = await getUser(userId) || {};
139
+ } catch (e) { console.error("Fetch stats error", e); }
140
+
141
+ // Calculate Stats
142
+ const totalLikes = Object.values(userProgress).reduce((acc, p) => acc + (p.likes || 0), 0);
143
+ const completedCounts = {
144
+ 0: 0,
145
+ 1: cachedChallenges.filter(c => c.level === 'beginner' && userProgress[c.id]?.status === 'completed').length,
146
+ 2: cachedChallenges.filter(c => c.level === 'intermediate' && userProgress[c.id]?.status === 'completed').length,
147
+ 3: cachedChallenges.filter(c => c.level === 'advanced' && userProgress[c.id]?.status === 'completed').length
148
+ };
149
+
150
+ // 1. Calculate Potential Stage (What they qualify for)
151
+ let potentialStage = 0;
152
+ if (completedCounts[1] >= 5) potentialStage = 1;
153
+ if (completedCounts[2] >= 5 && potentialStage >= 1) potentialStage = 2;
154
+ if (completedCounts[3] >= 5 && potentialStage >= 2) potentialStage = 3;
155
+
156
+ // 2. Get Actual Stage (What they have chosen/evolved to)
157
+ const actualStage = userProfile.monster_stage || 0;
158
+
159
+ // 3. Determine Display Logic
160
+ // If Potential > Actual => Evolution Available.
161
+ // If they don't evolve, they get BIGGER.
162
+ // Scale Factor: 1.0 + (Potential - Actual) * 0.3
163
+ const scale = 1 + (potentialStage - actualStage) * 0.3;
164
+ const canEvolve = potentialStage > actualStage;
165
+
166
+ // Get Monster Data for Current Actual Stage
167
+ const monster = getNextMonster(actualStage, totalLikes, classSize);
168
+
169
+ // Monster UI HTML
170
+ const monsterHtml = `
171
+ <div class="fixed top-6 left-6 z-50 flex flex-col items-center group pointer-events-none sm:pointer-events-auto">
172
+ <!-- Monster with Dynamic Scale -->
173
+ <div class="pixel-art-container relative transform transition-transform duration-500 ease-out hover:scale-110 origin-center" style="transform: scale(${scale});">
174
+ <div class="pixel-monster w-20 h-20 sm:w-24 sm:h-24 drop-shadow-2xl filter" style="animation: breathe 3s infinite ease-in-out;">
175
+ ${generateMonsterSVG(monster)}
176
+ </div>
177
+
178
+ <!-- Stage Indicator Badge -->
179
+ <div class="absolute -bottom-2 -right-2 bg-gray-900/80 text-xs text-yellow-500 px-1.5 py-0.5 rounded border border-yellow-500/30 font-mono transform scale-75 origin-top-left">
180
+ Lv.${monster.stage}
181
+ </div>
182
+ </div>
183
+
184
+ <!-- Evolution Trigger (Only if canEvolve) -->
185
+ ${canEvolve ? `
186
+ <div class="animate-bounce mt-4 pointer-events-auto">
187
+ <button onclick="window.triggerEvolution(${actualStage + 1})" class="bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-400 hover:to-purple-500 text-white font-bold py-2 px-4 rounded-full shadow-[0_0_15px_rgba(236,72,153,0.5)] border-2 border-white/20 text-xs sm:text-sm flex items-center space-x-2 transition-all hover:scale-105 active:scale-95">
188
+ <span class="text-lg">✨</span>
189
+ <span>進化 (Evolve)!</span>
190
+ </button>
191
+ <div class="text-[10px] text-center text-pink-300 mt-1 text-shadow bg-black/50 rounded px-1">
192
+ 或是忽略並保持巨大化!
193
+ </div>
194
+ </div>
195
+ ` : ''}
196
+
197
+ <!-- Stats Tooltip (Show on Hover) -->
198
+ <div class="opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gray-900/90 backdrop-blur text-xs text-gray-300 p-2 rounded-lg border border-gray-700 mt-2 text-center pointer-events-auto shadow-xl ${canEvolve ? 'mt-1' : 'mt-4'}">
199
+ <div class="font-bold text-white mb-1">${monster.name}</div>
200
+ <div>💖 Likes: ${totalLikes}</div>
201
+ <div class="text-[10px] text-gray-500">Class: ${classSize}</div>
202
+ ${scale > 1 ? `<div class="text-green-400 font-bold mt-1">巨大化 x${scale.toFixed(1)}</div>` : ''}
203
+ </div>
204
+ </div>
205
+
206
+ <style>
207
+ @keyframes breathe {
208
+ 0%, 100% { transform: translateY(0); }
209
+ 50% { transform: translateY(-3px); }
210
+ }
211
+ </style>
212
+ `;
213
+
214
  // Accordion Layout
215
  return `
216
+ ${monsterHtml}
217
+ <div class="min-h-screen p-4 pb-32 max-w-md mx-auto sm:max-w-4xl pt-24 sm:pt-4">
218
+ <header class="flex justify-end items-center mb-6 sticky top-0 bg-slate-900/95 backdrop-blur z-20 py-4 px-2 -mx-2 border-b border-gray-800">
219
+ <div class="flex flex-col items-end">
220
  <div class="flex items-center space-x-2">
221
  <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
222
  <span class="text-gray-400 text-sm truncate max-w-[150px]">${nickname}</span>
223
  </div>
224
  <div class="text-xs text-gray-500 mt-1">教室: <span class="font-mono text-cyan-400 font-bold">${roomCode}</span></div>
225
  </div>
226
+ <!-- Logo removed/minimized since we have Monster -->
227
+ <!-- <div>
228
  <h1 class="text-xl font-bold italic text-white tracking-widest">VIBECODING</h1>
229
+ </div> -->
230
  </header>
231
 
232
  <div class="space-y-4">
 
517
  loadPeerPrompts(select.value);
518
  }
519
  };
520
+
521
+ window.triggerEvolution = async (nextStage) => {
522
+ const userId = localStorage.getItem('vibecoding_user_id');
523
+ if (!confirm(`是否要進化到階段 ${nextStage}?\n(進化後怪獸大小將重置)`)) return;
524
+
525
+ try {
526
+ const { updateUserStage } = await import("../services/classroom.js");
527
+ await updateUserStage(userId, nextStage);
528
+ alert("✨ 進化成功!");
529
+ // Reload view
530
+ const app = document.querySelector('#app');
531
+ app.innerHTML = await renderStudentView();
532
+ } catch (e) {
533
+ console.error(e);
534
+ alert("進化失敗");
535
+ }
536
+ };