All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m46s
- 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
158 lines
5.1 KiB
TypeScript
Executable File
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,
|
|
};
|
|
},
|
|
});
|