Lashtw commited on
Commit
7d9742d
·
verified ·
1 Parent(s): 5d979f9

Upload 10 files

Browse files
src/services/classroom.js CHANGED
@@ -446,6 +446,11 @@ export async function markNotificationRead(notificationId) {
446
  });
447
  }
448
 
 
 
 
 
 
449
  /**
450
  * Gets the number of users in a room
451
  * @param {string} roomCode
@@ -460,6 +465,23 @@ export async function getClassSize(roomCode) {
460
  return snapshot.data().count;
461
  }
462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  /**
464
  * Updates a student's monster stage and specific form
465
  * @param {string} userId
 
446
  });
447
  }
448
 
449
+ /**
450
+ * Gets the number of users in a room
451
+ * @param {string} roomCode
452
+ * @returns {Promise<number>}
453
+ */
454
  /**
455
  * Gets the number of users in a room
456
  * @param {string} roomCode
 
465
  return snapshot.data().count;
466
  }
467
 
468
+ /**
469
+ * Gets the number of students who have reached a higher or equal stage
470
+ * Used for determining percentile ranking
471
+ * @param {string} roomCode
472
+ * @param {number} targetStage
473
+ * @returns {Promise<number>}
474
+ */
475
+ export async function getHigherStageCount(roomCode, targetStage) {
476
+ const q = query(
477
+ collection(db, USERS_COLLECTION),
478
+ where("current_room", "==", roomCode),
479
+ where("monster_stage", ">=", targetStage)
480
+ );
481
+ const snapshot = await getCountFromServer(q);
482
+ return snapshot.data().count;
483
+ }
484
+
485
  /**
486
  * Updates a student's monster stage and specific form
487
  * @param {string} userId
src/utils/monsterUtils.js CHANGED
@@ -404,7 +404,16 @@ export function generateMonsterSVG(monster) {
404
  * @param {string} [currentMonsterId] - ID of the current monster to determine lineage
405
  * @returns {Object} Selected Monster Definition
406
  */
