From 2f407071a41e0e8de368f320f175c8a8044db59a Mon Sep 17 00:00:00 2001 From: zhipu Date: Fri, 27 Mar 2026 09:33:48 +0000 Subject: [PATCH] Fix study system: per-hour mana cost instead of upfront, fix game time freeze bug, improve mobile UI --- AGENTS.md | 22 +++ src/app/page.tsx | 18 +- src/components/game/CalendarDisplay.tsx | 18 +- src/lib/game/store.ts | 212 ++++++++++++++---------- src/lib/game/study-slice.ts | 44 +++-- src/lib/game/types.ts | 2 + 6 files changed, 197 insertions(+), 119 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d3b986d..dcc65fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,28 @@ This document provides a comprehensive overview of the project architecture for --- +## 🔑 Git Credentials (SAVE THESE) + +**Repository:** `git@gitea.tailf367e3.ts.net:Anexim/Mana-Loop.git` + +**HTTPS URL with credentials:** +``` +https://zhipu:5LlnutmdsC2WirDwWgnZuRH7@gitea.tailf367e3.ts.net/Anexim/Mana-Loop.git +``` + +**Credentials:** +- **User:** zhipu +- **Email:** zhipu@local.local +- **Password:** 5LlnutmdsC2WirDwWgnZuRH7 + +**To configure git:** +```bash +git config --global user.name "zhipu" +git config --global user.email "zhipu@local.local" +``` + +--- + ## ⚠️ MANDATORY GIT WORKFLOW - MUST BE FOLLOWED **Before starting ANY work, you MUST:** diff --git a/src/app/page.tsx b/src/app/page.tsx index d165635..37ddc89 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -228,15 +228,15 @@ export default function ManaLoopGame() { {/* Right Panel - Tabs */}
- - ⚔️ Spire - 📚 Skills - ✨ Spells - 🛡️ Gear - 🔧 Craft - 🔬 Lab - 📊 Stats - 📖 Grimoire + + ⚔️ Spire + 📚 Skills + ✨ Spells + 🛡️ Gear + 🔧 Craft + 🔬 Lab + 📊 Stats + 📖 Grimoire diff --git a/src/components/game/CalendarDisplay.tsx b/src/components/game/CalendarDisplay.tsx index c9e0995..eabc08d 100644 --- a/src/components/game/CalendarDisplay.tsx +++ b/src/components/game/CalendarDisplay.tsx @@ -4,18 +4,20 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { MAX_DAY, INCURSION_START_DAY } from '@/lib/game/constants'; interface CalendarDisplayProps { - currentDay: number; + day: number; + hour: number; + incursionStrength?: number; } -export function CalendarDisplay({ currentDay }: CalendarDisplayProps) { +export function CalendarDisplay({ day }: CalendarDisplayProps) { const days: React.ReactElement[] = []; for (let d = 1; d <= MAX_DAY; d++) { - let dayClass = 'w-7 h-7 rounded text-xs flex items-center justify-center font-mono border transition-all '; + let dayClass = 'w-6 h-6 sm:w-7 sm:h-7 rounded text-xs flex items-center justify-center font-mono border transition-all '; - if (d < currentDay) { + if (d < day) { dayClass += 'bg-blue-900/30 border-blue-800/50 text-blue-400'; - } else if (d === currentDay) { + } else if (d === day) { dayClass += 'bg-blue-600/40 border-blue-500 text-blue-300 shadow-lg shadow-blue-500/30'; } else { dayClass += 'bg-gray-800/30 border-gray-700/50 text-gray-500'; @@ -40,5 +42,9 @@ export function CalendarDisplay({ currentDay }: CalendarDisplayProps) { ); } - return <>{days}; + return ( +
+ {days} +
+ ); } diff --git a/src/lib/game/store.ts b/src/lib/game/store.ts index f450012..c1e0e9d 100755 --- a/src/lib/game/store.ts +++ b/src/lib/game/store.ts @@ -409,6 +409,9 @@ export const useGameStore = create()( let rawMana = Math.min(state.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana); let totalManaGathered = state.totalManaGathered; let elements = state.elements; + + // Increment total ticks early (needed for study tracking) + const newTotalTicks = state.totalTicks + 1; // Study progress let currentStudyTarget = state.currentStudyTarget; @@ -419,97 +422,130 @@ export const useGameStore = create()( let unlockedEffects = state.unlockedEffects; let consecutiveStudyHours = state.consecutiveStudyHours || 0; let studyStartedAt = state.studyStartedAt; + let lastStudyCost = state.lastStudyCost; if (state.currentAction === 'study' && currentStudyTarget) { - // Track when study started (for STUDY_RUSH) - if (studyStartedAt === null) { - studyStartedAt = newTotalTicks; - } - - // Calculate study speed with all bonuses - let studySpeedMult = getStudySpeedMultiplier(skills); - - // MENTAL_CLARITY: +10% study speed when mana > 75% - if (hasSpecial(effects, SPECIAL_EFFECTS.MENTAL_CLARITY) && state.rawMana >= maxMana * 0.75) { - studySpeedMult *= 1.1; - } - - // STUDY_RUSH: First hour of study is 2x speed - const hoursStudied = (newTotalTicks - studyStartedAt) * HOURS_PER_TICK; - if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_RUSH) && hoursStudied < 1) { - studySpeedMult *= 2; - } - - // STUDY_MOMENTUM: +5% study speed per consecutive hour (max +50%) - if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_MOMENTUM)) { - const momentumBonus = Math.min(0.5, consecutiveStudyHours * 0.05); - studySpeedMult *= 1 + momentumBonus; - } - - const progressGain = HOURS_PER_TICK * studySpeedMult; - - // KNOWLEDGE_ECHO: 10% instant study chance - let instantProgress = 0; - if (hasSpecial(effects, SPECIAL_EFFECTS.KNOWLEDGE_ECHO) && Math.random() < 0.1) { - instantProgress = currentStudyTarget.required - currentStudyTarget.progress; - log = [`⚡ Knowledge Echo! Instant study progress!`, ...log.slice(0, 49)]; - } - - currentStudyTarget = { - ...currentStudyTarget, - progress: currentStudyTarget.progress + progressGain + instantProgress, - }; - - // Increment consecutive study hours - consecutiveStudyHours += HOURS_PER_TICK; - - // Check if study is complete - if (currentStudyTarget.progress >= currentStudyTarget.required) { - // STUDY_REFUND: 25% mana back on study complete - if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_REFUND) && state.lastStudyCost > 0) { - const refund = Math.floor(state.lastStudyCost * 0.25); - rawMana = Math.min(rawMana + refund, maxMana); - log = [`💰 Study Refund! Recovered ${refund} mana!`, ...log.slice(0, 49)]; - } - - // Reset study tracking - studyStartedAt = null; - consecutiveStudyHours = 0; - + // Calculate mana cost for this tick + const manaCostPerTick = (currentStudyTarget.manaCostPerHour || 0) * HOURS_PER_TICK; + + // Check if we have enough mana to continue studying + if (rawMana < manaCostPerTick) { + // Not enough mana - pause study and save progress + const retentionBonus = 1 + (state.skills.knowledgeRetention || 0) * 0.2; + const savedProgress = Math.min(currentStudyTarget.progress, currentStudyTarget.required * retentionBonus); + if (currentStudyTarget.type === 'skill') { - const skillId = currentStudyTarget.id; - const currentLevel = skills[skillId] || 0; - const newLevel = currentLevel + 1; - skills = { ...skills, [skillId]: newLevel }; - skillProgress = { ...skillProgress, [skillId]: 0 }; - log = [`✅ ${SKILLS_DEF[skillId]?.name} Lv.${newLevel} mastered!`, ...log.slice(0, 49)]; - - // Check if this skill unlocks effects (research skills) - const effectsToUnlock = EFFECT_RESEARCH_MAPPING[skillId]; - if (effectsToUnlock && newLevel >= (SKILLS_DEF[skillId]?.max || 1)) { - const newEffects = effectsToUnlock.filter(e => !unlockedEffects.includes(e)); - if (newEffects.length > 0) { - unlockedEffects = [...unlockedEffects, ...newEffects]; - log = [`🔬 Unlocked ${newEffects.length} new enchantment effect(s)!`, ...log.slice(0, 49)]; - } - } - - // Special case: When enchanting skill reaches level 1, unlock mana bolt - if (skillId === 'enchanting' && newLevel >= 1) { - const enchantingEffects = ENCHANTING_UNLOCK_EFFECTS.filter(e => !unlockedEffects.includes(e)); - if (enchantingEffects.length > 0) { - unlockedEffects = [...unlockedEffects, ...enchantingEffects]; - log = [`✨ Enchantment design unlocked! Mana Bolt effect available.`, ...log.slice(0, 49)]; - } - } + skillProgress = { ...skillProgress, [currentStudyTarget.id]: savedProgress }; } else if (currentStudyTarget.type === 'spell') { - // Spells can no longer be studied directly - they come from equipment - // This branch is kept for backward compatibility but should not be used - const spellId = currentStudyTarget.id; - spells = { ...spells, [spellId]: { learned: true, level: 1, studyProgress: 0 } }; - log = [`📖 ${SPELLS_DEF[spellId]?.name} learned!`, ...log.slice(0, 49)]; + spells = { + ...spells, + [currentStudyTarget.id]: { + ...(spells[currentStudyTarget.id] || { learned: false, level: 0 }), + studyProgress: savedProgress, + }, + }; } + + log = [`⚠️ Not enough mana to continue studying! Progress saved.`, ...log.slice(0, 49)]; currentStudyTarget = null; + consecutiveStudyHours = 0; + studyStartedAt = null; + } else { + // Deduct mana for this tick of study + rawMana -= manaCostPerTick; + lastStudyCost = (lastStudyCost || 0) + manaCostPerTick; + + // Track when study started (for STUDY_RUSH) + if (studyStartedAt === null) { + studyStartedAt = newTotalTicks; + } + + // Calculate study speed with all bonuses + let studySpeedMult = getStudySpeedMultiplier(skills); + + // MENTAL_CLARITY: +10% study speed when mana > 75% + if (hasSpecial(effects, SPECIAL_EFFECTS.MENTAL_CLARITY) && rawMana >= maxMana * 0.75) { + studySpeedMult *= 1.1; + } + + // STUDY_RUSH: First hour of study is 2x speed + const hoursStudied = (newTotalTicks - studyStartedAt) * HOURS_PER_TICK; + if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_RUSH) && hoursStudied < 1) { + studySpeedMult *= 2; + } + + // STUDY_MOMENTUM: +5% study speed per consecutive hour (max +50%) + if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_MOMENTUM)) { + const momentumBonus = Math.min(0.5, consecutiveStudyHours * 0.05); + studySpeedMult *= 1 + momentumBonus; + } + + const progressGain = HOURS_PER_TICK * studySpeedMult; + + // KNOWLEDGE_ECHO: 10% instant study chance + let instantProgress = 0; + if (hasSpecial(effects, SPECIAL_EFFECTS.KNOWLEDGE_ECHO) && Math.random() < 0.1) { + instantProgress = currentStudyTarget.required - currentStudyTarget.progress; + log = [`⚡ Knowledge Echo! Instant study progress!`, ...log.slice(0, 49)]; + } + + currentStudyTarget = { + ...currentStudyTarget, + progress: currentStudyTarget.progress + progressGain + instantProgress, + }; + + // Increment consecutive study hours + consecutiveStudyHours += HOURS_PER_TICK; + + // Check if study is complete + if (currentStudyTarget.progress >= currentStudyTarget.required) { + // STUDY_REFUND: 25% mana back on study complete + if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_REFUND) && lastStudyCost > 0) { + const refund = Math.floor(lastStudyCost * 0.25); + rawMana = Math.min(rawMana + refund, maxMana); + log = [`💰 Study Refund! Recovered ${refund} mana!`, ...log.slice(0, 49)]; + } + + // Reset study tracking + studyStartedAt = null; + consecutiveStudyHours = 0; + lastStudyCost = 0; + + if (currentStudyTarget.type === 'skill') { + const skillId = currentStudyTarget.id; + const currentLevel = skills[skillId] || 0; + const newLevel = currentLevel + 1; + skills = { ...skills, [skillId]: newLevel }; + skillProgress = { ...skillProgress, [skillId]: 0 }; + log = [`✅ ${SKILLS_DEF[skillId]?.name} Lv.${newLevel} mastered!`, ...log.slice(0, 49)]; + + // Check if this skill unlocks effects (research skills) + const effectsToUnlock = EFFECT_RESEARCH_MAPPING[skillId]; + if (effectsToUnlock && newLevel >= (SKILLS_DEF[skillId]?.max || 1)) { + const newEffects = effectsToUnlock.filter(e => !unlockedEffects.includes(e)); + if (newEffects.length > 0) { + unlockedEffects = [...unlockedEffects, ...newEffects]; + log = [`🔬 Unlocked ${newEffects.length} new enchantment effect(s)!`, ...log.slice(0, 49)]; + } + } + + // Special case: When enchanting skill reaches level 1, unlock mana bolt + if (skillId === 'enchanting' && newLevel >= 1) { + const enchantingEffects = ENCHANTING_UNLOCK_EFFECTS.filter(e => !unlockedEffects.includes(e)); + if (enchantingEffects.length > 0) { + unlockedEffects = [...unlockedEffects, ...enchantingEffects]; + log = [`✨ Enchantment design unlocked! Mana Bolt effect available.`, ...log.slice(0, 49)]; + } + } + } else if (currentStudyTarget.type === 'spell') { + // Spells can no longer be studied directly - they come from equipment + // This branch is kept for backward compatibility but should not be used + const spellId = currentStudyTarget.id; + spells = { ...spells, [spellId]: { learned: true, level: 1, studyProgress: 0 } }; + log = [`📖 ${SPELLS_DEF[spellId]?.name} learned!`, ...log.slice(0, 49)]; + } + currentStudyTarget = null; + } } } else { // Reset consecutive study hours when not studying @@ -540,12 +576,9 @@ export const useGameStore = create()( } // Combat - MULTI-SPELL casting from all equipped weapons - let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, equipmentSpellStates, combo, totalTicks, lootInventory, achievements, totalDamageDealt, totalSpellsCast } = state; + let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, equipmentSpellStates, combo, lootInventory, achievements, totalDamageDealt, totalSpellsCast } = state; const floorElement = getFloorElement(currentFloor); - // Increment total ticks - const newTotalTicks = totalTicks + 1; - // Combo decay - decay combo when not climbing or when decay timer expires let newCombo = { ...combo }; if (state.currentAction !== 'climb') { @@ -878,6 +911,7 @@ export const useGameStore = create()( totalSpellsCast, consecutiveStudyHours, studyStartedAt, + lastStudyCost, ...craftingUpdates, }); }, diff --git a/src/lib/game/study-slice.ts b/src/lib/game/study-slice.ts index 7f817e0..cd9ef96 100644 --- a/src/lib/game/study-slice.ts +++ b/src/lib/game/study-slice.ts @@ -21,7 +21,7 @@ export function createStudySlice( get: () => GameState ): StudyActions { return { - // Start studying a skill + // Start studying a skill - mana is deducted per hour, not upfront startStudyingSkill: (skillId: string) => { const state = get(); const sk = SKILLS_DEF[skillId]; @@ -37,22 +37,26 @@ export function createStudySlice( } } - // Check mana cost (with focused mind reduction) + // Calculate total mana cost and cost per hour const costMult = getStudyCostMultiplier(state.skills); - const cost = Math.floor(sk.base * (currentLevel + 1) * costMult); - if (state.rawMana < cost) return; + const totalCost = Math.floor(sk.base * (currentLevel + 1) * costMult); + const manaCostPerHour = Math.ceil(totalCost / sk.studyTime); + + // Must have at least 1 hour worth of mana to start + if (state.rawMana < manaCostPerHour) return; - // Start studying + // Start studying (no upfront cost - mana is deducted per hour during study) set({ - rawMana: state.rawMana - cost, currentAction: 'study', currentStudyTarget: { type: 'skill', id: skillId, progress: state.skillProgress[skillId] || 0, required: sk.studyTime, + manaCostPerHour: manaCostPerHour, + totalCost: totalCost, }, - log: [`📚 Started studying ${sk.name}...`, ...state.log.slice(0, 49)], + log: [`📚 Started studying ${sk.name} (${manaCostPerHour} mana/hr)...`, ...state.log.slice(0, 49)], }); }, @@ -62,28 +66,31 @@ export function createStudySlice( const sp = SPELLS_DEF[spellId]; if (!sp || state.spells[spellId]?.learned) return; - // Check mana cost (with focused mind reduction) + // Calculate total mana cost and cost per hour const costMult = getStudyCostMultiplier(state.skills); - const cost = Math.floor(sp.unlock * costMult); - if (state.rawMana < cost) return; + const totalCost = Math.floor(sp.unlock * costMult); + const studyTime = sp.studyTime || (sp.tier * 4); + const manaCostPerHour = Math.ceil(totalCost / studyTime); + + // Must have at least 1 hour worth of mana to start + if (state.rawMana < manaCostPerHour) return; - const studyTime = sp.studyTime || (sp.tier * 4); // Default study time based on tier - - // Start studying + // Start studying (no upfront cost - mana is deducted per hour during study) set({ - rawMana: state.rawMana - cost, currentAction: 'study', currentStudyTarget: { type: 'spell', id: spellId, progress: state.spells[spellId]?.studyProgress || 0, required: studyTime, + manaCostPerHour: manaCostPerHour, + totalCost: totalCost, }, spells: { ...state.spells, [spellId]: { ...(state.spells[spellId] || { learned: false, level: 0 }), studyProgress: state.spells[spellId]?.studyProgress || 0 }, }, - log: [`📚 Started studying ${sp.name}...`, ...state.log.slice(0, 49)], + log: [`📚 Started studying ${sp.name} (${manaCostPerHour} mana/hr)...`, ...state.log.slice(0, 49)], }); }, @@ -141,12 +148,19 @@ export function createStudySlice( // Can't study same thing in parallel if (state.currentStudyTarget.id === skillId) return; + // Calculate mana cost for parallel study + const costMult = getStudyCostMultiplier(state.skills); + const totalCost = Math.floor(sk.base * (currentLevel + 1) * costMult); + const manaCostPerHour = Math.ceil(totalCost / sk.studyTime); + set({ parallelStudyTarget: { type: 'skill', id: skillId, progress: state.skillProgress[skillId] || 0, required: sk.studyTime, + manaCostPerHour: Math.ceil(manaCostPerHour / 2), // Half speed = half mana cost per tick + totalCost: totalCost, }, log: [`📚 Started parallel study of ${sk.name}... (50% speed)`, ...state.log.slice(0, 49)], }); diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index fef717a..3fbcb24 100755 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -330,6 +330,8 @@ export interface StudyTarget { id: string; progress: number; // Hours studied required: number; // Total hours needed + manaCostPerHour: number; // Mana cost per hour of study + totalCost: number; // Total mana cost for the entire study } export interface GameState {