2805f75f5e
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 57s
- Delete src/lib/game/computed-stats.ts (root-level re-export shim) - Delete src/lib/game/store/index.ts (nothing imports from it) - Update __tests__/computed-stats.test.ts to import from ../utils instead - Clean up craftingStore.ts imports (remove unused useGameStore, CraftingApply) Typecheck and lint pass (pre-existing DisciplinesTab.tsx errors unchanged)
282 lines
10 KiB
TypeScript
Executable File
282 lines
10 KiB
TypeScript
Executable File
// ─── Game Store (Coordinator) ─────────────────────────────────────────────────
|
|
// Manages: day, hour, incursionStrength, containmentWards
|
|
// Coordinate tick function across all stores
|
|
|
|
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
import { TICK_MS, HOURS_PER_TICK, MAX_DAY, SPELLS_DEF, GUARDIANS, getStudySpeedMultiplier } from '../constants';
|
|
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
|
|
import {
|
|
computeMaxMana,
|
|
computeRegen,
|
|
getFloorElement,
|
|
getFloorMaxHP,
|
|
getMeditationBonus,
|
|
getIncursionStrength,
|
|
calcInsight,
|
|
calcDamage,
|
|
deductSpellCost,
|
|
canAffordSpellCost,
|
|
} from '../utils';
|
|
import { useUIStore } from './uiStore';
|
|
import { usePrestigeStore } from './prestigeStore';
|
|
import { useManaStore } from './manaStore';
|
|
import { useCombatStore, makeInitialSpells } from './combatStore';
|
|
import { useAttunementStore } from './attunementStore';
|
|
import { ATTUNEMENTS_DEF, getAttunementConversionRate } from '../data/attunements';
|
|
import { createResetGame, createGatherMana } from './gameActions';
|
|
import { createStartNewLoop } from './gameLoopActions';
|
|
|
|
export interface GameCoordinatorState {
|
|
day: number;
|
|
hour: number;
|
|
incursionStrength: number;
|
|
containmentWards: number;
|
|
initialized: boolean;
|
|
}
|
|
|
|
export interface GameCoordinatorStore extends GameCoordinatorState {
|
|
tick: () => void;
|
|
resetGame: () => void;
|
|
togglePause: () => void;
|
|
startNewLoop: () => void;
|
|
initGame: () => void;
|
|
gatherMana: () => void;
|
|
}
|
|
|
|
const initialState: GameCoordinatorState = {
|
|
day: 1,
|
|
hour: 0,
|
|
incursionStrength: 0,
|
|
containmentWards: 0,
|
|
initialized: false,
|
|
};
|
|
|
|
export const useGameStore = create<GameCoordinatorStore>()(
|
|
persist(
|
|
(set, get) => ({
|
|
...initialState,
|
|
|
|
initGame: () => {
|
|
set({ initialized: true });
|
|
},
|
|
|
|
tick: () => {
|
|
const uiState = useUIStore.getState();
|
|
if (uiState.gameOver || uiState.paused) return;
|
|
|
|
// Helper for logging
|
|
const addLog = (msg: string) => useUIStore.getState().addLog(msg);
|
|
|
|
// Get all store states
|
|
const prestigeState = usePrestigeStore.getState();
|
|
const manaState = useManaStore.getState();
|
|
const combatState = useCombatStore.getState();
|
|
|
|
const maxMana = computeMaxMana(
|
|
{ skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
|
|
undefined
|
|
);
|
|
const baseRegen = computeRegen(
|
|
{ skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunement: {} },
|
|
undefined
|
|
);
|
|
|
|
// Time progression
|
|
let hour = get().hour + HOURS_PER_TICK;
|
|
let day = get().day;
|
|
if (hour >= 24) {
|
|
hour -= 24;
|
|
day += 1;
|
|
}
|
|
|
|
// Check for loop end
|
|
if (day > MAX_DAY) {
|
|
const insightGained = calcInsight({
|
|
maxFloorReached: combatState.maxFloorReached,
|
|
totalManaGathered: manaState.totalManaGathered,
|
|
signedPacts: prestigeState.signedPacts,
|
|
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
|
skills: {},
|
|
});
|
|
|
|
addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`);
|
|
useUIStore.getState().setGameOver(true, false);
|
|
usePrestigeStore.getState().setLoopInsight(insightGained);
|
|
set({ day, hour });
|
|
return;
|
|
}
|
|
|
|
// Check for victory
|
|
if (combatState.maxFloorReached >= 100 && prestigeState.signedPacts.includes(100)) {
|
|
const insightGained = calcInsight({
|
|
maxFloorReached: combatState.maxFloorReached,
|
|
totalManaGathered: manaState.totalManaGathered,
|
|
signedPacts: prestigeState.signedPacts,
|
|
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
|
skills: {},
|
|
}) * 3;
|
|
|
|
addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`);
|
|
useUIStore.getState().setGameOver(true, true);
|
|
usePrestigeStore.getState().setLoopInsight(insightGained);
|
|
return;
|
|
}
|
|
|
|
// Incursion
|
|
const incursionStrength = getIncursionStrength(day, hour);
|
|
|
|
// Meditation bonus tracking and regen calculation
|
|
let meditateTicks = manaState.meditateTicks;
|
|
let meditationMultiplier = 1;
|
|
|
|
if (combatState.currentAction === 'meditate') {
|
|
meditateTicks++;
|
|
meditationMultiplier = getMeditationBonus(meditateTicks, {}, 1);
|
|
} else {
|
|
meditateTicks = 0;
|
|
}
|
|
|
|
// Calculate total attunement conversion per tick (to subtract from regen)
|
|
const attunementState = useAttunementStore.getState();
|
|
let totalConversionPerTick = 0;
|
|
Object.entries(attunementState.attunements).forEach(([id, state]) => {
|
|
if (!state.active) return;
|
|
const def = ATTUNEMENTS_DEF[id];
|
|
if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
|
|
|
|
const scaledRate = getAttunementConversionRate(id, state.level || 1);
|
|
totalConversionPerTick += scaledRate * HOURS_PER_TICK;
|
|
});
|
|
|
|
// Calculate effective regen with incursion, meditation, and attunement conversion
|
|
const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
|
|
|
|
// Mana regeneration (now includes attunement conversion deduction)
|
|
let rawMana = Math.min(manaState.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
|
|
let elements = { ...manaState.elements };
|
|
|
|
// Apply attunement conversion (add to primary mana types)
|
|
Object.entries(attunementState.attunements).forEach(([id, state]) => {
|
|
if (!state.active) return;
|
|
const def = ATTUNEMENTS_DEF[id];
|
|
if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
|
|
|
|
const scaledRate = getAttunementConversionRate(id, state.level || 1);
|
|
const conversionThisTick = scaledRate * HOURS_PER_TICK;
|
|
|
|
// Add to primary mana type (cost already deducted from regen)
|
|
if (elements[def.primaryManaType]) {
|
|
elements[def.primaryManaType].current = Math.min(
|
|
elements[def.primaryManaType].max,
|
|
elements[def.primaryManaType].current + conversionThisTick
|
|
);
|
|
}
|
|
});
|
|
let totalManaGathered = manaState.totalManaGathered;
|
|
|
|
// Convert action - delegate to manaStore
|
|
if (combatState.currentAction === 'convert') {
|
|
const convertResult = useManaStore.getState().processConvertAction(rawMana);
|
|
if (convertResult) {
|
|
rawMana = convertResult.rawMana;
|
|
elements = convertResult.elements;
|
|
}
|
|
}
|
|
|
|
// Pact ritual progress
|
|
if (prestigeState.pactRitualFloor !== null) {
|
|
const guardian = GUARDIANS[prestigeState.pactRitualFloor];
|
|
if (guardian) {
|
|
const pactAffinityBonus = 1 - (prestigeState.prestigeUpgrades.pactAffinity || 0) * 0.1;
|
|
const requiredTime = guardian.pactTime * pactAffinityBonus;
|
|
const newProgress = prestigeState.pactRitualProgress + HOURS_PER_TICK;
|
|
|
|
if (newProgress >= requiredTime) {
|
|
addLog(`📜 Pact signed with ${guardian.name}! You have gained their boons.`);
|
|
usePrestigeStore.getState().addSignedPact(prestigeState.pactRitualFloor);
|
|
usePrestigeStore.getState().removeDefeatedGuardian(prestigeState.pactRitualFloor);
|
|
usePrestigeStore.getState().setPactRitualFloor(null);
|
|
} else {
|
|
usePrestigeStore.getState().updatePactRitualProgress(newProgress);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Combat - delegate to combatStore
|
|
if (combatState.currentAction === 'climb') {
|
|
const combatResult = useCombatStore.getState().processCombatTick(
|
|
{},
|
|
rawMana,
|
|
elements,
|
|
maxMana,
|
|
1,
|
|
(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
|
|
let dmg = damage;
|
|
|
|
// Executioner: +100% damage to enemies below 25% HP
|
|
if (hasSpecial({}, SPECIAL_EFFECTS.EXECUTIONER) && combatState.floorHP / combatState.floorMaxHP < 0.25) {
|
|
dmg *= 2;
|
|
}
|
|
|
|
// Berserker: +50% damage when below 50% mana
|
|
if (hasSpecial({}, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
|
dmg *= 1.5;
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
// Update all stores with new state
|
|
useManaStore.setState({
|
|
rawMana,
|
|
meditateTicks,
|
|
totalManaGathered,
|
|
elements,
|
|
});
|
|
|
|
set({
|
|
day,
|
|
hour,
|
|
incursionStrength,
|
|
});
|
|
},
|
|
|
|
resetGame: createResetGame(set, initialState),
|
|
togglePause: () => {
|
|
useUIStore.getState().togglePause();
|
|
},
|
|
startNewLoop: createStartNewLoop(set),
|
|
gatherMana: createGatherMana(),
|
|
}),
|
|
{
|
|
name: 'mana-loop-game-storage',
|
|
partialize: (state) => ({
|
|
day: state.day,
|
|
hour: state.hour,
|
|
incursionStrength: state.incursionStrength,
|
|
containmentWards: state.containmentWards,
|
|
}),
|
|
}
|
|
)
|
|
);
|