From 6793461a9fa31d2bcdce07ca2990d9f6f9555fe8 Mon Sep 17 00:00:00 2001 From: n8n-gitea Date: Sun, 31 May 2026 01:18:01 +0200 Subject: [PATCH] fix: issues #221 #217 #225 #227 #224 #226 - crafting refunds, mana tracking, cancel slot, multi-element guardians, spell kill advance --- docs/circular-deps.txt | 2 +- docs/dependency-graph.json | 2 +- src/app/components/LeftPanel.tsx | 2 + src/components/game/ActionButtons.tsx | 23 +++-- .../EquipmentTypeSelector.tsx | 2 +- .../game/__tests__/regression-fixes.test.ts | 71 ++++++++++++++-- .../game/__tests__/tick-integration.test.ts | 23 +++-- .../crafting-actions/application-actions.ts | 19 +++++ .../crafting-actions/preparation-actions.ts | 19 +++++ src/lib/game/stores/combat-actions.ts | 43 ++++++++-- src/lib/game/stores/craftingStore.ts | 17 +++- src/lib/game/stores/craftingStore.types.ts | 2 +- src/lib/game/stores/gameStore.ts | 6 +- .../stores/pipelines/equipment-crafting.ts | 83 ++++++++++++++----- src/lib/game/utils/combat-utils.ts | 11 +++ src/lib/game/utils/index.ts | 1 + 16 files changed, 263 insertions(+), 63 deletions(-) diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index ff8cdf3..bc44613 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,4 +1,4 @@ # Circular Dependencies -Generated: 2026-05-30T20:28:32.289Z +Generated: 2026-05-30T20:28:51.293Z No circular dependencies found. ✅ diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 8e202df..0963f54 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-05-30T20:28:30.386Z", + "generated": "2026-05-30T20:28:49.474Z", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." }, diff --git a/src/app/components/LeftPanel.tsx b/src/app/components/LeftPanel.tsx index dceff08..f8ab0a8 100644 --- a/src/app/components/LeftPanel.tsx +++ b/src/app/components/LeftPanel.tsx @@ -30,6 +30,7 @@ export function LeftPanel() { const currentAction = useCombatStore((s) => s.currentAction); const designProgress = useCraftingStore((s) => s.designProgress); const designProgress2 = useCraftingStore((s) => s.designProgress2); + const cancelDesign = useCraftingStore((s) => s.cancelDesign); const preparationProgress = useCraftingStore((s) => s.preparationProgress); const applicationProgress = useCraftingStore((s) => s.applicationProgress); const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress); @@ -101,6 +102,7 @@ export function LeftPanel() { preparationProgress={preparationProgress} applicationProgress={applicationProgress} equipmentCraftingProgress={equipmentCraftingProgress} + cancelDesign={cancelDesign} /> diff --git a/src/components/game/ActionButtons.tsx b/src/components/game/ActionButtons.tsx index 5fa49bc..2defaa2 100755 --- a/src/components/game/ActionButtons.tsx +++ b/src/components/game/ActionButtons.tsx @@ -12,7 +12,7 @@ interface ActionButtonsProps { preparationProgress: { progress: number; required: number } | null; applicationProgress: { progress: number; required: number } | null; equipmentCraftingProgress: { progress: number; required: number } | null; -} + cancelDesign?: (slot: 1 | 2) => void; // Map action IDs to labels and icons const ACTION_CONFIG: Record = { @@ -50,6 +50,7 @@ export function ActionButtons({ preparationProgress, applicationProgress, equipmentCraftingProgress, + cancelDesign, }: ActionButtonsProps) { const config = ACTION_CONFIG[currentAction] || { label: currentAction, icon: Sparkles, color: 'text-gray-400' }; const Icon = config.icon; @@ -135,12 +136,22 @@ export function ActionButtons({ {/* Show second design slot if active */} {designProgress2 && (
-
- - Second Design Slot +
+
+ + Second Design Slot +
+ {cancelDesign && ( + + )}
- diff --git a/src/components/game/crafting/EnchantmentDesigner/EquipmentTypeSelector.tsx b/src/components/game/crafting/EnchantmentDesigner/EquipmentTypeSelector.tsx index d0005bf..3d897db 100644 --- a/src/components/game/crafting/EnchantmentDesigner/EquipmentTypeSelector.tsx +++ b/src/components/game/crafting/EnchantmentDesigner/EquipmentTypeSelector.tsx @@ -31,7 +31,7 @@ export function EquipmentTypeSelector({ />
{designProgress.progress.toFixed(1)}h / {designProgress.required.toFixed(1)}h - Cancel + cancelDesign(1)}>Cancel
) : ( diff --git a/src/lib/game/__tests__/regression-fixes.test.ts b/src/lib/game/__tests__/regression-fixes.test.ts index 7625417..f8c5b16 100644 --- a/src/lib/game/__tests__/regression-fixes.test.ts +++ b/src/lib/game/__tests__/regression-fixes.test.ts @@ -143,14 +143,13 @@ describe('Issue 80 — Combat store partialize', () => { }); }); -// ─── Issue 78: cancelDesign logic ───────────────────────────────────────────── +// ─── Issue 78 / #225: cancelDesign slot targeting ───────────────────────────── -describe('Issue 78 — cancelDesign logic', () => { - it('should cancel designProgress first when both slots are active', async () => { +describe('Issue 78 / #225 — cancelDesign slot targeting', () => { + it('should cancel slot 1 when slot=1 is specified', async () => { const { useCraftingStore } = await import('../stores/craftingStore'); const store = useCraftingStore.getState(); - // Set both slots active store.setDesignProgress({ designId: 'test-1', progress: 0, @@ -160,15 +159,70 @@ describe('Issue 78 — cancelDesign logic', () => { effects: [], }); store.setDesignProgress2({ - design: 'test-2', + designId: 'test-2', progress: 0, required: 100, name: 'Design 2', equipmentType: 'basicStaff', effects: [], - } as any); + }); + + store.cancelDesign(1); + + const state = useCraftingStore.getState(); + expect(state.designProgress).toBeNull(); + expect(state.designProgress2).not.toBeNull(); + }); + + it('should cancel slot 2 when slot=2 is specified, leaving slot 1 alone', async () => { + const { useCraftingStore } = await import('../stores/craftingStore'); + const store = useCraftingStore.getState(); + + store.setDesignProgress({ + designId: 'test-1', + progress: 0, + required: 100, + name: 'Design 1', + equipmentType: 'basicStaff', + effects: [], + }); + store.setDesignProgress2({ + designId: 'test-2', + progress: 0, + required: 100, + name: 'Design 2', + equipmentType: 'basicStaff', + effects: [], + }); + + store.cancelDesign(2); + + const state = useCraftingStore.getState(); + expect(state.designProgress).not.toBeNull(); + expect(state.designProgress2).toBeNull(); + }); + + it('should cancel slot 1 by default when no slot specified and slot 1 is active', async () => { + const { useCraftingStore } = await import('../stores/craftingStore'); + const store = useCraftingStore.getState(); + + store.setDesignProgress({ + designId: 'test-1', + progress: 0, + required: 100, + name: 'Design 1', + equipmentType: 'basicStaff', + effects: [], + }); + store.setDesignProgress2({ + designId: 'test-2', + progress: 0, + required: 100, + name: 'Design 2', + equipmentType: 'basicStaff', + effects: [], + }); - // Cancel should remove designProgress (slot 1), not designProgress2 store.cancelDesign(); const state = useCraftingStore.getState(); @@ -176,11 +230,10 @@ describe('Issue 78 — cancelDesign logic', () => { expect(state.designProgress2).not.toBeNull(); }); - it('should cancel designProgress2 when designProgress is null', async () => { + it('should cancel slot 2 by default when slot 1 is null', async () => { const { useCraftingStore } = await import('../stores/craftingStore'); const store = useCraftingStore.getState(); - // Only slot 2 active store.setDesignProgress(null); store.setDesignProgress2({ designId: 'test-2', diff --git a/src/lib/game/__tests__/tick-integration.test.ts b/src/lib/game/__tests__/tick-integration.test.ts index 53111a7..9db4667 100644 --- a/src/lib/game/__tests__/tick-integration.test.ts +++ b/src/lib/game/__tests__/tick-integration.test.ts @@ -126,13 +126,20 @@ describe('Tick Integration', () => { expect(useManaStore.getState().rawMana).toBeLessThanOrEqual(100); }); - it('should not decrease totalManaGathered on tick', () => { - // Note: passive regen in tick() updates rawMana directly, not via addRawMana, - // so totalManaGathered only increases from gatherMana or combat loot. - // This is expected behavior — totalManaGathered tracks active gathering. + it('should increase totalManaGathered from meditation regen', () => { + // Passive meditation regen should count toward totalManaGathered useManaStore.setState({ rawMana: 50, totalManaGathered: 5 }); useGameStore.getState().tick(); - expect(useManaStore.getState().totalManaGathered).toBeGreaterThanOrEqual(5); + expect(useManaStore.getState().totalManaGathered).toBeGreaterThan(5); + }); + + it('should not count regen toward totalManaGathered when at cap', () => { + // Regen that would exceed max mana should not count toward totalManaGathered + useManaStore.setState({ rawMana: 100, totalManaGathered: 50 }); + useGameStore.getState().tick(); + // When rawMana equals or exceeds maxMana, no regen is added to totalManaGathered + // Use toBeCloseTo with tolerance for floating point drift from base regen calculations + expect(useManaStore.getState().totalManaGathered).toBeLessThanOrEqual(50.05); }); }); @@ -304,13 +311,13 @@ describe('Tick Integration', () => { expect(useGameStore.getState().hour).toBeCloseTo(1, 5); }); - it('should not lose totalManaGathered over ticks', () => { + it('should accumulate totalManaGathered from meditation over ticks', () => { useManaStore.setState({ rawMana: 10, totalManaGathered: 42 }); for (let i = 0; i < 10; i++) { useGameStore.getState().tick(); } - // totalManaGathered should stay at 42 (passive regen doesn't change it) - expect(useManaStore.getState().totalManaGathered).toBe(42); + // Passive meditation regen should now count toward totalManaGathered + expect(useManaStore.getState().totalManaGathered).toBeGreaterThan(42); }); }); diff --git a/src/lib/game/crafting-actions/application-actions.ts b/src/lib/game/crafting-actions/application-actions.ts index ec3bc17..2e29aa0 100644 --- a/src/lib/game/crafting-actions/application-actions.ts +++ b/src/lib/game/crafting-actions/application-actions.ts @@ -3,6 +3,8 @@ import type { CraftingState } from '../stores/craftingStore.types'; import type { GameAction } from '../types'; import * as CraftingApply from '../crafting-apply'; +import { useManaStore } from '../stores/manaStore'; +import { useUIStore } from '../stores/uiStore'; /** * Start applying an enchantment design to an equipment instance. @@ -66,8 +68,25 @@ export function resumeApplication( } export function cancelApplication( + get: () => CraftingState, set: (partial: Partial) => void ) { + const state = get(); + const progress = state.applicationProgress; + if (!progress) return; + + // Refund mana proportionally to remaining progress + 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); + + if (manaRefund > 0) { + useManaStore.setState((s) => ({ rawMana: s.rawMana + manaRefund })); + } + useUIStore.getState().addLog(`🚫 Enchantment application cancelled. Refunded ${manaRefund} mana.`); set({ applicationProgress: null, }); diff --git a/src/lib/game/crafting-actions/preparation-actions.ts b/src/lib/game/crafting-actions/preparation-actions.ts index f90ede6..6b28b16 100644 --- a/src/lib/game/crafting-actions/preparation-actions.ts +++ b/src/lib/game/crafting-actions/preparation-actions.ts @@ -2,6 +2,8 @@ import type { CraftingState } from '../stores/craftingStore.types'; import * as CraftingPrep from '../crafting-prep'; +import { useManaStore } from '../stores/manaStore'; +import { useUIStore } from '../stores/uiStore'; export function startPreparing( equipmentInstanceId: string, @@ -35,8 +37,25 @@ export function startPreparing( } export function cancelPreparation( + get: () => CraftingState, set: (partial: Partial) => void ) { + const state = get(); + const progress = state.preparationProgress; + if (!progress) return; + + // Refund mana proportionally to remaining progress + 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.manaCostPaid * refundRate); + + if (manaRefund > 0) { + useManaStore.setState((s) => ({ rawMana: s.rawMana + manaRefund })); + } + useUIStore.getState().addLog(`🚫 Preparation cancelled. Refunded ${manaRefund} mana.`); set({ preparationProgress: null, }); diff --git a/src/lib/game/stores/combat-actions.ts b/src/lib/game/stores/combat-actions.ts index 39c9d7d..4b02c64 100644 --- a/src/lib/game/stores/combat-actions.ts +++ b/src/lib/game/stores/combat-actions.ts @@ -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 diff --git a/src/lib/game/stores/craftingStore.ts b/src/lib/game/stores/craftingStore.ts index e6fbdb8..769c35c 100644 --- a/src/lib/game/stores/craftingStore.ts +++ b/src/lib/game/stores/craftingStore.ts @@ -82,9 +82,18 @@ export const useCraftingStore = create()( 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()( }, cancelApplication: () => { - ApplicationActions.cancelApplication(set as unknown as (partial: Partial) => void); + ApplicationActions.cancelApplication(get, set as unknown as (partial: Partial) => void); useCombatStore.setState({ currentAction: 'meditate' }); }, @@ -169,7 +178,7 @@ export const useCraftingStore = create()( }, cancelPreparation: () => { - PreparationActions.cancelPreparation(set); + PreparationActions.cancelPreparation(get, set); useCombatStore.setState({ currentAction: 'meditate' }); }, diff --git a/src/lib/game/stores/craftingStore.types.ts b/src/lib/game/stores/craftingStore.types.ts index 8b543a7..23f3c4e 100644 --- a/src/lib/game/stores/craftingStore.types.ts +++ b/src/lib/game/stores/craftingStore.types.ts @@ -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; diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts index d07f774..da19a75 100644 --- a/src/lib/game/stores/gameStore.ts +++ b/src/lib/game/stores/gameStore.ts @@ -182,8 +182,12 @@ export const useGameStore = create()( 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); diff --git a/src/lib/game/stores/pipelines/equipment-crafting.ts b/src/lib/game/stores/pipelines/equipment-crafting.ts index 47ed8d5..49ce2f8 100644 --- a/src/lib/game/stores/pipelines/equipment-crafting.ts +++ b/src/lib/game/stores/pipelines/equipment-crafting.ts @@ -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 { diff --git a/src/lib/game/utils/combat-utils.ts b/src/lib/game/utils/combat-utils.ts index 1da8fef..be1091d 100644 --- a/src/lib/game/utils/combat-utils.ts +++ b/src/lib/game/utils/combat-utils.ts @@ -50,6 +50,17 @@ export function getElementalBonus(spellElem: string, floorElem: string): number return 1.0; // Neutral } +/** + * Get the elemental bonus against a multi-element floor/guardian. + * Uses the minimum bonus across all elements so multi-element guardians + * are not trivially countered (each element can resist different spells). + */ +export function getMultiElementBonus(spellElem: string, floorElems: string[]): number { + if (floorElems.length === 0) return 1.0; + if (floorElems.length === 1) return getElementalBonus(spellElem, floorElems[0]); + return Math.min(...floorElems.map(e => getElementalBonus(spellElem, e))); +} + // ─── Boon Bonuses ───────────────────────────────────────────────────────────── export interface BoonBonuses { diff --git a/src/lib/game/utils/index.ts b/src/lib/game/utils/index.ts index b8f659a..7045f61 100644 --- a/src/lib/game/utils/index.ts +++ b/src/lib/game/utils/index.ts @@ -18,6 +18,7 @@ export { // computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades export { getElementalBonus, + getMultiElementBonus, getBoonBonuses, calcDamage, calcInsight,