`;
}
export async function renderStudentView() {
const nickname = localStorage.getItem('vibecoding_nickname') || 'Guest';
const roomCode = localStorage.getItem('vibecoding_room_code') || 'Unknown';
const userId = localStorage.getItem('vibecoding_user_id');
// Fetch challenges if empty
if (cachedChallenges.length === 0) {
try {
cachedChallenges = await getChallenges();
} catch (e) {
console.error("Failed to fetch challenges", e);
throw new Error("無法讀取題目列表 (Error: " + e.message + ")");
}
}
// Fetch User Progress
let userProgress = {};
if (userId) {
try {
userProgress = await getUserProgress(userId);
} catch (e) {
console.error("Failed to fetch progress", e);
}
}
const levelGroups = {
beginner: cachedChallenges.filter(c => c.level === 'beginner'),
intermediate: cachedChallenges.filter(c => c.level === 'intermediate'),
advanced: cachedChallenges.filter(c => c.level === 'advanced')
};
const levelNames = {
beginner: "初級 (Beginner)",
intermediate: "中級 (Intermediate)",
advanced: "高級 (Advanced)"
};
// --- Monster Section Render Function ---
// --- Monster Section Helper Functions ---
// 1. Calculate Monster State (Separated logic)
const calculateMonsterState = (currentUserProgress, classSize, userProfile) => {
const totalLikes = Object.values(currentUserProgress).reduce((acc, p) => acc + (p.likes || 0), 0);
// Count completions per level
const counts = {
1: cachedChallenges.filter(c => c.level === 'beginner' && currentUserProgress[c.id]?.status === 'completed').length,
2: cachedChallenges.filter(c => c.level === 'intermediate' && currentUserProgress[c.id]?.status === 'completed').length,
3: cachedChallenges.filter(c => c.level === 'advanced' && currentUserProgress[c.id]?.status === 'completed').length
};
const totalCompleted = counts[1] + counts[2] + counts[3];
let potentialStage = 0;
if (counts[1] >= 5) potentialStage = 1;
if (counts[2] >= 5 && potentialStage >= 1) potentialStage = 2;
if (counts[3] >= 5 && potentialStage >= 2) potentialStage = 3;
const actualStage = userProfile.monster_stage || 0;
const actualMonsterId = userProfile.monster_id || 'Egg';
// Regression Logic Check
if (actualStage > potentialStage) {
// Regression Logic: Pass 'Egg' (or null) to enforce downgrade
const targetId = potentialStage === 0 ? 'Egg' : null; // If going to 0, force Egg. Else keep null to let system decide? Or should we query?
// Actually, if we devolve, we lose the specific form. So we should probably reset ID to something generic or clear it.
// If we go to stage 0, 'Egg' is safe.
// If we go to stage 1 (from 2), we might want to keep the stage 1 form (if we knew it).
// But simplifying: just clear the ID so getNextMonster picks default for that stage.
updateUserMonster(userId, potentialStage, targetId).then(() => {
setTimeout(() => window.location.reload(), 500);
});
// Return corrected state temporary
return {
...calculateMonsterState(currentUserProgress, classSize, { ...userProfile, monster_stage: potentialStage, monster_id: targetId }),
isRegressing: true
};
}
const canEvolve = potentialStage > actualStage;
// Scale Logic
const growthFactor = 0.08;
const baseScale = 1.0;
const currentScale = baseScale + (totalCompleted * growthFactor);
// Get Monster Data
let monster = getNextMonster(actualStage, 0, 0, actualMonsterId);
if (actualMonsterId && actualMonsterId !== 'Egg') {
const stored = MONSTER_DEFS.find(m => m.id === actualMonsterId);
if (stored) monster = stored;
} else {
monster = getNextMonster(actualStage, totalLikes, classSize);
}
return {
monster,
currentScale,
totalCompleted,
totalLikes,
canEvolve,
actualStage,
classSize,
counts
};
};
// 2. Render Stats HTML (Partial Update Target)
const renderMonsterStats = (state) => {
return `
${state.monster.name}
💖${state.totalLikes}
`;
};
// 3. Render Full Monster Container (Initial or Full Update)
// Now positioned stats to TOP
const renderMonsterSection = (currentUserProgress, classSize, userProfile) => {
const state = calculateMonsterState(currentUserProgress, classSize, userProfile);
// Left sidebar position: Responsive
// Mobile: Top Left (in header space)
// Desktop: Fixed Left Sidebar
return `
${renderMonsterStats(state)}
${generateMonsterSVG(state.monster)}
Lv.${1 + state.totalCompleted}
${state.canEvolve ? `
咦,小怪獸的樣子 正在發生變化...
` : ''}
`;
};
// Inject Initial Monster UI
const monsterContainerId = 'monster-ui-layer';
let monsterContainer = document.getElementById(monsterContainerId);
if (!monsterContainer) {
monsterContainer = document.createElement('div');
monsterContainer.id = monsterContainerId;
document.body.appendChild(monsterContainer);
}
// Initial Render
let classSize = 1;
let userProfile = {};
try {
classSize = await getClassSize(roomCode);
userProfile = await getUser(userId) || {};
} catch (e) { console.error("Fetch stats error", e); }
monsterContainer.innerHTML = renderMonsterSection(userProgress, classSize, userProfile);
// Setup Real-time Subscription
if (window.currentProgressUnsub) window.currentProgressUnsub();
window.currentProgressUnsub = subscribeToUserProgress(userId, (newProgressMap) => {
// Merge updates
const updatedProgress = { ...userProgress, ...newProgressMap };
// Smart Update: Check if visual refresh is needed
const newState = calculateMonsterState(updatedProgress, classSize, userProfile);
const fixedContainer = document.getElementById('monster-container-fixed');
const currentMonsterId = fixedContainer?.getAttribute('data-monster-id');
// Tolerance for scale check usually not needed if we want scale to update visuals immediately?
// Actually, scale change usually means totalCompleted changed.
// If we want smooth growth, replacing DOM resets animation which looks slight jumpy but acceptable.
// But the user complained about "position reset" (walk cycle reset).
if (fixedContainer && String(currentMonsterId) === String(newState.monster.id)) {
// Monster ID is same (no evolution/devolution).
// Just update stats tooltip
const statsContainer = document.getElementById('monster-stats-content');
if (statsContainer) {
statsContainer.innerHTML = renderMonsterStats(newState);
}
// What if level up (scale change)?
// If we don't replace DOM, scale won't update in style attribute.
// We should update the style manually.
const artContainer = fixedContainer.querySelector('.pixel-art-container');
if (artContainer && newState.currentScale) {
// Update animation with new scale
// Note: Modifying 'transform' directly might conflict with keyframes unless keyframes use relative or we update style variable.
// Keyframes use: scale(${currentScale}). This is hardcoded in specific keyframes string in