Spaces:
Running
Running
Upload 10 files
Browse files- monster_preview.html +586 -0
- src/services/classroom.js +39 -1
- src/utils/monsterUtils.js +464 -0
- src/views/AdminView.js +59 -14
- src/views/InstructorView.js +34 -3
- src/views/StudentView.js +108 -6
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 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
</div>
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
// Expose helpers globally for onclick
|
| 125 |
window.editChallenge = (id) => {
|
|
@@ -134,14 +177,16 @@ async function loadChallenges() {
|
|
| 134 |
};
|
| 135 |
}
|
| 136 |
|
| 137 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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, '"').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, '"').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 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
| 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 |
-
<
|
|
|
|
| 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 |
+
};
|