407
- export function getNextMonster(currentStage, likes, classSize, currentMonsterId = null) {
 
 
 
 
 
 
 
 
 
408
  if (currentStage === 0) return MONSTER_DEFS.find(m => m.id === 'Egg');
409
 
410
  // Find current monster family if we have an ID
@@ -415,28 +424,50 @@ export function getNextMonster(currentStage, likes, classSize, currentMonsterId
415
  }
416
 
417
  // Stage 1: Basic Families (Origin -> Family)
418
- // If coming from Egg (Stage 0), we pick a base family.
419
  if (currentStage === 1) {
420
- // Simple distribution:
421
- if (likes >= 5) return MONSTER_DEFS.find(m => m.id === 'L1_A'); // Spirit (Blue)
422
- if (likes >= 2) return MONSTER_DEFS.find(m => m.id === 'L1_B'); // Beast (Yellow)
423
- return MONSTER_DEFS.find(m => m.id === 'L1_C'); // Dust (Red)
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  }
425
 
426
  // Branching Logic for Stage 2 & 3
427
- // A: High Likes ( > classSize / 2 )
428
- // B: Normal Likes ( > 1 )
429
- // C: Low Likes ( <= 1 )
430
 
431
- const ratio = classSize > 0 ? likes / classSize : 0;
 
 
 
 
 
 
 
432
 
433
  // Tier Logic
434
  let tier = 'C';
435
- if (likes > classSize / 2) tier = 'A';
436
- else if (likes > 1) tier = 'B';
437
 
438
- // Safety check for small classes
439
- if (classSize <= 2 && likes >= 1) tier = 'A'; // Boost for testing pairs
 
 
 
440
 
441
  // Filter potential monsters for this Stage
442
  let candidates = MONSTER_DEFS.filter(m => m.stage === currentStage);
@@ -449,8 +480,8 @@ export function getNextMonster(currentStage, likes, classSize, currentMonsterId
449
  if (isStandardId) {
450
  // Extract path suffix (e.g. "A" from "L1_A", "AB" from "L2_AB")
451
  const currentPath = currentMonsterId.split('_')[1];
452
- // Filter candidates: Must start with L{nextStage}_{currentPath}
453
- // FIXED: currentStage IS the target stage (e.g. 3), so we look for L3_...
454
  const strictMatches = candidates.filter(m => m.id.startsWith(`L${currentStage}_${currentPath}`));
455
 
456
  if (strictMatches.length > 0) {
 
404
  * @param {string} [currentMonsterId] - ID of the current monster to determine lineage
405
  * @returns {Object} Selected Monster Definition
406
  */
407
+ /**
408
+ * Determines the next monster form based on parameters and lineage
409
+ * @param {number} currentStage - Current evolutionary stage (0-3)
410
+ * @param {number} stageLikes - Likes earned IN THIS STAGE
411
+ * @param {number} classSize - Active class size
412
+ * @param {string} [currentMonsterId] - ID of the current monster to determine lineage
413
+ * @param {number} [percentile] - Speed ranking percentile (0.0 - 1.0, lower is faster). Default 1.0
414
+ * @returns {Object} Selected Monster Definition
415
+ */
416
+ export function getNextMonster(currentStage, stageLikes, classSize, currentMonsterId = null, percentile = 1.0) {
417
  if (currentStage === 0) return MONSTER_DEFS.find(m => m.id === 'Egg');
418
 
419
  // Find current monster family if we have an ID
 
424
  }
425
 
426
  // Stage 1: Basic Families (Origin -> Family)
427
+ // If coming from Egg (Stage 0), we pick a base family based on performance.
428
  if (currentStage === 1) {
429
+ // Simple distribution for Stage 1 (Egg -> Base)
430
+ // We can use the same Score Logic or keep simple likes.
431
+ // Let's use the Score Logic to be consistent with design.
432
+
433
+ let score = 0;
434
+ // Speed Score
435
+ if (percentile <= 0.3) score += 5; // Top 30%
436
+ else if (percentile <= 0.6) score += 3; // Top 60%
437
+ else score += 1; // Rest
438
+
439
+ // Like Score
440
+ if (stageLikes >= 2) score += 2;
441
+ else if (stageLikes > 0) score += 1;
442
+
443
+ if (score >= 5) return MONSTER_DEFS.find(m => m.id === 'L1_A'); // High (Spirit)
444
+ if (score >= 3) return MONSTER_DEFS.find(m => m.id === 'L1_B'); // Mid (Beast)
445
+ return MONSTER_DEFS.find(m => m.id === 'L1_C'); // Low (Dust)
446
  }
447
 
448
  // Branching Logic for Stage 2 & 3
449
+ // Score Calculation
450
+ let score = 0;
 
451
 
452
+ // 1. Speed Score
453
+ if (percentile <= 0.3) score += 5; // Top 30%
454
+ else if (percentile <= 0.6) score += 3; // Top 60%
455
+ else score += 1; // Rest
456
+
457
+ // 2. Like Score
458
+ if (stageLikes >= 2) score += 2; // High Likes
459
+ else if (stageLikes > 0) score += 1; // Any Likes
460
 
461
  // Tier Logic
462
  let tier = 'C';
463
+ if (score >= 5) tier = 'A'; // High Tier
464
+ else if (score >= 3) tier = 'B'; // Mid Tier
465
 
466
+ // Safety check for small classes (Boost if alone)
467
+ if (classSize <= 2) {
468
+ if (stageLikes >= 1) tier = 'A';
469
+ else tier = 'B';
470
+ }
471
 
472
  // Filter potential monsters for this Stage
473
  let candidates = MONSTER_DEFS.filter(m => m.stage === currentStage);
 
480
  if (isStandardId) {
481
  // Extract path suffix (e.g. "A" from "L1_A", "AB" from "L2_AB")
482
  const currentPath = currentMonsterId.split('_')[1];
483
+ // Filter candidates: Must start with L{targetStage}_{currentPath}
484
+ // currentStage IS the target stage (e.g. 3), so we look for L3_...
485
  const strictMatches = candidates.filter(m => m.id.startsWith(`L${currentStage}_${currentPath}`));
486
 
487
  if (strictMatches.length > 0) {
src/views/StudentView.js CHANGED
@@ -696,18 +696,67 @@ window.triggerEvolution = async (currentStage, nextStage, likes, classSize, curr
696
 
697
  try {
698
  const { getNextMonster, generateMonsterSVG, MONSTER_STAGES } = await import("../utils/monsterUtils.js");
699
- const { updateUserMonster } = await import("../services/classroom.js");
700
 
 
 
 
701
 
702
- // Calculate Next Monster with Lineage
703
- // IMPORTANT: Ensure we pass currentMonsterId to enforce lineage!
704
- const currentMonster = getNextMonster(currentStage, likes, classSize, currentMonsterId);
 
 
 
 
 
 
 
 
 
 
 
705
 
706
- // Next Stage: If evolving, we need to find what this specific monster turns into.
707
- // We pass 'currentMonsterId' to the next stage calculation too, so it knows the family 'Origin'.
708
- const nextMonster = getNextMonster(nextStage, likes, classSize, currentMonsterId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
 
710
- console.log("Evolving from:", currentMonster.name, "to:", nextMonster.name);
711
 
712
  const container = document.querySelector('#monster-container-fixed .pixel-monster');
713
  const containerWrapper = document.querySelector('#monster-container-fixed .pixel-art-container');
@@ -720,7 +769,7 @@ window.triggerEvolution = async (currentStage, nextStage, likes, classSize, curr
720
  const maxFlickers = 12; // Increased duration
721
  let speed = 300;
722
 
723
- const svgCurrent = generateMonsterSVG(currentMonster);
724
  const svgNext = generateMonsterSVG(nextMonster);
725
 
726
  const setFrame = (svg, isSilhouette) => {
 
696
 
697
  try {
698
  const { getNextMonster, generateMonsterSVG, MONSTER_STAGES } = await import("../utils/monsterUtils.js");
699
+ const { updateUserMonster, getHigherStageCount } = await import("../services/classroom.js");
700
 
701
+ // Calculate Ranking Percentile
702
+ const roomCode = localStorage.getItem('vibecoding_room_code');
703
+ let percentile = 1.0;
704
 
705
+ // Query how many already at next stage (or same level but evolved)
706
+ // Actually, we want to know how many have *completed* this stage essentially.
707
+ // Since we trigger evolution AFTER completion, anyone AT or ABOVE nextStage has completed it.
708
+ // So we count how many are >= nextStage.
709
+ // Wait, if I am the first to evolve to stage 1, count >= 1 is 0.
710
+ // So rank is (0 + 1) / classSize.
711
+
712
+ try {
713
+ const higherCount = await getHigherStageCount(roomCode, nextStage);
714
+ percentile = (higherCount + 1) / classSize;
715
+ console.log(`Evolution Rank: ${higherCount + 1}/${classSize} (${percentile})`);
716
+ } catch (e) {
717
+ console.error("Rank calc failed", e);
718
+ }
719
 
720
+ // Calculate Stage-Specific Likes
721
+ // We need to fetch challenges to filter by level...
722
+ // cachedChallenges is available in module scope
723
+ // We need user progress...
724
+ // We can't easily get it here without re-fetching or passing it in.
725
+ // But we have 'likes' passed in, which is TOTAL likes.
726
+ // Re-fetching progress is safer.
727
+ const userId = localStorage.getItem('vibecoding_user_id');
728
+ const { getUserProgress } = await import("../services/classroom.js");
729
+ const progress = await getUserProgress(userId);
730
+
731
+ let stageLikes = 0;
732
+ // Map stage to challenge level
733
+ // Stage 0 -> 1 requires Beginner (Level 1) likes
734
+ // Stage 1 -> 2 requires Intermediate (Level 2) likes
735
+ // Stage 2 -> 3 requires Advanced (Level 3) likes
736
+
737
+ // Note: nextStage parameter is the TARGET stage.
738
+ // If nextStage is 1 (Egg->Baby), we count Beginner likes.
739
+
740
+ const targetLevelMap = {
741
+ 1: 'beginner',
742
+ 2: 'intermediate',
743
+ 3: 'advanced'
744
+ };
745
+ const targetLevel = targetLevelMap[nextStage];
746
+
747
+ if (targetLevel) {
748
+ stageLikes = cachedChallenges
749
+ .filter(c => c.level === targetLevel)
750
+ .reduce((acc, c) => acc + (progress[c.id]?.likes || 0), 0);
751
+ }
752
+ console.log(`Stage ${nextStage} Likes: ${stageLikes} (Total: ${likes})`);
753
+
754
+ // Calculate Next Monster with Lineage & Ranking
755
+ // IMPORTANT: Ensure we pass currentMonsterId to enforce lineage!
756
+ // We pass 'stageLikes' instead of total 'likes' now
757
+ const nextMonster = getNextMonster(currentStage, stageLikes, classSize, currentMonsterId, percentile);
758
 
759
+ console.log("Evolving from:", currentMonsterId, "to:", nextMonster.name);
760
 
761
  const container = document.querySelector('#monster-container-fixed .pixel-monster');
762
  const containerWrapper = document.querySelector('#monster-container-fixed .pixel-art-container');
 
769
  const maxFlickers = 12; // Increased duration
770
  let speed = 300;
771
 
772
+ const currentMonster = MONSTER_DEFS.find(m => m.id === currentMonsterId) || getNextMonster(currentStage, 0, 0, currentMonsterId);
773
  const svgNext = generateMonsterSVG(nextMonster);
774
 
775
  const setFrame = (svg, isSilhouette) => {
test_evolution.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getNextMonster, MONSTER_DEFS } from './src/utils/monsterUtils.js';
2
+
3
+ const mockMonster = { id: 'L2_CA', name: '駭客蟲', stage: 2 };
4
+ const classSizes = [10, 30];
5
+ const likeCounts = [0, 5, 20]; // Test Low, Mid, High tiers
6
+
7
+ console.log(`Testing Evolution for: ${mockMonster.name} (${mockMonster.id})`);
8
+
9
+ classSizes.forEach(size => {
10
+ likeCounts.forEach(likes => {
11
+ // We want to see what it evolves INTO, so we ask for Stage 3
12
+ const next = getNextMonster(3, likes, size, mockMonster.id);
13
+ console.log(`Class: ${size}, Likes: ${likes} -> Next: ${next.name} (${next.id})`);
14
+ });
15
+ });
16
+
17
+ console.log('\n--- Checking Evolution for Trash Mob (L2_CC) ---');
18
+ // Verify if L2_CC evolves to Virus King (L3_CCA) or others
19
+ const trashMob = { id: 'L2_CC', name: '垃圾怪', stage: 2 };
20
+ const nextTrash = getNextMonster(3, 5, 10, trashMob.id);
21
+ console.log(`Trash Mob (L2_CC) w/ High Likes -> ${nextTrash.name} (${nextTrash.id})`);
22
+
23
+ console.log('\n--- Debugging candidates for L2_CA ---');
24
+ // Manually replicate the util logic step
25
+ const candidates = MONSTER_DEFS.filter(m => m.stage === 3);
26
+ const currentPath = 'CA';
27
+ const rigidMatches = candidates.filter(m => m.id.startsWith(`L3_${currentPath}`));
28
+ console.log('Strict Matches for L3_CA*:');
29
+ rigidMatches.forEach(m => console.log(m.id, m.name));