refactor: Major codebase refactoring for maintainability
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 3m4s
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 3m4s
Store refactoring (2138 → 1651 lines, 23% reduction): - Extract computed-stats.ts with 18 utility functions - Extract navigation-slice.ts for floor navigation actions - Extract study-slice.ts for study-related actions - Move fmt/fmtDec to computed-stats, re-export from formatting Page refactoring (2554 → 1695 lines, 34% reduction): - Use existing SpireTab component instead of inline render - Extract ActionButtons component - Extract CalendarDisplay component - Extract CraftingProgress component - Extract StudyProgress component - Extract ManaDisplay component - Extract TimeDisplay component - Create tabs/index.ts for cleaner exports This improves code organization and makes the codebase more maintainable.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect, LootInventory } from './types';
|
||||
import type { GameState, GameAction, StudyTarget, SkillUpgradeChoice, EquipmentSlot, EnchantmentDesign, DesignEffect, LootInventory } from './types';
|
||||
import {
|
||||
ELEMENTS,
|
||||
GUARDIANS,
|
||||
@@ -17,19 +17,13 @@ import {
|
||||
INCURSION_START_DAY,
|
||||
MANA_PER_ELEMENT,
|
||||
getStudySpeedMultiplier,
|
||||
getStudyCostMultiplier,
|
||||
ELEMENT_OPPOSITES,
|
||||
EFFECT_RESEARCH_MAPPING,
|
||||
BASE_UNLOCKED_EFFECTS,
|
||||
ENCHANTING_UNLOCK_EFFECTS,
|
||||
} from './constants';
|
||||
import { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects } from './upgrade-effects';
|
||||
import {
|
||||
computeAllEffects,
|
||||
getUnifiedEffects,
|
||||
computeEquipmentEffects,
|
||||
type UnifiedEffects
|
||||
} from './effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from './upgrade-effects';
|
||||
import { getUnifiedEffects } from './effects';
|
||||
import { SKILL_EVOLUTION_PATHS } from './skill-evolution';
|
||||
import {
|
||||
createStartingEquipment,
|
||||
@@ -37,7 +31,6 @@ import {
|
||||
getSpellsFromEquipment,
|
||||
type CraftingActions
|
||||
} from './crafting-slice';
|
||||
import { EQUIPMENT_TYPES } from './data/equipment';
|
||||
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
|
||||
import {
|
||||
createFamiliarSlice,
|
||||
@@ -47,228 +40,56 @@ import {
|
||||
type FamiliarBonuses,
|
||||
DEFAULT_FAMILIAR_BONUSES,
|
||||
} from './familiar-slice';
|
||||
import {
|
||||
createNavigationSlice,
|
||||
type NavigationActions,
|
||||
} from './navigation-slice';
|
||||
import {
|
||||
createStudySlice,
|
||||
type StudyActions,
|
||||
} from './study-slice';
|
||||
import { rollLootDrops, LOOT_DROPS } from './data/loot-drops';
|
||||
import { CRAFTING_RECIPES, canCraftRecipe } from './data/crafting-recipes';
|
||||
import { EQUIPMENT_TYPES } from './data/equipment';
|
||||
import type { EquipmentInstance } from './types';
|
||||
// Import computed stats and utility functions from computed-stats.ts
|
||||
import {
|
||||
DEFAULT_EFFECTS,
|
||||
fmt,
|
||||
fmtDec,
|
||||
getFloorMaxHP,
|
||||
getFloorElement,
|
||||
getActiveEquipmentSpells,
|
||||
getEffectiveSkillLevel,
|
||||
computeMaxMana,
|
||||
computeElementMax,
|
||||
computeRegen,
|
||||
computeEffectiveRegen,
|
||||
computeClickMana,
|
||||
calcDamage,
|
||||
calcInsight,
|
||||
getMeditationBonus,
|
||||
getIncursionStrength,
|
||||
canAffordSpellCost,
|
||||
deductSpellCost,
|
||||
} from './computed-stats';
|
||||
|
||||
// Default empty effects for when effects aren't provided
|
||||
const DEFAULT_EFFECTS: ComputedEffects = {
|
||||
maxManaMultiplier: 1,
|
||||
maxManaBonus: 0,
|
||||
regenMultiplier: 1,
|
||||
regenBonus: 0,
|
||||
clickManaMultiplier: 1,
|
||||
clickManaBonus: 0,
|
||||
meditationEfficiency: 1,
|
||||
spellCostMultiplier: 1,
|
||||
conversionEfficiency: 1,
|
||||
baseDamageMultiplier: 1,
|
||||
baseDamageBonus: 0,
|
||||
attackSpeedMultiplier: 1,
|
||||
critChanceBonus: 0,
|
||||
critDamageMultiplier: 1.5,
|
||||
elementalDamageMultiplier: 1,
|
||||
studySpeedMultiplier: 1,
|
||||
studyCostMultiplier: 1,
|
||||
progressRetention: 0,
|
||||
instantStudyChance: 0,
|
||||
freeStudyChance: 0,
|
||||
elementCapMultiplier: 1,
|
||||
elementCapBonus: 0,
|
||||
conversionCostMultiplier: 1,
|
||||
doubleCraftChance: 0,
|
||||
permanentRegenBonus: 0,
|
||||
specials: new Set(),
|
||||
activeUpgrades: [],
|
||||
// Re-export formatting functions and computed stats for backward compatibility
|
||||
export {
|
||||
fmt,
|
||||
fmtDec,
|
||||
getFloorElement,
|
||||
computeMaxMana,
|
||||
computeRegen,
|
||||
computeClickMana,
|
||||
calcDamage,
|
||||
getMeditationBonus,
|
||||
getIncursionStrength,
|
||||
canAffordSpellCost,
|
||||
getFloorMaxHP,
|
||||
};
|
||||
|
||||
// ─── Helper Functions ─────────────────────────────────────────────────────────
|
||||
|
||||
export function fmt(n: number): string {
|
||||
if (!isFinite(n) || isNaN(n)) return '0';
|
||||
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
|
||||
if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
|
||||
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
||||
return Math.floor(n).toString();
|
||||
}
|
||||
|
||||
export function fmtDec(n: number, d: number = 1): string {
|
||||
return isFinite(n) ? n.toFixed(d) : '0';
|
||||
}
|
||||
|
||||
export function getFloorMaxHP(floor: number): number {
|
||||
if (GUARDIANS[floor]) return GUARDIANS[floor].hp;
|
||||
// Improved scaling: slower early game, faster late game
|
||||
const baseHP = 100;
|
||||
const floorScaling = floor * 50;
|
||||
const exponentialScaling = Math.pow(floor, 1.7);
|
||||
return Math.floor(baseHP + floorScaling + exponentialScaling);
|
||||
}
|
||||
|
||||
export function getFloorElement(floor: number): string {
|
||||
return FLOOR_ELEM_CYCLE[(floor - 1) % 8];
|
||||
}
|
||||
|
||||
// Get all spells from equipped caster weapons (staves, wands, etc.)
|
||||
// Returns array of { spellId, equipmentInstanceId }
|
||||
function getActiveEquipmentSpells(
|
||||
equippedInstances: Record<string, string | null>,
|
||||
equipmentInstances: Record<string, EquipmentInstance>
|
||||
): Array<{ spellId: string; equipmentId: string }> {
|
||||
const spells: Array<{ spellId: string; equipmentId: string }> = [];
|
||||
|
||||
// Check main hand and off hand for caster equipment
|
||||
const weaponSlots = ['mainHand', 'offHand'] as const;
|
||||
|
||||
for (const slot of weaponSlots) {
|
||||
const instanceId = equippedInstances[slot];
|
||||
if (!instanceId) continue;
|
||||
|
||||
const instance = equipmentInstances[instanceId];
|
||||
if (!instance) continue;
|
||||
|
||||
// Check if this is a caster-type equipment
|
||||
const equipType = EQUIPMENT_TYPES[instance.typeId];
|
||||
if (!equipType || equipType.category !== 'caster') continue;
|
||||
|
||||
// Get spells from enchantments
|
||||
for (const ench of instance.enchantments) {
|
||||
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
|
||||
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
|
||||
spells.push({
|
||||
spellId: effectDef.effect.spellId,
|
||||
equipmentId: instanceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return spells;
|
||||
}
|
||||
|
||||
// ─── Computed Stats Functions ─────────────────────────────────────────────────
|
||||
|
||||
// Helper to get effective skill level accounting for tiers
|
||||
function getEffectiveSkillLevel(
|
||||
skills: Record<string, number>,
|
||||
baseSkillId: string,
|
||||
skillTiers: Record<string, number> = {}
|
||||
): { level: number; tier: number; tierMultiplier: number } {
|
||||
// Find the highest tier the player has for this base skill
|
||||
const currentTier = skillTiers[baseSkillId] || 1;
|
||||
|
||||
// Look for the tiered skill ID (e.g., manaFlow_t2)
|
||||
const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId;
|
||||
const level = skills[tieredSkillId] || skills[baseSkillId] || 0;
|
||||
|
||||
// Tier multiplier: each tier is 10x more powerful
|
||||
const tierMultiplier = Math.pow(10, currentTier - 1);
|
||||
|
||||
return { level, tier: currentTier, tierMultiplier };
|
||||
}
|
||||
|
||||
export function computeMaxMana(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const base =
|
||||
100 +
|
||||
(state.skills.manaWell || 0) * 100 +
|
||||
(pu.manaWell || 0) * 500;
|
||||
|
||||
// If effects not provided, compute unified effects (includes equipment)
|
||||
if (!effects && state.equipmentInstances && state.equippedInstances) {
|
||||
effects = getUnifiedEffects(state as any);
|
||||
}
|
||||
|
||||
// Apply effects if available (now includes equipment bonuses)
|
||||
if (effects) {
|
||||
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export function computeElementMax(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
|
||||
effects?: ComputedEffects
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const base = 10 + (state.skills.elemAttune || 0) * 50 + (pu.elementalAttune || 0) * 25;
|
||||
|
||||
// Apply upgrade effects if provided
|
||||
if (effects) {
|
||||
return Math.floor((base + effects.elementCapBonus) * effects.elementCapMultiplier);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export function computeRegen(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
|
||||
const base =
|
||||
2 +
|
||||
(state.skills.manaFlow || 0) * 1 +
|
||||
(state.skills.manaSpring || 0) * 2 +
|
||||
(pu.manaFlow || 0) * 0.5;
|
||||
|
||||
let regen = base * temporalBonus;
|
||||
|
||||
// If effects not provided, compute unified effects (includes equipment)
|
||||
if (!effects && state.equipmentInstances && state.equippedInstances) {
|
||||
effects = getUnifiedEffects(state as any);
|
||||
}
|
||||
|
||||
// Apply effects if available (now includes equipment bonuses)
|
||||
if (effects) {
|
||||
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
|
||||
}
|
||||
|
||||
return regen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute regen with dynamic special effects (needs current mana, max mana, incursion)
|
||||
*/
|
||||
export function computeEffectiveRegen(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): number {
|
||||
// Base regen from existing function
|
||||
let regen = computeRegen(state, effects);
|
||||
|
||||
const maxMana = computeMaxMana(state, effects);
|
||||
const currentMana = state.rawMana;
|
||||
const incursionStrength = state.incursionStrength || 0;
|
||||
|
||||
// Apply incursion penalty
|
||||
regen *= (1 - incursionStrength);
|
||||
|
||||
return regen;
|
||||
}
|
||||
|
||||
export function computeClickMana(
|
||||
state: Pick<GameState, 'skills' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): number {
|
||||
const base =
|
||||
1 +
|
||||
(state.skills.manaTap || 0) * 1 +
|
||||
(state.skills.manaSurge || 0) * 3;
|
||||
|
||||
// If effects not provided, compute unified effects (includes equipment)
|
||||
if (!effects && state.equipmentInstances && state.equippedInstances) {
|
||||
effects = getUnifiedEffects(state as any);
|
||||
}
|
||||
|
||||
// Apply effects if available (now includes equipment bonuses)
|
||||
if (effects) {
|
||||
return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
// ─── Local Helper Functions ────────────────────────────────────────────────────
|
||||
|
||||
// Elemental damage bonus: +50% if spell element opposes floor element (super effective)
|
||||
// -25% if spell element matches its own opposite (weak)
|
||||
@@ -288,129 +109,6 @@ function getElementalBonus(spellElem: string, floorElem: string): number {
|
||||
return 1.0; // Neutral
|
||||
}
|
||||
|
||||
export function calcDamage(
|
||||
state: Pick<GameState, 'skills' | 'signedPacts'>,
|
||||
spellId: string,
|
||||
floorElem?: string
|
||||
): number {
|
||||
const sp = SPELLS_DEF[spellId];
|
||||
if (!sp) return 5;
|
||||
const skills = state.skills;
|
||||
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5;
|
||||
const pct = 1 + (skills.arcaneFury || 0) * 0.1;
|
||||
|
||||
// Elemental mastery bonus
|
||||
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
|
||||
|
||||
// Guardian bane bonus
|
||||
const guardianBonus = floorElem && GUARDIANS[Object.values(GUARDIANS).find(g => g.element === floorElem)?.hp ? 0 : 0]
|
||||
? 1 + (skills.guardianBane || 0) * 0.2
|
||||
: 1;
|
||||
|
||||
const critChance = (skills.precision || 0) * 0.05;
|
||||
const pactMult = state.signedPacts.reduce(
|
||||
(m, f) => m * (GUARDIANS[f]?.pact || 1),
|
||||
1
|
||||
);
|
||||
|
||||
let damage = baseDmg * pct * pactMult * elemMasteryBonus;
|
||||
|
||||
// Apply elemental bonus if floor element provided
|
||||
if (floorElem) {
|
||||
damage *= getElementalBonus(sp.elem, floorElem);
|
||||
}
|
||||
|
||||
// Apply crit
|
||||
if (Math.random() < critChance) {
|
||||
damage *= 1.5;
|
||||
}
|
||||
|
||||
return damage;
|
||||
}
|
||||
|
||||
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1;
|
||||
const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus;
|
||||
return Math.floor(
|
||||
(state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult
|
||||
);
|
||||
}
|
||||
|
||||
// Meditation bonus now affects regen rate directly
|
||||
export function getMeditationBonus(meditateTicks: number, skills: Record<string, number>, meditationEfficiency: number = 1): number {
|
||||
const hasMeditation = skills.meditation === 1;
|
||||
const hasDeepTrance = skills.deepTrance === 1;
|
||||
const hasVoidMeditation = skills.voidMeditation === 1;
|
||||
|
||||
const hours = meditateTicks * HOURS_PER_TICK;
|
||||
|
||||
// Base meditation: ramps up over 4 hours to 1.5x
|
||||
let bonus = 1 + Math.min(hours / 4, 0.5);
|
||||
|
||||
// With Meditation Focus: up to 2.5x after 4 hours
|
||||
if (hasMeditation && hours >= 4) {
|
||||
bonus = 2.5;
|
||||
}
|
||||
|
||||
// With Deep Trance: up to 3.0x after 6 hours
|
||||
if (hasDeepTrance && hours >= 6) {
|
||||
bonus = 3.0;
|
||||
}
|
||||
|
||||
// With Void Meditation: up to 5.0x after 8 hours
|
||||
if (hasVoidMeditation && hours >= 8) {
|
||||
bonus = 5.0;
|
||||
}
|
||||
|
||||
// Apply meditation efficiency from upgrades (Deep Wellspring, etc.)
|
||||
bonus *= meditationEfficiency;
|
||||
|
||||
return bonus;
|
||||
}
|
||||
|
||||
export function getIncursionStrength(day: number, hour: number): number {
|
||||
if (day < INCURSION_START_DAY) return 0;
|
||||
const totalHours = (day - INCURSION_START_DAY) * 24 + hour;
|
||||
const maxHours = (MAX_DAY - INCURSION_START_DAY) * 24;
|
||||
return Math.min(0.95, (totalHours / maxHours) * 0.95);
|
||||
}
|
||||
|
||||
// Check if player can afford spell cost
|
||||
export function canAffordSpellCost(
|
||||
cost: SpellCost,
|
||||
rawMana: number,
|
||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>
|
||||
): boolean {
|
||||
if (cost.type === 'raw') {
|
||||
return rawMana >= cost.amount;
|
||||
} else {
|
||||
const elem = elements[cost.element || ''];
|
||||
return elem && elem.unlocked && elem.current >= cost.amount;
|
||||
}
|
||||
}
|
||||
|
||||
// Deduct spell cost from appropriate mana pool
|
||||
function deductSpellCost(
|
||||
cost: SpellCost,
|
||||
rawMana: number,
|
||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>
|
||||
): { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> } {
|
||||
const newElements = { ...elements };
|
||||
|
||||
if (cost.type === 'raw') {
|
||||
return { rawMana: rawMana - cost.amount, elements: newElements };
|
||||
} else if (cost.element && newElements[cost.element]) {
|
||||
newElements[cost.element] = {
|
||||
...newElements[cost.element],
|
||||
current: newElements[cost.element].current - cost.amount
|
||||
};
|
||||
return { rawMana, elements: newElements };
|
||||
}
|
||||
|
||||
return { rawMana, elements: newElements };
|
||||
}
|
||||
|
||||
// ─── Initial State Factory ────────────────────────────────────────────────────
|
||||
|
||||
function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||
@@ -587,17 +285,12 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||
|
||||
// ─── Game Store ───────────────────────────────────────────────────────────────
|
||||
|
||||
interface GameStore extends GameState, CraftingActions, FamiliarActions {
|
||||
interface GameStore extends GameState, CraftingActions, FamiliarActions, NavigationActions, StudyActions {
|
||||
// Actions
|
||||
tick: () => void;
|
||||
gatherMana: () => void;
|
||||
setAction: (action: GameAction) => void;
|
||||
setSpell: (spellId: string) => void;
|
||||
startStudyingSkill: (skillId: string) => void;
|
||||
startStudyingSpell: (spellId: string) => void;
|
||||
startParallelStudySkill: (skillId: string) => void;
|
||||
cancelStudy: () => void;
|
||||
cancelParallelStudy: () => void;
|
||||
convertMana: (element: string, amount: number) => void;
|
||||
unlockElement: (element: string) => void;
|
||||
craftComposite: (target: string) => void;
|
||||
@@ -611,10 +304,6 @@ interface GameStore extends GameState, CraftingActions, FamiliarActions {
|
||||
commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => void;
|
||||
tierUpSkill: (skillId: string) => void;
|
||||
|
||||
// Floor Navigation
|
||||
setClimbDirection: (direction: 'up' | 'down') => void;
|
||||
changeFloor: (direction: 'up' | 'down') => void;
|
||||
|
||||
// Inventory Management
|
||||
updateLootInventory: (inventory: LootInventory) => void;
|
||||
|
||||
@@ -633,6 +322,8 @@ export const useGameStore = create<GameStore>()(
|
||||
(set, get) => ({
|
||||
...makeInitial(),
|
||||
...createFamiliarSlice(set, get),
|
||||
...createNavigationSlice(set, get),
|
||||
...createStudySlice(set, get),
|
||||
|
||||
getMaxMana: () => computeMaxMana(get()),
|
||||
getRegen: () => computeRegen(get()),
|
||||
@@ -1221,108 +912,6 @@ export const useGameStore = create<GameStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
startStudyingSkill: (skillId: string) => {
|
||||
const state = get();
|
||||
const sk = SKILLS_DEF[skillId];
|
||||
if (!sk) return;
|
||||
|
||||
const currentLevel = state.skills[skillId] || 0;
|
||||
if (currentLevel >= sk.max) return;
|
||||
|
||||
// Check prerequisites
|
||||
if (sk.req) {
|
||||
for (const [r, rl] of Object.entries(sk.req)) {
|
||||
if ((state.skills[r] || 0) < rl) return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check mana cost (with focused mind reduction)
|
||||
const costMult = getStudyCostMultiplier(state.skills);
|
||||
const cost = Math.floor(sk.base * (currentLevel + 1) * costMult);
|
||||
if (state.rawMana < cost) return;
|
||||
|
||||
// Start studying
|
||||
set({
|
||||
rawMana: state.rawMana - cost,
|
||||
currentAction: 'study',
|
||||
currentStudyTarget: {
|
||||
type: 'skill',
|
||||
id: skillId,
|
||||
progress: state.skillProgress[skillId] || 0,
|
||||
required: sk.studyTime,
|
||||
},
|
||||
log: [`📚 Started studying ${sk.name}...`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
},
|
||||
|
||||
startStudyingSpell: (spellId: string) => {
|
||||
const state = get();
|
||||
const sp = SPELLS_DEF[spellId];
|
||||
if (!sp || state.spells[spellId]?.learned) return;
|
||||
|
||||
// Check mana cost (with focused mind reduction)
|
||||
const costMult = getStudyCostMultiplier(state.skills);
|
||||
const cost = Math.floor(sp.unlock * costMult);
|
||||
if (state.rawMana < cost) return;
|
||||
|
||||
const studyTime = sp.studyTime || (sp.tier * 4); // Default study time based on tier
|
||||
|
||||
// Start studying
|
||||
set({
|
||||
rawMana: state.rawMana - cost,
|
||||
currentAction: 'study',
|
||||
currentStudyTarget: {
|
||||
type: 'spell',
|
||||
id: spellId,
|
||||
progress: state.spells[spellId]?.studyProgress || 0,
|
||||
required: studyTime,
|
||||
},
|
||||
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)],
|
||||
});
|
||||
},
|
||||
|
||||
cancelStudy: () => {
|
||||
const state = get();
|
||||
if (!state.currentStudyTarget) return;
|
||||
|
||||
// Knowledge retention bonus
|
||||
const retentionBonus = 1 + (state.skills.knowledgeRetention || 0) * 0.2;
|
||||
const savedProgress = Math.min(
|
||||
state.currentStudyTarget.progress,
|
||||
state.currentStudyTarget.required * retentionBonus
|
||||
);
|
||||
|
||||
// Save progress
|
||||
if (state.currentStudyTarget.type === 'skill') {
|
||||
set({
|
||||
currentStudyTarget: null,
|
||||
currentAction: 'meditate',
|
||||
skillProgress: {
|
||||
...state.skillProgress,
|
||||
[state.currentStudyTarget.id]: savedProgress,
|
||||
},
|
||||
log: [`📖 Study interrupted. Progress saved.`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
} else if (state.currentStudyTarget.type === 'spell') {
|
||||
set({
|
||||
currentStudyTarget: null,
|
||||
currentAction: 'meditate',
|
||||
spells: {
|
||||
...state.spells,
|
||||
[state.currentStudyTarget.id]: {
|
||||
...(state.spells[state.currentStudyTarget.id] || { learned: false, level: 0 }),
|
||||
studyProgress: savedProgress,
|
||||
},
|
||||
},
|
||||
log: [`📖 Study interrupted. Progress saved.`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
convertMana: (element: string, amount: number = 1) => {
|
||||
const state = get();
|
||||
const e = state.elements[element];
|
||||
@@ -1527,42 +1116,7 @@ export const useGameStore = create<GameStore>()(
|
||||
log: [`🌟 ${SKILLS_DEF[baseSkillId]?.name || baseSkillId} evolved to Tier ${nextTier}!`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
},
|
||||
|
||||
startParallelStudySkill: (skillId: string) => {
|
||||
const state = get();
|
||||
if (state.parallelStudyTarget) return; // Already have parallel study
|
||||
if (!state.currentStudyTarget) return; // Need primary study
|
||||
|
||||
const sk = SKILLS_DEF[skillId];
|
||||
if (!sk) return;
|
||||
|
||||
const currentLevel = state.skills[skillId] || 0;
|
||||
if (currentLevel >= sk.max) return;
|
||||
|
||||
// Can't study same thing in parallel
|
||||
if (state.currentStudyTarget.id === skillId) return;
|
||||
|
||||
set({
|
||||
parallelStudyTarget: {
|
||||
type: 'skill',
|
||||
id: skillId,
|
||||
progress: state.skillProgress[skillId] || 0,
|
||||
required: sk.studyTime,
|
||||
},
|
||||
log: [`📚 Started parallel study of ${sk.name}... (50% speed)`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
},
|
||||
|
||||
cancelParallelStudy: () => {
|
||||
set((state) => {
|
||||
if (!state.parallelStudyTarget) return state;
|
||||
return {
|
||||
parallelStudyTarget: null,
|
||||
log: ['📖 Parallel study cancelled.', ...state.log.slice(0, 49)],
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => {
|
||||
const state = get();
|
||||
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
||||
@@ -2007,47 +1561,6 @@ export const useGameStore = create<GameStore>()(
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// ─── Floor Navigation ────────────────────────────────────────────────────────
|
||||
|
||||
setClimbDirection: (direction: 'up' | 'down') => {
|
||||
set({ climbDirection: direction });
|
||||
},
|
||||
|
||||
changeFloor: (direction: 'up' | 'down') => {
|
||||
const state = get();
|
||||
const currentFloor = state.currentFloor;
|
||||
|
||||
// Calculate next floor
|
||||
const nextFloor = direction === 'up'
|
||||
? Math.min(currentFloor + 1, 100)
|
||||
: Math.max(currentFloor - 1, 1);
|
||||
|
||||
// Can't stay on same floor
|
||||
if (nextFloor === currentFloor) return;
|
||||
|
||||
// Mark current floor as cleared (it will respawn when we come back)
|
||||
const clearedFloors = { ...state.clearedFloors };
|
||||
clearedFloors[currentFloor] = true;
|
||||
|
||||
// Check if next floor was cleared (needs respawn)
|
||||
const nextFloorCleared = clearedFloors[nextFloor];
|
||||
if (nextFloorCleared) {
|
||||
// Respawn the floor
|
||||
delete clearedFloors[nextFloor];
|
||||
}
|
||||
|
||||
set({
|
||||
currentFloor: nextFloor,
|
||||
floorMaxHP: getFloorMaxHP(nextFloor),
|
||||
floorHP: getFloorMaxHP(nextFloor),
|
||||
maxFloorReached: Math.max(state.maxFloorReached, nextFloor),
|
||||
clearedFloors,
|
||||
climbDirection: direction,
|
||||
equipmentSpellStates: state.equipmentSpellStates.map(s => ({ ...s, castProgress: 0 })),
|
||||
log: [`🚶 Moved to floor ${nextFloor}${nextFloorCleared ? ' (respawned)' : ''}.`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'mana-loop-storage',
|
||||
|
||||
Reference in New Issue
Block a user