import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPrompts, resetProgress, toggleLike, subscribeToNotifications, markNotificationRead, getClassSize, updateUserMonster, getUser, subscribeToUserProgress } from "../services/classroom.js"; import { generateMonsterSVG, getNextMonster, MONSTER_STAGES, MONSTER_DEFS } from "../utils/monsterUtils.js"; // Cache challenges locally let cachedChallenges = []; function renderTaskCard(c, userProgress) { const p = userProgress[c.id] || {}; const isCompleted = p.status === 'completed'; const isStarted = p.status === 'started'; // 1. Completed State: Collapsed with Trophy if (isCompleted) { return `
🏆

${c.title}

已通關
`; } // 2. Started or Not Started return `

${c.title}

任務說明

${c.description}

${!isStarted ? `
` : `
`}
`; } 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