fix: Bug fixes #218 #222 #220 #223 #215 #216 - attunement free mana, transference circular ref, guardian defeat tracking, discipline negative mana, guardian data, crafting refunds
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
// ─── Combat Tick Callback Builder ─────────────────────────────────────────────
|
||||
// Extracts the large combat callback lambdas from gameStore.ts tick()
|
||||
// to keep the coordinator under the 400-line file limit.
|
||||
|
||||
import { HOURS_PER_TICK } from '../../constants';
|
||||
import { getGuardianForFloor } from '../../data/guardian-encounters';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../../effects/special-effects';
|
||||
import type { ComputedEffects } from '../../effects/upgrade-effects.types';
|
||||
|
||||
interface BuildCombatCallbacksParams {
|
||||
ctx: {
|
||||
combat: {
|
||||
floorHP: number;
|
||||
floorMaxHP: number;
|
||||
currentFloor: number;
|
||||
guardianShield: number;
|
||||
guardianShieldMax: number;
|
||||
guardianBarrier: number;
|
||||
guardianBarrierMax: number;
|
||||
};
|
||||
};
|
||||
effects: ComputedEffects;
|
||||
maxMana: number;
|
||||
addLog: (msg: string) => void;
|
||||
useCombatStore: { setState: (s: Record<string, unknown>) => void };
|
||||
usePrestigeStore: { getState: () => { addDefeatedGuardian: (floor: number) => void } };
|
||||
}
|
||||
|
||||
export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
||||
const { ctx, effects, maxMana, addLog, useCombatStore, usePrestigeStore } = params;
|
||||
|
||||
const onFloorCleared = (floor: number, wasGuardian: boolean) => {
|
||||
if (wasGuardian) {
|
||||
const defeatedGuardian = getGuardianForFloor(floor);
|
||||
addLog((defeatedGuardian?.name || 'Unknown') + ' defeated! Visit the Grimoire to sign a pact.');
|
||||
usePrestigeStore.getState().addDefeatedGuardian(floor);
|
||||
} else if (floor % 5 === 0) {
|
||||
addLog('Floor ' + floor + ' cleared!');
|
||||
}
|
||||
useCombatStore.setState({ guardianShield: 0, guardianShieldMax: 0, guardianBarrier: 0, guardianBarrierMax: 0 });
|
||||
};
|
||||
|
||||
// Returns a function matching the processCombatTick onDamageDealt signature.
|
||||
// The returned function closes over the current tick's rawMana/elements references.
|
||||
const makeOnDamageDealt = (rawManaRef: () => number, elementsRef: () => Record<string, { current: number; max: number; unlocked: boolean }>) => {
|
||||
return (damage: number) => {
|
||||
const rawMana = rawManaRef();
|
||||
const elements = elementsRef();
|
||||
let dmg = damage;
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && ctx.combat.floorHP / ctx.combat.floorMaxHP < 0.25) {
|
||||
dmg *= 2;
|
||||
}
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||
dmg *= 1.5;
|
||||
}
|
||||
|
||||
const guardian = getGuardianForFloor(ctx.combat.currentFloor);
|
||||
if (guardian && (guardian.shield || guardian.barrier || guardian.healthRegen)) {
|
||||
let shield = ctx.combat.guardianShield;
|
||||
const shieldMax = ctx.combat.guardianShieldMax;
|
||||
let barrier = ctx.combat.guardianBarrier;
|
||||
const barrierMax = ctx.combat.guardianBarrierMax;
|
||||
|
||||
if (guardian.shieldRegen && shield < shieldMax) {
|
||||
shield = Math.min(shieldMax, shield + guardian.shieldRegen * HOURS_PER_TICK);
|
||||
}
|
||||
if (guardian.barrierRegen && barrier < barrierMax) {
|
||||
barrier = Math.min(barrierMax, barrier + guardian.barrierRegen * HOURS_PER_TICK);
|
||||
}
|
||||
|
||||
if (shield > 0 && dmg > 0) {
|
||||
const absorb = Math.min(shield, dmg);
|
||||
shield -= absorb;
|
||||
dmg -= absorb;
|
||||
}
|
||||
if (barrier > 0 && dmg > 0) {
|
||||
dmg *= (1 - barrier);
|
||||
}
|
||||
if (guardian.healthRegen && guardian.healthRegen > 0) {
|
||||
const healAmount = guardian.healthRegenIsPercent
|
||||
? Math.floor(ctx.combat.floorMaxHP * guardian.healthRegen / 100 * HOURS_PER_TICK)
|
||||
: Math.floor(guardian.healthRegen * HOURS_PER_TICK);
|
||||
dmg -= healAmount;
|
||||
}
|
||||
|
||||
useCombatStore.setState({
|
||||
guardianShield: shield,
|
||||
guardianShieldMax: shieldMax,
|
||||
guardianBarrier: barrier,
|
||||
guardianBarrierMax: barrierMax,
|
||||
});
|
||||
}
|
||||
|
||||
return { rawMana, elements, modifiedDamage: dmg };
|
||||
};
|
||||
};
|
||||
|
||||
return { onFloorCleared, makeOnDamageDealt };
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// ─── Equipment Crafting Pipeline ──────────────────────────────────────────────
|
||||
// Extracted from craftingStore.ts to keep it under the 400-line file limit.
|
||||
// Handles start/cancel of equipment crafting and fabricator crafting.
|
||||
|
||||
import type { CraftingState } from '../craftingStore.types';
|
||||
import * as CraftingEquipment from '../../crafting-equipment';
|
||||
import {
|
||||
getFabricatorRecipe,
|
||||
deductFabricatorMana,
|
||||
deductMaterials,
|
||||
makeFabricatorProgress,
|
||||
} from '../../crafting-fabricator';
|
||||
import { useManaStore } from '../manaStore';
|
||||
import { useCombatStore } from '../combatStore';
|
||||
import { useUIStore } from '../uiStore';
|
||||
|
||||
type GetFn = () => CraftingState;
|
||||
type SetFn = (partial: Partial<CraftingState>) => void;
|
||||
|
||||
export function startCraftingEquipment(
|
||||
blueprintId: string,
|
||||
get: GetFn,
|
||||
set: SetFn,
|
||||
): boolean {
|
||||
const state = get();
|
||||
const rawMana = useManaStore.getState().rawMana;
|
||||
const currentAction = useCombatStore.getState().currentAction;
|
||||
const check = CraftingEquipment.canStartEquipmentCrafting(
|
||||
blueprintId,
|
||||
state.lootInventory.blueprints.includes(blueprintId),
|
||||
state.lootInventory.materials,
|
||||
rawMana,
|
||||
currentAction,
|
||||
);
|
||||
if (!check.canCraft) return false;
|
||||
const result = CraftingEquipment.initializeEquipmentCrafting(
|
||||
blueprintId,
|
||||
state.lootInventory.materials,
|
||||
rawMana,
|
||||
);
|
||||
set((s) => ({
|
||||
lootInventory: { ...s.lootInventory, materials: result.newMaterials },
|
||||
equipmentCraftingProgress: result.progress,
|
||||
}));
|
||||
useManaStore.setState((s) => ({ rawMana: s.rawMana - result.manaCost }));
|
||||
useCombatStore.setState({ currentAction: 'craft' });
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
set({ equipmentCraftingProgress: null, lootInventory: { ...get().lootInventory, materials: refundedMaterials } });
|
||||
} else {
|
||||
set({ equipmentCraftingProgress: null });
|
||||
}
|
||||
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 {
|
||||
const state = get();
|
||||
const currentAction = useCombatStore.getState().currentAction;
|
||||
if (currentAction !== 'meditate') return false;
|
||||
|
||||
const recipe = getFabricatorRecipe(recipeId);
|
||||
if (!recipe) return false;
|
||||
|
||||
const rawMana = useManaStore.getState().rawMana;
|
||||
const elements = useManaStore.getState().elements;
|
||||
|
||||
const deducted = deductFabricatorMana(recipe, rawMana, elements);
|
||||
if (!deducted) return false;
|
||||
|
||||
const newMaterials = deductMaterials(recipe, state.lootInventory.materials);
|
||||
const progress = makeFabricatorProgress(recipeId, recipe.equipmentTypeId, recipe.craftTime, recipe.manaCost);
|
||||
|
||||
useManaStore.setState({ rawMana: deducted.rawMana, elements: deducted.elements });
|
||||
set((s) => ({ lootInventory: { ...s.lootInventory, materials: newMaterials }, equipmentCraftingProgress: progress }));
|
||||
useCombatStore.setState({ currentAction: 'craft' });
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user