fix: issues #221 #217 #225 #227 #224 #226 - crafting refunds, mana tracking, cancel slot, multi-element guardians, spell kill advance
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m59s

This commit is contained in:
2026-05-31 01:18:01 +02:00
parent e4f4b297e8
commit 6793461a9f
16 changed files with 263 additions and 63 deletions
+34 -9
View File
@@ -6,7 +6,7 @@ import { SPELLS_DEF, HOURS_PER_TICK } from '../constants';
import { getGuardianForFloor } from '../data/guardian-encounters';
import type { CombatStore, CombatState } from './combat-state.types';
import type { SpellState } from '../types';
import { getFloorMaxHP, getFloorElement, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
import { getFloorMaxHP, getFloorElement, getMultiElementBonus, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
import { computeDisciplineEffects } from '../effects/discipline-effects';
/**
@@ -95,14 +95,21 @@ export function processCombatTick(
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
rawMana = afterCost.rawMana;
elements = afterCost.elements;
// Calculate base damage
// Calculate base damage (without elemental bonus first)
const floorElement = getFloorElement(currentFloor);
const damage = calcDamage(
const baseDamage = calcDamage(
{ signedPacts },
spellId,
floorElement,
undefined,
disciplineEffects,
);
// Apply elemental bonus — for multi-element guardians, use all elements
const guardian = getGuardianForFloor(currentFloor);
const floorElems = guardian && guardian.element.length > 0
? guardian.element
: [floorElement];
const multiElemBonus = getMultiElementBonus(spellDef.elem, floorElems);
const damage = baseDamage * multiElemBonus;
// Let gameStore apply damage modifiers (executioner, berserker)
const result = onDamageDealt(damage);
@@ -125,7 +132,6 @@ export function processCombatTick(
if (floorHP <= 0) {
const guardian = getGuardianForFloor(currentFloor);
onFloorCleared(currentFloor, !!guardian);
currentFloor = Math.min(currentFloor + 1, 100);
floorMaxHP = getFloorMaxHP(currentFloor);
floorHP = floorMaxHP;
@@ -159,14 +165,20 @@ export function processCombatTick(
const eAfterCost = deductSpellCost(eSpellDef.cost, rawMana, elements);
rawMana = eAfterCost.rawMana;
elements = eAfterCost.elements;
// Calculate damage
// Calculate damage — for multi-element guardians, use all elements
const eFloorElement = getFloorElement(currentFloor);
const eDamage = calcDamage(
const eBaseDamage = calcDamage(
{ signedPacts },
eSpell.spellId,
eFloorElement,
undefined,
disciplineEffects,
);
const eGuardian = getGuardianForFloor(currentFloor);
const eFloorElems = eGuardian && eGuardian.element.length > 0
? eGuardian.element
: [eFloorElement];
const eMultiElemBonus = getMultiElementBonus(eSpellDef.elem, eFloorElems);
const eDamage = eBaseDamage * eMultiElemBonus;
const eResult = onDamageDealt(eDamage);
rawMana = eResult.rawMana;
@@ -182,7 +194,20 @@ export function processCombatTick(
eCastProgress -= 1;
eSafetyCounter++;
if (floorHP <= 0) break; // Floor cleared, stop processing
if (floorHP <= 0) {
const eGuardian = getGuardianForFloor(currentFloor);
onFloorCleared(currentFloor, !!eGuardian);
currentFloor = Math.min(currentFloor + 1, 100);
floorMaxHP = getFloorMaxHP(currentFloor);
floorHP = floorMaxHP;
eCastProgress = 0;
if (eGuardian) {
logMessages.push(`\u2694\ufe0f ${eGuardian.name} defeated!`);
} else if (currentFloor % 5 === 0) {
logMessages.push(`🏰 Floor ${currentFloor - 1} cleared!`);
}
break;
}
}
// Update equipment spell state
+13 -4
View File
@@ -82,9 +82,18 @@ export const useCraftingStore = create<CraftingStore>()(
return true;
},
cancelDesign: () => {
cancelDesign: (slot?: 1 | 2) => {
const state = get();
if (state.designProgress) {
if (slot === 2) {
if (state.designProgress2) {
set({ designProgress2: null });
}
} else if (slot === 1) {
if (state.designProgress) {
set({ designProgress: null });
useCombatStore.setState({ currentAction: 'meditate' });
}
} else if (state.designProgress) {
set({ designProgress: null });
useCombatStore.setState({ currentAction: 'meditate' });
} else if (state.designProgress2) {
@@ -136,7 +145,7 @@ export const useCraftingStore = create<CraftingStore>()(
},
cancelApplication: () => {
ApplicationActions.cancelApplication(set as unknown as (partial: Partial<CraftingState>) => void);
ApplicationActions.cancelApplication(get, set as unknown as (partial: Partial<CraftingState>) => void);
useCombatStore.setState({ currentAction: 'meditate' });
},
@@ -169,7 +178,7 @@ export const useCraftingStore = create<CraftingStore>()(
},
cancelPreparation: () => {
PreparationActions.cancelPreparation(set);
PreparationActions.cancelPreparation(get, set);
useCombatStore.setState({ currentAction: 'meditate' });
},
+1 -1
View File
@@ -48,7 +48,7 @@ export interface CraftingActions {
setApplicationProgress: (progress: ApplicationProgress | null) => void;
setEquipmentCraftingProgress: (progress: EquipmentCraftingProgress | null) => void;
startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => boolean;
cancelDesign: () => void;
cancelDesign: (slot?: 1 | 2) => void;
saveDesign: (design: EnchantmentDesign) => void;
deleteDesign: (designId: string) => void;
startApplying: (equipmentInstanceId: string, designId: string) => boolean;
+5 -1
View File
@@ -182,8 +182,12 @@ export const useGameStore = create<GameCoordinatorStore>()(
const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
const rawAfterConversion = ctx.mana.rawMana + rawManaDelta;
const regenFromMeditation = Math.max(0, effectiveRegen * HOURS_PER_TICK);
const roomLeft = Math.max(0, maxMana - Math.max(0, rawAfterConversion));
// Only count regen that actually fits below the cap (fix #224)
const actualRegenAdded = Math.floor(Math.min(regenFromMeditation, roomLeft) * 1000) / 1000;
let rawMana = Math.max(0, Math.min(rawAfterConversion + effectiveRegen * HOURS_PER_TICK, maxMana));
let totalManaGathered = ctx.mana.totalManaGathered;
let totalManaGathered = ctx.mana.totalManaGathered + Math.max(0, actualRegenAdded);
if (ctx.combat.currentAction === 'convert') {
const convertResult = useManaStore.getState().processConvertAction(rawMana);
@@ -9,6 +9,7 @@ import {
deductFabricatorMana,
deductMaterials,
makeFabricatorProgress,
refundFabricatorMana,
} from '../../crafting-fabricator';
import { useManaStore } from '../manaStore';
import { useCombatStore } from '../combatStore';
@@ -50,33 +51,71 @@ export function startCraftingEquipment(
export function cancelEquipmentCrafting(get: GetFn, set: SetFn): void {
const progress = get().equipmentCraftingProgress;
if (!progress) return;
const cancelResult = CraftingEquipment.cancelEquipmentCrafting(
progress.blueprintId,
progress.manaSpent,
progress.progress,
progress.required,
);
// Refund materials proportionally to remaining progress
const recipe = CraftingEquipment.getRecipe(progress.blueprintId);
if (recipe) {
const remainingFraction = progress.required > 0
? Math.max(0, (progress.required - progress.progress) / progress.required)
: 1;
const currentMaterials = get().lootInventory.materials;
const refundedMaterials = { ...currentMaterials };
for (const [matId, amount] of Object.entries(recipe.materials)) {
const refundAmount = Math.floor(amount * remainingFraction);
if (refundAmount > 0) {
refundedMaterials[matId] = (refundedMaterials[matId] || 0) + refundAmount;
const isFabricator = progress.blueprintId.startsWith('fabricator-');
if (isFabricator) {
// Fabricator recipe cancel: refund elemental/raw mana and materials
const recipeId = progress.blueprintId.replace('fabricator-', '');
const recipe = getFabricatorRecipe(recipeId);
if (recipe) {
const remainingFraction = progress.required > 0
? Math.max(0, (progress.required - progress.progress) / progress.required)
: 1;
// Full refund for unspent progress, 50% for spent progress
const refundRate = remainingFraction + (1 - remainingFraction) * 0.5;
const manaRefund = Math.floor(progress.manaSpent * refundRate);
// Refund the correct mana type
const rawMana = useManaStore.getState().rawMana;
const elements = useManaStore.getState().elements;
const refunded = refundFabricatorMana(recipe, manaRefund, rawMana, elements);
useManaStore.setState({ rawMana: refunded.rawMana, elements: refunded.elements });
// Refund materials
const currentMaterials = get().lootInventory.materials;
const refundedMaterials = { ...currentMaterials };
for (const [matId, amount] of Object.entries(recipe.materials)) {
const refundAmount = Math.floor(amount * remainingFraction);
if (refundAmount > 0) {
refundedMaterials[matId] = (refundedMaterials[matId] || 0) + refundAmount;
}
}
set({ equipmentCraftingProgress: null, lootInventory: { ...get().lootInventory, materials: refundedMaterials } });
useUIStore.getState().addLog(`🚫 Fabricator crafting cancelled. Refunded ${manaRefund} ${recipe.manaType} mana.`);
} else {
set({ equipmentCraftingProgress: null });
}
set({ equipmentCraftingProgress: null, lootInventory: { ...get().lootInventory, materials: refundedMaterials } });
} else {
set({ equipmentCraftingProgress: null });
// Standard equipment crafting cancel
const cancelResult = CraftingEquipment.cancelEquipmentCrafting(
progress.blueprintId,
progress.manaSpent,
progress.progress,
progress.required,
);
// Refund materials proportionally to remaining progress
const recipe = CraftingEquipment.getRecipe(progress.blueprintId);
if (recipe) {
const remainingFraction = progress.required > 0
? Math.max(0, (progress.required - progress.progress) / progress.required)
: 1;
const currentMaterials = get().lootInventory.materials;
const refundedMaterials = { ...currentMaterials };
for (const [matId, amount] of Object.entries(recipe.materials)) {
const refundAmount = Math.floor(amount * remainingFraction);
if (refundAmount > 0) {
refundedMaterials[matId] = (refundedMaterials[matId] || 0) + refundAmount;
}
}
set({ equipmentCraftingProgress: null, lootInventory: { ...get().lootInventory, materials: refundedMaterials } });
} else {
set({ equipmentCraftingProgress: null });
}
useManaStore.setState((s) => ({ rawMana: s.rawMana + cancelResult.manaRefund }));
useUIStore.getState().addLog(cancelResult.logMessage);
}
useManaStore.setState((s) => ({ rawMana: s.rawMana + cancelResult.manaRefund }));
useCombatStore.setState({ currentAction: 'meditate' });
useUIStore.getState().addLog(cancelResult.logMessage);
}
export function startFabricatorCrafting(recipeId: string, get: GetFn, set: SetFn): boolean {