refactor: split bloated state types into State + Actions interfaces (issue #102)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s

- CombatState: split into CombatState (data) + CombatActions + CombatStore
- PrestigeState: split into PrestigeState (data) + PrestigeActions + PrestigeStore
- ManaState: split into ManaState (data) + ManaActions + ManaStore
- GameState: deprecated, removed from barrel exports
- crafting-actions: updated to use CraftingState instead of GameState
- combat-utils/mana-utils: replaced Pick<GameState,...> with focused interfaces
- DisciplineCardProps: split into Definition + Runtime + Callbacks
- stores/index.ts: now exports both State and Actions types
This commit is contained in:
2026-05-20 21:05:22 +02:00
parent ee893e8973
commit 8a7ddaae27
24 changed files with 411 additions and 321 deletions
+63 -39
View File
@@ -1,34 +1,59 @@
// ─── Combat Utilities ────────────────────────────────────────────────────────
import type { GameState, SpellCost, EquipmentInstance } from '../types';
import type { SpellCost, EquipmentInstance } from '../types';
import type { DisciplineBonuses } from './mana-utils';
import { GUARDIANS, SPELLS_DEF, ELEMENT_OPPOSITES, INCURSION_START_DAY, MAX_DAY } from '../constants';
import { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects';
// ─── Damage Calculation Params ──────────────────────────────────────────────
export interface DamageCalcParams {
skills: Record<string, number>;
signedPacts: number[];
}
// ─── Insight Calculation Params ─────────────────────────────────────────────
export interface InsightCalcParams {
maxFloorReached: number;
totalManaGathered: number;
signedPacts: number[];
prestigeUpgrades: Record<string, number>;
skills: Record<string, number>;
}
// ─── DPS Calculation Params ─────────────────────────────────────────────────
export interface DPSCalcParams {
skills: Record<string, number>;
signedPacts: number[];
equippedInstances: Record<string, string | null>;
equipmentInstances: Record<string, EquipmentInstance>;
spells: Record<string, { learned: boolean; level: number }>;
prestigeUpgrades: Record<string, number>;
}
// ─── Elemental Damage Bonus ──────────────────────────────────────────────────
// Elemental damage bonus: +50% if spell element opposes floor element (super effective)
// -25% if spell element matches its own opposite (weak)
export function getElementalBonus(spellElem: string, floorElem: string): number {
if (spellElem === 'raw') return 1.0; // Raw mana has no elemental bonus
if (spellElem === floorElem) return 1.25; // Same element: +25% damage
// Check for super effective first: spell is the opposite of floor
// e.g., casting water (opposite of fire) at fire floor = super effective
if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; // Super effective: +50% damage
// Check for weak: spell's opposite matches floor
// e.g., casting fire (whose opposite is water) at water floor = weak
if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; // Weak: -25% damage
return 1.0; // Neutral
}
// ─── Boon Bonuses ─────────────────────────────────────────────────────────────
// Helper to calculate total boon bonuses from signed pacts
export function getBoonBonuses(signedPacts: number[]): {
export interface BoonBonuses {
maxMana: number;
manaRegen: number;
castingSpeed: number;
@@ -41,8 +66,11 @@ export function getBoonBonuses(signedPacts: number[]): {
insightGain: number;
studySpeed: number;
prestigeInsight: number;
} {
const bonuses = {
}
// Helper to calculate total boon bonuses from signed pacts
export function getBoonBonuses(signedPacts: number[]): BoonBonuses {
const bonuses: BoonBonuses = {
maxMana: 0,
manaRegen: 0,
castingSpeed: 0,
@@ -56,11 +84,11 @@ export function getBoonBonuses(signedPacts: number[]): {
studySpeed: 0,
prestigeInsight: 0,
};
for (const floor of signedPacts) {
const guardian = GUARDIANS[floor];
if (!guardian) continue;
for (const boon of guardian.boons) {
switch (boon.type) {
case 'maxMana':
@@ -102,14 +130,14 @@ export function getBoonBonuses(signedPacts: number[]): {
}
}
}
return bonuses;
}
// ─── Damage Calculation ───────────────────────────────────────────────────────
export function calcDamage(
state: Pick<GameState, 'skills' | 'signedPacts'>,
state: DamageCalcParams,
spellId: string,
floorElem?: string,
discipline?: DisciplineBonuses,
@@ -118,18 +146,18 @@ export function calcDamage(
if (!sp) return 5;
const skills = state.skills;
// Base damage: spell base + skill bonus + discipline bonus (spell-casting → baseDamageBonus)
// Base damage: spell base + skill bonus + discipline bonus
const discBaseDmg = discipline?.bonuses?.baseDamageBonus || 0;
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5 + discBaseDmg;
// Percentage multiplier: skill arcaneFury + discipline void-manipulation (baseDamageMultiplier)
// Percentage multiplier
const discDmgMult = discipline?.bonuses?.baseDamageMultiplier || 0;
const pct = 1 + (skills.arcaneFury || 0) * 0.1 + discDmgMult;
// Elemental mastery bonus
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
// Guardian bane bonus - check if current floor has a guardian with matching element
// Guardian bane bonus
const isGuardianFloor = floorElem && Object.values(GUARDIANS).some(g => g.element === floorElem);
const guardianBonus = isGuardianFloor
? 1 + (skills.guardianBane || 0) * 0.2
@@ -163,20 +191,20 @@ export function calcDamage(
// ─── Insight Calculation ──────────────────────────────────────────────────────
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>, discipline?: DisciplineBonuses): number {
export function calcInsight(state: InsightCalcParams, discipline?: DisciplineBonuses): number {
const pu = state.prestigeUpgrades;
const discInsightBonus = discipline?.bonuses?.insightGainBonus || 0;
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1 + discInsightBonus;
// Get boon bonuses for insight gain
const boons = getBoonBonuses(state.signedPacts);
const boonInsightMult = 1 + boons.insightGain / 100;
const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus * boonInsightMult;
// Add prestigeInsight bonus per loop
const prestigeInsightBonus = boons.prestigeInsight;
return Math.floor(
(state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150 + prestigeInsightBonus) * mult
);
@@ -195,8 +223,8 @@ export function getIncursionStrength(day: number, hour: number): number {
// Check if player can afford spell cost
export function canAffordSpellCost(
cost: SpellCost,
rawMana: number,
cost: SpellCost,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>
): boolean {
if (cost.type === 'raw') {
@@ -214,14 +242,12 @@ export function deductSpellCost(
elements: Record<string, { current: number; max: number; unlocked: boolean }>
): { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> } {
const newElements = { ...elements };
if (cost.type === 'raw') {
// Don't allow rawMana to go below zero
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];
// Don't allow elemental mana to go below zero
const deductedAmount = Math.min(elem.current, cost.amount);
newElements[cost.element] = {
...elem,
@@ -229,7 +255,7 @@ export function deductSpellCost(
};
return { rawMana, elements: newElements };
}
return { rawMana, elements: newElements };
}
@@ -248,15 +274,14 @@ export function getActiveEquipmentSpells(
): ActiveEquipmentSpell[] {
const equippedIds = Object.values(equippedInstances || {}).filter((id): id is string => id !== null);
const spells: ActiveEquipmentSpell[] = [];
for (const id of equippedIds) {
const instance = equipmentInstances[id];
if (!instance) continue;
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
// Check if we already have this spell from this equipment
const exists = spells.some(s => s.spellId === effectDef.effect.spellId && s.equipmentId === id);
if (!exists) {
spells.push({ spellId: effectDef.effect.spellId, equipmentId: id });
@@ -264,7 +289,7 @@ export function getActiveEquipmentSpells(
}
}
}
return spells;
}
@@ -272,7 +297,7 @@ export function getActiveEquipmentSpells(
// Compute total DPS from all sources (spells, equipment, etc.)
export function getTotalDPS(
state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances' | 'spells' | 'prestigeUpgrades'>,
state: DPSCalcParams,
upgradeEffects: { spellDamageBonus?: number; attackSpeedMultiplier?: number; [key: string]: unknown },
floorElem?: string,
discipline?: DisciplineBonuses,
@@ -289,23 +314,22 @@ export function getTotalDPS(
// Calculate damage per cast
const damage = calcDamage(state, spellId, floorElem, discipline);
// Get cast speed (spells per second)
// Base cast time is 1 second, modified by casting speed bonuses
const baseCastTime = spellDef.baseCastTime || 1.0;
const castingSpeedBonus = 1 + (state.skills.castingSpeed || 0) * 0.1;
const equipmentAttackSpeed = upgradeEffects.attackSpeedMultiplier || 1;
const castTime = baseCastTime / (castingSpeedBonus * equipmentAttackSpeed);
// DPS for this spell
const spellDPS = damage / castTime;
totalDPS += spellDPS;
}
// Add equipment DPS bonuses from upgrade effects
if (upgradeEffects.spellDamageBonus) {
totalDPS *= (1 + upgradeEffects.spellDamageBonus / 100);
}
return totalDPS;
}