fix: resolve priority 4 issues — discipline mutation, skill→discipline migration, uiStore persistence, game loop interval, toast listener leak, page re-renders
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s

- Issue 70: Fix discipline-slice.ts nested element mutation (use immutable spread)
- Issue 71: Add persist middleware to uiStore for paused/gameOver/victory
- Issue 72: Wire discipline effects into calcDamage (spell-casting, void-manipulation)
- Issue 73: Fix useGameLoop interval recreation (use getState() + empty deps)
- Issue 74: Fix use-toast.ts listener leak (change [state] dep to [])
- Issue 75: Reduce page.tsx re-renders with useShallow for multi-field subscriptions
- Issue 76: Fix createGatherMana hardcoded click mana (use computeClickMana with discipline effects)
- Issue 77: Pass discipline effects to computeMaxMana/computeRegen/calcInsight in tick()
- Export DisciplineBonuses type and useDisciplineStore from barrel exports
- Update tests to match new function signatures
This commit is contained in:
2026-05-19 13:53:33 +02:00
parent ebcaab62bf
commit 50a9a62060
17 changed files with 215 additions and 154 deletions
+31 -21
View File
@@ -1,6 +1,7 @@
// ─── Combat Utilities ────────────────────────────────────────────────────────
import type { GameState, 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';
@@ -108,56 +109,64 @@ export function getBoonBonuses(signedPacts: number[]): {
// ─── Damage Calculation ───────────────────────────────────────────────────────
export function calcDamage(
state: Pick<GameState, 'skills' | 'signedPacts'>,
spellId: string,
floorElem?: string
state: Pick<GameState, 'skills' | 'signedPacts'>,
spellId: string,
floorElem?: string,
discipline?: DisciplineBonuses,
): number {
const sp = SPELLS_DEF[spellId];
if (!sp) return 5;
const skills = state.skills;
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5;
const pct = 1 + (skills.arcaneFury || 0) * 0.1;
// Base damage: spell base + skill bonus + discipline bonus (spell-casting → baseDamageBonus)
const discBaseDmg = discipline?.bonuses?.baseDamageBonus || 0;
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5 + discBaseDmg;
// Percentage multiplier: skill arcaneFury + discipline void-manipulation (baseDamageMultiplier)
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
const isGuardianFloor = floorElem && Object.values(GUARDIANS).some(g => g.element === floorElem);
const guardianBonus = isGuardianFloor
? 1 + (skills.guardianBane || 0) * 0.2
: 1;
// Get boon bonuses from pacts
const boons = getBoonBonuses(state.signedPacts);
// Apply raw damage and elemental damage bonuses
const rawDamageMult = 1 + boons.rawDamage / 100;
const elemDamageMult = 1 + boons.elementalDamage / 100;
// Apply crit chance and damage from boons
const critChance = (skills.precision || 0) * 0.05 + boons.critChance / 100;
const critDamageMult = 1.5 + boons.critDamage / 100;
let damage = baseDmg * pct * elemMasteryBonus * guardianBonus * rawDamageMult * elemDamageMult;
// Apply elemental bonus if floor element provided
if (floorElem) {
damage *= getElementalBonus(sp.elem, floorElem);
}
// Apply crit
if (Math.random() < critChance) {
damage *= critDamageMult;
}
return damage;
}
// ─── Insight Calculation ──────────────────────────────────────────────────────
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number {
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>, discipline?: DisciplineBonuses): number {
const pu = state.prestigeUpgrades;
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1;
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);
@@ -265,20 +274,21 @@ export function getActiveEquipmentSpells(
export function getTotalDPS(
state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances' | 'spells' | 'prestigeUpgrades'>,
upgradeEffects: { spellDamageBonus?: number; attackSpeedMultiplier?: number; [key: string]: unknown },
floorElem?: string
floorElem?: string,
discipline?: DisciplineBonuses,
): number {
let totalDPS = 0;
// Get active equipment spells
const activeSpells = getActiveEquipmentSpells(state.equippedInstances, state.equipmentInstances);
// Calculate DPS for each active spell
for (const { spellId } of activeSpells) {
const spellDef = SPELLS_DEF[spellId];
if (!spellDef) continue;
// Calculate damage per cast
const damage = calcDamage(state, spellId, floorElem);
const damage = calcDamage(state, spellId, floorElem, discipline);
// Get cast speed (spells per second)
// Base cast time is 1 second, modified by casting speed bonuses
+2 -1
View File
@@ -9,7 +9,8 @@ export {
computeEffectiveRegen,
computeEffectiveRegenForDisplay,
computeClickMana,
getMeditationBonus
getMeditationBonus,
type DisciplineBonuses,
} from './mana-utils';
// computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades
export {
+50 -32
View File
@@ -5,16 +5,23 @@ import type { ComputedEffects } from '../effects/upgrade-effects.types';
import { HOURS_PER_TICK } from '../constants';
import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements';
export interface DisciplineBonuses {
bonuses: Record<string, number>;
multipliers: Record<string, number>;
}
export function computeMaxMana(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects
effects?: ComputedEffects,
discipline?: DisciplineBonuses,
): number {
const pu = state.prestigeUpgrades;
const base =
const base =
100 +
((state.skills || {}).manaWell || 0) * 100 +
((pu || {}).manaWell || 0) * 500;
((pu || {}).manaWell || 0) * 500 +
(discipline?.bonuses?.maxManaBonus || 0);
// Apply upgrade effects if provided
if (effects) {
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
@@ -28,7 +35,8 @@ export function computeMaxMana(
export function computeRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
effects?: ComputedEffects
effects?: ComputedEffects,
discipline?: DisciplineBonuses,
): number {
const pu = state.prestigeUpgrades;
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
@@ -37,35 +45,41 @@ export function computeRegen(
((state.skills || {}).manaFlow || 0) * 1 +
((state.skills || {}).manaSpring || 0) * 2 +
((pu || {}).manaFlow || 0) * 0.5;
let regen = base * temporalBonus;
// Add attunement raw mana regen
const attunementRegen = getTotalAttunementRegen(state.attunements || {});
regen += attunementRegen;
// Apply discipline regen bonus
if (discipline?.bonuses?.regenBonus) {
regen += discipline.bonuses.regenBonus;
}
// Apply upgrade effects if provided
if (effects) {
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
}
return regen;
}
// Compute the effective regen (raw regen minus conversion drains) for display purposes
export function computeEffectiveRegenForDisplay(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
effects?: ComputedEffects
effects?: ComputedEffects,
discipline?: DisciplineBonuses,
): { rawRegen: number; conversionDrain: number; effectiveRegen: number } {
// Get the full raw regen (without conversion drain)
const rawRegen = computeRegen(state, effects);
const rawRegen = computeRegen(state, effects, discipline);
// Calculate conversion drain
const conversionDrain = getTotalAttunementConversionDrain(state.attunements || {});
// Effective regen is what actually increases raw mana
const effectiveRegen = Math.max(0, rawRegen - conversionDrain);
return { rawRegen, conversionDrain, effectiveRegen };
}
@@ -74,25 +88,29 @@ export function computeEffectiveRegenForDisplay(
*/
export function computeEffectiveRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
effects?: ComputedEffects
effects?: ComputedEffects,
discipline?: DisciplineBonuses,
): number {
// Base regen from existing function
let regen = computeRegen(state, effects);
let regen = computeRegen(state, effects, discipline);
const incursionStrength = state.incursionStrength || 0;
// Apply incursion penalty
regen *= (1 - incursionStrength);
return regen;
}
export function computeClickMana(state: Pick<GameState, 'skills'>): number {
return (
1 +
((state.skills || {}).manaTap || 0) * 1 +
((state.skills || {}).manaSurge || 0) * 3
);
export function computeClickMana(
state: Pick<GameState, 'skills'>,
discipline?: DisciplineBonuses,
): number {
const skillTap = ((state.skills || {}).manaTap || 0) * 1;
const skillSurge = ((state.skills || {}).manaSurge || 0) * 3;
const discClickMult = discipline?.bonuses?.clickManaMultiplier || 0;
return 1 + skillTap + skillSurge + discClickMult;
}
// Meditation bonus now affects regen rate directly
@@ -100,29 +118,29 @@ export function getMeditationBonus(meditateTicks: number, skills: Record<string,
const hasMeditation = skills.meditation === 1;
const hasDeepTrance = skills.deepTrance === 1;
const hasVoidMeditation = skills.voidMeditation === 1;
const hours = meditateTicks * HOURS_PER_TICK;
// Base meditation: ramps up over 4 hours to 1.5x
let bonus = 1 + Math.min(hours / 4, 0.5);
// With Meditation Focus: up to 2.5x after 4 hours
if (hasMeditation && hours >= 4) {
bonus = 2.5;
}
// With Deep Trance: up to 3.0x after 6 hours
if (hasDeepTrance && hours >= 6) {
bonus = 3.0;
}
// With Void Meditation: up to 5.0x after 8 hours
if (hasVoidMeditation && hours >= 8) {
bonus = 5.0;
}
// Apply meditation efficiency from upgrades (Deep Wellspring, etc.)
bonus *= meditationEfficiency;
return bonus;
}