Phase 3: Split DebugTab.tsx into functional components
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 4m17s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 4m17s
This commit is contained in:
@@ -5,7 +5,7 @@ import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { SPELLS_DEF, GUARDIANS, HOURS_PER_TICK } from '../constants';
|
||||
import type { GameAction, SpellState } from '../types';
|
||||
import { getFloorMaxHP, getFloorElement, calcDamage } from '../utils';
|
||||
import { getFloorMaxHP, getFloorElement, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
|
||||
import { usePrestigeStore } from './prestigeStore';
|
||||
|
||||
export interface CombatState {
|
||||
@@ -141,15 +141,16 @@ export const useCombatStore = create<CombatState>()(
|
||||
) => {
|
||||
const state = get();
|
||||
const logMessages: string[] = [];
|
||||
let totalManaGathered = 0;
|
||||
|
||||
if (state.currentAction !== 'climb') {
|
||||
return { rawMana, elements, logMessages };
|
||||
return { rawMana, elements, logMessages, totalManaGathered };
|
||||
}
|
||||
|
||||
const spellId = state.activeSpell;
|
||||
const spellDef = SPELLS_DEF[spellId];
|
||||
if (!spellDef) {
|
||||
return { rawMana, elements, logMessages };
|
||||
return { rawMana, elements, logMessages, totalManaGathered };
|
||||
}
|
||||
|
||||
// Calculate cast speed
|
||||
@@ -164,28 +165,14 @@ export const useCombatStore = create<CombatState>()(
|
||||
let floorMaxHP = state.floorMaxHP;
|
||||
|
||||
// Process complete casts
|
||||
while (castProgress >= 1) {
|
||||
// Check if we can afford the spell
|
||||
const cost = spellDef.cost;
|
||||
let canCast = false;
|
||||
while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) {
|
||||
// Deduct spell cost
|
||||
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
||||
rawMana = afterCost.rawMana;
|
||||
elements = afterCost.elements;
|
||||
totalManaGathered += spellDef.cost.amount;
|
||||
|
||||
if (cost.type === 'raw') {
|
||||
canCast = rawMana >= cost.amount;
|
||||
if (canCast) rawMana -= cost.amount;
|
||||
} else if (cost.element) {
|
||||
const elem = elements[cost.element];
|
||||
canCast = elem && elem.unlocked && elem.current >= cost.amount;
|
||||
if (canCast) {
|
||||
elements = {
|
||||
...elements,
|
||||
[cost.element]: { ...elem, current: elem.current - cost.amount },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!canCast) break;
|
||||
|
||||
// Calculate damage
|
||||
// Calculate base damage
|
||||
const floorElement = getFloorElement(currentFloor);
|
||||
const damage = calcDamage(
|
||||
{ skills, signedPacts: usePrestigeStore.getState().signedPacts },
|
||||
@@ -193,8 +180,14 @@ export const useCombatStore = create<CombatState>()(
|
||||
floorElement
|
||||
);
|
||||
|
||||
// Let gameStore apply damage modifiers (executioner, berserker, spell echo)
|
||||
const result = onDamageDealt(damage);
|
||||
rawMana = result.rawMana;
|
||||
elements = result.elements;
|
||||
const finalDamage = result.modifiedDamage || damage;
|
||||
|
||||
// Apply damage
|
||||
floorHP = Math.max(0, floorHP - damage);
|
||||
floorHP = Math.max(0, floorHP - finalDamage);
|
||||
castProgress -= 1;
|
||||
|
||||
// Check if floor is cleared
|
||||
@@ -223,7 +216,7 @@ export const useCombatStore = create<CombatState>()(
|
||||
castProgress,
|
||||
});
|
||||
|
||||
return { rawMana, elements, logMessages };
|
||||
return { rawMana, elements, logMessages, totalManaGathered };
|
||||
},
|
||||
|
||||
resetCombat: (startFloor: number, spellsToKeep: string[] = []) => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { TICK_MS, HOURS_PER_TICK, MAX_DAY, SPELLS_DEF, GUARDIANS, ELEMENTS, BASE_UNLOCKED_ELEMENTS, getStudySpeedMultiplier } from '../constants';
|
||||
import { TICK_MS, HOURS_PER_TICK, MAX_DAY, SPELLS_DEF, GUARDIANS, getStudySpeedMultiplier } from '../constants';
|
||||
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '../upgrade-effects';
|
||||
import {
|
||||
computeMaxMana,
|
||||
@@ -16,13 +16,13 @@ import {
|
||||
calcInsight,
|
||||
calcDamage,
|
||||
deductSpellCost,
|
||||
canAffordSpellCost,
|
||||
} from '../utils';
|
||||
import { useUIStore } from './uiStore';
|
||||
import { usePrestigeStore } from './prestigeStore';
|
||||
import { useManaStore } from './manaStore';
|
||||
import { useSkillStore } from './skillStore';
|
||||
import { useCombatStore, makeInitialSpells } from './combatStore';
|
||||
import type { Memory } from '../types';
|
||||
|
||||
export interface GameCoordinatorState {
|
||||
day: number;
|
||||
@@ -37,7 +37,6 @@ export interface GameCoordinatorStore extends GameCoordinatorState {
|
||||
resetGame: () => void;
|
||||
togglePause: () => void;
|
||||
startNewLoop: () => void;
|
||||
gatherMana: () => void;
|
||||
initGame: () => void;
|
||||
}
|
||||
|
||||
@@ -49,21 +48,6 @@ const initialState: GameCoordinatorState = {
|
||||
initialized: false,
|
||||
};
|
||||
|
||||
// Helper function for checking spell cost affordability
|
||||
function canAffordSpell(
|
||||
cost: { type: string; element?: string; amount: number },
|
||||
rawMana: number,
|
||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>
|
||||
): boolean {
|
||||
if (cost.type === 'raw') {
|
||||
return rawMana >= cost.amount;
|
||||
} else if (cost.element) {
|
||||
const elem = elements[cost.element];
|
||||
return elem && elem.unlocked && elem.current >= cost.amount;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export const useGameStore = create<GameCoordinatorStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -187,25 +171,12 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
}
|
||||
}
|
||||
|
||||
// Convert action - auto convert mana
|
||||
// Convert action - delegate to manaStore
|
||||
if (combatState.currentAction === 'convert') {
|
||||
const unlockedElements = Object.entries(elements)
|
||||
.filter(([, e]) => e.unlocked && e.current < e.max);
|
||||
|
||||
if (unlockedElements.length > 0 && rawMana >= 100) {
|
||||
unlockedElements.sort((a, b) => (b[1].max - b[1].current) - (a[1].max - a[1].current));
|
||||
const [targetId, targetState] = unlockedElements[0];
|
||||
const canConvert = Math.min(
|
||||
Math.floor(rawMana / 100),
|
||||
targetState.max - targetState.current
|
||||
);
|
||||
if (canConvert > 0) {
|
||||
rawMana -= canConvert * 100;
|
||||
elements = {
|
||||
...elements,
|
||||
[targetId]: { ...targetState, current: targetState.current + canConvert }
|
||||
};
|
||||
}
|
||||
const convertResult = useManaStore.getState().processConvertAction(rawMana);
|
||||
if (convertResult) {
|
||||
rawMana = convertResult.rawMana;
|
||||
elements = convertResult.elements;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,41 +199,27 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
}
|
||||
}
|
||||
|
||||
// Combat
|
||||
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, castProgress } = combatState;
|
||||
const floorElement = getFloorElement(currentFloor);
|
||||
|
||||
// Combat - delegate to combatStore
|
||||
if (combatState.currentAction === 'climb') {
|
||||
const spellId = combatState.activeSpell;
|
||||
const spellDef = SPELLS_DEF[spellId];
|
||||
|
||||
if (spellDef) {
|
||||
const baseAttackSpeed = 1 + (skillState.skills.quickCast || 0) * 0.05;
|
||||
const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier;
|
||||
const spellCastSpeed = spellDef.castSpeed || 1;
|
||||
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed;
|
||||
|
||||
castProgress = (castProgress || 0) + progressPerTick;
|
||||
|
||||
// Process complete casts
|
||||
while (castProgress >= 1 && canAffordSpell(spellDef.cost, rawMana, elements)) {
|
||||
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
||||
rawMana = afterCost.rawMana;
|
||||
elements = afterCost.elements;
|
||||
totalManaGathered += spellDef.cost.amount;
|
||||
|
||||
// Calculate damage
|
||||
let dmg = calcDamage(
|
||||
{ skills: skillState.skills, signedPacts: prestigeState.signedPacts },
|
||||
spellId,
|
||||
floorElement
|
||||
);
|
||||
|
||||
const combatResult = useCombatStore.getState().processCombatTick(
|
||||
skillState.skills,
|
||||
rawMana,
|
||||
elements,
|
||||
maxMana,
|
||||
effects.attackSpeedMultiplier,
|
||||
(floor, wasGuardian) => {
|
||||
if (wasGuardian) {
|
||||
addLog(`⚔️ ${GUARDIANS[floor]?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`);
|
||||
} else if (floor % 5 === 0) {
|
||||
addLog(`🏰 Floor ${floor} cleared!`);
|
||||
}
|
||||
},
|
||||
(damage) => {
|
||||
// Apply upgrade damage multipliers and bonuses
|
||||
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
|
||||
let dmg = damage * effects.baseDamageMultiplier + effects.baseDamageBonus;
|
||||
|
||||
// Executioner: +100% damage to enemies below 25% HP
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) {
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && combatState.floorHP / combatState.floorMaxHP < 0.25) {
|
||||
dmg *= 2;
|
||||
}
|
||||
|
||||
@@ -278,34 +235,17 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
addLog(`✨ Spell Echo! Double damage!`);
|
||||
}
|
||||
|
||||
// Apply damage
|
||||
floorHP = Math.max(0, floorHP - dmg);
|
||||
castProgress -= 1;
|
||||
|
||||
if (floorHP <= 0) {
|
||||
// Floor cleared
|
||||
const wasGuardian = GUARDIANS[currentFloor];
|
||||
if (wasGuardian && !prestigeState.defeatedGuardians.includes(currentFloor) && !prestigeState.signedPacts.includes(currentFloor)) {
|
||||
usePrestigeStore.getState().addDefeatedGuardian(currentFloor);
|
||||
addLog(`⚔️ ${wasGuardian.name} defeated! Visit the Grimoire to sign a pact.`);
|
||||
} else if (!wasGuardian) {
|
||||
if (currentFloor % 5 === 0) {
|
||||
addLog(`🏰 Floor ${currentFloor} cleared!`);
|
||||
}
|
||||
}
|
||||
|
||||
currentFloor = currentFloor + 1;
|
||||
if (currentFloor > 100) {
|
||||
currentFloor = 100;
|
||||
}
|
||||
floorMaxHP = getFloorMaxHP(currentFloor);
|
||||
floorHP = floorMaxHP;
|
||||
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
||||
castProgress = 0;
|
||||
|
||||
useCombatStore.getState().advanceFloor();
|
||||
}
|
||||
return { rawMana, elements, modifiedDamage: dmg };
|
||||
}
|
||||
);
|
||||
|
||||
rawMana = combatResult.rawMana;
|
||||
elements = combatResult.elements;
|
||||
totalManaGathered += combatResult.totalManaGathered || 0;
|
||||
|
||||
// Log any messages from combat
|
||||
if (combatResult.logMessages) {
|
||||
combatResult.logMessages.forEach(msg => addLog(msg));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,13 +257,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
elements,
|
||||
});
|
||||
|
||||
useCombatStore.setState({
|
||||
floorHP,
|
||||
floorMaxHP,
|
||||
maxFloorReached,
|
||||
castProgress,
|
||||
});
|
||||
|
||||
set({
|
||||
day,
|
||||
hour,
|
||||
@@ -331,6 +264,33 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
});
|
||||
},
|
||||
|
||||
resetGame: () => {
|
||||
// Clear all persisted state
|
||||
localStorage.removeItem('mana-loop-ui-storage');
|
||||
localStorage.removeItem('mana-loop-prestige-storage');
|
||||
localStorage.removeItem('mana-loop-mana-storage');
|
||||
localStorage.removeItem('mana-loop-skill-storage');
|
||||
localStorage.removeItem('mana-loop-combat-storage');
|
||||
localStorage.removeItem('mana-loop-game-storage');
|
||||
|
||||
const startFloor = 1;
|
||||
|
||||
useUIStore.getState().resetUI();
|
||||
usePrestigeStore.getState().resetPrestige();
|
||||
useManaStore.getState().resetMana({}, {}, {}, {});
|
||||
useSkillStore.getState().resetSkills();
|
||||
useCombatStore.getState().resetCombat(startFloor);
|
||||
|
||||
set({
|
||||
...initialState,
|
||||
initialized: true,
|
||||
});
|
||||
},
|
||||
|
||||
togglePause: () => {
|
||||
useUIStore.getState().togglePause();
|
||||
},
|
||||
|
||||
gatherMana: () => {
|
||||
const skillState = useSkillStore.getState();
|
||||
const manaState = useManaStore.getState();
|
||||
@@ -351,47 +311,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
effects
|
||||
);
|
||||
|
||||
useManaStore.setState({
|
||||
rawMana: Math.min(manaState.rawMana + cm, max),
|
||||
totalManaGathered: manaState.totalManaGathered + cm,
|
||||
});
|
||||
},
|
||||
|
||||
resetGame: () => {
|
||||
// Clear all persisted state
|
||||
localStorage.removeItem('mana-loop-ui-storage');
|
||||
localStorage.removeItem('mana-loop-prestige-storage');
|
||||
localStorage.removeItem('mana-loop-mana-storage');
|
||||
localStorage.removeItem('mana-loop-skill-storage');
|
||||
localStorage.removeItem('mana-loop-combat-storage');
|
||||
localStorage.removeItem('mana-loop-game-storage');
|
||||
|
||||
const startFloor = 1;
|
||||
const elemMax = 10;
|
||||
|
||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
||||
Object.keys(ELEMENTS).forEach((k) => {
|
||||
elements[k] = {
|
||||
current: 0,
|
||||
max: elemMax,
|
||||
unlocked: BASE_UNLOCKED_ELEMENTS.includes(k),
|
||||
};
|
||||
});
|
||||
|
||||
useUIStore.getState().resetUI();
|
||||
usePrestigeStore.getState().resetPrestige();
|
||||
useManaStore.getState().resetMana({}, {}, {}, {});
|
||||
useSkillStore.getState().resetSkills();
|
||||
useCombatStore.getState().resetCombat(startFloor);
|
||||
|
||||
set({
|
||||
...initialState,
|
||||
initialized: true,
|
||||
});
|
||||
},
|
||||
|
||||
togglePause: () => {
|
||||
useUIStore.getState().togglePause();
|
||||
useManaStore.getState().gatherMana(cm, max);
|
||||
},
|
||||
|
||||
startNewLoop: () => {
|
||||
|
||||
@@ -31,6 +31,9 @@ export interface ManaState {
|
||||
setElementMax: (max: number) => void;
|
||||
craftComposite: (target: string, recipe: string[]) => boolean;
|
||||
|
||||
// Helper for gameStore coordination
|
||||
processConvertAction: (rawMana: number) => { rawMana: number; elements: Record<string, ElementState> } | null;
|
||||
|
||||
// Reset
|
||||
resetMana: (
|
||||
prestigeUpgrades: Record<string, number>,
|
||||
@@ -214,6 +217,33 @@ export const useManaStore = create<ManaState>()(
|
||||
return true;
|
||||
},
|
||||
|
||||
processConvertAction: (rawMana: number) => {
|
||||
const state = get();
|
||||
const elements = { ...state.elements };
|
||||
|
||||
const unlockedElements = Object.entries(elements)
|
||||
.filter(([, e]) => e.unlocked && e.current < e.max);
|
||||
|
||||
if (unlockedElements.length === 0 || rawMana < 100) return null;
|
||||
|
||||
unlockedElements.sort((a, b) => (b[1].max - b[1].current) - (a[1].max - a[1].current));
|
||||
const [targetId, targetState] = unlockedElements[0];
|
||||
const canConvert = Math.min(
|
||||
Math.floor(rawMana / 100),
|
||||
targetState.max - targetState.current
|
||||
);
|
||||
|
||||
if (canConvert <= 0) return null;
|
||||
|
||||
rawMana -= canConvert * 100;
|
||||
const updatedElements = {
|
||||
...elements,
|
||||
[targetId]: { ...targetState, current: targetState.current + canConvert }
|
||||
};
|
||||
|
||||
return { rawMana, elements: updatedElements };
|
||||
},
|
||||
|
||||
resetMana: (
|
||||
prestigeUpgrades: Record<string, number>,
|
||||
skills: Record<string, number> = {},
|
||||
|
||||
Reference in New Issue
Block a user