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
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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user