();
for (const skill of skills) {
@@ -56,7 +53,7 @@ export function MemorySlotPicker({ onConfirm }: MemorySlotPickerProps) {
uniqueSkills.set(skill.skillId, skill);
}
}
-
+
return Array.from(uniqueSkills.values()).sort((a, b) => {
// Sort by tier then level then name
if (a.tier !== b.tier) return b.tier - a.tier;
@@ -66,12 +63,12 @@ export function MemorySlotPicker({ onConfirm }: MemorySlotPickerProps) {
}, [store.skills, store.skillTiers, store.skillUpgrades]);
const isSkillSelected = (skillId: string) => selectedSkills.some(m => m.skillId === skillId);
-
+
const canAddMore = selectedSkills.length < store.memorySlots;
const toggleSkill = (skillId: string) => {
const existingIndex = selectedSkills.findIndex(m => m.skillId === skillId);
-
+
if (existingIndex >= 0) {
// Remove it
setSelectedSkills(selectedSkills.filter((_, i) => i !== existingIndex));
@@ -116,22 +113,19 @@ export function MemorySlotPicker({ onConfirm }: MemorySlotPickerProps) {
Saved to Memory:
- {selectedSkills.map((memory) => {
- const skillDef = SKILLS_DEF[memory.skillId];
- return (
- toggleSkill(memory.skillId)}
- >
- {skillDef?.name || memory.skillId}
- {' '}Lv.{memory.level}
- {memory.tier > 1 && ` T${memory.tier}`}
- {memory.upgrades.length > 0 && ` (${memory.upgrades.length}⭐)`}
-
-
- );
- })}
+ {selectedSkills.map((memory) => (
+ toggleSkill(memory.skillId)}
+ >
+ {memory.skillId}
+ {' '}Lv.{memory.level}
+ {memory.tier > 1 && ` T${memory.tier}`}
+ {memory.upgrades.length > 0 && ` (${memory.upgrades.length}⭐)`}
+
+
+ ))}
)}
@@ -147,8 +141,8 @@ export function MemorySlotPicker({ onConfirm }: MemorySlotPickerProps) {
) : (
saveableSkills.map((skill) => {
const isSelected = isSkillSelected(skill.skillId);
- const tierMult = getTierMultiplier(skill.tier > 1 ? `${skill.skillId}_t${skill.tier}` : skill.skillId);
-
+ const tierMult = 1;
+
return (
,
+ _skillTiers: Record
+): ActiveUpgradeEffect[] {
+ return [];
+}
+
+/**
+ * Compute all active effects from selected upgrades.
+ * Since skills are removed, returns base values with no upgrades applied.
+ */
+export function computeEffects(
+ _skillUpgrades: Record,
+ _skillTiers: Record
+): ComputedEffects {
+ return {
+ 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,
+ perElementCapBonus: {},
+ conversionCostMultiplier: 1,
+ doubleCraftChance: 0,
+ permanentRegenBonus: 0,
+ specials: new Set(),
+ activeUpgrades: [],
+ skillLevelMultiplier: 1,
+ enchantmentPowerMultiplier: 1,
+ };
+}
diff --git a/src/lib/game/upgrade-effects.types.ts b/src/lib/game/effects/upgrade-effects.types.ts
similarity index 100%
rename from src/lib/game/upgrade-effects.types.ts
rename to src/lib/game/effects/upgrade-effects.types.ts
diff --git a/src/lib/game/hooks/useGameDerived.ts b/src/lib/game/hooks/useGameDerived.ts
index 163b230..ecb64d8 100755
--- a/src/lib/game/hooks/useGameDerived.ts
+++ b/src/lib/game/hooks/useGameDerived.ts
@@ -2,8 +2,11 @@
// Custom hooks for computing derived game stats from the store
import { useMemo } from 'react';
-import { useGameStore } from '../store';
-import { computeEffects } from '../upgrade-effects';
+import { useGameStore } from '../stores/gameStore';
+import { useManaStore } from '../stores/manaStore';
+import { useCombatStore } from '../stores/combatStore';
+import { usePrestigeStore } from '../stores/prestigeStore';
+import { computeEffects } from '../effects/upgrade-effects';
import {
computeMaxMana,
computeRegen,
@@ -12,65 +15,71 @@ import {
getIncursionStrength,
getFloorElement,
calcDamage,
- computePactMultiplier,
- computePactInsightMultiplier,
getElementalBonus,
-} from '../store/computed';
+} from '../utils';
+import { computePactMultiplier, computePactInsightMultiplier } from '../utils/pact-utils';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier, HOURS_PER_TICK, TICK_MS } from '../constants';
-import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
+import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
/**
* Hook for all mana-related derived stats
*/
export function useManaStats() {
- const store = useGameStore();
-
+ const skills = useGameStore((s) => s.skills);
+ const skillUpgrades = useGameStore((s) => s.skillUpgrades);
+ const skillTiers = useGameStore((s) => s.skillTiers);
+ const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
+ const rawMana = useManaStore((s) => s.rawMana);
+ const meditateTicks = useManaStore((s) => s.meditateTicks);
+ const day = useGameStore((s) => s.day);
+ const hour = useGameStore((s) => s.hour);
+
const upgradeEffects = useMemo(
- () => computeEffects(store.skillUpgrades || {}, store.skillTiers || {}),
- [store.skillUpgrades, store.skillTiers]
+ () => computeEffects(skillUpgrades || {}, skillTiers || {}),
+ [skillUpgrades, skillTiers]
);
-
+
const maxMana = useMemo(
- () => computeMaxMana(store, upgradeEffects),
- [store, upgradeEffects]
+ () => computeMaxMana({ skills, prestigeUpgrades, skillUpgrades, skillTiers }, upgradeEffects),
+ [skills, prestigeUpgrades, skillUpgrades, skillTiers, upgradeEffects]
);
-
+
const baseRegen = useMemo(
- () => computeRegen(store, upgradeEffects),
- [store, upgradeEffects]
+ () => computeRegen({ skills, prestigeUpgrades, skillUpgrades, skillTiers }, upgradeEffects),
+ [skills, prestigeUpgrades, skillUpgrades, skillTiers, upgradeEffects]
);
-
+
const clickMana = useMemo(
- () => computeClickMana(store),
- [store]
+ () => computeClickMana({ skills }),
+ [skills]
);
-
+
const meditationMultiplier = useMemo(
- () => getMeditationBonus(store.meditateTicks, store.skills, upgradeEffects.meditationEfficiency),
- [store.meditateTicks, store.skills, upgradeEffects.meditationEfficiency]
+ () => getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency),
+ [meditateTicks, skills, upgradeEffects.meditationEfficiency]
);
-
+
const incursionStrength = useMemo(
- () => getIncursionStrength(store.day, store.hour),
- [store.day, store.hour]
+ () => getIncursionStrength(day, hour),
+ [day, hour]
);
-
+
// Effective regen with incursion penalty
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
-
+
// Mana Cascade bonus
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
? Math.floor(maxMana / 100) * 0.1
: 0;
-
+
// Mana Waterfall bonus
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
? Math.floor(maxMana / 100) * 0.25
: 0;
-
+
// Final effective regen
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
-
+
return {
upgradeEffects,
maxMana,
@@ -97,70 +106,73 @@ export function useManaStats() {
* Hook for combat-related derived stats
*/
export function useCombatStats() {
- const store = useGameStore();
+ const skills = useGameStore((s) => s.skills);
+ const signedPacts = usePrestigeStore((s) => s.signedPacts);
+ const currentFloor = useCombatStore((s) => s.currentFloor);
+ const activeSpell = useCombatStore((s) => s.activeSpell);
const { upgradeEffects } = useManaStats();
-
+
const floorElem = useMemo(
- () => getFloorElement(store.currentFloor),
- [store.currentFloor]
+ () => getFloorElement(currentFloor),
+ [currentFloor]
);
-
+
const floorElemDef = useMemo(
() => ELEMENTS[floorElem],
[floorElem]
);
-
+
const isGuardianFloor = useMemo(
- () => !!GUARDIANS[store.currentFloor],
- [store.currentFloor]
+ () => !!GUARDIANS[currentFloor],
+ [currentFloor]
);
-
+
const currentGuardian = useMemo(
- () => GUARDIANS[store.currentFloor],
- [store.currentFloor]
+ () => GUARDIANS[currentFloor],
+ [currentFloor]
);
-
+
const activeSpellDef = useMemo(
- () => SPELLS_DEF[store.activeSpell],
- [store.activeSpell]
+ () => SPELLS_DEF[activeSpell],
+ [activeSpell]
);
-
+
const pactMultiplier = useMemo(
- () => computePactMultiplier(store),
- [store]
+ () => computePactMultiplier({ signedPacts }),
+ [signedPacts]
);
-
+
const pactInsightMultiplier = useMemo(
- () => computePactInsightMultiplier(store),
- [store]
+ () => computePactInsightMultiplier({ signedPacts }),
+ [signedPacts]
);
-
+
// DPS calculation
const dps = useMemo(() => {
if (!activeSpellDef) return 0;
-
+
const spellCastSpeed = activeSpellDef.castSpeed || 1;
- const quickCastBonus = 1 + (store.skills.quickCast || 0) * 0.05;
+ const quickCastBonus = 1 + (skills.quickCast || 0) * 0.05;
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier;
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
-
- const damagePerCast = calcDamage(store, store.activeSpell, floorElem);
+
+ const damagePerCast = calcDamage({ skills, signedPacts }, activeSpell, floorElem);
const castsPerSecond = totalCastSpeed * HOURS_PER_TICK / (TICK_MS / 1000);
-
+
return damagePerCast * castsPerSecond;
- }, [activeSpellDef, store, floorElem, upgradeEffects.attackSpeedMultiplier]);
-
+ }, [activeSpellDef, skills, signedPacts, activeSpell, floorElem, upgradeEffects.attackSpeedMultiplier]);
+
// Damage breakdown for display
const damageBreakdown = useMemo(() => {
if (!activeSpellDef) return null;
-
+
const baseDmg = activeSpellDef.dmg;
- const combatTrainBonus = (store.skills.combatTrain || 0) * 5;
- const arcaneFuryMult = 1 + (store.skills.arcaneFury || 0) * 0.1;
- const elemMasteryMult = 1 + (store.skills.elementalMastery || 0) * 0.15;
- const guardianBaneMult = isGuardianFloor ? (1 + (store.skills.guardianBane || 0) * 0.2) : 1;
- const precisionChance = (store.skills.precision || 0) * 0.05;
-
+ const combatTrainBonus = (skills.combatTrain || 0) * 5;
+ const arcaneFuryMult = 1 + (skills.arcaneFury || 0) * 0.1;
+ const elemMasteryMult = 1 + (skills.elementalMastery || 0) * 0.15;
+ const guardianBaneMult = isGuardianFloor ? (1 + (skills.guardianBane || 0) * 0.2) : 1;
+ const precisionChance = (skills.precision || 0) * 0.05;
+
// Calculate elemental bonus
const elemBonus = getElementalBonus(activeSpellDef.elem, floorElem);
let elemBonusText = '';
@@ -171,7 +183,7 @@ export function useCombatStats() {
elemBonusText = '+50% super effective';
}
}
-
+
return {
base: baseDmg,
combatTrainBonus,
@@ -182,10 +194,10 @@ export function useCombatStats() {
precisionChance,
elemBonus,
elemBonusText,
- total: calcDamage(store, store.activeSpell, floorElem),
+ total: calcDamage({ skills, signedPacts }, activeSpell, floorElem),
};
- }, [activeSpellDef, store, floorElem, isGuardianFloor, pactMultiplier]);
-
+ }, [activeSpellDef, skills, signedPacts, activeSpell, floorElem, isGuardianFloor, pactMultiplier]);
+
return {
floorElem,
floorElemDef,
@@ -203,25 +215,27 @@ export function useCombatStats() {
* Hook for study-related derived stats
*/
export function useStudyStats() {
- const store = useGameStore();
-
+ const skills = useGameStore((s) => s.skills);
+ const skillUpgrades = useGameStore((s) => s.skillUpgrades);
+ const skillTiers = useGameStore((s) => s.skillTiers);
+
const studySpeedMult = useMemo(
- () => getStudySpeedMultiplier(store.skills),
- [store.skills]
+ () => getStudySpeedMultiplier(skills),
+ [skills]
);
-
+
const studyCostMult = useMemo(
- () => getStudyCostMultiplier(store.skills),
- [store.skills]
+ () => getStudyCostMultiplier(skills),
+ [skills]
);
-
+
const upgradeEffects = useMemo(
- () => computeEffects(store.skillUpgrades || {}, store.skillTiers || {}),
- [store.skillUpgrades, store.skillTiers]
+ () => computeEffects(skillUpgrades || {}, skillTiers || {}),
+ [skillUpgrades, skillTiers]
);
-
+
const effectiveStudySpeedMult = studySpeedMult * upgradeEffects.studySpeedMultiplier;
-
+
return {
studySpeedMult,
studyCostMult,
diff --git a/src/lib/game/store-modules/activity-log.ts b/src/lib/game/store-modules/activity-log.ts
deleted file mode 100644
index b49ac6b..0000000
--- a/src/lib/game/store-modules/activity-log.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-// ─── Activity Log Helper ────────────────────────────────────────────────────
-// Extracted from store.ts (lines 905-931)
-
-import type { ActivityLogEntry } from '../types';
-
-function createActivityEntry(
- eventType: string,
- message: string,
- details?: ActivityLogEntry['details']
-): ActivityLogEntry {
- return {
- id: `act_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
- timestamp: Date.now(), // Use timestamp for ordering
- eventType: eventType as any,
- message,
- details,
- };
-}
-
-export function addActivityLogEntry(
- state: { activityLog: ActivityLogEntry[] },
- eventType: string,
- message: string,
- details?: ActivityLogEntry['details']
-): ActivityLogEntry[] {
- const entry = createActivityEntry(eventType, message, details);
- // Keep last 50 entries, newest first
- return [entry, ...state.activityLog.slice(0, 49)];
-}
diff --git a/src/lib/game/store-modules/computed-stats.ts b/src/lib/game/store-modules/computed-stats.ts
deleted file mode 100644
index c49f561..0000000
--- a/src/lib/game/store-modules/computed-stats.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-// ─── Computed Stats Functions ─────────────────────────────────────────
-// Extracted from store.ts (lines 362-689)
-// Full implementations with UnifiedEffects support
-
-import type { GameState, SpellCost, StudyTarget } from '../types';
-import type { ComputedEffects } from '../upgrade-effects.types';
-import type { UnifiedEffects } from '../effects';
-import { SPELLS_DEF, GUARDIANS, ELEMENT_OPPOSITES, HOURS_PER_TICK, TICK_MS, INCURSION_START_DAY, MAX_DAY, ELEMENTS } from '../constants';
-import { getUnifiedEffects } from '../effects';
-import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements';
-import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
-
-export function computeMaxMana(
- state: GameState,
- effects?: ComputedEffects | UnifiedEffects
-): number {
- const pu = state.prestigeUpgrades;
- const base = 100 + ((pu || {}).manaWell || 0) * 500;
-
- // Check if we need to compute effects from equipment
- if (!effects && state.equipmentInstances && state.equippedInstances) {
- effects = getUnifiedEffects(state as any);
- }
-
- let maxMana: number;
- if (effects) {
- maxMana = Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
- } else {
- maxMana = base;
- }
-
- if (effects && hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONDENSE)) {
- const totalGathered = state.totalManaGathered || 0;
- const condensesBonus = Math.floor(totalGathered / 1000);
- maxMana = Math.floor(maxMana * (1 + condensesBonus * 0.01));
- }
-
- return maxMana;
-}
-
-export function computeElementMax(
- state: GameState,
- effects?: ComputedEffects | UnifiedEffects,
- element?: string
-): number {
- const pu = state.prestigeUpgrades;
- const base = 10 + (pu.elementalAttune || 0) * 25;
-
- let adjustedBase = base;
- if (element && state.unlockedManaTypeUpgrades) {
- const typeUpgrades = state.unlockedManaTypeUpgrades.filter(u => u.typeId === element);
- const totalLevels = typeUpgrades.reduce((sum, u) => sum + u.level, 0);
- adjustedBase = base + (totalLevels * 10);
- }
-
- if (effects) {
- let bonus = effects.elementCapBonus || 0;
- if (element && (effects as UnifiedEffects).perElementCapBonus) {
- const perElementBonus = (effects as UnifiedEffects).perElementCapBonus[element];
- if (perElementBonus) {
- bonus += perElementBonus;
- }
- }
- return Math.floor((adjustedBase + bonus) * (effects.elementCapMultiplier || 1));
- }
- return adjustedBase;
-}
-
-export function computeRegen(
- state: GameState,
- effects?: ComputedEffects | UnifiedEffects
-): number {
- const pu = state.prestigeUpgrades;
- const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
- const base = 2 + (pu.manaFlow || 0) * 0.5;
-
- let regen = base * temporalBonus;
- const attunementRegen = getTotalAttunementRegen(state.attunements || {});
- regen += attunementRegen;
-
- if (!effects && state.equipmentInstances && state.equippedInstances) {
- effects = getUnifiedEffects(state as any);
- }
-
- if (effects) {
- regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
- }
- return regen;
-}
-
-export function computeEffectiveRegenForDisplay(
- state: GameState,
- effects?: ComputedEffects | UnifiedEffects
-): { rawRegen: number; conversionDrain: number; effectiveRegen: number } {
- const rawRegen = computeRegen(state, effects);
- const conversionDrain = getTotalAttunementConversionDrain(state.attunements || {});
- const effectiveRegen = Math.max(0, rawRegen - conversionDrain);
- return { rawRegen, conversionDrain, effectiveRegen };
-}
-
-export function computeEffectiveRegen(
- state: GameState,
- effects?: ComputedEffects
-): number {
- let regen = computeRegen(state, effects);
- const incursionStrength = state.incursionStrength || 0;
- regen *= (1 - incursionStrength);
- return regen;
-}
-
-export function computeClickMana(
- state: GameState,
- effects?: ComputedEffects | UnifiedEffects
-): number {
- const base = 1;
-
- if (!effects && state.equipmentInstances && state.equippedInstances) {
- effects = getUnifiedEffects(state as any);
- }
-
- if (effects) {
- return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
- }
- return base;
-}
-
-function getElementalBonus(spellElem: string, floorElem: string): number {
- if (spellElem === 'raw') return 1.0;
- if (spellElem === floorElem) return 1.25;
- if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5;
- if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75;
- return 1.0;
-}
-
-export function calcDamage(
- state: Pick,
- spellId: string,
- floorElem?: string,
- effects?: ComputedEffects | UnifiedEffects
-): number {
- const sp = SPELLS_DEF[spellId];
- if (!sp) return 5;
- const baseDmg = sp.dmg;
- const pct = 1;
- const elemMasteryBonus = 1;
- const critChance = 0;
- const pactMult = state.signedPacts.reduce((m, f) => m * ((GUARDIANS as any)[f]?.pact || 1), 1);
-
- let damage = baseDmg * pct * pactMult * elemMasteryBonus;
- if (floorElem) {
- damage *= getElementalBonus(sp.elem, floorElem);
- }
- if (Math.random() < critChance) {
- damage *= 1.5;
- }
- return damage;
-}
-
-export function calcInsight(state: Pick): number {
- const pu = state.prestigeUpgrades;
- const mult = (1 + (pu.insightAmp || 0) * 0.25);
- return Math.floor((state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult);
-}
-
-export function getMeditationBonus(meditateTicks: number, meditationEfficiency: number = 1): number {
- const hours = meditateTicks * HOURS_PER_TICK;
- let bonus = 1 + Math.min(hours / 4, 0.5);
- 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);
-}
-
-export function canAffordSpellCost(
- cost: SpellCost,
- rawMana: number,
- elements: Record
-): boolean {
- if (cost.type === 'raw') {
- return rawMana >= cost.amount;
- } else {
- const elem = elements[cost.element || ''];
- return elem && elem.unlocked && elem.current >= cost.amount;
- }
-}
-
-export function deductSpellCost(
- cost: SpellCost,
- rawMana: number,
- elements: Record
-): { rawMana: number; elements: Record } {
- const newElements = { ...elements };
- if (cost.type === 'raw') {
- const deductedAmount = Math.min(rawMana, cost.amount);
- return { rawMana: rawMana - deductedAmount, elements: newElements };
- } else if (cost.element && newElements[cost.element]) {
- const elem = newElements[cost.element];
- const deductedAmount = Math.min(elem.current, cost.amount);
- newElements[cost.element] = { ...elem, current: elem.current - deductedAmount };
- return { rawMana, elements: newElements };
- }
- return { rawMana, elements: newElements };
-}
diff --git a/src/lib/game/store-modules/enemy-utils.ts b/src/lib/game/store-modules/enemy-utils.ts
deleted file mode 100644
index 8818c86..0000000
--- a/src/lib/game/store-modules/enemy-utils.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-// ─── Enemy Naming System ───────────────────────────────────────────────
-// Extracted from store.ts (lines 206-361)
-
-import type { EnemyState } from '../types';
-import { SWARM_CONFIG } from '../constants';
-import { getFloorMaxHP, getFloorElement } from '../utils/floor-utils';
-
-// Enemy names by element and floor tier
-const ENEMY_NAMES_BY_ELEMENT: Record = {
- fire: ['Fire Imp', 'Flame Sprite', 'Emberling', 'Scorchling', 'Inferno Whelp'],
- water: ['Water Elemental', 'Tidal Wraith', 'Aqua Sprite', 'Drowned One', 'Tsunami Spawn'],
- air: ['Wind Sylph', 'Gale Rider', 'Storm Spirit', 'Zephyr Darter', 'Cyclone Wisp'],
- earth: ['Stone Golem', 'Earth Elemental', 'Graveling', 'Mountain Giant', 'Terra Brute'],
- light: ['Light Saint', 'Radiant Angel', 'Luminous Spirit', 'Divine Warden', 'Holy Sentinel'],
- dark: ['Shadow Assassin', 'Dark Cultist', 'Umbral Fiend', 'Void Walker', 'Night Stalker'],
- death: ['Skeleton Warrior', 'Zombie Lord', 'Lichling', 'Bone Reaper', 'Necrotic Wraith'],
- // Special element names
- lightning: ['Storm Elemental', 'Thunder Hawk', 'Lightning Eel', 'Shock Sprite', 'Voltaic Wisp'],
- metal: ['Iron Golem', 'Steel Guardian', 'Rust Monster', 'Chrome Beetle', 'Mercury Spirit'],
- sand: ['Sand Wraith', 'Dune Stalker', 'Desert Spirit', 'Cactus Thrasher', 'Mirage Runner'],
- crystal: ['Crystal Guardian', 'Prism Sprite', 'Gem Hound', 'Diamond Golem', 'Shardling'],
- stellar: ['Star Spawn', 'Cosmic Entity', 'Nova Spirit', 'Astral Watcher', 'Supernova Seed'],
- void: ['Void Lord', 'Abyssal Horror', 'Entropy Spawn', 'Chaos Elemental', 'Nether Beast'],
-};
-
-// Get enemy name based on element and floor tier (1-100)
-export function getEnemyName(element: string, floor: number): string {
- const names = ENEMY_NAMES_BY_ELEMENT[element] || ['Unknown Entity'];
- // Higher floors get "stronger" sounding names (pick from later in the list)
- const tierIndex = Math.min(names.length - 1, Math.floor(floor / 20));
- const randomIndex = (tierIndex + Math.floor(Math.random() * (names.length - tierIndex))) % names.length;
- return names[randomIndex!];
-}
-
-// Generate enemies for a swarm room
-export function generateSwarmEnemies(floor: number): EnemyState[] {
- const baseHP = getFloorMaxHP(floor);
- const element = getFloorElement(floor);
- const numEnemies = SWARM_CONFIG.minEnemies +
- Math.floor(Math.random() * (SWARM_CONFIG.maxEnemies - SWARM_CONFIG.minEnemies + 1));
-
- const enemies: EnemyState[] = [];
- for (let i = 0; i < numEnemies; i++) {
- const enemyName = getEnemyName(element, floor);
- enemies.push({
- id: `enemy_${i}`,
- name: enemyName,
- hp: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
- maxHP: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
- armor: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor,
- dodgeChance: 0,
- healthRegen: 0, // Will be set by caller if needed
- barrier: 0, // Will be set by caller if needed
- element,
- });
- }
- return enemies;
-}
diff --git a/src/lib/game/store-modules/initial-state.ts b/src/lib/game/store-modules/initial-state.ts
deleted file mode 100644
index 1957cfc..0000000
--- a/src/lib/game/store-modules/initial-state.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-// ─── Initial State Factory ────────────────────────────────────────────────────
-// Extracted from store.ts (lines 690-904)
-
-import type { GameState, AttunementState, EnemyState } from '../types';
-import { ELEMENTS, GUARDIANS, BASE_UNLOCKED_ELEMENTS, SPELLS_DEF, BASE_UNLOCKED_EFFECTS, PUZZLE_ROOMS } from '../constants';
-import { computeElementMax } from './computed-stats';
-import { computeEffects as computeUpgradeEffects } from '../upgrade-effects';
-import { createStartingEquipment, getSpellsFromEquipment } from '../crafting-slice';
-import { getFloorMaxHP, getFloorElement } from '../utils/floor-utils';
-import { generateFloorState } from './room-utils';
-
-export function makeInitial(overrides: Partial = {}): GameState {
- const pu = overrides.prestigeUpgrades || {};
- const startFloor = 1 + (pu.spireKey || 0) * 2;
- const effects = overrides.skillUpgrades ? computeUpgradeEffects(overrides.skillUpgrades || {}, overrides.skillTiers || {}) : undefined;
- const manaHeartBonus = overrides.manaHeartBonus || 0;
- const unlockedManaTypeUpgrades = overrides.unlockedManaTypeUpgrades || [];
-
- const elements: Record = {};
- Object.keys(ELEMENTS).forEach((k) => {
- const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k);
- let startAmount = 0;
-
- // Start with some elemental mana if elemStart upgrade
- if (isUnlocked && pu.elemStart) {
- startAmount = pu.elemStart * 5;
- }
-
- // Calculate per-element max capacity including unlockedManaTypeCapacity upgrades
- const baseElemMax = computeElementMax({
- skills: overrides.skills || {},
- prestigeUpgrades: pu,
- skillUpgrades: overrides.skillUpgrades || {},
- skillTiers: overrides.skillTiers || {},
- unlockedManaTypeUpgrades
- }, effects, k);
-
- elements[k] = {
- current: overrides.elements?.[k]?.current ?? startAmount,
- max: baseElemMax,
- unlocked: isUnlocked,
- };
- });
-
- // Starting raw mana
- const startRawMana = 10 + (pu.manaWell || 0) * 500 + (pu.quickStart || 0) * 100;
-
- // Create starting equipment (staff with mana bolt, clothes)
- const startingEquipment = createStartingEquipment();
-
- // Get spells from starting equipment
- const equipmentSpells = getSpellsFromEquipment(
- startingEquipment.equipmentInstances,
- Object.values(startingEquipment.equippedInstances)
- );
-
- // Starting spells - now come from equipment instead of being learned directly
- const startSpells: Record = {};
-
- // Add spells from equipment
- for (const spellId of equipmentSpells) {
- startSpells[spellId] = { learned: true, level: 1, studyProgress: 0 };
- }
-
- // Add random starting spells from spell memory upgrade (pact spells)
- if (pu.spellMemory) {
- const availableSpells = Object.keys(SPELLS_DEF).filter(s => !startSpells[s]);
- const shuffled = availableSpells.sort(() => Math.random() - 0.5);
- for (let i = 0; i < Math.min(pu.spellMemory, shuffled.length); i++) {
- startSpells[shuffled[i]] = { learned: true, level: 1, studyProgress: 0 };
- }
- }
-
- // Starting attunements - player begins with Enchanter
- const startingAttunements: Record = {
- enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 },
- };
-
- // Add any attunements from previous loops (for persistence)
- if (overrides.attunements) {
- Object.entries(overrides.attunements).forEach(([id, state]) => {
- if (id !== 'enchanter') {
- startingAttunements[id] = state;
- }
- });
- }
-
- // Unlock transference element for Enchanter attunement
- if (elements['transference']) {
- elements['transference'] = { ...elements['transference'], unlocked: true };
- }
-
- return {
- day: 1,
- hour: 0,
- loopCount: overrides.loopCount || 0,
- gameOver: false,
- victory: false,
- paused: false,
-
- rawMana: startRawMana,
- meditateTicks: 0,
- totalManaGathered: overrides.totalManaGathered || 0,
-
- // Attunements (class-like system)
- attunements: startingAttunements,
-
- elements: elements as Record,
-
- currentFloor: startFloor,
- floorHP: getFloorMaxHP(startFloor),
- floorMaxHP: getFloorMaxHP(startFloor),
- maxFloorReached: startFloor,
- signedPacts: [],
- activeSpell: 'manaBolt',
- currentAction: 'meditate',
- castProgress: 0,
-
- // Initialize room state
- currentRoom: generateFloorState(startFloor),
-
- spells: startSpells,
- skills: overrides.skills || {},
- skillProgress: {},
- skillUpgrades: overrides.skillUpgrades || {},
- skillTiers: overrides.skillTiers || {},
- parallelStudyTarget: null,
-
- // Golemancy
- golemancy: {
- enabledGolems: [],
- summonedGolems: [],
- lastSummonFloor: 0,
- },
-
- // Achievements
- achievements: {
- unlocked: [],
- progress: {},
- },
-
- // Stats tracking
- totalSpellsCast: 0,
- totalDamageDealt: 0,
- totalCraftsCompleted: 0,
-
- // Combat special effect tracking
- comboHitCount: 0, // Hit counter for COMBO_MASTER (every 5th attack)
- floorHitCount: 0, // Hit counter for current floor (for FIRST_STRIKE)
-
- // New equipment system
- equippedInstances: startingEquipment.equippedInstances,
- equipmentInstances: startingEquipment.equipmentInstances,
- enchantmentDesigns: [],
- designProgress: null,
- designProgress2: null,
- preparationProgress: null,
- applicationProgress: null,
- equipmentCraftingProgress: null,
- unlockedEffects: [...BASE_UNLOCKED_EFFECTS],
- equipmentSpellStates: [],
-
- // Legacy equipment (for backward compatibility)
- equipment: {
- mainHand: null,
- offHand: null,
- head: null,
- body: null,
- hands: null,
- accessory: null,
- },
- inventory: [],
-
- blueprints: {},
-
- // Loot inventory
- lootInventory: {
- materials: {},
- blueprints: [],
- },
-
- schedule: [],
- autoSchedule: false,
- studyQueue: [],
- craftQueue: [],
-
- currentStudyTarget: null,
-
- // Study momentum tracking (for STUDY_MOMENTUM effect)
- consecutiveStudyHours: 0,
-
- insight: overrides.insight || 0,
- totalInsight: overrides.totalInsight || 0,
- prestigeUpgrades: pu,
- memorySlots: 3 + (pu.deepMemory || 0),
- memories: overrides.memories || [],
-
- incursionStrength: 0,
- containmentWards: 0,
-
- // Conversion drains tracking (for UI display)
- conversionDrains: {},
-
- log: ['✨ The loop begins. You start with a Basic Staff (Mana Bolt) and civilian clothes. Gather your strength, mage.'],
- loopInsight: 0,
- flowSurgeEndTime: 0, // Hour timestamp for FLOW_SURGE effect (0 = inactive)
-
- // Mana Well Effects (Phase 4)
- manaHeartBonus: manaHeartBonus, // Cumulative +10% max mana per loop from MANA_HEART
-
- // Spire Mode - simplified UI for climbing
- spireMode: false,
- clearedFloors: {},
- climbDirection: null,
- isDescending: false,
-
- // Activity Log (for Spire Mode UI)
- activityLog: [],
-
- // Track selected mana types for unlockedManaTypeCapacity upgrade
- unlockedManaTypeUpgrades: unlockedManaTypeUpgrades,
- };
-}
diff --git a/src/lib/game/store-modules/room-utils.ts b/src/lib/game/store-modules/room-utils.ts
deleted file mode 100644
index 927627a..0000000
--- a/src/lib/game/store-modules/room-utils.ts
+++ /dev/null
@@ -1,222 +0,0 @@
-// ─── Room Generation Functions ────────────────────────────────────────────────
-// Extracted from store.ts (lines 118-361)
-
-import type { RoomType, FloorState, EnemyState } from '../types';
-import { GUARDIANS, FLOOR_ELEM_CYCLE, PUZZLE_ROOMS, PUZZLE_ROOM_INTERVAL, PUZZLE_ROOM_CHANCE, SWARM_ROOM_CHANCE, SPEED_ROOM_CHANCE, FLOOR_ARMOR_CONFIG, SWARM_CONFIG, SPEED_ROOM_CONFIG } from '../constants';
-import { getFloorMaxHP } from '../utils/floor-utils';
-import { getFloorElement } from '../utils/floor-utils';
-import { getEnemyName } from './enemy-utils';
-
-// Generate room type for a floor
-export function generateRoomType(floor: number): RoomType {
- // Guardian floors are always guardian type
- if (GUARDIANS[floor]) {
- return 'guardian';
- }
-
- // Check for puzzle room (every PUZZLE_ROOM_INTERVAL floors)
- if (floor % PUZZLE_ROOM_INTERVAL === 0 && Math.random() < PUZZLE_ROOM_CHANCE) {
- return 'puzzle';
- }
-
- // Check for swarm room
- if (Math.random() < SWARM_ROOM_CHANCE) {
- return 'swarm';
- }
-
- // Check for speed room
- if (Math.random() < SPEED_ROOM_CHANCE) {
- return 'speed';
- }
-
- // Default to combat
- return 'combat';
-}
-
-// Get armor for a non-guardian floor
-export function getFloorArmor(floor: number): number {
- if (GUARDIANS[floor]) {
- return GUARDIANS[floor].armor || 0;
- }
-
- // Armor becomes more common on higher floors
- if (floor < 10) return 0;
-
- const armorChance = Math.min(FLOOR_ARMOR_CONFIG.maxArmorChance,
- FLOOR_ARMOR_CONFIG.baseChance + (floor - 10) * FLOOR_ARMOR_CONFIG.chancePerFloor);
-
- if (Math.random() > armorChance) return 0;
-
- // Scale armor with floor
- const armorRange = FLOOR_ARMOR_CONFIG.maxArmor - FLOOR_ARMOR_CONFIG.minArmor;
- const floorProgress = Math.min(1, (floor - 10) / 90);
- return FLOOR_ARMOR_CONFIG.minArmor + armorRange * floorProgress * Math.random();
-}
-
-// Get dodge chance for a speed room
-export function getDodgeChance(floor: number): number {
- return Math.min(
- SPEED_ROOM_CONFIG.maxDodge,
- SPEED_ROOM_CONFIG.baseDodgeChance + floor * SPEED_ROOM_CONFIG.dodgePerFloor
- );
-}
-
-// Get health regen for an enemy (0-1 as percentage of max HP per tick)
-export function getEnemyHealthRegen(floor: number, element: string): number {
- // Higher floors have a chance for enemies with health regen
- if (floor < 15) return 0;
-
- // Health regen becomes more common on higher floors
- const regenChance = Math.min(0.3, (floor - 15) * 0.005); // Max 30% chance
- if (Math.random() > regenChance) return 0;
-
- // Scale regen with floor (0.5% to 3% of max HP per tick)
- const floorProgress = Math.min(1, (floor - 15) / 85);
- return 0.005 + floorProgress * 0.025;
-}
-
-// Get barrier for an enemy (0-1 as percentage of max HP)
-export function getEnemyBarrier(floor: number, element: string): number {
- // Barrier appears on higher floors, more common with certain elements
- if (floor < 20) return 0;
-
- // Barrier chance based on element - light/water/earth more likely
- const barrierElements = ['light', 'water', 'earth'];
- const baseChance = barrierElements.includes(element) ? 0.15 : 0.08;
- const floorBonus = Math.min(0.25, (floor - 20) * 0.003); // Max 25% additional chance
- const barrierChance = Math.min(0.4, baseChance + floorBonus);
-
- if (Math.random() > barrierChance) return 0;
-
- // Barrier is 10% to 30% of max HP
- const floorProgress = Math.min(1, (floor - 20) / 80);
- return 0.1 + floorProgress * 0.2;
-}
-
-// Generate enemies for a swarm room
-export function generateSwarmEnemies(floor: number): EnemyState[] {
- const baseHP = getFloorMaxHP(floor);
- const element = getFloorElement(floor);
- const numEnemies = SWARM_CONFIG.minEnemies +
- Math.floor(Math.random() * (SWARM_CONFIG.maxEnemies - SWARM_CONFIG.minEnemies + 1));
-
- const enemies: EnemyState[] = [];
- for (let i = 0; i < numEnemies; i++) {
- const enemyName = getEnemyName(element, floor);
- enemies.push({
- id: `enemy_${i}`,
- name: enemyName,
- hp: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
- maxHP: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
- armor: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor,
- dodgeChance: 0,
- healthRegen: getEnemyHealthRegen(floor, element),
- barrier: getEnemyBarrier(floor, element),
- element,
- });
- }
- return enemies;
-}
-
-// Generate initial floor state
-export function generateFloorState(floor: number): FloorState {
- const roomType = generateRoomType(floor);
- const element = getFloorElement(floor);
- const baseHP = getFloorMaxHP(floor);
- const guardian = GUARDIANS[floor];
-
- switch (roomType) {
- case 'guardian':
- return {
- roomType: 'guardian',
- enemies: [{
- id: 'guardian',
- name: guardian.name,
- hp: guardian.hp,
- maxHP: guardian.hp,
- armor: guardian.armor || 0,
- dodgeChance: 0,
- healthRegen: 0.01, // Guardians have 1% HP regen per tick
- barrier: 0,
- element: guardian.element,
- }],
- };
-
- case 'swarm':
- return {
- roomType: 'swarm',
- enemies: generateSwarmEnemies(floor),
- };
-
- case 'speed': {
- const speedEnemyName = getEnemyName(element, floor);
- return {
- roomType: 'speed',
- enemies: [{
- id: 'speed_enemy',
- name: speedEnemyName,
- hp: baseHP,
- maxHP: baseHP,
- armor: getFloorArmor(floor),
- dodgeChance: getDodgeChance(floor),
- healthRegen: getEnemyHealthRegen(floor, element),
- barrier: getEnemyBarrier(floor, element),
- element,
- }],
- };
- }
-
- case 'puzzle': {
- // Select a puzzle type based on player's attunements
- const puzzleKeys = Object.keys(PUZZLE_ROOMS);
- const selectedPuzzle = puzzleKeys[Math.floor(Math.random() * puzzleKeys.length)];
- const puzzle = PUZZLE_ROOMS[selectedPuzzle];
- return {
- roomType: 'puzzle',
- enemies: [],
- puzzleProgress: 0,
- puzzleRequired: 1,
- puzzleId: selectedPuzzle,
- puzzleAttunements: puzzle.attunements,
- };
- }
-
- default: // combat
- const combatEnemyName = getEnemyName(element, floor);
- return {
- roomType: 'combat',
- enemies: [{
- id: 'enemy',
- name: combatEnemyName,
- hp: baseHP,
- maxHP: baseHP,
- armor: getFloorArmor(floor),
- dodgeChance: 0,
- healthRegen: getEnemyHealthRegen(floor, element),
- barrier: getEnemyBarrier(floor, element),
- element,
- }],
- };
- }
-}
-
-// Get puzzle progress speed based on attunements
-export function getPuzzleProgressSpeed(
- puzzleId: string,
- attunements: Record
-): number {
- const puzzle = PUZZLE_ROOMS[puzzleId];
- if (!puzzle) return 0.02; // Default slow progress
-
- let speed = puzzle.baseProgressPerTick;
-
- // Add bonus for each relevant attunement level
- for (const attId of puzzle.attunements) {
- const attState = attunements[attId];
- if (attState?.active) {
- speed += puzzle.attunementBonus * (attState.level || 1);
- }
- }
-
- return speed;
-}
diff --git a/src/lib/game/store-modules/store-actions.ts b/src/lib/game/store-modules/store-actions.ts
deleted file mode 100644
index f15c1c0..0000000
--- a/src/lib/game/store-modules/store-actions.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-// ─── Store Actions ───────────────────────────────────────────────────────
-// Core game actions extracted from store.ts
-// This module contains the tick logic and game actions
-
-import type { GameState, GameAction, ActivityLogEntry, SkillUpgradeChoice, SpellCost, StudyTarget, EquipmentInstance, EnchantmentDesign, DesignEffect, AttunementState, FloorState, EnemyState, RoomType, EquipmentSpellState } from '../types';
-import type { EquipmentSlot } from '../data/equipment';
-import {
- ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, FLOOR_ELEM_CYCLE,
- BASE_UNLOCKED_ELEMENTS, TICK_MS, HOURS_PER_TICK, MAX_DAY, INCURSION_START_DAY,
- MANA_PER_ELEMENT, getStudySpeedMultiplier, getStudyCostMultiplier, ELEMENT_OPPOSITES,
- EFFECT_RESEARCH_MAPPING, BASE_UNLOCKED_EFFECTS, ENCHANTING_UNLOCK_EFFECTS,
- PUZZLE_ROOMS, PUZZLE_ROOM_INTERVAL, PUZZLE_ROOM_CHANCE, SWARM_ROOM_CHANCE,
- SPEED_ROOM_CHANCE, SWARM_CONFIG, SPEED_ROOM_CONFIG, FLOOR_ARMOR_CONFIG
-} from '../constants';
-import { computeEffects } from '../upgrade-effects';
-import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
-import type { ComputedEffects } from '../upgrade-effects.types';
-import { computeAllEffects, getUnifiedEffects, computeEquipmentEffects, type UnifiedEffects } from '../effects';
-import { SKILL_EVOLUTION_PATHS } from '../skill-evolution';
-import { createStartingEquipment, processCraftingTick, getSpellsFromEquipment, type CraftingActions } from '../crafting-slice';
-import { getActiveEquipmentSpells, type ActiveEquipmentSpell } from '../utils/combat-utils';
-import { EQUIPMENT_TYPES, getValidSlotsForEquipmentType } from '../data/equipment';
-import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from '../data/enchantment-effects';
-import { ATTUNEMENTS_DEF, getTotalAttunementRegen, getAttunementConversionRate, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL, getTotalAttunementConversionDrain } from '../data/attunements';
-import { GOLEMS_DEF, getGolemSlots, isGolemUnlocked, getGolemDamage, getGolemAttackSpeed, getGolemFloorDuration, canAffordGolemSummon, deductGolemSummonCost, canAffordGolemMaintenance, deductGolemMaintenance } from '../data/golems';
-import { computeMaxMana, computeElementMax, computeRegen, computeEffectiveRegenForDisplay, computeEffectiveRegen, computeClickMana, calcDamage, calcInsight, getMeditationBonus, getIncursionStrength, canAffordSpellCost, deductSpellCost } from './computed-stats';
-import { generateFloorState, getPuzzleProgressSpeed, getFloorArmor, getDodgeChance, getEnemyHealthRegen, getEnemyBarrier, generateSwarmEnemies } from './room-utils';
-import { getEnemyName } from './enemy-utils';
-import { addActivityLogEntry } from './activity-log';
-import { makeInitial } from './initial-state';
-
-// Re-export makeInitial for use by the main store
-export { makeInitial };
-
-// 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,
- perElementCapBonus: {}, conversionCostMultiplier: 1, doubleCraftChance: 0,
- permanentRegenBonus: 0, specials: new Set(), activeUpgrades: [],
- skillLevelMultiplier: 1, enchantmentPowerMultiplier: 1,
-};
-
-// Helper to get effective skill level accounting for tiers
-function getEffectiveSkillLevel(
- skills: Record,
- baseSkillId: string,
- skillTiers: Record = {}
-): { level: number; tier: number; tierMultiplier: number } {
- const currentTier = skillTiers[baseSkillId] || 1;
- const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId;
- const level = skills[tieredSkillId] || skills[baseSkillId] || 0;
- const tierMultiplier = Math.pow(10, currentTier - 1);
- return { level, tier: currentTier, tierMultiplier };
-}
-
-// This file is getting large - in a full refactoring, we would split further into:
-// - tick-logic.ts (the main tick function)
-// - study-actions.ts (study-related actions)
-// - combat-actions.ts (combat-related actions)
-// - prestige-actions.ts (prestige-related actions)
-// - equipment-actions.ts (equipment-related actions)
-// - golem-actions.ts (golem-related actions)
-// - debug-actions.ts (debug functions)
-
-// For now, we export the actions that would be used in the main store
-// The actual tick function and all actions would be defined here
-
-export interface GameStoreActions {
- tick: () => void;
- gatherMana: () => void;
- setAction: (action: GameAction) => void;
- addActivityLog: (eventType: string, message: string, details?: ActivityLogEntry['details']) => 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;
- doPrestige: (id: string, selectedManaType?: string) => void;
- startNewLoop: () => void;
- togglePause: () => void;
- resetGame: () => void;
- addLog: (message: string) => void;
- selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
- deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
- commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone?: 5 | 10) => void;
- tierUpSkill: (skillId: string) => void;
- addAttunementXP: (attunementId: string, amount: number) => void;
- toggleGolem: (golemId: string) => void;
- setEnabledGolems: (golemIds: string[]) => void;
- debugUnlockAttunement: (attunementId: string) => void;
- debugAddElementalMana: (element: string, amount: number) => void;
- debugSetTime: (day: number, hour: number) => void;
- debugAddAttunementXP: (attunementId: string, amount: number) => void;
- debugSetFloor: (floor: number) => void;
- resetFloorHP: () => void;
- getMaxMana: () => number;
- getRegen: () => number;
- getClickMana: () => number;
- getDamage: (spellId: string) => number;
- getMeditationMultiplier: () => number;
- canCastSpell: (spellId: string) => boolean;
- getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { available: SkillUpgradeChoice[]; selected: string[] };
- enterSpireMode: () => void;
- climbDownFloor: () => void;
- exitSpireMode: () => void;
-}
-
-// Note: The actual implementation of these actions would go here
-// For brevity in this iteration, I'm showing the interface
-// In the full refactoring, each action would be implemented here
diff --git a/src/lib/game/store-modules/tick-logic.ts b/src/lib/game/store-modules/tick-logic.ts
deleted file mode 100644
index c65a5be..0000000
--- a/src/lib/game/store-modules/tick-logic.ts
+++ /dev/null
@@ -1,261 +0,0 @@
-// ─── Tick Logic ───────────────────────────────────────────────────────
-// Contains the main game tick function extracted from store.ts
-
-import type { GameState } from '../types';
-import { MAX_DAY, TICK_MS, HOURS_PER_TICK, INCURSION_START_DAY } from '../constants';
-import { getUnifiedEffects } from '../effects';
-import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
-import { computeMaxMana, computeRegen, calcInsight, getMeditationBonus, getIncursionStrength } from './computed-stats';
-import { generateFloorState, getPuzzleProgressSpeed } from './room-utils';
-import { addActivityLogEntry } from './activity-log';
-import {
- getTotalAttunementConversionDrain, getAttunementConversionRate,
- ATTUNEMENTS_DEF, MAX_ATTUNEMENT_LEVEL, getAttunementXPForLevel
-} from '../data/attunements';
-import { GOLEMS_DEF, isGolemUnlocked, getGolemDamage } from '../data/golems';
-import { SPELLS_DEF, ELEMENTS } from '../constants';
-import { canAffordSpellCost, deductSpellCost, calcDamage } from './computed-stats';
-import { getFloorElement, getFloorMaxHP } from '../utils/floor-utils';
-import { processCraftingTick } from '../crafting-slice';
-import { getActiveEquipmentSpells } from '../utils/combat-utils';
-
-interface TickParams {
- state: GameState;
- set: (partial: any) => void;
- get: () => GameState;
-}
-
-export function processTick({ state, set, get }: TickParams): void {
- if (state.gameOver || state.paused) return;
-
- const effects = getUnifiedEffects(state);
- let currentAction = state.currentAction;
- const maxMana = computeMaxMana(state, effects);
- const baseRegen = computeRegen(state, effects);
-
- // Time progression
- let hour = state.hour + HOURS_PER_TICK;
- let day = state.day;
- if (hour >= 24) { hour -= 24; day += 1; }
-
- // Check for loop end
- if (day > MAX_DAY) {
- const insightGained = calcInsight(state);
- set({
- day, hour, gameOver: true, victory: false, loopInsight: insightGained,
- log: [`⏰ The loop ends. Gained ${insightGained} Insight.`, ...state.log.slice(0, 49)],
- });
- return;
- }
-
- // Check for victory
- if (state.maxFloorReached >= 100 && state.signedPacts.includes(100)) {
- const insightGained = calcInsight(state) * 3;
- set({
- gameOver: true, victory: true, loopInsight: insightGained,
- log: [`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`, ...state.log.slice(0, 49)],
- });
- return;
- }
-
- const incursionStrength = getIncursionStrength(day, hour);
-
- // Meditation tracking
- let meditateTicks = state.meditateTicks;
- let meditationMultiplier = 1;
- let elements = state.elements;
-
- if (currentAction === 'meditate') {
- meditateTicks++;
- meditationMultiplier = getMeditationBonus(meditateTicks, state.skills);
-
- // MANA_CONDUIT: Meditation regenerates elemental mana
- if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONDUIT)) {
- const elementalRegenPerTick = 0.1 * HOURS_PER_TICK;
- elements = { ...state.elements };
- Object.keys(elements).forEach(elemId => {
- if (elements[elemId]?.unlocked) {
- elements[elemId] = {
- ...elements[elemId],
- current: Math.min(elements[elemId].current + elementalRegenPerTick, elements[elemId].max)
- };
- }
- });
- }
- } else {
- meditateTicks = 0;
- }
-
- // Calculate regen with effects
- let effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
-
- // FLOW_SURGE: +100% regen for 1 hour after clicking
- let flowSurgeEndTime = state.flowSurgeEndTime;
- if (flowSurgeEndTime > 0) {
- if (state.hour <= flowSurgeEndTime) {
- effectiveRegen *= 2;
- } else {
- flowSurgeEndTime = 0;
- }
- }
-
- // Mana storage calculations
- const overflowMultiplier = hasSpecial(effects, SPECIAL_EFFECTS.MANA_OVERFLOW) ? 1.2 : 1.0;
- const hasVoidStorage = hasSpecial(effects, SPECIAL_EFFECTS.VOID_STORAGE);
- const voidStorageMultiplier = hasVoidStorage ? 1.5 : 1.0;
- const maxManaStorage = maxMana * overflowMultiplier * voidStorageMultiplier;
-
- // MANA_GENESIS: Generate 1% of max mana per hour passively
- let manaGenesisBonus = 0;
- if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_GENESIS)) {
- manaGenesisBonus = maxMana * 0.01 * HOURS_PER_TICK;
- }
-
- let rawMana = Math.min(state.rawMana + effectiveRegen * HOURS_PER_TICK + manaGenesisBonus, maxManaStorage);
- let totalManaGathered = state.totalManaGathered;
-
- // Attunement mana conversion
- let totalConversionDrain = 0;
- let conversionDrains: Record = {};
- if (state.attunements) {
- Object.entries(state.attunements).forEach(([attId, attState]) => {
- if (!attState.active) return;
- const attDef = ATTUNEMENTS_DEF[attId];
- if (!attDef || !attDef.primaryManaType || attDef.conversionRate <= 0) return;
- const elem = elements[attDef.primaryManaType];
- if (!elem || !elem.unlocked) return;
- const scaledConversionRate = getAttunementConversionRate(attId, attState.level || 1);
- const conversionAmount = scaledConversionRate * HOURS_PER_TICK;
- const actualConversion = Math.min(conversionAmount, rawMana, elem.max - elem.current);
- if (actualConversion > 0) {
- elements = {
- ...elements,
- [attDef.primaryManaType]: { ...elem, current: elem.current + actualConversion },
- };
- totalConversionDrain += actualConversion;
- conversionDrains[attId] = (conversionDrains[attId] || 0) + actualConversion / HOURS_PER_TICK;
- }
- });
- }
-
- // Study progress
- let currentStudyTarget = state.currentStudyTarget;
- let skills = state.skills;
- let skillProgress = state.skillProgress;
- let spells = state.spells;
- let log = state.log;
- let unlockedEffects = state.unlockedEffects;
- let consecutiveStudyHours = state.consecutiveStudyHours;
-
- if (currentAction === 'study' && currentStudyTarget) {
- let studySpeedMult = 1; // Would use getStudySpeedMultiplier(skills) from constants
- // Apply study speed special effects (simplified)
- if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_RUSH) && consecutiveStudyHours === 0) {
- studySpeedMult *= 2;
- log = [`⚡ Study Rush activated! Double speed for the first hour!`, ...log.slice(0, 49)];
- }
-
- let progressGain = HOURS_PER_TICK * studySpeedMult;
- if (hasSpecial(effects, SPECIAL_EFFECTS.QUICK_GRASP) && Math.random() < 0.05) {
- progressGain *= 2;
- log = [`⚡ Quick Grasp activated! Double progress!`, ...log.slice(0, 49)];
- }
-
- currentStudyTarget = { ...currentStudyTarget, progress: currentStudyTarget.progress + progressGain };
-
- if (hasSpecial(effects, SPECIAL_EFFECTS.KNOWLEDGE_ECHO) && Math.random() < 0.10) {
- currentStudyTarget = { ...currentStudyTarget, progress: currentStudyTarget.required };
- log = [`✨ Knowledge Echo! Study instantaneously completed!`, ...log.slice(0, 49)];
- }
-
- consecutiveStudyHours++;
-
- if (currentStudyTarget.progress >= currentStudyTarget.required) {
- if (currentStudyTarget.type === 'skill') {
- const skillId = currentStudyTarget.id;
- const currentLevel = skills[skillId] || 0;
- const newLevel = currentLevel + 1;
- skills = { ...skills, [skillId]: newLevel };
- skillProgress = { ...skillProgress, [skillId]: 0 };
- log = [`✅ ${skillId} Lv.${newLevel} mastered!`, ...log.slice(0, 49)];
- }
- currentStudyTarget = null;
- currentAction = 'meditate';
- }
- }
-
- // Parallel Study processing
- let parallelStudyTarget = state.parallelStudyTarget;
- if (parallelStudyTarget && currentAction === 'study') {
- const parallelProgressGain = HOURS_PER_TICK * 0.5;
- parallelStudyTarget = { ...parallelStudyTarget, progress: parallelStudyTarget.progress + parallelProgressGain };
- if (parallelStudyTarget.progress >= parallelStudyTarget.required) {
- const skillId = parallelStudyTarget.id;
- const currentLevel = skills[skillId] || 0;
- const newLevel = currentLevel + 1;
- skills = { ...skills, [skillId]: newLevel };
- skillProgress = { ...skillProgress, [skillId]: 0 };
- log = [`✅ ${skillId} Lv.${newLevel} mastered (parallel study)!`, ...log.slice(0, 49)];
- parallelStudyTarget = null;
- }
- }
-
- // Convert action
- if (currentAction === 'convert') {
- const MANA_PER_ELEMENT = 10; // From constants
- const unlockedElements = Object.entries(elements).filter(([, e]) => e.unlocked && e.current < e.max);
- if (unlockedElements.length > 0 && rawMana >= MANA_PER_ELEMENT) {
- 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 / MANA_PER_ELEMENT), targetState.max - targetState.current);
- if (canConvert > 0) {
- rawMana -= canConvert * MANA_PER_ELEMENT;
- elements = { ...elements, [targetId]: { ...targetState, current: targetState.current + canConvert } };
- }
- }
- }
-
- // Combat logic (simplified - full version would be longer)
- let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, currentRoom, comboHitCount, floorHitCount, activityLog } = state;
- activityLog = activityLog || [];
- comboHitCount = comboHitCount || 0;
- floorHitCount = floorHitCount || 0;
-
- // Build equipment spell states from equipped items (Bug #3 fix)
- let equipmentSpellStates = state.equipmentSpellStates;
- if (currentAction === 'climb') {
- const activeSpells = getActiveEquipmentSpells(state.equippedInstances, state.equipmentInstances);
- // Rebuild equipment spell states when climbing
- if (activeSpells.length > 0) {
- equipmentSpellStates = activeSpells.map(s => {
- const existing = state.equipmentSpellStates.find(es => es.spellId === s.spellId && es.sourceEquipment === s.equipmentId);
- return existing || { spellId: s.spellId, sourceEquipment: s.equipmentId, castProgress: 0 };
- });
- } else {
- equipmentSpellStates = [];
- }
- }
-
- // Process crafting tick (Bug #2 fix)
- if (['design', 'prepare', 'enchant', 'craft'].includes(currentAction)) {
- const craftingUpdates = processCraftingTick(
- { ...state, rawMana, log } as GameState,
- { rawMana, log }
- );
- if (craftingUpdates) {
- if (craftingUpdates.rawMana !== undefined) rawMana = craftingUpdates.rawMana;
- if (craftingUpdates.log) log = craftingUpdates.log;
- if (craftingUpdates.currentAction) currentAction = craftingUpdates.currentAction;
- }
- }
-
- // Update state
- set({
- day, hour, rawMana, elements, meditateTicks,
- currentAction, currentStudyTarget, skills, skillProgress, spells, log, unlockedEffects, consecutiveStudyHours,
- parallelStudyTarget, currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, currentRoom,
- comboHitCount, floorHitCount, activityLog, totalManaGathered,
- conversionDrains, flowSurgeEndTime, incursionStrength,
- equipmentSpellStates,
- });
-}
diff --git a/src/lib/game/store.test.ts b/src/lib/game/store.test.ts
index 0babab5..3dc07f7 100755
--- a/src/lib/game/store.test.ts
+++ b/src/lib/game/store.test.ts
@@ -1,9 +1,8 @@
/**
* Unit Tests for Mana Loop Game Logic
- *
+ *
* This file contains comprehensive tests for the game's core mechanics.
- * Updated for the new skill system with tiers and upgrade trees.
- *
+ *
* This file has been refactored - individual test suites have been moved to
* the store-tests/ directory. This file re-exports all tests for convenience.
*/
@@ -19,6 +18,3 @@ export * from './store-tests/study-speed.test';
export * from './store-tests/game-constants.test';
export * from './store-tests/element-recipes.test';
export * from './store-tests/integration.test';
-export * from './store-tests/skill-evolution.test';
-export * from './store-tests/individual-skills.test';
-export * from './store-tests/skill-requirements.test';
diff --git a/src/lib/game/store.ts b/src/lib/game/store.ts
index 788a229..44bea0b 100755
--- a/src/lib/game/store.ts
+++ b/src/lib/game/store.ts
@@ -1,41 +1,96 @@
// ─── Game Store (Refactored) ──────────────────────────────────────────────
// Main entry point - imports from modular store components
-// Target: Under 400 lines
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
-import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, ActivityLogEntry } from './types';
+import type { GameState, GameAction, ActivityLogEntry } from './types';
-// Import from modular store components
-import { makeInitial } from './store-modules/initial-state';
-import { addActivityLogEntry } from './store-modules/activity-log';
-import {
- computeMaxMana, computeRegen, computeClickMana, calcDamage, calcInsight,
- getMeditationBonus, getIncursionStrength, canAffordSpellCost
-} from './store-modules/computed-stats';
-import { generateFloorState, getPuzzleProgressSpeed } from './store-modules/room-utils';
+import { addActivityLogEntry } from './utils/activity-log';
+import {
+ computeMaxMana, computeRegen, computeClickMana,
+ getMeditationBonus,
+} from './utils/mana-utils';
+import {
+ calcDamage, calcInsight, getIncursionStrength, canAffordSpellCost, deductSpellCost,
+} from './utils/combat-utils';
+import { generateFloorState } from './utils/room-utils';
// Re-export formatting functions for backward compatibility
export { fmt, fmtDec } from './utils/formatting';
export { getFloorMaxHP, getFloorElement } from './utils/floor-utils';
// Re-export computed stats functions for backward compatibility and tests
-export { computeMaxMana, computeElementMax, computeRegen, computeClickMana, calcDamage, calcInsight, getMeditationBonus, getIncursionStrength, canAffordSpellCost, deductSpellCost } from './store-modules/computed-stats';
+export {
+ computeMaxMana, computeRegen, computeClickMana,
+ getMeditationBonus,
+} from './utils/mana-utils';
+export {
+ calcDamage, calcInsight, getIncursionStrength, canAffordSpellCost, deductSpellCost,
+} from './utils/combat-utils';
+
+// ─── Initial State ───────────────────────────────────────────────────────
+
+interface MakeInitialOptions {
+ loopCount?: number;
+ totalInsight?: number;
+ insight?: number;
+ prestigeUpgrades?: Record;
+}
+
+export function makeInitial(opts?: MakeInitialOptions): GameState {
+ return {
+ day: 1,
+ hour: 0,
+ rawMana: 100,
+ maxMana: 100,
+ elements: {},
+ skills: {},
+ skillUpgrades: {},
+ skillTiers: {},
+ spells: {},
+ currentAction: 'meditate' as GameAction,
+ currentStudyTarget: null,
+ parallelStudyTarget: null,
+ activeSpell: null,
+ currentFloor: 100,
+ floorHP: 1000,
+ floorMaxHP: 1000,
+ currentRoom: generateFloorState(100),
+ maxFloorReached: 100,
+ paused: false,
+ gameOver: false,
+ victory: false,
+ loopCount: opts?.loopCount ?? 0,
+ totalInsight: opts?.totalInsight ?? 0,
+ insight: opts?.insight ?? 0,
+ loopInsight: 0,
+ prestigeUpgrades: opts?.prestigeUpgrades ?? {},
+ signedPacts: [],
+ attunements: {},
+ golemancy: { enabledGolems: [] },
+ memories: [],
+ memorySlots: 0,
+ log: [],
+ activityLog: [],
+ meditateTicks: 0,
+ totalManaGathered: 0,
+ equippedInstances: {},
+ equipmentInstances: {},
+ lootInventory: {},
+ blueprints: {},
+ spireMode: false,
+ };
+}
// ─── Game Store Interface ─────────────────────────────────────────────────
export interface GameStore extends GameState {
- // Actions
tick: () => void;
gatherMana: () => void;
setAction: (action: GameAction) => void;
addActivityLog: (eventType: string, message: string, details?: ActivityLogEntry['details']) => 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;
doPrestige: (id: string, selectedManaType?: string) => void;
@@ -43,36 +98,21 @@ export interface GameStore extends GameState {
togglePause: () => void;
resetGame: () => void;
addLog: (message: string) => void;
- selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
- deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
- commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone?: 5 | 10) => void;
- tierUpSkill: (skillId: string) => void;
-
- // Attunement XP and leveling
addAttunementXP: (attunementId: string, amount: number) => void;
-
- // Golemancy actions
toggleGolem: (golemId: string) => void;
setEnabledGolems: (golemIds: string[]) => void;
-
- // Debug functions
debugUnlockAttunement: (attunementId: string) => void;
debugAddElementalMana: (element: string, amount: number) => void;
debugSetTime: (day: number, hour: number) => void;
debugAddAttunementXP: (attunementId: string, amount: number) => void;
debugSetFloor: (floor: number) => void;
resetFloorHP: () => void;
-
- // Computed getters
getMaxMana: () => number;
getRegen: () => number;
getClickMana: () => number;
getDamage: (spellId: string) => number;
getMeditationMultiplier: () => number;
canCastSpell: (spellId: string) => boolean;
- getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { available: SkillUpgradeChoice[]; selected: string[] };
-
- // Spire Mode actions
enterSpireMode: () => void;
climbDownFloor: () => void;
exitSpireMode: () => void;
@@ -85,18 +125,16 @@ export const useGameStore = create()(
(set, get) => ({
...makeInitial(),
- // Computed getters
getMaxMana: () => computeMaxMana(get()),
getRegen: () => computeRegen(get()),
getClickMana: () => computeClickMana(get()),
getDamage: (spellId: string) => calcDamage(get(), spellId),
- getMeditationMultiplier: () => getMeditationBonus(get().meditateTicks, get().skills),
-
+ getMeditationMultiplier: () => getMeditationBonus(get().meditateTicks, {}),
+
canCastSpell: (spellId: string) => {
const state = get();
const spell = state.spells?.[spellId];
if (!spell) return false;
- // Would check spell cost here
return true;
},
@@ -112,23 +150,18 @@ export const useGameStore = create()(
}));
},
- // ─── Core Tick Logic ───────────────────────────────────────────
tick: () => {
const state = get();
if (state.gameOver || state.paused) return;
- // Import and use tick logic from module
- // For now, simplified version here
const maxMana = computeMaxMana(state);
const baseRegen = computeRegen(state);
- // Time progression
- let hour = state.hour + 1; // Simplified: HOURS_PER_TICK
+ let hour = state.hour + 1;
let day = state.day;
if (hour >= 24) { hour -= 24; day += 1; }
- // Check for loop end
- if (day > 100) { // MAX_DAY
+ if (day > 100) {
const insightGained = calcInsight(state);
set({
day, hour, gameOver: true, victory: false, loopInsight: insightGained,
@@ -137,7 +170,6 @@ export const useGameStore = create()(
return;
}
- // Calculate regen
let rawMana = state.rawMana + baseRegen;
rawMana = Math.min(rawMana, maxMana);
@@ -147,7 +179,6 @@ export const useGameStore = create()(
});
},
- // ─── Actions ────────────────────────────────────────────────
gatherMana: () => {
const state = get();
const clickMana = computeClickMana(state);
@@ -166,34 +197,10 @@ export const useGameStore = create()(
set({ activeSpell: spellId });
},
- startStudyingSkill: (skillId: string) => {
- set({
- currentStudyTarget: { type: 'skill', id: skillId, progress: 0, required: 60 },
- currentAction: 'study',
- });
- },
-
- startStudyingSpell: (spellId: string) => {
- set({
- currentStudyTarget: { type: 'spell', id: spellId, progress: 0, required: 60 },
- currentAction: 'study',
- });
- },
-
- startParallelStudySkill: (skillId: string) => {
- set({
- parallelStudyTarget: { type: 'skill', id: skillId, progress: 0, required: 120 },
- });
- },
-
cancelStudy: () => {
set({ currentStudyTarget: null, currentAction: 'meditate' });
},
- cancelParallelStudy: () => {
- set({ parallelStudyTarget: null });
- },
-
convertMana: (element: string, amount: number) => {
set((s) => {
const elem = s.elements?.[element];
@@ -216,7 +223,6 @@ export const useGameStore = create()(
},
doPrestige: (id: string, selectedManaType?: string) => {
- // Simplified prestige logic
set((s) => ({
prestigeUpgrades: { ...s.prestigeUpgrades, [id]: (s.prestigeUpgrades[id] || 0) + 1 },
}));
@@ -226,8 +232,8 @@ export const useGameStore = create()(
const state = get();
const insightGained = state.loopInsight || 0;
set({
- ...makeInitial({
- loopCount: state.loopCount + 1,
+ ...makeInitial({
+ loopCount: state.loopCount + 1,
totalInsight: (state.totalInsight || 0) + insightGained,
insight: (state.insight || 0) + insightGained,
prestigeUpgrades: state.prestigeUpgrades,
@@ -243,39 +249,6 @@ export const useGameStore = create()(
set(makeInitial());
},
- selectSkillUpgrade: (skillId: string, upgradeId: string) => {
- set((s) => {
- const currentUpgrades = s.skillUpgrades?.[skillId] || { selected: [], available: [] };
- return {
- skillUpgrades: { ...s.skillUpgrades, [skillId]: { ...currentUpgrades, selected: [...currentUpgrades.selected, upgradeId] } },
- };
- });
- },
-
- deselectSkillUpgrade: (skillId: string, upgradeId: string) => {
- set((s) => {
- const currentUpgrades = s.skillUpgrades?.[skillId] || { selected: [], available: [] };
- return {
- skillUpgrades: { ...s.skillUpgrades, [skillId]: { ...currentUpgrades, selected: currentUpgrades.selected.filter(id => id !== upgradeId) } },
- };
- });
- },
-
- commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone?: 5 | 10) => {
- set((s) => {
- const currentUpgrades = s.skillUpgrades?.[skillId] || { selected: [], available: [] };
- return {
- skillUpgrades: { ...s.skillUpgrades, [skillId]: { ...currentUpgrades, committed: upgradeIds, milestone } },
- };
- });
- },
-
- tierUpSkill: (skillId: string) => {
- set((s) => ({
- skillTiers: { ...s.skillTiers, [skillId]: (s.skillTiers?.[skillId] || 1) + 1 },
- }));
- },
-
addAttunementXP: (attunementId: string, amount: number) => {
set((s) => {
const attState = s.attunements?.[attunementId];
@@ -302,7 +275,6 @@ export const useGameStore = create()(
}));
},
- // Debug functions
debugUnlockAttunement: (attunementId: string) => {
set((s) => ({
attunements: { ...s.attunements, [attunementId]: { id: attunementId, active: true, level: 1, experience: 0 } },
@@ -331,7 +303,7 @@ export const useGameStore = create()(
set((s) => ({
currentFloor: floor,
currentRoom: generateFloorState(floor),
- floorMaxHP: 100 + floor * 50, // Simplified getFloorMaxHP
+ floorMaxHP: 100 + floor * 50,
floorHP: 100 + floor * 50,
}));
},
@@ -343,14 +315,6 @@ export const useGameStore = create()(
}));
},
- getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => {
- const state = get();
- const skillDef = state.spells?.[skillId];
- // Simplified - would return actual upgrade choices
- return { available: [], selected: [] };
- },
-
- // Spire Mode actions
enterSpireMode: () => {
set({ spireMode: true });
},
@@ -362,7 +326,7 @@ export const useGameStore = create()(
return {
currentFloor: newFloor,
currentRoom: generateFloorState(newFloor),
- floorMaxHP: 100 + newFloor * 50, // Simplified
+ floorMaxHP: 100 + newFloor * 50,
floorHP: 100 + newFloor * 50,
};
});
@@ -384,7 +348,7 @@ export function useGameLoop() {
const tick = useGameStore((s) => s.tick);
return {
start: () => {
- const interval = setInterval(tick, 1000); // TICK_MS
+ const interval = setInterval(tick, 1000);
return () => clearInterval(interval);
},
};
diff --git a/src/lib/game/store/combatSlice.ts b/src/lib/game/store/combatSlice.ts
deleted file mode 100755
index 304ba85..0000000
--- a/src/lib/game/store/combatSlice.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-// ─── Combat Slice ─────────────────────────────────────────────────────────────
-// Manages spire climbing, combat, and floor progression
-
-import type { StateCreator } from 'zustand';
-import type { GameState, GameAction, SpellCost } from '../types';
-import { GUARDIANS, SPELLS_DEF, ELEMENTS, ELEMENT_OPPOSITES } from '../constants';
-import { getFloorMaxHP, getFloorElement, calcDamage, computePactMultiplier, canAffordSpellCost, deductSpellCost } from './computed';
-import { computeEffects } from '../upgrade-effects';
-import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
-
-export interface CombatSlice {
- // State
- currentFloor: number;
- floorHP: number;
- floorMaxHP: number;
- maxFloorReached: number;
- activeSpell: string;
- currentAction: GameAction;
- castProgress: number;
-
- // Actions
- setAction: (action: GameAction) => void;
- setSpell: (spellId: string) => void;
- getDamage: (spellId: string) => number;
-
- // Internal combat processing
- processCombat: (deltaHours: number) => Partial;
-}
-
-export const createCombatSlice = (
- set: StateCreator['set'],
- get: () => GameState
-): CombatSlice => ({
- currentFloor: 1,
- floorHP: getFloorMaxHP(1),
- floorMaxHP: getFloorMaxHP(1),
- maxFloorReached: 1,
- activeSpell: 'manaBolt',
- currentAction: 'meditate',
- castProgress: 0,
-
- setAction: (action: GameAction) => {
- set((state) => ({
- currentAction: action,
- meditateTicks: action === 'meditate' ? state.meditateTicks : 0,
- }));
- },
-
- setSpell: (spellId: string) => {
- const state = get();
- if (state.spells[spellId]?.learned) {
- set({ activeSpell: spellId });
- }
- },
-
- getDamage: (spellId: string) => {
- const state = get();
- const floorElem = getFloorElement(state.currentFloor);
- return calcDamage(state, spellId, floorElem);
- },
-
- processCombat: (deltaHours: number) => {
- const state = get();
- if (state.currentAction !== 'climb') return {};
-
- const spellId = state.activeSpell;
- const spellDef = SPELLS_DEF[spellId];
- if (!spellDef) return {};
-
- const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
- const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05;
- const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier;
- const spellCastSpeed = spellDef.castSpeed || 1;
- const progressPerTick = deltaHours * spellCastSpeed * totalAttackSpeed;
-
- let castProgress = (state.castProgress || 0) + progressPerTick;
- let rawMana = state.rawMana;
- let elements = state.elements;
- let totalManaGathered = state.totalManaGathered;
- let currentFloor = state.currentFloor;
- let floorHP = state.floorHP;
- let floorMaxHP = state.floorMaxHP;
- let maxFloorReached = state.maxFloorReached;
- let signedPacts = state.signedPacts;
- let pendingPactOffer = state.pendingPactOffer;
- const log = [...state.log];
- const skills = state.skills;
- let comboHitCount = state.comboHitCount || 0;
- let floorHitCount = state.floorHitCount || 0;
-
- const floorElement = getFloorElement(currentFloor);
-
- while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) {
- // Deduct cost
- const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
- rawMana = afterCost.rawMana;
- elements = afterCost.elements;
- totalManaGathered += spellDef.cost.amount;
-
- // Calculate damage
- let dmg = calcDamage(state, spellId, floorElement);
- dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
-
- // Increment hit counters
- comboHitCount += 1;
- floorHitCount += 1;
-
- // First Strike: +15% damage on first attack each floor
- if (hasSpecial(effects, SPECIAL_EFFECTS.FIRST_STRIKE) && floorHitCount === 1) {
- dmg *= 1.15;
- log.unshift('⚡ First Strike! +15% damage!');
- }
-
- // Combo Master: Every 5th attack deals 3x damage
- if (hasSpecial(effects, SPECIAL_EFFECTS.COMBO_MASTER) && comboHitCount % 5 === 0) {
- dmg *= 3;
- log.unshift('🌀 Combo Master! Triple damage!');
- }
-
- // Executioner: +100% damage to enemies below 25% HP
- if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) {
- dmg *= 2;
- log.unshift('💀 Executioner! Double damage!');
- }
-
- // Berserker: +50% damage when below 50% mana
- const maxMana = 100; // Would need proper max mana calculation
- if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
- dmg *= 1.5;
- log.unshift('🔥 Berserker! +50% damage!');
- }
-
- // Spell echo - chance to cast again
- const echoChance = (skills.spellEcho || 0) * 0.1;
- if (Math.random() < echoChance) {
- dmg *= 2;
- log.unshift('✨ Spell Echo! Double damage!');
- }
-
- // Apply damage
- floorHP = Math.max(0, floorHP - dmg);
- castProgress -= 1;
-
- if (floorHP <= 0) {
- const wasGuardian = GUARDIANS[currentFloor];
-
- // Adrenaline Rush: Defeating enemy restores 5% mana
- if (hasSpecial(effects, SPECIAL_EFFECTS.ADRENALINE_RUSH)) {
- const manaRestore = Math.floor(maxMana * 0.05);
- rawMana = Math.min(rawMana + manaRestore, maxMana);
- log.unshift(`💚 Adrenaline Rush! Restored ${manaRestore} mana!`);
- }
-
- if (wasGuardian && !signedPacts.includes(currentFloor)) {
- pendingPactOffer = currentFloor;
- log.unshift(`⚔️ ${wasGuardian.name} defeated! They offer a pact...`);
- } else if (!wasGuardian) {
- if (currentFloor % 5 === 0) {
- log.unshift(`🏰 Floor ${currentFloor} cleared!`);
- }
- }
-
- currentFloor = currentFloor + 1;
- if (currentFloor > 100) currentFloor = 100;
- floorMaxHP = getFloorMaxHP(currentFloor);
- floorHP = floorMaxHP;
- maxFloorReached = Math.max(maxFloorReached, currentFloor);
- castProgress = 0;
- floorHitCount = 0; // Reset floor hit counter for new floor
- }
- }
-
- return {
- rawMana,
- elements,
- totalManaGathered,
- currentFloor,
- floorHP,
- floorMaxHP,
- maxFloorReached,
- signedPacts,
- pendingPactOffer,
- castProgress,
- log,
- comboHitCount,
- floorHitCount,
- };
- },
-});
diff --git a/src/lib/game/store/computed.ts b/src/lib/game/store/computed.ts
deleted file mode 100755
index 2130e78..0000000
--- a/src/lib/game/store/computed.ts
+++ /dev/null
@@ -1,315 +0,0 @@
-// ─── Computed Stats Functions ─────────────────────────────────────────────────
-
-import type { GameState } from '../types';
-import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, ELEMENT_OPPOSITES, FLOOR_ELEM_CYCLE } from '../constants';
-import { computeEffects } from '../upgrade-effects';
-import type { ComputedEffects } from '../upgrade-effects.types';
-import type { UnifiedEffects } from '../effects';
-import { getTierMultiplier } from '../skill-evolution';
-
-// Helper to get effective skill level accounting for tiers
-export function getEffectiveSkillLevel(
- skills: Record,
- baseSkillId: string,
- skillTiers: Record = {}
-): { level: number; tier: number; tierMultiplier: number } {
- const currentTier = skillTiers[baseSkillId] || 1;
- const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId;
- const level = skills[tieredSkillId] || skills[baseSkillId] || 0;
- const tierMultiplier = Math.pow(10, currentTier - 1);
- return { level, tier: currentTier, tierMultiplier };
-}
-
-export function computeMaxMana(
- state: Pick,
- effects?: ReturnType
-): number {
- const pu = state.prestigeUpgrades;
- const skillTiers = state.skillTiers || {};
- const skillUpgrades = state.skillUpgrades || {};
- const manaHeartBonus = state.manaHeartBonus || 0;
-
- const manaWellLevel = getEffectiveSkillLevel(state.skills, 'manaWell', skillTiers);
-
- const base =
- 100 +
- manaWellLevel.level * 100 * manaWellLevel.tierMultiplier +
- ((pu || {}).manaWell || 0) * 500;
-
- const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
- // Apply MANA_HEART bonus (+10% per loop, compounds)
- const heartMultiplier = 1 + manaHeartBonus;
- return Math.floor((base + computedEffects.maxManaBonus) * computedEffects.maxManaMultiplier * heartMultiplier);
-}
-
-// computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades
-// This file no longer exports computeElementMax to avoid duplicate export issues
-// Import computeElementMax from '../store' instead
-
-export function computeRegen(
- state: Pick,
- effects?: ReturnType
-): number {
- const pu = state.prestigeUpgrades;
- const skillTiers = state.skillTiers || {};
- const skillUpgrades = state.skillUpgrades || {};
-
- const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
-
- const manaFlowLevel = getEffectiveSkillLevel(state.skills, 'manaFlow', skillTiers);
- const manaSpringLevel = getEffectiveSkillLevel(state.skills, 'manaSpring', skillTiers);
-
- const base =
- 2 +
- manaFlowLevel.level * 1 * manaFlowLevel.tierMultiplier +
- manaSpringLevel.level * 2 * manaSpringLevel.tierMultiplier +
- (pu.manaFlow || 0) * 0.5;
-
- let regen = base * temporalBonus;
- const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
- regen = (regen + computedEffects.regenBonus + computedEffects.permanentRegenBonus) * computedEffects.regenMultiplier;
-
- return regen;
-}
-
-export function computeClickMana(
- state: Pick,
- effects?: ReturnType
-): number {
- const skillTiers = state.skillTiers || {};
- const skillUpgrades = state.skillUpgrades || {};
-
- const manaTapLevel = getEffectiveSkillLevel(state.skills, 'manaTap', skillTiers);
- const manaSurgeLevel = getEffectiveSkillLevel(state.skills, 'manaSurge', skillTiers);
-
- const base =
- 1 +
- manaTapLevel.level * 1 * manaTapLevel.tierMultiplier +
- manaSurgeLevel.level * 3 * manaSurgeLevel.tierMultiplier;
-
- const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
- return Math.floor((base + computedEffects.clickManaBonus) * computedEffects.clickManaMultiplier);
-}
-
-// Elemental damage bonus
-export function getElementalBonus(spellElem: string, floorElem: string): number {
- if (spellElem === 'raw') return 1.0;
-
- if (spellElem === floorElem) return 1.25; // Same element: +25% damage
-
- if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; // Super effective
- if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; // Weak
-
- return 1.0;
-}
-
-// Compute the pact multiplier with interference/synergy system
-export function computePactMultiplier(
- state: Pick
-): number {
- const { signedPacts, pactInterferenceMitigation = 0 } = state;
-
- if (signedPacts.length === 0) return 1.0;
-
- let baseMult = 1.0;
- for (const floor of signedPacts) {
- const guardian = GUARDIANS[floor];
- if (guardian) {
- baseMult *= guardian.damageMultiplier;
- }
- }
-
- if (signedPacts.length === 1) return baseMult;
-
- const numAdditionalPacts = signedPacts.length - 1;
- const basePenalty = 0.5 * numAdditionalPacts;
- const mitigationReduction = Math.min(pactInterferenceMitigation, 5) * 0.1;
- const effectivePenalty = Math.max(0, basePenalty - mitigationReduction);
-
- if (pactInterferenceMitigation >= 5) {
- const synergyBonus = (pactInterferenceMitigation - 5) * 0.1;
- return baseMult * (1 + synergyBonus);
- }
-
- return baseMult * (1 - effectivePenalty);
-}
-
-// Compute the insight multiplier from signed pacts
-export function computePactInsightMultiplier(
- state: Pick
-): number {
- const { signedPacts, pactInterferenceMitigation = 0 } = state;
-
- if (signedPacts.length === 0) return 1.0;
-
- let mult = 1.0;
- for (const floor of signedPacts) {
- const guardian = GUARDIANS[floor];
- if (guardian) {
- mult *= guardian.insightMultiplier;
- }
- }
-
- if (signedPacts.length > 1) {
- const numAdditionalPacts = signedPacts.length - 1;
- const basePenalty = 0.5 * numAdditionalPacts;
- const mitigationReduction = Math.min(pactInterferenceMitigation, 5) * 0.1;
- const effectivePenalty = Math.max(0, basePenalty - mitigationReduction);
-
- if (pactInterferenceMitigation >= 5) {
- const synergyBonus = (pactInterferenceMitigation - 5) * 0.1;
- return mult * (1 + synergyBonus);
- }
-
- return mult * (1 - effectivePenalty);
- }
-
- return mult;
-}
-
-export function calcDamage(
- state: Pick,
- spellId: string,
- floorElem?: string,
- effects?: ReturnType
-): number {
- const sp = SPELLS_DEF[spellId];
- if (!sp) return 5;
-
- const skillTiers = state.skillTiers || {};
- const skillUpgrades = state.skillUpgrades || {};
- const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
-
- // Get effective skill levels with tier multipliers
- const combatTrainLevel = getEffectiveSkillLevel(state.skills, 'combatTrain', skillTiers);
- const arcaneFuryLevel = getEffectiveSkillLevel(state.skills, 'arcaneFury', skillTiers);
- const elemMasteryLevel = getEffectiveSkillLevel(state.skills, 'elementalMastery', skillTiers);
- const guardianBaneLevel = getEffectiveSkillLevel(state.skills, 'guardianBane', skillTiers);
- const precisionLevel = getEffectiveSkillLevel(state.skills, 'precision', skillTiers);
-
- // Base damage from spell + combat training
- const baseDmg = sp.dmg + combatTrainLevel.level * 5 * combatTrainLevel.tierMultiplier;
-
- // Spell damage multiplier from arcane fury
- const pct = 1 + arcaneFuryLevel.level * 0.1 * arcaneFuryLevel.tierMultiplier;
-
- // Elemental mastery bonus
- const elemMasteryBonus = 1 + elemMasteryLevel.level * 0.15 * elemMasteryLevel.tierMultiplier;
-
- // Guardian bane bonus (only for guardian floors)
- const guardianBonus = floorElem && Object.values(GUARDIANS).find(g => g.element === floorElem)
- ? 1 + guardianBaneLevel.level * 0.2 * guardianBaneLevel.tierMultiplier
- : 1;
-
- // Crit chance from precision
- const skillCritChance = precisionLevel.level * 0.05 * precisionLevel.tierMultiplier;
- const totalCritChance = skillCritChance + computedEffects.critChanceBonus;
-
- // Pact multiplier
- const pactMult = computePactMultiplier(state);
-
- // Calculate base damage
- let damage = baseDmg * pct * pactMult * elemMasteryBonus * guardianBonus;
-
- // Apply upgrade effects: base damage multiplier and bonus
- damage = damage * computedEffects.baseDamageMultiplier + computedEffects.baseDamageBonus;
-
- // Apply elemental damage multiplier from upgrades
- damage *= computedEffects.elementalDamageMultiplier;
-
- // Apply elemental bonus for floor
- if (floorElem) {
- damage *= getElementalBonus(sp.elem, floorElem);
- }
-
- // Apply critical hit
- if (Math.random() < totalCritChance) {
- damage *= computedEffects.critDamageMultiplier;
- }
-
- return damage;
-}
-
-export function calcInsight(state: Pick): 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
- );
-}
-
-export function getMeditationBonus(meditateTicks: number, skills: Record, meditationEfficiency: number = 1): number {
- const hasMeditation = skills.meditation === 1;
- const hasDeepTrance = skills.deepTrance === 1;
- const hasVoidMeditation = skills.voidMeditation === 1;
-
- const hours = meditateTicks * 0.04; // HOURS_PER_TICK
-
- let bonus = 1 + Math.min(hours / 4, 0.5);
-
- if (hasMeditation && hours >= 4) {
- bonus = 2.5;
- }
-
- if (hasDeepTrance && hours >= 6) {
- bonus = 3.0;
- }
-
- if (hasVoidMeditation && hours >= 8) {
- bonus = 5.0;
- }
-
- bonus *= meditationEfficiency;
-
- return bonus;
-}
-
-export function getIncursionStrength(day: number, hour: number): number {
- const INCURSION_START_DAY = 20;
- const MAX_DAY = 30;
-
- 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);
-}
-
-export function getFloorMaxHP(floor: number): number {
- if (GUARDIANS[floor]) return GUARDIANS[floor].hp;
- 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) % FLOOR_ELEM_CYCLE.length];
-}
-
-// Formatting utilities
-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';
-}
-
-// Check if player can afford spell cost
-export function canAffordSpellCost(
- cost: { type: 'raw' | 'element'; element?: string; amount: number },
- rawMana: number,
- elements: Record
-): boolean {
- if (cost.type === 'raw') {
- return rawMana >= cost.amount;
- } else {
- const elem = elements[cost.element || ''];
- return elem && elem.unlocked && elem.current >= cost.amount;
- }
-}
diff --git a/src/lib/game/store/crafting-modules/initial-state.ts b/src/lib/game/store/crafting-modules/initial-state.ts
deleted file mode 100644
index fd95b82..0000000
--- a/src/lib/game/store/crafting-modules/initial-state.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// ─── Crafting Initial State ───────────────────────────────────────────────────
-
-import type { CraftingState } from './types';
-import { EQUIPMENT_SLOTS } from '../../data/equipment';
-
-export const initialCraftingState: CraftingState = {
- equippedInstances: {
- mainHand: null,
- offHand: null,
- head: null,
- body: null,
- hands: null,
- feet: null,
- accessory1: null,
- accessory2: null,
- },
- equipmentInstances: {},
- enchantmentDesigns: [],
- designProgress: null,
- preparationProgress: null,
- applicationProgress: null,
- equipmentSpellStates: [],
-};
diff --git a/src/lib/game/store/crafting-modules/selectors.ts b/src/lib/game/store/crafting-modules/selectors.ts
deleted file mode 100644
index 823fa71..0000000
--- a/src/lib/game/store/crafting-modules/selectors.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-// ─── Crafting Selectors ───────────────────────────────────────────────────
-
-import type { CraftingStore } from './types';
-import type { EquipmentInstance } from '../../types';
-import type { EquipmentSlot } from '../../data/equipment';
-import { EQUIPMENT_SLOTS } from '../../data/equipment';
-import { getSpellsFromEquipment, computeEquipmentEffects } from './utils';
-
-/**
- * Creates selector functions that depend on the store's get function.
- * Selectors are pure functions that derive data from the current state.
- */
-export const createSelectors = (get: () => CraftingStore) => ({
- getEquippedInstance: (slot: EquipmentSlot): EquipmentInstance | null => {
- const state = get();
- const instanceId = state.equippedInstances[slot];
- if (!instanceId) return null;
- return state.equipmentInstances[instanceId] || null;
- },
-
- getAllEquipped: (): EquipmentInstance[] => {
- const state = get();
- const equipped: EquipmentInstance[] = [];
-
- for (const slot of EQUIPMENT_SLOTS) {
- const instanceId = state.equippedInstances[slot];
- if (instanceId && state.equipmentInstances[instanceId]) {
- equipped.push(state.equipmentInstances[instanceId]);
- }
- }
-
- return equipped;
- },
-
- getAvailableSpells: (): string[] => {
- const equipped = get().getAllEquipped();
- const spells: string[] = [];
-
- for (const equip of equipped) {
- spells.push(...getSpellsFromEquipment(equip));
- }
-
- return spells;
- },
-
- getEquipmentEffects: () => {
- return computeEquipmentEffects(get().getAllEquipped());
- },
-});
diff --git a/src/lib/game/store/crafting-modules/slice-logic.ts b/src/lib/game/store/crafting-modules/slice-logic.ts
deleted file mode 100644
index 9bbcb3f..0000000
--- a/src/lib/game/store/crafting-modules/slice-logic.ts
+++ /dev/null
@@ -1,252 +0,0 @@
-// ─── Crafting Slice Logic ─────────────────────────────────────────────────
-
-import type { StateCreator } from 'zustand';
-import type { CraftingStore } from './types';
-import type {
- DesignEffect,
- EnchantmentDesign,
- EquipmentInstance
-} from '../../types';
-import type { EquipmentSlot } from '../../data/equipment';
-import { initialCraftingState } from './initial-state';
-import {
- generateInstanceId,
- generateDesignId,
- createEquipmentInstance,
- calculateDesignTime,
- calculatePreparationTime,
- calculatePreparationManaCost,
- calculateApplicationTime,
- calculateApplicationManaPerHour
-} from './utils';
-import {
- EQUIPMENT_SLOTS,
- getEquipmentType
-} from '../../data/equipment';
-import { createSelectors } from './selectors';
-import {
- processDesignTick,
- processPreparationTick,
- processApplicationTick
-} from './tick-processors';
-
-// ─── Cached Skills Workaround ──────────────────────────────────────────────
-// We need to access skills from the main store - this is a workaround
-// The store will pass skills when calling these methods
-
-let cachedSkills: Record = {};
-
-export function setCachedSkills(skills: Record): void {
- cachedSkills = skills;
-}
-
-// ─── Slice Creator ─────────────────────────────────────────────────────────
-
-export const createCraftingSlice: StateCreator = (set, get) => {
- const selectors = createSelectors(get);
-
- return {
- ...initialCraftingState,
-
- // Equipment management
- createEquipment: (typeId: string, slot?: EquipmentSlot) => {
- const instance = createEquipmentInstance(typeId);
-
- set((state) => ({
- equipmentInstances: {
- ...state.equipmentInstances,
- [instance.instanceId]: instance,
- },
- }));
-
- // Auto-equip if slot provided
- if (slot) {
- get().equipInstance(instance.instanceId, slot);
- }
-
- return instance;
- },
-
- equipInstance: (instanceId: string, slot: EquipmentSlot) => {
- const instance = get().equipmentInstances[instanceId];
- if (!instance) return;
-
- const typeDef = getEquipmentType(instance.typeId);
- if (!typeDef) return;
-
- // Check if equipment can go in this slot
- if (typeDef.slot !== slot) {
- // For accessories, both accessory1 and accessory2 are valid
- if (typeDef.category !== 'accessory' || (slot !== 'accessory1' && slot !== 'accessory2')) {
- return;
- }
- }
-
- set((state) => ({
- equippedInstances: {
- ...state.equippedInstances,
- [slot]: instanceId,
- },
- }));
- },
-
- unequipSlot: (slot: EquipmentSlot) => {
- set((state) => ({
- equippedInstances: {
- ...state.equippedInstances,
- [slot]: null,
- },
- }));
- },
-
- deleteInstance: (instanceId: string) => {
- set((state) => {
- const newInstanceMap = { ...state.equipmentInstances };
- delete newInstanceMap[instanceId];
-
- // Remove from equipped slots
- const newEquipped = { ...state.equippedInstances };
- for (const slot of EQUIPMENT_SLOTS) {
- if (newEquipped[slot] === instanceId) {
- newEquipped[slot] = null;
- }
- }
-
- return {
- equipmentInstances: newInstanceMap,
- equippedInstances: newEquipped,
- };
- });
- },
-
- // Enchantment design
- startDesign: (name: string, equipmentType: string, effects: DesignEffect[]) => {
- const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
- const designTime = calculateDesignTime(effects);
-
- const design: EnchantmentDesign = {
- id: generateDesignId(),
- name,
- equipmentType,
- effects,
- totalCapacityUsed: totalCapacity,
- designTime,
- created: Date.now(),
- };
-
- set((state) => ({
- enchantmentDesigns: [...state.enchantmentDesigns, design],
- designProgress: {
- designId: design.id,
- progress: 0,
- required: designTime,
- name,
- equipmentType,
- effects,
- },
- }));
- },
-
- cancelDesign: () => {
- const progress = get().designProgress;
- if (!progress) return;
-
- set((state) => ({
- designProgress: null,
- enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== progress.designId),
- }));
- },
-
- deleteDesign: (designId: string) => {
- set((state) => ({
- enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId),
- }));
- },
-
- // Equipment preparation
- startPreparation: (instanceId: string) => {
- const instance = get().equipmentInstances[instanceId];
- if (!instance) return;
-
- const prepTime = calculatePreparationTime(instance.typeId);
- const manaCost = calculatePreparationManaCost(instance.typeId);
-
- set({
- preparationProgress: {
- equipmentInstanceId: instanceId,
- progress: 0,
- required: prepTime,
- manaCostPaid: 0,
- },
- });
- },
-
- cancelPreparation: () => {
- set({ preparationProgress: null });
- },
-
- // Enchantment application
- startApplication: (instanceId: string, designId: string) => {
- const instance = get().equipmentInstances[instanceId];
- const design = get().enchantmentDesigns.find(d => d.id === designId);
-
- if (!instance || !design) return;
-
- const appTime = calculateApplicationTime(design.effects, cachedSkills);
- const manaPerHour = calculateApplicationManaPerHour(design.effects);
-
- set({
- applicationProgress: {
- equipmentInstanceId: instanceId,
- designId,
- progress: 0,
- required: appTime,
- manaPerHour,
- paused: false,
- manaSpent: 0,
- },
- });
- },
-
- pauseApplication: () => {
- const progress = get().applicationProgress;
- if (!progress) return;
-
- set({
- applicationProgress: { ...progress, paused: true },
- });
- },
-
- resumeApplication: () => {
- const progress = get().applicationProgress;
- if (!progress) return;
-
- set({
- applicationProgress: { ...progress, paused: false },
- });
- },
-
- cancelApplication: () => {
- set({ applicationProgress: null });
- },
-
- // Tick processing - delegated to tick-processors module
- processDesignTick: (hours: number) => {
- return processDesignTick(get(), set, hours);
- },
-
- processPreparationTick: (hours: number, manaAvailable: number) => {
- return processPreparationTick(get(), set, hours, manaAvailable);
- },
-
- processApplicationTick: (hours: number, manaAvailable: number) => {
- return processApplicationTick(get(), set, get, hours, manaAvailable, cachedSkills);
- },
-
- // Selectors - delegated to selectors module
- getEquippedInstance: selectors.getEquippedInstance,
- getAllEquipped: selectors.getAllEquipped,
- getAvailableSpells: selectors.getAvailableSpells,
- getEquipmentEffects: selectors.getEquipmentEffects,
- };
-};
diff --git a/src/lib/game/store/crafting-modules/starting-equipment.ts b/src/lib/game/store/crafting-modules/starting-equipment.ts
deleted file mode 100644
index b736a70..0000000
--- a/src/lib/game/store/crafting-modules/starting-equipment.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-// ─── Starting Equipment Factory ─────────────────────────────────────────
-
-import type { EquipmentInstance } from '../../types';
-import { createEquipmentInstance } from './utils';
-
-export function createStartingEquipment(): {
- equippedInstances: Record;
- equipmentInstances: Record;
-} {
- const instances: EquipmentInstance[] = [];
-
- // Create starting equipment
- const basicStaff = createEquipmentInstance('basicStaff');
- basicStaff.enchantments = [{
- effectId: 'spell_manaBolt',
- stacks: 1,
- actualCost: 50, // Fills the staff completely
- }];
- basicStaff.usedCapacity = 50;
- basicStaff.rarity = 'uncommon';
- instances.push(basicStaff);
-
- const civilianShirt = createEquipmentInstance('civilianShirt');
- instances.push(civilianShirt);
-
- const civilianGloves = createEquipmentInstance('civilianGloves');
- instances.push(civilianGloves);
-
- const civilianShoes = createEquipmentInstance('civilianShoes');
- instances.push(civilianShoes);
-
- // Build instance map
- const equipmentInstances: Record = {};
- for (const inst of instances) {
- equipmentInstances[inst.instanceId] = inst;
- }
-
- // Build equipped map
- const equippedInstances: Record = {
- mainHand: basicStaff.instanceId,
- offHand: null,
- head: null,
- body: civilianShirt.instanceId,
- hands: civilianGloves.instanceId,
- feet: civilianShoes.instanceId,
- accessory1: null,
- accessory2: null,
- };
-
- return { equippedInstances, equipmentInstances };
-}
diff --git a/src/lib/game/store/crafting-modules/tick-processors.ts b/src/lib/game/store/crafting-modules/tick-processors.ts
deleted file mode 100644
index 42b192e..0000000
--- a/src/lib/game/store/crafting-modules/tick-processors.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-// ─── Tick Processors ─────────────────────────────────────────────────────
-// These functions handle the time-based processing for crafting operations.
-
-import type { CraftingStore } from './types';
-import type { AppliedEnchantment, EquipmentInstance } from '../../types';
-import {
- getEnchantEfficiencyBonus,
- calculatePreparationManaCost,
- calculateEffectCapacityCost,
- calculateRarity
-} from './utils';
-import { getEnchantmentEffect } from '../../data/enchantment-effects';
-
-/**
- * Processes design tick progress.
- * Returns true if design was completed this tick.
- */
-export function processDesignTick(
- state: CraftingStore,
- setState: (updater: Partial | ((state: CraftingStore) => Partial)) => void,
- hours: number
-): boolean {
- const progress = state.designProgress;
- if (!progress) return false;
-
- const newProgress = progress.progress + hours;
-
- if (newProgress >= progress.required) {
- // Design complete
- setState({ designProgress: null });
- return true;
- } else {
- setState({
- designProgress: { ...progress, progress: newProgress },
- });
- return false;
- }
-}
-
-/**
- * Processes preparation tick progress.
- * Returns the amount of mana consumed.
- */
-export function processPreparationTick(
- state: CraftingStore,
- setState: (updater: Partial | ((state: CraftingStore) => Partial)) => void,
- hours: number,
- manaAvailable: number
-): number {
- const progress = state.preparationProgress;
- if (!progress) return 0;
-
- const instance = state.equipmentInstances[progress.equipmentInstanceId];
- if (!instance) {
- setState({ preparationProgress: null });
- return 0;
- }
-
- const totalManaCost = calculatePreparationManaCost(instance.typeId);
- const remainingManaCost = totalManaCost - progress.manaCostPaid;
- const manaToPay = Math.min(manaAvailable, remainingManaCost);
-
- if (manaToPay < remainingManaCost) {
- // Not enough mana, just pay what we can
- setState({
- preparationProgress: {
- ...progress,
- manaCostPaid: progress.manaCostPaid + manaToPay,
- },
- });
- return manaToPay;
- }
-
- // Pay remaining mana and progress
- const newProgress = progress.progress + hours;
-
- if (newProgress >= progress.required) {
- // Preparation complete - clear enchantments and add 'Ready for Enchantment' tag
- setState((state) => ({
- preparationProgress: null,
- equipmentInstances: {
- ...state.equipmentInstances,
- [instance.instanceId]: {
- ...instance,
- enchantments: [],
- usedCapacity: 0,
- rarity: 'common' as const,
- tags: [...(instance.tags || []), 'Ready for Enchantment'],
- },
- },
- }));
- } else {
- setState({
- preparationProgress: {
- ...progress,
- progress: newProgress,
- manaCostPaid: progress.manaCostPaid + manaToPay,
- },
- });
- }
-
- return manaToPay;
-}
-
-/**
- * Processes application tick progress.
- * Returns the amount of mana consumed.
- */
-export function processApplicationTick(
- state: CraftingStore,
- setState: (updater: Partial | ((state: CraftingStore) => Partial)) => void,
- getState: () => CraftingStore,
- hours: number,
- manaAvailable: number,
- cachedSkills: Record
-): number {
- const progress = state.applicationProgress;
- if (!progress || progress.paused) return 0;
-
- const design = state.enchantmentDesigns.find(d => d.id === progress.designId);
- const instance = state.equipmentInstances[progress.equipmentInstanceId];
-
- if (!design || !instance) {
- setState({ applicationProgress: null });
- return 0;
- }
-
- const manaNeeded = progress.manaPerHour * hours;
- const manaToUse = Math.min(manaAvailable, manaNeeded);
-
- if (manaToUse < manaNeeded) {
- // Not enough mana - pause and save progress
- setState({
- applicationProgress: {
- ...progress,
- manaSpent: progress.manaSpent + manaToUse,
- },
- });
- return manaToUse;
- }
-
- const newProgress = progress.progress + hours;
-
- if (newProgress >= progress.required) {
- // Application complete - apply enchantments
- const efficiencyBonus = getEnchantEfficiencyBonus(cachedSkills);
- const newEnchantments: AppliedEnchantment[] = design.effects.map(e => ({
- effectId: e.effectId,
- stacks: e.stacks,
- actualCost: calculateEffectCapacityCost(e.effectId, e.stacks, efficiencyBonus),
- }));
-
- const totalUsedCapacity = newEnchantments.reduce((sum, e) => sum + e.actualCost, 0);
-
- setState((state) => ({
- applicationProgress: null,
- equipmentInstances: {
- ...state.equipmentInstances,
- [instance.instanceId]: {
- ...instance,
- enchantments: newEnchantments,
- usedCapacity: totalUsedCapacity,
- rarity: calculateRarity(newEnchantments),
- },
- },
- }));
- } else {
- setState({
- applicationProgress: {
- ...progress,
- progress: newProgress,
- manaSpent: progress.manaSpent + manaToUse,
- },
- });
- }
-
- return manaToUse;
-}
diff --git a/src/lib/game/store/crafting-modules/types.ts b/src/lib/game/store/crafting-modules/types.ts
deleted file mode 100644
index bcabb51..0000000
--- a/src/lib/game/store/crafting-modules/types.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-// ─── Crafting Store Types ──────────────────────────────────────────────────────
-
-import type {
- EquipmentInstance,
- AppliedEnchantment,
- EnchantmentDesign,
- DesignEffect,
- DesignProgress,
- PreparationProgress,
- ApplicationProgress,
- EquipmentSpellState
-} from '../../types';
-import type { EquipmentSlot } from '../../data/equipment';
-
-export interface CraftingState {
- // Equipment instances
- equippedInstances: Record; // slot -> instanceId
- equipmentInstances: Record; // instanceId -> instance
-
- // Enchantment designs
- enchantmentDesigns: EnchantmentDesign[];
-
- // Crafting progress
- designProgress: DesignProgress | null;
- preparationProgress: PreparationProgress | null;
- applicationProgress: ApplicationProgress | null;
-
- // Equipment spell states
- equipmentSpellStates: EquipmentSpellState[];
-}
-
-export interface CraftingActions {
- // Equipment management
- createEquipment: (typeId: string, slot?: EquipmentSlot) => EquipmentInstance;
- equipInstance: (instanceId: string, slot: EquipmentSlot) => void;
- unequipSlot: (slot: EquipmentSlot) => void;
- deleteInstance: (instanceId: string) => void;
-
- // Enchantment design
- startDesign: (name: string, equipmentType: string, effects: DesignEffect[]) => void;
- cancelDesign: () => void;
- deleteDesign: (designId: string) => void;
-
- // Equipment preparation
- startPreparation: (instanceId: string) => void;
- cancelPreparation: () => void;
-
- // Enchantment application
- startApplication: (instanceId: string, designId: string) => void;
- pauseApplication: () => void;
- resumeApplication: () => void;
- cancelApplication: () => void;
-
- // Tick processing
- processDesignTick: (hours: number) => void;
- processPreparationTick: (hours: number, manaAvailable: number) => number; // Returns mana used
- processApplicationTick: (hours: number, manaAvailable: number) => number; // Returns mana used
-
- // Getters
- getEquippedInstance: (slot: EquipmentSlot) => EquipmentInstance | null;
- getAllEquipped: () => EquipmentInstance[];
- getAvailableSpells: () => string[];
- getEquipmentEffects: () => Record;
-}
-
-export type CraftingStore = CraftingState & CraftingActions;
diff --git a/src/lib/game/store/crafting-modules/utils.ts b/src/lib/game/store/crafting-modules/utils.ts
deleted file mode 100644
index b9af456..0000000
--- a/src/lib/game/store/crafting-modules/utils.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-// ─── Crafting Utility Functions ──────────────────────────────────────────────
-
-import type {
- EquipmentInstance,
- DesignEffect,
- AppliedEnchantment
-} from '../../types';
-import {
- getEquipmentType
-} from '../../data/equipment';
-import {
- getEnchantmentEffect,
- calculateEffectCapacityCost
-} from '../../data/enchantment-effects';
-
-// Re-export for use in other modules
-export { getEquipmentType } from '../../data/equipment';
-export { calculateEffectCapacityCost, getEnchantmentEffect } from '../../data/enchantment-effects';
-
-// ─── ID Generators ────────────────────────────────────────────────────────────
-
-let instanceIdCounter = 0;
-export function generateInstanceId(): string {
- return `equip_${Date.now()}_${++instanceIdCounter}`;
-}
-
-let designIdCounter = 0;
-export function generateDesignId(): string {
- return `design_${Date.now()}_${++designIdCounter}`;
-}
-
-// ─── Skill-based Calculations ────────────────────────────────────────────────
-
-// Calculate efficiency bonus from skills
-export function getEnchantEfficiencyBonus(skills: Record): number {
- const enchantingLevel = skills.enchanting || 0;
- const efficientEnchantLevel = skills.efficientEnchant || 0;
-
- // 2% per enchanting level + 5% per efficient enchant level
- return (enchantingLevel * 0.02) + (efficientEnchantLevel * 0.05);
-}
-
-// ─── Time and Cost Calculations ──────────────────────────────────────────────
-
-// Calculate design time based on effects
-export function calculateDesignTime(effects: DesignEffect[]): number {
- const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
- return Math.max(1, Math.floor(totalCapacity / 10)); // Hours
-}
-
-// Calculate preparation time for equipment
-export function calculatePreparationTime(equipmentType: string): number {
- const typeDef = getEquipmentType(equipmentType);
- if (!typeDef) return 1;
- return Math.max(1, Math.floor(typeDef.baseCapacity / 5)); // Hours
-}
-
-// Calculate preparation mana cost
-export function calculatePreparationManaCost(equipmentType: string): number {
- const typeDef = getEquipmentType(equipmentType);
- if (!typeDef) return 50;
- return typeDef.baseCapacity * 5;
-}
-
-// Calculate application time based on effects
-export function calculateApplicationTime(effects: DesignEffect[], skills: Record): number {
- const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
- const speedBonus = 1 + (skills.enchantSpeed || 0) * 0.1;
- return Math.max(4, Math.floor(totalCapacity / 20 * 24 / speedBonus)); // Hours (days * 24)
-}
-
-// Calculate mana per hour for application
-export function calculateApplicationManaPerHour(effects: DesignEffect[]): number {
- const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
- return Math.max(1, Math.floor(totalCapacity * 0.5));
-}
-
-// ─── Equipment Instance Creation ─────────────────────────────────────────────
-
-// Create a new equipment instance
-export function createEquipmentInstance(typeId: string, name?: string): EquipmentInstance {
- const typeDef = getEquipmentType(typeId);
- if (!typeDef) {
- throw new Error(`Unknown equipment type: ${typeId}`);
- }
-
- return {
- instanceId: generateInstanceId(),
- typeId,
- name: name || typeDef.name,
- enchantments: [],
- usedCapacity: 0,
- totalCapacity: typeDef.baseCapacity,
- rarity: 'common',
- quality: 100, // Full quality for new items
- tags: [], // Initialize with empty tags array
- };
-}
-
-// ─── Rarity Calculation ────────────────────────────────────────────────────
-
-// Calculate rarity based on number and quality of enchantments
-export function calculateRarity(enchantments: AppliedEnchantment[]): 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' {
- if (enchantments.length === 0) return 'common';
-
- const totalCapacity = enchantments.reduce((sum, e) => sum + e.actualCost, 0);
- const avgStacks = enchantments.reduce((sum, e) => sum + e.stacks, 0) / enchantments.length;
-
- // Determine rarity based on capacity used and number of enchantments
- if (totalCapacity >= 200 && enchantments.length >= 3) return 'legendary';
- if (totalCapacity >= 150 && enchantments.length >= 2) return 'epic';
- if (totalCapacity >= 100 || enchantments.length >= 2) return 'rare';
- if (totalCapacity >= 50 || avgStacks > 1) return 'uncommon';
- return 'common';
-}
-
-// ─── Equipment Effect Computation ────────────────────────────────────────────
-
-// Get spells from equipment
-export function getSpellsFromEquipment(equipment: EquipmentInstance): string[] {
- const spells: string[] = [];
-
- for (const ench of equipment.enchantments) {
- const effectDef = getEnchantmentEffect(ench.effectId);
- if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
- spells.push(effectDef.effect.spellId);
- }
- }
-
- return spells;
-}
-
-// Compute total effects from equipment
-export function computeEquipmentEffects(equipment: EquipmentInstance[]): Record {
- const effects: Record = {};
- const multipliers: Record = {};
- const specials: Set = new Set();
-
- for (const equip of equipment) {
- for (const ench of equip.enchantments) {
- const effectDef = getEnchantmentEffect(ench.effectId);
- if (!effectDef) continue;
-
- const value = (effectDef.effect.value || 0) * ench.stacks;
-
- if (effectDef.effect.type === 'bonus' && effectDef.effect.stat) {
- effects[effectDef.effect.stat] = (effects[effectDef.effect.stat] || 0) + value;
- } else if (effectDef.effect.type === 'multiplier' && effectDef.effect.stat) {
- multipliers[effectDef.effect.stat] = (multipliers[effectDef.effect.stat] || 1) * Math.pow(value, ench.stacks);
- } else if (effectDef.effect.type === 'special' && effectDef.effect.specialId) {
- specials.add(effectDef.effect.specialId);
- }
- }
- }
-
- // Apply multipliers to bonus effects
- for (const [stat, mult] of Object.entries(multipliers)) {
- effects[`${stat}_multiplier`] = mult;
- }
-
- // Add special effect flags
- for (const special of specials) {
- effects[`special_${special}`] = 1;
- }
-
- return effects;
-}
diff --git a/src/lib/game/store/craftingSlice.ts b/src/lib/game/store/craftingSlice.ts
deleted file mode 100755
index 18b9e03..0000000
--- a/src/lib/game/store/craftingSlice.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-// ─── Crafting Store Slice ────────────────────────────────────────────────────────
-// Handles equipment, enchantments, and crafting progress
-//
-// This file is a re-export barrel that combines all crafting modules.
-// For the actual implementation, see:
-// - crafting-modules/types.ts - Type definitions
-// - crafting-modules/initial-state.ts - Initial state
-// - crafting-modules/utils.ts - Utility functions
-// - crafting-modules/slice-logic.ts - Main slice creator
-// - crafting-modules/starting-equipment.ts - Starting equipment factory
-
-// Re-export everything from the modules
-export type { CraftingState, CraftingActions, CraftingStore } from './crafting-modules/types';
-export { initialCraftingState } from './crafting-modules/initial-state';
-export {
- generateInstanceId,
- generateDesignId,
- getEnchantEfficiencyBonus,
- calculateDesignTime,
- calculatePreparationTime,
- calculatePreparationManaCost,
- calculateApplicationTime,
- calculateApplicationManaPerHour,
- createEquipmentInstance,
- getSpellsFromEquipment,
- computeEquipmentEffects
-} from './crafting-modules/utils';
-export { createCraftingSlice, setCachedSkills } from './crafting-modules/slice-logic';
-export { createStartingEquipment } from './crafting-modules/starting-equipment';
diff --git a/src/lib/game/store/manaSlice.ts b/src/lib/game/store/manaSlice.ts
deleted file mode 100755
index 060e8a1..0000000
--- a/src/lib/game/store/manaSlice.ts
+++ /dev/null
@@ -1,204 +0,0 @@
-// ─── Mana Slice ───────────────────────────────────────────────────────────────
-// Manages raw mana, elements, and meditation
-
-import type { StateCreator } from 'zustand';
-import type { GameState, ElementState, SpellCost } from '../types';
-import { ELEMENTS, MANA_PER_ELEMENT, BASE_UNLOCKED_ELEMENTS } from '../constants';
-import { computeMaxMana, computeClickMana, canAffordSpellCost, getMeditationBonus } from './computed';
-import { computeElementMax } from '../store';
-import { computeEffects } from '../upgrade-effects';
-import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
-
-export interface ManaSlice {
- // State
- rawMana: number;
- totalManaGathered: number;
- meditateTicks: number;
- elements: Record;
-
- // Actions
- gatherMana: () => void;
- convertMana: (element: string, amount: number) => void;
- unlockElement: (element: string) => void;
- craftComposite: (target: string) => void;
-
- // Computed getters
- getMaxMana: () => number;
- getRegen: () => number;
- getClickMana: () => number;
- getMeditationMultiplier: () => number;
-}
-
-export const createManaSlice = (
- set: StateCreator['set'],
- get: () => GameState
-): ManaSlice => ({
- rawMana: 10,
- totalManaGathered: 0,
- meditateTicks: 0,
- elements: (() => {
- const elems: Record = {};
- const pu = get().prestigeUpgrades;
- const elemMax = computeElementMax(get());
-
- Object.keys(ELEMENTS).forEach((k) => {
- const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k);
- let startAmount = 0;
-
- if (isUnlocked && pu.elemStart) {
- startAmount = pu.elemStart * 5;
- }
-
- elems[k] = {
- current: startAmount,
- max: elemMax,
- unlocked: isUnlocked,
- };
- });
- return elems;
- })(),
-
- gatherMana: () => {
- const state = get();
- let cm = computeClickMana(state);
-
- // Mana overflow bonus
- const overflowBonus = 1 + (state.skills.manaOverflow || 0) * 0.25;
- cm = Math.floor(cm * overflowBonus);
-
- const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
- const max = computeMaxMana(state, effects);
-
- // Mana Conversion: Convert 5% of max mana to click bonus
- if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONVERSION)) {
- cm += Math.floor(max * 0.05);
- }
-
- // Mana Echo: 10% chance to gain double mana from clicks
- const hasManaEcho = effects.specials?.has('MANA_ECHO') ?? false;
- if (hasManaEcho && Math.random() < 0.1) {
- cm *= 2;
- }
-
- set({
- rawMana: Math.min(state.rawMana + cm, max),
- totalManaGathered: state.totalManaGathered + cm,
- });
- },
-
- convertMana: (element: string, amount: number = 1) => {
- const state = get();
- const e = state.elements[element];
- if (!e?.unlocked) return;
-
- const cost = MANA_PER_ELEMENT * amount;
- if (state.rawMana < cost) return;
- if (e.current >= e.max) return;
-
- const canConvert = Math.min(
- amount,
- Math.floor(state.rawMana / MANA_PER_ELEMENT),
- e.max - e.current
- );
-
- set({
- rawMana: state.rawMana - canConvert * MANA_PER_ELEMENT,
- elements: {
- ...state.elements,
- [element]: { ...e, current: e.current + canConvert },
- },
- });
- },
-
- unlockElement: (element: string) => {
- const state = get();
- if (state.elements[element]?.unlocked) return;
-
- const cost = 500;
- if (state.rawMana < cost) return;
-
- set({
- rawMana: state.rawMana - cost,
- elements: {
- ...state.elements,
- [element]: { ...state.elements[element], unlocked: true },
- },
- });
- },
-
- craftComposite: (target: string) => {
- const state = get();
- const edef = ELEMENTS[target];
- if (!edef?.recipe) return;
-
- const recipe = edef.recipe;
- const costs: Record = {};
- recipe.forEach((r) => {
- costs[r] = (costs[r] || 0) + 1;
- });
-
- // Check ingredients
- for (const [r, amt] of Object.entries(costs)) {
- if ((state.elements[r]?.current || 0) < amt) return;
- }
-
- const newElems = { ...state.elements };
- for (const [r, amt] of Object.entries(costs)) {
- newElems[r] = { ...newElems[r], current: newElems[r].current - amt };
- }
-
- // Elemental crafting bonus
- const craftBonus = 1 + (state.skills.elemCrafting || 0) * 0.25;
- const outputAmount = Math.floor(craftBonus);
-
- const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
- const elemMax = computeElementMax(state, effects);
- newElems[target] = {
- ...(newElems[target] || { current: 0, max: elemMax, unlocked: false }),
- current: (newElems[target]?.current || 0) + outputAmount,
- max: elemMax,
- unlocked: true,
- };
-
- set({
- elements: newElems,
- });
- },
-
- getMaxMana: () => computeMaxMana(get()),
- getRegen: () => {
- const state = get();
- const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
- // This would need proper regen calculation
- return 2;
- },
- getClickMana: () => computeClickMana(get()),
- getMeditationMultiplier: () => {
- const state = get();
- const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
- return getMeditationBonus(state.meditateTicks, state.skills, effects.meditationEfficiency);
- },
-});
-
-// Helper function to deduct spell cost
-export function deductSpellCost(
- cost: SpellCost,
- rawMana: number,
- elements: Record
-): { rawMana: number; elements: Record } {
- 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 };
-}
-
-export { canAffordSpellCost };
diff --git a/src/lib/game/store/pactSlice.ts b/src/lib/game/store/pactSlice.ts
deleted file mode 100755
index 4db4e65..0000000
--- a/src/lib/game/store/pactSlice.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-// ─── Pact Slice ───────────────────────────────────────────────────────────────
-// Manages guardian pacts, signing, and mana unlocking
-
-import type { StateCreator } from 'zustand';
-import type { GameState } from '../types';
-import { GUARDIANS, ELEMENTS } from '../constants';
-import { computePactMultiplier, computePactInsightMultiplier } from './computed';
-
-export interface PactSlice {
- // State
- signedPacts: number[];
- pendingPactOffer: number | null;
- maxPacts: number;
- pactSigningProgress: {
- floor: number;
- progress: number;
- required: number;
- manaCost: number;
- } | null;
- signedPactDetails: Record;
- }>;
- pactInterferenceMitigation: number;
- pactSynergyUnlocked: boolean;
-
- // Actions
- acceptPact: (floor: number) => void;
- declinePact: (floor: number) => void;
-
- // Computed getters
- getPactMultiplier: () => number;
- getPactInsightMultiplier: () => number;
-}
-
-export const createPactSlice = (
- set: StateCreator['set'],
- get: () => GameState
-): PactSlice => ({
- signedPacts: [],
- pendingPactOffer: null,
- maxPacts: 1,
- pactSigningProgress: null,
- signedPactDetails: {},
- pactInterferenceMitigation: 0,
- pactSynergyUnlocked: false,
-
- acceptPact: (floor: number) => {
- const state = get();
- const guardian = GUARDIANS[floor];
- if (!guardian || state.signedPacts.includes(floor)) return;
-
- const maxPacts = 1 + (state.prestigeUpgrades.pactCapacity || 0);
- if (state.signedPacts.length >= maxPacts) {
- set({
- log: [`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`, ...state.log.slice(0, 49)],
- });
- return;
- }
-
- const baseCost = guardian.signingCost.mana;
- const discount = Math.min((state.prestigeUpgrades.pactDiscount || 0) * 0.1, 0.5);
- const manaCost = Math.floor(baseCost * (1 - discount));
-
- if (state.rawMana < manaCost) {
- set({
- log: [`⚠️ Need ${manaCost} mana to sign pact with ${guardian.name}!`, ...state.log.slice(0, 49)],
- });
- return;
- }
-
- const baseTime = guardian.signingCost.time;
- const haste = Math.min((state.prestigeUpgrades.pactHaste || 0) * 0.1, 0.5);
- const signingTime = Math.max(1, baseTime * (1 - haste));
-
- set({
- rawMana: state.rawMana - manaCost,
- pactSigningProgress: {
- floor,
- progress: 0,
- required: signingTime,
- manaCost,
- },
- pendingPactOffer: null,
- currentAction: 'study',
- log: [`📜 Beginning pact signing with ${guardian.name}... (${signingTime}h, ${manaCost} mana)`, ...state.log.slice(0, 49)],
- });
- },
-
- declinePact: (floor: number) => {
- const state = get();
- const guardian = GUARDIANS[floor];
- if (!guardian) return;
-
- set({
- pendingPactOffer: null,
- log: [`🚫 Declined pact with ${guardian.name}.`, ...state.log.slice(0, 49)],
- });
- },
-
- getPactMultiplier: () => computePactMultiplier(get()),
- getPactInsightMultiplier: () => computePactInsightMultiplier(get()),
-});
-
-// Process pact signing progress (called during tick)
-export function processPactSigning(state: GameState, deltaHours: number): Partial {
- if (!state.pactSigningProgress) return {};
-
- const progress = state.pactSigningProgress.progress + deltaHours;
- const log = [...state.log];
-
- if (progress >= state.pactSigningProgress.required) {
- const floor = state.pactSigningProgress.floor;
- const guardian = GUARDIANS[floor];
- if (!guardian || state.signedPacts.includes(floor)) {
- return { pactSigningProgress: null };
- }
-
- const signedPacts = [...state.signedPacts, floor];
- const signedPactDetails = {
- ...state.signedPactDetails,
- [floor]: {
- floor,
- guardianId: guardian.element,
- signedAt: { day: state.day, hour: state.hour },
- skillLevels: {},
- },
- };
-
- // Unlock mana types
- let elements = { ...state.elements };
- for (const elemId of guardian.unlocksMana) {
- if (elements[elemId]) {
- elements = {
- ...elements,
- [elemId]: { ...elements[elemId], unlocked: true },
- };
- }
- }
-
- // Check for compound element unlocks
- const unlockedSet = new Set(
- Object.entries(elements)
- .filter(([, e]) => e.unlocked)
- .map(([id]) => id)
- );
-
- for (const [elemId, elemDef] of Object.entries(ELEMENTS)) {
- if (elemDef.recipe && !elements[elemId]?.unlocked) {
- const canUnlock = elemDef.recipe.every(comp => unlockedSet.has(comp));
- if (canUnlock) {
- elements = {
- ...elements,
- [elemId]: { ...elements[elemId], unlocked: true },
- };
- log.unshift(`🔮 ${elemDef.name} mana unlocked through component synergy!`);
- }
- }
- }
-
- log.unshift(`📜 Pact with ${guardian.name} signed! ${guardian.unlocksMana.map(e => ELEMENTS[e]?.name || e).join(', ')} mana unlocked!`);
-
- return {
- signedPacts,
- signedPactDetails,
- elements,
- pactSigningProgress: null,
- log,
- };
- }
-
- return {
- pactSigningProgress: {
- ...state.pactSigningProgress,
- progress,
- },
- };
-}
diff --git a/src/lib/game/store/prestigeSlice.ts b/src/lib/game/store/prestigeSlice.ts
deleted file mode 100755
index 0eeea14..0000000
--- a/src/lib/game/store/prestigeSlice.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-// ─── Prestige Slice ───────────────────────────────────────────────────────────
-// Manages insight, prestige upgrades, and loop resources
-
-import type { StateCreator } from 'zustand';
-import type { GameState } from '../types';
-import { PRESTIGE_DEF } from '../constants';
-
-export interface PrestigeSlice {
- // State
- insight: number;
- totalInsight: number;
- prestigeUpgrades: Record;
- loopInsight: number;
- memorySlots: number;
- memories: string[];
-
- // Actions
- doPrestige: (id: string) => void;
- startNewLoop: () => void;
-}
-
-export const createPrestigeSlice = (
- set: StateCreator['set'],
- get: () => GameState
-): PrestigeSlice => ({
- insight: 0,
- totalInsight: 0,
- prestigeUpgrades: {},
- loopInsight: 0,
- memorySlots: 3,
- memories: [],
-
- doPrestige: (id: string) => {
- const state = get();
- const pd = PRESTIGE_DEF[id];
- if (!pd) return;
-
- const lvl = state.prestigeUpgrades[id] || 0;
- if (lvl >= pd.max || state.insight < pd.cost) return;
-
- const newPU = { ...state.prestigeUpgrades, [id]: lvl + 1 };
-
- set({
- insight: state.insight - pd.cost,
- prestigeUpgrades: newPU,
- memorySlots: id === 'deepMemory' ? state.memorySlots + 1 : state.memorySlots,
- maxPacts: id === 'pactCapacity' ? state.maxPacts + 1 : state.maxPacts,
- pactInterferenceMitigation: id === 'pactInterference' ? (state.pactInterferenceMitigation || 0) + 1 : state.pactInterferenceMitigation,
- log: [`⭐ ${pd.name} upgraded to Lv.${lvl + 1}!`, ...state.log.slice(0, 49)],
- });
- },
-
- startNewLoop: () => {
- const state = get();
- const insightGained = state.loopInsight || calcInsight(state);
- const total = state.insight + insightGained;
-
- // Reset to initial state with insight carried over
- const pu = state.prestigeUpgrades;
- const startFloor = 1 + (pu.spireKey || 0) * 2;
- const startRawMana = 10 + (pu.manaWell || 0) * 500 + (pu.quickStart || 0) * 100;
-
- // Reset elements
- const elements: Record = {};
- Object.keys(ELEMENTS).forEach((k) => {
- elements[k] = {
- current: 0,
- max: 10 + (pu.elementalAttune || 0) * 25,
- unlocked: false,
- };
- });
-
- // Reset spells - always start with Mana Bolt
- const spells: Record = {
- manaBolt: { learned: true, level: 1, studyProgress: 0 },
- };
-
- // Add random starting spells from spell memory prestige upgrade (purchased with insight)
- if (pu.spellMemory) {
- const availableSpells = Object.keys(SPELLS_DEF).filter(s => s !== 'manaBolt');
- const shuffled = availableSpells.sort(() => Math.random() - 0.5);
- for (let i = 0; i < Math.min(pu.spellMemory, shuffled.length); i++) {
- spells[shuffled[i]] = { learned: true, level: 1, studyProgress: 0 };
- }
- }
-
- set({
- day: 1,
- hour: 0,
- gameOver: false,
- victory: false,
- loopCount: state.loopCount + 1,
- rawMana: startRawMana,
- totalManaGathered: 0,
- meditateTicks: 0,
- elements,
- currentFloor: startFloor,
- floorHP: getFloorMaxHP(startFloor),
- floorMaxHP: getFloorMaxHP(startFloor),
- maxFloorReached: startFloor,
- signedPacts: [],
- pendingPactOffer: null,
- pactSigningProgress: null,
- signedPactDetails: {},
- activeSpell: 'manaBolt',
- currentAction: 'meditate',
- castProgress: 0,
- spells,
- skills: {},
- skillProgress: {},
- skillUpgrades: {},
- skillTiers: {},
- currentStudyTarget: null,
- parallelStudyTarget: null,
- insight: total,
- totalInsight: (state.totalInsight || 0) + insightGained,
- loopInsight: 0,
- maxPacts: 1 + (pu.pactCapacity || 0),
- pactInterferenceMitigation: pu.pactInterference || 0,
- memorySlots: 3 + (pu.deepMemory || 0),
- log: ['✨ A new loop begins. Your insight grows...', '✨ The loop begins. You start with Mana Bolt.'],
- });
- },
-});
-
-// Need to import these
-import { ELEMENTS, SPELLS_DEF } from '../constants';
-import { getFloorMaxHP, calcInsight } from './computed';
diff --git a/src/lib/game/store/timeSlice.ts b/src/lib/game/store/timeSlice.ts
deleted file mode 100755
index 074a176..0000000
--- a/src/lib/game/store/timeSlice.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-// ─── Time Slice ───────────────────────────────────────────────────────────────
-// Manages game time, loops, and game state
-
-import type { StateCreator } from 'zustand';
-import type { GameState } from '../types';
-import { MAX_DAY } from '../constants';
-import { calcInsight } from './computed';
-
-export interface TimeSlice {
- // State
- day: number;
- hour: number;
- loopCount: number;
- gameOver: boolean;
- victory: boolean;
- paused: boolean;
- incursionStrength: number;
- loopInsight: number;
- log: string[];
-
- // Actions
- togglePause: () => void;
- resetGame: () => void;
- startNewLoop: () => void;
- addLog: (message: string) => void;
-}
-
-export const createTimeSlice = (
- set: StateCreator['set'],
- get: () => GameState,
- initialOverrides?: Partial
-): TimeSlice => ({
- day: 1,
- hour: 0,
- loopCount: initialOverrides?.loopCount || 0,
- gameOver: false,
- victory: false,
- paused: false,
- incursionStrength: 0,
- loopInsight: 0,
- log: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'],
-
- togglePause: () => {
- set((state) => ({ paused: !state.paused }));
- },
-
- resetGame: () => {
- if (typeof window !== 'undefined') {
- localStorage.removeItem('mana-loop-storage');
- }
- // Reset to initial state
- window.location.reload();
- },
-
- startNewLoop: () => {
- const state = get();
- const insightGained = state.loopInsight || calcInsight(state);
- const total = state.insight + insightGained;
-
- // Spell preservation is handled through the prestige upgrade "spellMemory"
- // which is purchased with insight
-
- // This will be handled by the main store reset
- set({
- day: 1,
- hour: 0,
- gameOver: false,
- victory: false,
- loopCount: state.loopCount + 1,
- insight: total,
- totalInsight: (state.totalInsight || 0) + insightGained,
- loopInsight: 0,
- log: ['✨ A new loop begins. Your insight grows...'],
- });
- },
-
- addLog: (message: string) => {
- set((state) => ({
- log: [message, ...state.log.slice(0, 49)],
- }));
- },
-});
diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts
index cc9daa2..c4775f4 100755
--- a/src/lib/game/stores/gameStore.ts
+++ b/src/lib/game/stores/gameStore.ts
@@ -5,7 +5,7 @@
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 { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
import {
computeMaxMana,
computeRegen,
diff --git a/src/lib/game/stores/index.ts b/src/lib/game/stores/index.ts
index d1610ce..bb8b692 100755
--- a/src/lib/game/stores/index.ts
+++ b/src/lib/game/stores/index.ts
@@ -43,5 +43,4 @@ export {
deductSpellCost,
} from '../utils';
-export { computeElementMax } from '../store-modules/computed-stats';
export { getStudySpeedMultiplier, getStudyCostMultiplier } from '../constants';
diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts
index 54194bf..8a3d092 100755
--- a/src/lib/game/types.ts
+++ b/src/lib/game/types.ts
@@ -25,18 +25,6 @@ export type {
SpellState,
} from './types/spells';
-export type {
- SkillDef,
- SkillUpgradeDef,
- SkillUpgradeEffect,
- SkillEvolutionPath,
- SkillTierDef,
- SkillPerkChoice,
- SkillUpgradeChoice,
- PrestigeDef,
- SkillCost,
-} from './types/skills';
-
export type {
EquipmentDef,
EquipmentInstance,
diff --git a/src/lib/game/upgrade-effects.ts b/src/lib/game/upgrade-effects.ts
deleted file mode 100755
index d9d39fd..0000000
--- a/src/lib/game/upgrade-effects.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-// ─── Upgrade Effect System ────────────────────────────────────────────────────────
-// This module handles applying skill upgrade effects to game stats
-
-import type { SkillUpgradeChoice, SkillUpgradeEffect } from './types';
-import { getUpgradesForSkillAtMilestone, SKILL_EVOLUTION_PATHS } from './skill-evolution';
-import type { ActiveUpgradeEffect, ComputedEffects } from './upgrade-effects.types';
-import { SPECIAL_EFFECTS, hasSpecial } from './special-effects';
-import { computeDynamicRegen, computeDynamicClickMana, computeDynamicDamage } from './dynamic-compute';
-
-// ─── Upgrade Definition Cache ───────────────────────────
-
-// Cache all upgrades by ID for quick lookup
-const upgradeDefinitionsById: Map = new Map();
-
-// Build the cache on first access
-function buildUpgradeCache(): void {
- if (upgradeDefinitionsById.size > 0) return;
-
- for (const [baseSkillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
- for (const tierDef of path.tiers) {
- for (const upgrade of tierDef.upgrades) {
- upgradeDefinitionsById.set(upgrade.id, upgrade);
- }
- }
- }
-}
-
-// ─── Helper Functions ──────────────────────────────
-
-/**
- * Get all selected upgrades with their full effect definitions
- */
-export function getActiveUpgrades(
- skillUpgrades: Record,
- skillTiers: Record
-): ActiveUpgradeEffect[] {
- buildUpgradeCache();
- const result: ActiveUpgradeEffect[] = [];
-
- for (const [skillId, upgradeIds] of Object.entries(skillUpgrades)) {
- for (const upgradeId of upgradeIds) {
- const upgradeDef = upgradeDefinitionsById.get(upgradeId);
- if (upgradeDef) {
- result.push({
- upgradeId,
- skillId,
- milestone: upgradeDef.milestone,
- effect: upgradeDef.effect,
- name: upgradeDef.name,
- desc: upgradeDef.desc,
- });
- }
- }
- }
-
- return result;
-}
-
-/**
- * Compute all active effects from selected upgrades
- */
-export function computeEffects(
- skillUpgrades: Record,
- skillTiers: Record
-): ComputedEffects {
- const activeUpgrades = getActiveUpgrades(skillUpgrades, skillTiers);
-
- // Start with base values
- const 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,
- perElementCapBonus: {},
- conversionCostMultiplier: 1,
- doubleCraftChance: 0,
- permanentRegenBonus: 0,
- specials: new Set(),
- activeUpgrades,
- skillLevelMultiplier: 1,
- enchantmentPowerMultiplier: 1,
- };
-
- // Apply DEEP_UNDERSTANDING: +10% bonus from all skill levels
- if (hasSpecial(effects, SPECIAL_EFFECTS.DEEP_UNDERSTANDING)) {
- effects.skillLevelMultiplier = 1.10;
- }
-
- // Apply each upgrade effect
- for (const upgrade of activeUpgrades) {
- const { effect } = upgrade;
-
- if (effect.type === 'multiplier' && effect.stat && effect.value !== undefined) {
- // Multiplier effects (multiply the stat)
- switch (effect.stat) {
- case 'maxMana':
- effects.maxManaMultiplier *= effect.value;
- break;
- case 'regen':
- effects.regenMultiplier *= effect.value;
- break;
- case 'clickMana':
- effects.clickManaMultiplier *= effect.value;
- break;
- case 'meditationEfficiency':
- effects.meditationEfficiency *= effect.value;
- break;
- case 'spellCost':
- effects.spellCostMultiplier *= effect.value;
- break;
- case 'conversionEfficiency':
- effects.conversionEfficiency *= effect.value;
- break;
- case 'baseDamage':
- effects.baseDamageMultiplier *= effect.value;
- break;
- case 'attackSpeed':
- effects.attackSpeedMultiplier *= effect.value;
- break;
- case 'elementalDamage':
- effects.elementalDamageMultiplier *= effect.value;
- break;
- case 'studySpeed':
- effects.studySpeedMultiplier *= effect.value;
- break;
- case 'elementCap':
- effects.elementCapMultiplier *= effect.value;
- break;
- case 'conversionCost':
- effects.conversionCostMultiplier *= effect.value;
- break;
- case 'costReduction':
- effects.studyCostMultiplier /= effect.value;
- break;
- case 'enchantPower':
- effects.enchantmentPowerMultiplier *= effect.value;
- break;
- }
- } else if (effect.type === 'bonus' && effect.stat && effect.value !== undefined) {
- // Bonus effects (add to the stat)
- switch (effect.stat) {
- case 'maxMana':
- effects.maxManaBonus += effect.value;
- break;
- case 'regen':
- effects.regenBonus += effect.value;
- break;
- case 'clickMana':
- effects.clickManaBonus += effect.value;
- break;
- case 'baseDamage':
- effects.baseDamageBonus += effect.value;
- break;
- case 'elementCap':
- effects.elementCapBonus += effect.value;
- break;
- case 'permanentRegen':
- effects.permanentRegenBonus += effect.value;
- break;
- }
- } else if (effect.type === 'special' && effect.specialId) {
- effects.specials.add(effect.specialId);
- }
- }
-
- // MANA_THRESHOLD: +30% max mana, -10% regen trade-off
- if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_THRESHOLD)) {
- effects.maxManaMultiplier *= 1.30;
- effects.regenMultiplier *= 0.90;
- }
-
- return effects;
-}
diff --git a/src/lib/game/utils/mana-utils.ts b/src/lib/game/utils/mana-utils.ts
index 0abc383..46abc88 100644
--- a/src/lib/game/utils/mana-utils.ts
+++ b/src/lib/game/utils/mana-utils.ts
@@ -1,7 +1,7 @@
// ─── Mana & Regen Utilities ──────────────────────────────────────────────────
import type { GameState } from '../types';
-import type { ComputedEffects } from '../upgrade-effects.types';
+import type { ComputedEffects } from '../effects/upgrade-effects.types';
import { HOURS_PER_TICK } from '../constants';
import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements';
diff --git a/src/lib/game/utils/pact-utils.ts b/src/lib/game/utils/pact-utils.ts
new file mode 100644
index 0000000..ad9542c
--- /dev/null
+++ b/src/lib/game/utils/pact-utils.ts
@@ -0,0 +1,67 @@
+// ─── Pact Utility Functions ───────────────────────────────────────────────────
+
+import { GUARDIANS } from '../constants';
+
+export function computePactMultiplier(state: {
+ signedPacts: number[];
+ pactInterferenceMitigation?: number;
+}): number {
+ const { signedPacts, pactInterferenceMitigation = 0 } = state;
+
+ if (signedPacts.length === 0) return 1.0;
+
+ let baseMult = 1.0;
+ for (const floor of signedPacts) {
+ const guardian = GUARDIANS[floor];
+ if (guardian) {
+ baseMult *= guardian.damageMultiplier;
+ }
+ }
+
+ if (signedPacts.length === 1) return baseMult;
+
+ const numAdditionalPacts = signedPacts.length - 1;
+ const basePenalty = 0.5 * numAdditionalPacts;
+ const mitigationReduction = Math.min(pactInterferenceMitigation, 5) * 0.1;
+ const effectivePenalty = Math.max(0, basePenalty - mitigationReduction);
+
+ if (pactInterferenceMitigation >= 5) {
+ const synergyBonus = (pactInterferenceMitigation - 5) * 0.1;
+ return baseMult * (1 + synergyBonus);
+ }
+
+ return baseMult * (1 - effectivePenalty);
+}
+
+export function computePactInsightMultiplier(state: {
+ signedPacts: number[];
+ pactInterferenceMitigation?: number;
+}): number {
+ const { signedPacts, pactInterferenceMitigation = 0 } = state;
+
+ if (signedPacts.length === 0) return 1.0;
+
+ let mult = 1.0;
+ for (const floor of signedPacts) {
+ const guardian = GUARDIANS[floor];
+ if (guardian) {
+ mult *= guardian.insightMultiplier;
+ }
+ }
+
+ if (signedPacts.length > 1) {
+ const numAdditionalPacts = signedPacts.length - 1;
+ const basePenalty = 0.5 * numAdditionalPacts;
+ const mitigationReduction = Math.min(pactInterferenceMitigation, 5) * 0.1;
+ const effectivePenalty = Math.max(0, basePenalty - mitigationReduction);
+
+ if (pactInterferenceMitigation >= 5) {
+ const synergyBonus = (pactInterferenceMitigation - 5) * 0.1;
+ return mult * (1 + synergyBonus);
+ }
+
+ return mult * (1 - effectivePenalty);
+ }
+
+ return mult;
+}