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
|
## ⚠️ MANDATORY GIT WORKFLOW - MUST BE FOLLOWED
|
||||||
|
|
||||||
**Before starting ANY work, you MUST:**
|
**Before starting ANY work, you MUST:**
|
||||||
|
|||||||
@@ -228,15 +228,15 @@ export default function ManaLoopGame() {
|
|||||||
{/* Right Panel - Tabs */}
|
{/* Right Panel - Tabs */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
<TabsList className="grid grid-cols-8 w-full mb-4">
|
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
|
||||||
<TabsTrigger value="spire">⚔️ Spire</TabsTrigger>
|
<TabsTrigger value="spire" className="text-xs px-2 py-1">⚔️ Spire</TabsTrigger>
|
||||||
<TabsTrigger value="skills">📚 Skills</TabsTrigger>
|
<TabsTrigger value="skills" className="text-xs px-2 py-1">📚 Skills</TabsTrigger>
|
||||||
<TabsTrigger value="spells">✨ Spells</TabsTrigger>
|
<TabsTrigger value="spells" className="text-xs px-2 py-1">✨ Spells</TabsTrigger>
|
||||||
<TabsTrigger value="equipment">🛡️ Gear</TabsTrigger>
|
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡️ Gear</TabsTrigger>
|
||||||
<TabsTrigger value="crafting">🔧 Craft</TabsTrigger>
|
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
|
||||||
<TabsTrigger value="lab">🔬 Lab</TabsTrigger>
|
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
|
||||||
<TabsTrigger value="stats">📊 Stats</TabsTrigger>
|
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
|
||||||
<TabsTrigger value="grimoire">📖 Grimoire</TabsTrigger>
|
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="spire">
|
<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';
|
import { MAX_DAY, INCURSION_START_DAY } from '@/lib/game/constants';
|
||||||
|
|
||||||
interface CalendarDisplayProps {
|
interface CalendarDisplayProps {
|
||||||
currentDay: number;
|
day: number;
|
||||||
|
hour: number;
|
||||||
|
incursionStrength?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CalendarDisplay({ currentDay }: CalendarDisplayProps) {
|
export function CalendarDisplay({ day }: CalendarDisplayProps) {
|
||||||
const days: React.ReactElement[] = [];
|
const days: React.ReactElement[] = [];
|
||||||
|
|
||||||
for (let d = 1; d <= MAX_DAY; d++) {
|
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';
|
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';
|
dayClass += 'bg-blue-600/40 border-blue-500 text-blue-300 shadow-lg shadow-blue-500/30';
|
||||||
} else {
|
} else {
|
||||||
dayClass += 'bg-gray-800/30 border-gray-700/50 text-gray-500';
|
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 totalManaGathered = state.totalManaGathered;
|
||||||
let elements = state.elements;
|
let elements = state.elements;
|
||||||
|
|
||||||
|
// Increment total ticks early (needed for study tracking)
|
||||||
|
const newTotalTicks = state.totalTicks + 1;
|
||||||
|
|
||||||
// Study progress
|
// Study progress
|
||||||
let currentStudyTarget = state.currentStudyTarget;
|
let currentStudyTarget = state.currentStudyTarget;
|
||||||
let skills = state.skills;
|
let skills = state.skills;
|
||||||
@@ -419,8 +422,39 @@ export const useGameStore = create<GameStore>()(
|
|||||||
let unlockedEffects = state.unlockedEffects;
|
let unlockedEffects = state.unlockedEffects;
|
||||||
let consecutiveStudyHours = state.consecutiveStudyHours || 0;
|
let consecutiveStudyHours = state.consecutiveStudyHours || 0;
|
||||||
let studyStartedAt = state.studyStartedAt;
|
let studyStartedAt = state.studyStartedAt;
|
||||||
|
let lastStudyCost = state.lastStudyCost;
|
||||||
|
|
||||||
if (state.currentAction === 'study' && currentStudyTarget) {
|
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)
|
// Track when study started (for STUDY_RUSH)
|
||||||
if (studyStartedAt === null) {
|
if (studyStartedAt === null) {
|
||||||
studyStartedAt = newTotalTicks;
|
studyStartedAt = newTotalTicks;
|
||||||
@@ -430,7 +464,7 @@ export const useGameStore = create<GameStore>()(
|
|||||||
let studySpeedMult = getStudySpeedMultiplier(skills);
|
let studySpeedMult = getStudySpeedMultiplier(skills);
|
||||||
|
|
||||||
// MENTAL_CLARITY: +10% study speed when mana > 75%
|
// 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;
|
studySpeedMult *= 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,8 +500,8 @@ export const useGameStore = create<GameStore>()(
|
|||||||
// Check if study is complete
|
// Check if study is complete
|
||||||
if (currentStudyTarget.progress >= currentStudyTarget.required) {
|
if (currentStudyTarget.progress >= currentStudyTarget.required) {
|
||||||
// STUDY_REFUND: 25% mana back on study complete
|
// STUDY_REFUND: 25% mana back on study complete
|
||||||
if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_REFUND) && state.lastStudyCost > 0) {
|
if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_REFUND) && lastStudyCost > 0) {
|
||||||
const refund = Math.floor(state.lastStudyCost * 0.25);
|
const refund = Math.floor(lastStudyCost * 0.25);
|
||||||
rawMana = Math.min(rawMana + refund, maxMana);
|
rawMana = Math.min(rawMana + refund, maxMana);
|
||||||
log = [`💰 Study Refund! Recovered ${refund} mana!`, ...log.slice(0, 49)];
|
log = [`💰 Study Refund! Recovered ${refund} mana!`, ...log.slice(0, 49)];
|
||||||
}
|
}
|
||||||
@@ -475,6 +509,7 @@ export const useGameStore = create<GameStore>()(
|
|||||||
// Reset study tracking
|
// Reset study tracking
|
||||||
studyStartedAt = null;
|
studyStartedAt = null;
|
||||||
consecutiveStudyHours = 0;
|
consecutiveStudyHours = 0;
|
||||||
|
lastStudyCost = 0;
|
||||||
|
|
||||||
if (currentStudyTarget.type === 'skill') {
|
if (currentStudyTarget.type === 'skill') {
|
||||||
const skillId = currentStudyTarget.id;
|
const skillId = currentStudyTarget.id;
|
||||||
@@ -511,6 +546,7 @@ export const useGameStore = create<GameStore>()(
|
|||||||
}
|
}
|
||||||
currentStudyTarget = null;
|
currentStudyTarget = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Reset consecutive study hours when not studying
|
// Reset consecutive study hours when not studying
|
||||||
consecutiveStudyHours = 0;
|
consecutiveStudyHours = 0;
|
||||||
@@ -540,12 +576,9 @@ export const useGameStore = create<GameStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Combat - MULTI-SPELL casting from all equipped weapons
|
// 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);
|
const floorElement = getFloorElement(currentFloor);
|
||||||
|
|
||||||
// Increment total ticks
|
|
||||||
const newTotalTicks = totalTicks + 1;
|
|
||||||
|
|
||||||
// Combo decay - decay combo when not climbing or when decay timer expires
|
// Combo decay - decay combo when not climbing or when decay timer expires
|
||||||
let newCombo = { ...combo };
|
let newCombo = { ...combo };
|
||||||
if (state.currentAction !== 'climb') {
|
if (state.currentAction !== 'climb') {
|
||||||
@@ -878,6 +911,7 @@ export const useGameStore = create<GameStore>()(
|
|||||||
totalSpellsCast,
|
totalSpellsCast,
|
||||||
consecutiveStudyHours,
|
consecutiveStudyHours,
|
||||||
studyStartedAt,
|
studyStartedAt,
|
||||||
|
lastStudyCost,
|
||||||
...craftingUpdates,
|
...craftingUpdates,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function createStudySlice(
|
|||||||
get: () => GameState
|
get: () => GameState
|
||||||
): StudyActions {
|
): StudyActions {
|
||||||
return {
|
return {
|
||||||
// Start studying a skill
|
// Start studying a skill - mana is deducted per hour, not upfront
|
||||||
startStudyingSkill: (skillId: string) => {
|
startStudyingSkill: (skillId: string) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const sk = SKILLS_DEF[skillId];
|
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 costMult = getStudyCostMultiplier(state.skills);
|
||||||
const cost = Math.floor(sk.base * (currentLevel + 1) * costMult);
|
const totalCost = Math.floor(sk.base * (currentLevel + 1) * costMult);
|
||||||
if (state.rawMana < cost) return;
|
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({
|
set({
|
||||||
rawMana: state.rawMana - cost,
|
|
||||||
currentAction: 'study',
|
currentAction: 'study',
|
||||||
currentStudyTarget: {
|
currentStudyTarget: {
|
||||||
type: 'skill',
|
type: 'skill',
|
||||||
id: skillId,
|
id: skillId,
|
||||||
progress: state.skillProgress[skillId] || 0,
|
progress: state.skillProgress[skillId] || 0,
|
||||||
required: sk.studyTime,
|
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];
|
const sp = SPELLS_DEF[spellId];
|
||||||
if (!sp || state.spells[spellId]?.learned) return;
|
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 costMult = getStudyCostMultiplier(state.skills);
|
||||||
const cost = Math.floor(sp.unlock * costMult);
|
const totalCost = Math.floor(sp.unlock * costMult);
|
||||||
if (state.rawMana < cost) return;
|
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({
|
set({
|
||||||
rawMana: state.rawMana - cost,
|
|
||||||
currentAction: 'study',
|
currentAction: 'study',
|
||||||
currentStudyTarget: {
|
currentStudyTarget: {
|
||||||
type: 'spell',
|
type: 'spell',
|
||||||
id: spellId,
|
id: spellId,
|
||||||
progress: state.spells[spellId]?.studyProgress || 0,
|
progress: state.spells[spellId]?.studyProgress || 0,
|
||||||
required: studyTime,
|
required: studyTime,
|
||||||
|
manaCostPerHour: manaCostPerHour,
|
||||||
|
totalCost: totalCost,
|
||||||
},
|
},
|
||||||
spells: {
|
spells: {
|
||||||
...state.spells,
|
...state.spells,
|
||||||
[spellId]: { ...(state.spells[spellId] || { learned: false, level: 0 }), studyProgress: state.spells[spellId]?.studyProgress || 0 },
|
[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
|
// Can't study same thing in parallel
|
||||||
if (state.currentStudyTarget.id === skillId) return;
|
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({
|
set({
|
||||||
parallelStudyTarget: {
|
parallelStudyTarget: {
|
||||||
type: 'skill',
|
type: 'skill',
|
||||||
id: skillId,
|
id: skillId,
|
||||||
progress: state.skillProgress[skillId] || 0,
|
progress: state.skillProgress[skillId] || 0,
|
||||||
required: sk.studyTime,
|
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)],
|
log: [`📚 Started parallel study of ${sk.name}... (50% speed)`, ...state.log.slice(0, 49)],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -330,6 +330,8 @@ export interface StudyTarget {
|
|||||||
id: string;
|
id: string;
|
||||||
progress: number; // Hours studied
|
progress: number; // Hours studied
|
||||||
required: number; // Total hours needed
|
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 {
|
export interface GameState {
|
||||||
|
|||||||
Reference in New Issue
Block a user