Lashtw commited on
Commit
5bdcc59
·
verified ·
1 Parent(s): 2db8c1d

Upload 9 files

Browse files
Files changed (1) hide show
  1. src/views/StudentView.js +93 -40
src/views/StudentView.js CHANGED
@@ -131,8 +131,10 @@ export async function renderStudentView() {
131
  };
132
 
133
  // --- Monster Section Render Function ---
134
- const renderMonsterSection = (currentUserProgress, classSize, userProfile) => {
135
- // Calculate Stats
 
 
136
  const totalLikes = Object.values(currentUserProgress).reduce((acc, p) => acc + (p.likes || 0), 0);
137
 
138
  // Count completions per level
@@ -143,30 +145,26 @@ export async function renderStudentView() {
143
  };
144
  const totalCompleted = counts[1] + counts[2] + counts[3];
145
 
146
- // 1. Calculate Potential Stage
147
  let potentialStage = 0;
148
  if (counts[1] >= 5) potentialStage = 1;
149
  if (counts[2] >= 5 && potentialStage >= 1) potentialStage = 2;
150
  if (counts[3] >= 5 && potentialStage >= 2) potentialStage = 3;
151
 
152
- // 2. Get Actual Stage & ID
153
  const actualStage = userProfile.monster_stage || 0;
154
  const actualMonsterId = userProfile.monster_id || 'Egg';
155
 
156
- // --- REGRESSION LOGIC (Auto-Devolve) ---
157
- // If actual stage > potential stage, it means tasks were reset/rejected.
158
- // We must devolve.
159
  if (actualStage > potentialStage) {
160
- // Regression Logic
161
- // Call DB update and then force reload to reflect changes
162
  updateUserMonster(userId, potentialStage, null).then(() => {
163
  setTimeout(() => window.location.reload(), 500);
164
  });
165
- // Show potential stage temporarily to avoid glitch
166
- return renderMonsterSection(currentUserProgress, classSize, { ...userProfile, monster_stage: potentialStage });
 
 
 
167
  }
168
 
169
- // 3. Display Logic
170
  const canEvolve = potentialStage > actualStage;
171
 
172
  // Scale Logic
@@ -174,61 +172,80 @@ export async function renderStudentView() {
174
  const baseScale = 1.0;
175
  const currentScale = baseScale + (totalCompleted * growthFactor);
176
 
177
- // Get Monster Data (Preserve Lineage)
178
- // If we have an actual ID, use it for display until evolution.
179
- // If we are about to evolve, we preview next? No, current.
180
- let monster = getNextMonster(actualStage, 0, 0, actualMonsterId); // Just lookup current by ID/Stage?
181
  if (actualMonsterId && actualMonsterId !== 'Egg') {
182
- // If we have a specific ID stored, try to use it directly
183
  const stored = MONSTER_DEFS.find(m => m.id === actualMonsterId);
184
  if (stored) monster = stored;
185
  } else {
186
- // Fallback for Egg or legacy data without ID
187
  monster = getNextMonster(actualStage, totalLikes, classSize);
188
  }
189
 
190
- // Left sidebar position: Fixed Left-8 or similar.
191
- // User wants it in the empty left area.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  return `
193
- <div id="monster-container-fixed" class="fixed top-32 left-8 z-50 flex flex-col items-center group pointer-events-none sm:pointer-events-auto w-32 h-32">
194
- <!-- Walking Container -->
 
 
 
 
 
 
195
  <!-- Walking Container (Handles Movement Only) -->
196
  <div class="pixel-art-container relative transform transition-transform duration-500 ease-out origin-center"
197
- style="transform: scale(${currentScale}); animation: patrol-move 15s linear infinite;">
198
 
199
  <!-- Monster Sprite (Handles Flip & Breathe) -->
200
  <div class="pixel-monster w-28 h-28 drop-shadow-2xl filter" style="animation: breathe 3s infinite ease-in-out, patrol-flip 15s linear infinite;">
201
- ${generateMonsterSVG(monster)}
202
  </div>
203
 
204
  <!-- Level Indicator (No Flip) -->
205
  <div class="absolute -bottom-2 -right-2 bg-gray-900/90 text-xs text-yellow-400 px-2 py-0.5 rounded-full border border-yellow-500/50 font-mono font-bold transform scale-75 origin-top-left whitespace-nowrap">
206
- Lv.${1 + totalCompleted}
207
  </div>
208
  </div>
209
 
210
  <!-- Evolution Prompt -->
211
- ${canEvolve ? `
212
  <div id="evolution-prompt" class="absolute top-full mt-2 pointer-events-auto animate-bounce z-50">
213
  <div class="flex flex-col items-center">
214
  <div class="bg-gray-900/90 text-pink-200 text-xs py-2 px-3 rounded-xl border border-pink-500/30 shadow-lg text-center font-bold mb-1 backdrop-blur-sm whitespace-nowrap">
215
  咦,小怪獸的樣子<br>正在發生變化...
216
  </div>
217
- <button onclick="window.triggerEvolution(${actualStage}, ${actualStage + 1}, ${totalLikes}, ${classSize}, '${monster.id}')"
218
  class="bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-500 hover:to-purple-500 text-white text-sm font-black py-2 px-6 rounded-full shadow-[0_0_15px_rgba(236,72,153,0.8)] border border-white/30 transition-all hover:scale-110 active:scale-95">
219
  進化!
220
  </button>
221
  </div>
222
  </div>
223
  ` : ''}
224
-
225
- <!-- Stats Tooltip -->
226
- <div class="opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gray-900/90 backdrop-blur text-xs text-slate-300 p-3 rounded-xl border border-slate-700 mt-6 text-left pointer-events-auto shadow-2xl min-w-[100px]">
227
- <div class="font-bold text-white text-sm mb-1 text-center border-b border-gray-700 pb-1">${monster.name}</div>
228
- <div class="space-y-1 mt-1 text-center">
229
- <div class=""><span>💖</span> <span class="text-pink-400 font-bold ml-1">${totalLikes}</span></div>
230
- </div>
231
- </div>
232
  </div>
233
 
234
  <style>
@@ -278,12 +295,48 @@ export async function renderStudentView() {
278
  window.currentProgressUnsub = subscribeToUserProgress(userId, (newProgressMap) => {
279
  // Merge updates
280
  const updatedProgress = { ...userProgress, ...newProgressMap };
281
- // Re-render only Monster Section
282
- monsterContainer.innerHTML = renderMonsterSection(updatedProgress, classSize, userProfile);
283
 
284
- // Update userProgress ref for other logic?
285
- // Note: 'userProgress' variable in this scope won't update for 'renderTaskCard' unless we reload.
286
- // But for Monster UI it's fine.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  });
288
 
289
  // Accordion Layout
 
131
  };
132
 
133
  // --- Monster Section Render Function ---
134
+ // --- Monster Section Helper Functions ---
135
+
136
+ // 1. Calculate Monster State (Separated logic)
137
+ const calculateMonsterState = (currentUserProgress, classSize, userProfile) => {
138
  const totalLikes = Object.values(currentUserProgress).reduce((acc, p) => acc + (p.likes || 0), 0);
139
 
140
  // Count completions per level
 
145
  };
146
  const totalCompleted = counts[1] + counts[2] + counts[3];
147
 
 
148
  let potentialStage = 0;
149
  if (counts[1] >= 5) potentialStage = 1;
150
  if (counts[2] >= 5 && potentialStage >= 1) potentialStage = 2;
151
  if (counts[3] >= 5 && potentialStage >= 2) potentialStage = 3;
152
 
 
153
  const actualStage = userProfile.monster_stage || 0;
154
  const actualMonsterId = userProfile.monster_id || 'Egg';
155
 
156
+ // Regression Logic Check
 
 
157
  if (actualStage > potentialStage) {
 
 
158
  updateUserMonster(userId, potentialStage, null).then(() => {
159
  setTimeout(() => window.location.reload(), 500);
160
  });
161
+ // Return corrected state temporary
162
+ return {
163
+ ...calculateMonsterState(currentUserProgress, classSize, { ...userProfile, monster_stage: potentialStage }),
164
+ isRegressing: true
165
+ };
166
  }
167
 
 
168
  const canEvolve = potentialStage > actualStage;
169
 
170
  // Scale Logic
 
172
  const baseScale = 1.0;
173
  const currentScale = baseScale + (totalCompleted * growthFactor);
174
 
175
+ // Get Monster Data
176
+ let monster = getNextMonster(actualStage, 0, 0, actualMonsterId);
 
 
177
  if (actualMonsterId && actualMonsterId !== 'Egg') {
 
178
  const stored = MONSTER_DEFS.find(m => m.id === actualMonsterId);
179
  if (stored) monster = stored;
180
  } else {
 
181
  monster = getNextMonster(actualStage, totalLikes, classSize);
182
  }
183
 
184
+ return {
185
+ monster,
186
+ currentScale,
187
+ totalCompleted,
188
+ totalLikes,
189
+ canEvolve,
190
+ actualStage,
191
+ classSize,
192
+ counts
193
+ };
194
+ };
195
+
196
+ // 2. Render Stats HTML (Partial Update Target)
197
+ const renderMonsterStats = (state) => {
198
+ return `
199
+ <div class="font-bold text-white text-sm mb-1 text-center border-b border-gray-700 pb-1">${state.monster.name}</div>
200
+ <div class="space-y-1 mt-1 text-center">
201
+ <div class=""><span>💖</span> <span class="text-pink-400 font-bold ml-1">${state.totalLikes}</span></div>
202
+ </div>
203
+ `;
204
+ };
205
+
206
+ // 3. Render Full Monster Container (Initial or Full Update)
207
+ // Now positioned stats to TOP
208
+ const renderMonsterSection = (currentUserProgress, classSize, userProfile) => {
209
+ const state = calculateMonsterState(currentUserProgress, classSize, userProfile);
210
+
211
  return `
212
+ <div id="monster-container-fixed" data-monster-id="${state.monster.id}" data-scale="${state.currentScale}"
213
+ class="fixed top-32 left-8 z-50 flex flex-col items-center group pointer-events-none sm:pointer-events-auto w-32 h-32">
214
+
215
+ <!-- Stats Tooltip (Moved to Top) -->
216
+ <div id="monster-stats-content" class="absolute bottom-full mb-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gray-900/90 backdrop-blur text-xs text-slate-300 p-3 rounded-xl border border-slate-700 text-left pointer-events-auto shadow-2xl min-w-[100px]">
217
+ ${renderMonsterStats(state)}
218
+ </div>
219
+
220
  <!-- Walking Container (Handles Movement Only) -->
221
  <div class="pixel-art-container relative transform transition-transform duration-500 ease-out origin-center"
222
+ style="transform: scale(${state.currentScale}); animation: patrol-move 15s linear infinite;">
223
 
224
  <!-- Monster Sprite (Handles Flip & Breathe) -->
225
  <div class="pixel-monster w-28 h-28 drop-shadow-2xl filter" style="animation: breathe 3s infinite ease-in-out, patrol-flip 15s linear infinite;">
226
+ ${generateMonsterSVG(state.monster)}
227
  </div>
228
 
229
  <!-- Level Indicator (No Flip) -->
230
  <div class="absolute -bottom-2 -right-2 bg-gray-900/90 text-xs text-yellow-400 px-2 py-0.5 rounded-full border border-yellow-500/50 font-mono font-bold transform scale-75 origin-top-left whitespace-nowrap">
231
+ Lv.${1 + state.totalCompleted}
232
  </div>
233
  </div>
234
 
235
  <!-- Evolution Prompt -->
236
+ ${state.canEvolve ? `
237
  <div id="evolution-prompt" class="absolute top-full mt-2 pointer-events-auto animate-bounce z-50">
238
  <div class="flex flex-col items-center">
239
  <div class="bg-gray-900/90 text-pink-200 text-xs py-2 px-3 rounded-xl border border-pink-500/30 shadow-lg text-center font-bold mb-1 backdrop-blur-sm whitespace-nowrap">
240
  咦,小怪獸的樣子<br>正在發生變化...
241
  </div>
242
+ <button onclick="window.triggerEvolution(${state.actualStage}, ${state.actualStage + 1}, ${state.totalLikes}, ${classSize}, '${state.monster.id}')"
243
  class="bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-500 hover:to-purple-500 text-white text-sm font-black py-2 px-6 rounded-full shadow-[0_0_15px_rgba(236,72,153,0.8)] border border-white/30 transition-all hover:scale-110 active:scale-95">
244
  進化!
245
  </button>
246
  </div>
247
  </div>
248
  ` : ''}
 
 
 
 
 
 
 
 
249
  </div>
250
 
251
  <style>
 
295
  window.currentProgressUnsub = subscribeToUserProgress(userId, (newProgressMap) => {
296
  // Merge updates
297
  const updatedProgress = { ...userProgress, ...newProgressMap };
 
 
298
 
299
+ // Smart Update: Check if visual refresh is needed
300
+ const newState = calculateMonsterState(updatedProgress, classSize, userProfile);
301
+ const fixedContainer = document.getElementById('monster-container-fixed');
302
+ const currentMonsterId = fixedContainer?.getAttribute('data-monster-id');
303
+
304
+ // Tolerance for scale check usually not needed if we want scale to update visuals immediately?
305
+ // Actually, scale change usually means totalCompleted changed.
306
+ // If we want smooth growth, replacing DOM resets animation which looks slight jumpy but acceptable.
307
+ // But the user complained about "position reset" (walk cycle reset).
308
+
309
+ if (fixedContainer && String(currentMonsterId) === String(newState.monster.id)) {
310
+ // Monster ID is same (no evolution/devolution).
311
+ // Just update stats tooltip
312
+ const statsContainer = document.getElementById('monster-stats-content');
313
+ if (statsContainer) {
314
+ statsContainer.innerHTML = renderMonsterStats(newState);
315
+ }
316
+
317
+ // What if level up (scale change)?
318
+ // If we don't replace DOM, scale won't update in style attribute.
319
+ // We should update the style manually.
320
+ const artContainer = fixedContainer.querySelector('.pixel-art-container');
321
+ if (artContainer && newState.currentScale) {
322
+ // Update animation with new scale
323
+ // Note: Modifying 'transform' directly might conflict with keyframes unless keyframes use relative or we update style variable.
324
+ // Keyframes use: scale(${currentScale}). This is hardcoded in specific keyframes string in <style>.
325
+ // We can't easily update keyframes dynamic values without replacing style block.
326
+
327
+ // If totalCompleted changed (level up), user *might* accept a reset because they levelled up.
328
+ // But simply giving a heart shouldn't reset.
329
+
330
+ const oldTotal = parseInt(fixedContainer.querySelector('.bg-gray-900\\/90')?.textContent?.replace('Lv.', '') || '1') - 1;
331
+ if (oldTotal !== newState.totalCompleted) {
332
+ // Level changed -> Scale changed -> Re-render full (reset animation is fine for Level Up)
333
+ monsterContainer.innerHTML = renderMonsterSection(updatedProgress, classSize, userProfile);
334
+ }
335
+ }
336
+ } else {
337
+ // Monster changed or clean slate -> Full Render
338
+ monsterContainer.innerHTML = renderMonsterSection(updatedProgress, classSize, userProfile);
339
+ }
340
  });
341
 
342
  // Accordion Layout