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
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m59s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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' });
|
||||
},
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user