Files
Mana-Loop/src/lib/game/store/combatSlice.ts
Z User a1f19e705b
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m46s
Remove impossible mechanics and fix game balance
- Remove lifesteal from spells (player has no health to heal)
- Remove execute effects (too powerful instant-kill mechanic)
- Remove freeze status effect (doesn't fit game design)
- Remove knowledgeRetention skill (study progress is now always saved)
- Fix soulBinding skill (now binds guardian essence to equipment)
- Buff ancientEcho skill (+1 capacity per level instead of per 2 levels)
- Rename lifesteal_5 to mana_siphon_5 in enchantment effects
- Update guardian perks:
  - Water: 10% double cast chance instead of lifesteal
  - Dark: 25% crit chance instead of lifesteal
  - Life: 30% damage restored as mana instead of healing
  - Death: +50% damage to enemies below 50% HP instead of execute
- Add floor armor system (flat damage reduction)
- Update spell effects display in UI
- Fix study cancellation - progress is always saved when pausing
2026-03-28 13:41:10 +00:00

158 lines
5.1 KiB
TypeScript
Executable File

// ─── 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, hasSpecial, SPECIAL_EFFECTS } from '../upgrade-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<GameState>;
}
export const createCombatSlice = (
set: StateCreator<GameState>['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;
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;
// Executioner: +100% damage to enemies below 25% HP
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) {
dmg *= 2;
}
// 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;
}
// 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];
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;
}
}
return {
rawMana,
elements,
totalManaGathered,
currentFloor,
floorHP,
floorMaxHP,
maxFloorReached,
signedPacts,
pendingPactOffer,
castProgress,
log,
};
},
});