Fix study system: per-hour mana cost instead of upfront, fix game time freeze bug, improve mobile UI
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m43s
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m43s
This commit is contained in:
22
AGENTS.md
22
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:**
|
||||
|
||||
@@ -228,15 +228,15 @@ export default function ManaLoopGame() {
|
||||
{/* Right Panel - Tabs */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid grid-cols-8 w-full mb-4">
|
||||
<TabsTrigger value="spire">⚔️ Spire</TabsTrigger>
|
||||
<TabsTrigger value="skills">📚 Skills</TabsTrigger>
|
||||
<TabsTrigger value="spells">✨ Spells</TabsTrigger>
|
||||
<TabsTrigger value="equipment">🛡️ Gear</TabsTrigger>
|
||||
<TabsTrigger value="crafting">🔧 Craft</TabsTrigger>
|
||||
<TabsTrigger value="lab">🔬 Lab</TabsTrigger>
|
||||
<TabsTrigger value="stats">📊 Stats</TabsTrigger>
|
||||
<TabsTrigger value="grimoire">📖 Grimoire</TabsTrigger>
|
||||
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
|
||||
<TabsTrigger value="spire" className="text-xs px-2 py-1">⚔️ Spire</TabsTrigger>
|
||||
<TabsTrigger value="skills" className="text-xs px-2 py-1">📚 Skills</TabsTrigger>
|
||||
<TabsTrigger value="spells" className="text-xs px-2 py-1">✨ Spells</TabsTrigger>
|
||||
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡️ Gear</TabsTrigger>
|
||||
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
|
||||
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
|
||||
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
|
||||
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="spire">
|
||||
|
||||
@@ -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 (
|
||||
<div className="grid grid-cols-7 sm:grid-cols-7 md:grid-cols-14 gap-1">
|
||||
{days}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -410,6 +410,9 @@ export const useGameStore = create<GameStore>()(
|
||||
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;
|
||||
let skills = state.skills;
|
||||
@@ -419,8 +422,39 @@ export const useGameStore = create<GameStore>()(
|
||||
let unlockedEffects = state.unlockedEffects;
|
||||
let consecutiveStudyHours = state.consecutiveStudyHours || 0;
|
||||
let studyStartedAt = state.studyStartedAt;
|
||||
let lastStudyCost = state.lastStudyCost;
|
||||
|
||||
if (state.currentAction === 'study' && currentStudyTarget) {
|
||||
// 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') {
|
||||
skillProgress = { ...skillProgress, [currentStudyTarget.id]: savedProgress };
|
||||
} else if (currentStudyTarget.type === 'spell') {
|
||||
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;
|
||||
@@ -430,7 +464,7 @@ export const useGameStore = create<GameStore>()(
|
||||
let studySpeedMult = getStudySpeedMultiplier(skills);
|
||||
|
||||
// MENTAL_CLARITY: +10% study speed when mana > 75%
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MENTAL_CLARITY) && state.rawMana >= maxMana * 0.75) {
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MENTAL_CLARITY) && rawMana >= maxMana * 0.75) {
|
||||
studySpeedMult *= 1.1;
|
||||
}
|
||||
|
||||
@@ -466,8 +500,8 @@ export const useGameStore = create<GameStore>()(
|
||||
// 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);
|
||||
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)];
|
||||
}
|
||||
@@ -475,6 +509,7 @@ export const useGameStore = create<GameStore>()(
|
||||
// Reset study tracking
|
||||
studyStartedAt = null;
|
||||
consecutiveStudyHours = 0;
|
||||
lastStudyCost = 0;
|
||||
|
||||
if (currentStudyTarget.type === 'skill') {
|
||||
const skillId = currentStudyTarget.id;
|
||||
@@ -511,6 +546,7 @@ export const useGameStore = create<GameStore>()(
|
||||
}
|
||||
currentStudyTarget = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Reset consecutive study hours when not studying
|
||||
consecutiveStudyHours = 0;
|
||||
@@ -540,12 +576,9 @@ export const useGameStore = create<GameStore>()(
|
||||
}
|
||||
|
||||
// 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<GameStore>()(
|
||||
totalSpellsCast,
|
||||
consecutiveStudyHours,
|
||||
studyStartedAt,
|
||||
lastStudyCost,
|
||||
...craftingUpdates,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
// Start studying
|
||||
// Must have at least 1 hour worth of mana to start
|
||||
if (state.rawMana < manaCostPerHour) return;
|
||||
|
||||
// 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);
|
||||
|
||||
const studyTime = sp.studyTime || (sp.tier * 4); // Default study time based on tier
|
||||
// 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: '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)],
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user