Spaces:
Running
Running
Upload 9 files
Browse files- src/services/classroom.js +2 -23
- src/utils/monsterUtils.js +42 -12
- src/views/StudentView.js +8 -0
src/services/classroom.js
CHANGED
|
@@ -175,13 +175,14 @@ export async function startChallenge(userId, roomCode, challengeId) {
|
|
| 175 |
// Let's update timestamp to reflect "last worked on"
|
| 176 |
await updateDoc(snapshot.docs[0].ref, {
|
| 177 |
status: 'started',
|
|
|
|
| 178 |
timestamp: serverTimestamp()
|
| 179 |
});
|
| 180 |
} else {
|
| 181 |
// Create new progress entry with 'started' status
|
| 182 |
await addDoc(progressRef, {
|
| 183 |
userId,
|
| 184 |
-
roomCode,
|
| 185 |
challengeId,
|
| 186 |
status: 'started',
|
| 187 |
startedAt: serverTimestamp(), // Keep original start time if we want to track duration
|
|
@@ -488,28 +489,6 @@ export async function getUser(userId) {
|
|
| 488 |
return snap.exists() ? snap.data() : null;
|
| 489 |
}
|
| 490 |
|
| 491 |
-
/**
|
| 492 |
-
* Subscribes to a single user's progress for real-time updates
|
| 493 |
-
* @param {string} userId
|
| 494 |
-
* @param {Function} callback (progressMap) => void
|
| 495 |
-
* @returns {Function} unsubscribe
|
| 496 |
-
*/
|
| 497 |
-
export function subscribeToUserProgress(userId, callback) {
|
| 498 |
-
const q = query(
|
| 499 |
-
collection(db, PROGRESS_COLLECTION),
|
| 500 |
-
where("userId", "==", userId)
|
| 501 |
-
);
|
| 502 |
-
|
| 503 |
-
return onSnapshot(q, (snapshot) => {
|
| 504 |
-
const progressMap = {};
|
| 505 |
-
snapshot.forEach(doc => {
|
| 506 |
-
const data = doc.data();
|
| 507 |
-
progressMap[data.challengeId] = data;
|
| 508 |
-
});
|
| 509 |
-
callback(progressMap);
|
| 510 |
-
});
|
| 511 |
-
}
|
| 512 |
-
|
| 513 |
/**
|
| 514 |
* Removes a user from the classroom (Kick)
|
| 515 |
* @param {string} userId
|
|
|
|
| 175 |
// Let's update timestamp to reflect "last worked on"
|
| 176 |
await updateDoc(snapshot.docs[0].ref, {
|
| 177 |
status: 'started',
|
| 178 |
+
roomCode: String(roomCode), // Ensure roomCode is updated
|
| 179 |
timestamp: serverTimestamp()
|
| 180 |
});
|
| 181 |
} else {
|
| 182 |
// Create new progress entry with 'started' status
|
| 183 |
await addDoc(progressRef, {
|
| 184 |
userId,
|
| 185 |
+
roomCode: String(roomCode),
|
| 186 |
challengeId,
|
| 187 |
status: 'started',
|
| 188 |
startedAt: serverTimestamp(), // Keep original start time if we want to track duration
|
|
|
|
| 489 |
return snap.exists() ? snap.data() : null;
|
| 490 |
}
|
| 491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
/**
|
| 493 |
* Removes a user from the classroom (Kick)
|
| 494 |
* @param {string} userId
|
src/utils/monsterUtils.js
CHANGED
|
@@ -441,18 +441,48 @@ export function getNextMonster(currentStage, likes, classSize, currentMonsterId
|
|
| 441 |
// Filter potential monsters for this Stage
|
| 442 |
let candidates = MONSTER_DEFS.filter(m => m.stage === currentStage);
|
| 443 |
|
| 444 |
-
//
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
//
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
}
|
| 457 |
|
| 458 |
// Filter by Tier (Suffix A/B/C)
|
|
|
|
| 441 |
// Filter potential monsters for this Stage
|
| 442 |
let candidates = MONSTER_DEFS.filter(m => m.stage === currentStage);
|
| 443 |
|
| 444 |
+
// 1. Strict Lineage by ID Pattern (L{stage}_{path})
|
| 445 |
+
// e.g. L1_A -> L2_A... / L2_AB -> L3_AB...
|
| 446 |
+
// This allows exact branching control as per design doc.
|
| 447 |
+
const isStandardId = currentMonsterId?.match(/^L\d+_[ABC]+$/);
|
| 448 |
+
|
| 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 |
+
const strictMatches = candidates.filter(m => m.id.startsWith(`L${currentStage + 1}_${currentPath}`));
|
| 454 |
+
|
| 455 |
+
if (strictMatches.length > 0) {
|
| 456 |
+
candidates = strictMatches;
|
| 457 |
+
}
|
| 458 |
+
}
|
| 459 |
+
// 2. Fallback: Family Tree Logic (for custom IDs or Egg)
|
| 460 |
+
else if (currentFam && currentFam !== 'Origin') {
|
| 461 |
+
// Define Family Tree Mapping
|
| 462 |
+
const FAMILY_TREE = {
|
| 463 |
+
// Stage 1 Root: Low Tier (Red Path)
|
| 464 |
+
'Dust': ['Dust', 'Glitch', 'Trash', 'Slime', 'Tech', 'Hacker', 'Virus'],
|
| 465 |
+
// Stage 1 Root: Mid Tier (Yellow Path)
|
| 466 |
+
'Beast': ['Beast', 'Wolf', 'Cat', 'Mech', 'Mech Lion', 'Animal', 'Grunge', 'Royal', 'Warrior', 'Undead', 'Elemental'],
|
| 467 |
+
// Stage 1 Root: High Tier (Blue Path)
|
| 468 |
+
'Spirit': ['Spirit', 'Ghost', 'Holy', 'Cosmos', 'Angel', 'Divin', 'Void', 'Undead']
|
| 469 |
+
};
|
| 470 |
+
|
| 471 |
+
// Find which root family the current family belongs to
|
| 472 |
+
let rootFamily = null;
|
| 473 |
+
for (const [root, children] of Object.entries(FAMILY_TREE)) {
|
| 474 |
+
if (root === currentFam || children.includes(currentFam) || children.some(c => currentFam.includes(c))) {
|
| 475 |
+
rootFamily = root;
|
| 476 |
+
break;
|
| 477 |
+
}
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
if (rootFamily) {
|
| 481 |
+
const allowedFamilies = FAMILY_TREE[rootFamily];
|
| 482 |
+
candidates = candidates.filter(m => {
|
| 483 |
+
return allowedFamilies.some(fam => m.fam.includes(fam) || fam.includes(m.fam));
|
| 484 |
+
});
|
| 485 |
+
}
|
| 486 |
}
|
| 487 |
|
| 488 |
// Filter by Tier (Suffix A/B/C)
|
src/views/StudentView.js
CHANGED
|
@@ -698,10 +698,17 @@ window.triggerEvolution = async (currentStage, nextStage, likes, classSize, curr
|
|
| 698 |
const { getNextMonster, generateMonsterSVG, MONSTER_STAGES } = await import("../utils/monsterUtils.js");
|
| 699 |
const { updateUserMonster } = await import("../services/classroom.js");
|
| 700 |
|
|
|
|
| 701 |
// Calculate Next Monster with Lineage
|
|
|
|
| 702 |
const currentMonster = getNextMonster(currentStage, likes, classSize, currentMonsterId);
|
|
|
|
|
|
|
|
|
|
| 703 |
const nextMonster = getNextMonster(nextStage, likes, classSize, currentMonsterId);
|
| 704 |
|
|
|
|
|
|
|
| 705 |
const container = document.querySelector('#monster-container-fixed .pixel-monster');
|
| 706 |
const containerWrapper = document.querySelector('#monster-container-fixed .pixel-art-container');
|
| 707 |
|
|
@@ -739,6 +746,7 @@ window.triggerEvolution = async (currentStage, nextStage, likes, classSize, curr
|
|
| 739 |
containerWrapper.style.transition = 'filter 0.8s ease-out';
|
| 740 |
containerWrapper.style.filter = 'drop-shadow(0 0 30px #ffffff) brightness(1.5)';
|
| 741 |
|
|
|
|
| 742 |
setFrame(svgNext, false);
|
| 743 |
|
| 744 |
setTimeout(async () => {
|
|
|
|
| 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');
|
| 714 |
|
|
|
|
| 746 |
containerWrapper.style.transition = 'filter 0.8s ease-out';
|
| 747 |
containerWrapper.style.filter = 'drop-shadow(0 0 30px #ffffff) brightness(1.5)';
|
| 748 |
|
| 749 |
+
// Force SVG update to next monster for final state
|
| 750 |
setFrame(svgNext, false);
|
| 751 |
|
| 752 |
setTimeout(async () => {
|