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:
@@ -1,4 +1,4 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-30T13:27:24.782Z
|
Generated: 2026-05-30T20:28:32.289Z
|
||||||
|
|
||||||
No circular dependencies found. ✅
|
No circular dependencies found. ✅
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-05-30T13:27:23.117Z",
|
"generated": "2026-05-30T20:28:30.386Z",
|
||||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
@@ -198,9 +198,7 @@
|
|||||||
],
|
],
|
||||||
"crafting-fabricator.ts": [
|
"crafting-fabricator.ts": [
|
||||||
"data/fabricator-recipes.ts",
|
"data/fabricator-recipes.ts",
|
||||||
"stores/combatStore.ts",
|
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/uiStore.ts",
|
|
||||||
"types.ts"
|
"types.ts"
|
||||||
],
|
],
|
||||||
"crafting-loot.ts": [
|
"crafting-loot.ts": [
|
||||||
@@ -568,14 +566,13 @@
|
|||||||
"crafting-actions/equipment-actions.ts",
|
"crafting-actions/equipment-actions.ts",
|
||||||
"crafting-actions/preparation-actions.ts",
|
"crafting-actions/preparation-actions.ts",
|
||||||
"crafting-design.ts",
|
"crafting-design.ts",
|
||||||
"crafting-equipment.ts",
|
|
||||||
"crafting-fabricator.ts",
|
|
||||||
"crafting-utils.ts",
|
"crafting-utils.ts",
|
||||||
"stores/combatStore.ts",
|
"stores/combatStore.ts",
|
||||||
"stores/crafting-equipment-tick.ts",
|
"stores/crafting-equipment-tick.ts",
|
||||||
"stores/crafting-initial-state.ts",
|
"stores/crafting-initial-state.ts",
|
||||||
"stores/craftingStore.types.ts",
|
"stores/craftingStore.types.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
|
"stores/pipelines/equipment-crafting.ts",
|
||||||
"stores/uiStore.ts",
|
"stores/uiStore.ts",
|
||||||
"types/equipmentSlot.ts",
|
"types/equipmentSlot.ts",
|
||||||
"utils/result.ts",
|
"utils/result.ts",
|
||||||
@@ -637,10 +634,8 @@
|
|||||||
"stores/gameStore.ts": [
|
"stores/gameStore.ts": [
|
||||||
"constants.ts",
|
"constants.ts",
|
||||||
"data/attunements.ts",
|
"data/attunements.ts",
|
||||||
"data/guardian-encounters.ts",
|
|
||||||
"effects.ts",
|
"effects.ts",
|
||||||
"effects/discipline-effects.ts",
|
"effects/discipline-effects.ts",
|
||||||
"effects/special-effects.ts",
|
|
||||||
"effects/upgrade-effects.types.ts",
|
"effects/upgrade-effects.types.ts",
|
||||||
"stores/attunementStore.ts",
|
"stores/attunementStore.ts",
|
||||||
"stores/combatStore.ts",
|
"stores/combatStore.ts",
|
||||||
@@ -650,6 +645,7 @@
|
|||||||
"stores/gameLoopActions.ts",
|
"stores/gameLoopActions.ts",
|
||||||
"stores/gameStore.types.ts",
|
"stores/gameStore.types.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
|
"stores/pipelines/combat-tick.ts",
|
||||||
"stores/pipelines/pact-ritual.ts",
|
"stores/pipelines/pact-ritual.ts",
|
||||||
"stores/prestigeStore.ts",
|
"stores/prestigeStore.ts",
|
||||||
"stores/tick-pipeline.ts",
|
"stores/tick-pipeline.ts",
|
||||||
@@ -681,6 +677,20 @@
|
|||||||
"utils/result.ts",
|
"utils/result.ts",
|
||||||
"utils/safe-persist.ts"
|
"utils/safe-persist.ts"
|
||||||
],
|
],
|
||||||
|
"stores/pipelines/combat-tick.ts": [
|
||||||
|
"constants.ts",
|
||||||
|
"data/guardian-encounters.ts",
|
||||||
|
"effects/special-effects.ts",
|
||||||
|
"effects/upgrade-effects.types.ts"
|
||||||
|
],
|
||||||
|
"stores/pipelines/equipment-crafting.ts": [
|
||||||
|
"crafting-equipment.ts",
|
||||||
|
"crafting-fabricator.ts",
|
||||||
|
"stores/combatStore.ts",
|
||||||
|
"stores/craftingStore.types.ts",
|
||||||
|
"stores/manaStore.ts",
|
||||||
|
"stores/uiStore.ts"
|
||||||
|
],
|
||||||
"stores/pipelines/pact-ritual.ts": [
|
"stores/pipelines/pact-ritual.ts": [
|
||||||
"constants.ts",
|
"constants.ts",
|
||||||
"data/guardian-encounters.ts",
|
"data/guardian-encounters.ts",
|
||||||
|
|||||||
@@ -349,6 +349,8 @@ Mana-Loop/
|
|||||||
│ │ │ │ └── useGameDerived.ts
|
│ │ │ │ └── useGameDerived.ts
|
||||||
│ │ │ ├── stores/
|
│ │ │ ├── stores/
|
||||||
│ │ │ │ ├── pipelines/
|
│ │ │ │ ├── pipelines/
|
||||||
|
│ │ │ │ │ ├── combat-tick.ts
|
||||||
|
│ │ │ │ │ ├── equipment-crafting.ts
|
||||||
│ │ │ │ │ └── pact-ritual.ts
|
│ │ │ │ │ └── pact-ritual.ts
|
||||||
│ │ │ │ ├── attunementStore.ts
|
│ │ │ │ ├── attunementStore.ts
|
||||||
│ │ │ │ ├── combat-actions.ts
|
│ │ │ │ ├── combat-actions.ts
|
||||||
|
|||||||
@@ -124,11 +124,20 @@ export interface CraftingCancelResult {
|
|||||||
logMessage: string;
|
logMessage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cancelEquipmentCrafting(blueprintId: string, manaSpent: number): CraftingCancelResult {
|
export function cancelEquipmentCrafting(blueprintId: string, manaSpent: number, currentProgress?: number, requiredProgress?: number): CraftingCancelResult {
|
||||||
const recipe = CRAFTING_RECIPES[blueprintId];
|
const recipe = CRAFTING_RECIPES[blueprintId];
|
||||||
if (!recipe) return { manaRefund: 0, logMessage: 'Invalid crafting recipe.' };
|
if (!recipe) return { manaRefund: 0, logMessage: 'Invalid crafting recipe.' };
|
||||||
|
|
||||||
const manaRefund = Math.floor(manaSpent * MANA_REFUND_RATE);
|
// Refund proportional to remaining progress: unspent portion + half of spent portion
|
||||||
|
let refundRate: number;
|
||||||
|
if (currentProgress !== undefined && requiredProgress && requiredProgress > 0) {
|
||||||
|
const remainingFraction = Math.max(0, (requiredProgress - currentProgress) / requiredProgress);
|
||||||
|
// Full refund for unspent progress, flat 50% for spent progress
|
||||||
|
refundRate = remainingFraction + (1 - remainingFraction) * MANA_REFUND_RATE;
|
||||||
|
} else {
|
||||||
|
refundRate = MANA_REFUND_RATE;
|
||||||
|
}
|
||||||
|
const manaRefund = Math.floor(manaSpent * refundRate);
|
||||||
return { manaRefund, logMessage: `🚫 Equipment crafting cancelled. Refunded ${manaRefund} mana.` };
|
return { manaRefund, logMessage: `🚫 Equipment crafting cancelled. Refunded ${manaRefund} mana.` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,6 @@ export const elementalRegenDisciplines: DisciplineDefinition[] = [
|
|||||||
difficultyFactor: 100,
|
difficultyFactor: 100,
|
||||||
scalingFactor: 50,
|
scalingFactor: 50,
|
||||||
drainBase: 1,
|
drainBase: 1,
|
||||||
sourceManaTypes: ['transference'],
|
sourceManaTypes: ['raw'],
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -198,31 +198,36 @@ const TIER2: Record<number, GuardianDef> = {
|
|||||||
[{ type: 'freeze', value: 0.2 }],
|
[{ type: 'freeze', value: 0.2 }],
|
||||||
{ shield: 900, shieldRegen: 22, barrier: 0.08, barrierRegen: 0.02 },
|
{ shield: 900, shieldRegen: 22, barrier: 0.08, barrierRegen: 0.02 },
|
||||||
),
|
),
|
||||||
130: mk(130, '', ['blackflame'], '#8B2500', 0.32, 4.5,
|
130: mk(130, '', ['metal', 'fire', 'earth'], '#8B2500', 0.32, 4.5,
|
||||||
[
|
[
|
||||||
{ type: 'elementalDamage', value: 20, desc: '+20% BlackFlame damage' },
|
{ type: 'elementalDamage', value: 10, desc: '+10% Metal damage' },
|
||||||
|
{ type: 'elementalDamage', value: 10, desc: '+10% Fire damage' },
|
||||||
|
{ type: 'elementalDamage', value: 10, desc: '+10% Earth damage' },
|
||||||
{ type: 'rawDamage', value: 10, desc: '+10% raw damage' },
|
{ type: 'rawDamage', value: 10, desc: '+10% raw damage' },
|
||||||
],
|
],
|
||||||
'BlackFlame spells apply a curse that reduces enemy resistances by 15%',
|
'Multi-element BlackFlame spells apply a curse that reduces enemy resistances by 15%',
|
||||||
[{ type: 'curse', value: 0.15 }, { type: 'burn', value: 0.15 }],
|
[{ type: 'curse', value: 0.15 }, { type: 'burn', value: 0.15 }, { type: 'armor_pierce', value: 0.2 }],
|
||||||
{ shield: 1000, shieldRegen: 25, healthRegen: 5, healthRegenIsPercent: true },
|
{ shield: 1000, shieldRegen: 25, healthRegen: 5, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
140: mk(140, '', ['radiantflames'], '#FFAA33', 0.25, 4.75,
|
140: mk(140, '', ['sand', 'earth', 'water'], '#FFAA33', 0.25, 4.75,
|
||||||
[
|
[
|
||||||
{ type: 'elementalDamage', value: 15, desc: '+15% Radiant Flames damage' },
|
{ type: 'elementalDamage', value: 10, desc: '+10% Sand damage' },
|
||||||
{ type: 'insightGain', value: 10, desc: '+10% insight gain' },
|
{ type: 'elementalDamage', value: 10, desc: '+10% Earth damage' },
|
||||||
|
{ type: 'elementalDamage', value: 10, desc: '+10% Water damage' },
|
||||||
],
|
],
|
||||||
'Radiant Flames spells blind enemies, reducing their accuracy and damage by 15%',
|
'Radiant Earth spells blind enemies, reducing their accuracy and damage by 15%',
|
||||||
[{ type: 'blind', value: 0.15 }, { type: 'burn', value: 0.1 }],
|
[{ type: 'blind', value: 0.15 }, { type: 'armor_pierce', value: 0.1 }],
|
||||||
{ barrier: 0.12, barrierRegen: 0.03, healthRegen: 6, healthRegenIsPercent: true },
|
{ barrier: 0.12, barrierRegen: 0.03, healthRegen: 6, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
150: mk(150, '', ['miasma'], '#6B8E23', 0.28, 5.0,
|
150: mk(150, '', ['lightning', 'fire', 'air'], '#6B8E23', 0.28, 5.0,
|
||||||
[
|
[
|
||||||
{ type: 'elementalDamage', value: 15, desc: '+15% Miasma damage' },
|
{ type: 'elementalDamage', value: 10, desc: '+10% Lightning damage' },
|
||||||
|
{ type: 'elementalDamage', value: 10, desc: '+10% Fire damage' },
|
||||||
|
{ type: 'elementalDamage', value: 10, desc: '+10% Air damage' },
|
||||||
{ type: 'maxMana', value: 200, desc: '+200 max mana' },
|
{ type: 'maxMana', value: 200, desc: '+200 max mana' },
|
||||||
],
|
],
|
||||||
'Miasma spells corrode armor and spread plague in swarm rooms',
|
'Storm Lightning spells corrode armor and spread chain lightning in swarm rooms',
|
||||||
[{ type: 'corrosion', value: 0.2 }, { type: 'poison', value: 0.15 }],
|
[{ type: 'chain', value: 2 }, { type: 'cast_speed', value: 0.1 }, { type: 'burn', value: 0.1 }],
|
||||||
{ shield: 1100, shieldRegen: 28, barrier: 0.05, barrierRegen: 0.01 },
|
{ shield: 1100, shieldRegen: 28, barrier: 0.05, barrierRegen: 0.01 },
|
||||||
),
|
),
|
||||||
160: mk(160, '', ['shadowglass'], '#2C2C54', 0.33, 5.25,
|
160: mk(160, '', ['shadowglass'], '#2C2C54', 0.33, 5.25,
|
||||||
@@ -272,22 +277,26 @@ const TIER3: Record<number, GuardianDef> = {
|
|||||||
[{ type: 'resist_ignore', value: 0.4 }],
|
[{ type: 'resist_ignore', value: 0.4 }],
|
||||||
{ shield: 2500, shieldRegen: 60, barrier: 0.10, barrierRegen: 0.02, healthRegen: 6, healthRegenIsPercent: true },
|
{ shield: 2500, shieldRegen: 60, barrier: 0.10, barrierRegen: 0.02, healthRegen: 6, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
200: mk(200, '', ['soul'], '#E8D5F5', 0.30, 7.0,
|
200: mk(200, '', ['crystal', 'stellar', 'void'], '#E8D5F5', 0.35, 7.0,
|
||||||
[
|
[
|
||||||
{ type: 'elementalDamage', value: 25, desc: '+25% Soul damage' },
|
{ type: 'elementalDamage', value: 20, desc: '+20% Crystal damage' },
|
||||||
{ type: 'rawDamage', value: 20, desc: '+20% raw damage' },
|
{ type: 'elementalDamage', value: 20, desc: '+20% Stellar damage' },
|
||||||
|
{ type: 'elementalDamage', value: 20, desc: '+20% Void damage' },
|
||||||
|
{ type: 'maxMana', value: 400, desc: '+400 max mana' },
|
||||||
],
|
],
|
||||||
'Soul spells bypass all defenses and shields',
|
'Exotic convergence: Crystal/Stellar/Void spells bypass all defenses and shields',
|
||||||
[{ type: 'defense_pierce', value: 0.5 }, { type: 'mana_drain', value: 0.2 }],
|
[{ type: 'defense_pierce', value: 0.3 }, { type: 'resist_ignore', value: 0.2 }, { type: 'reflect', value: 0.1 }],
|
||||||
{ shield: 2800, shieldRegen: 55, barrier: 0.08, barrierRegen: 0.02 },
|
{ shield: 2800, shieldRegen: 55, barrier: 0.08, barrierRegen: 0.02 },
|
||||||
),
|
),
|
||||||
210: mk(210, '', ['time'], '#C5B99A', 0.32, 7.5,
|
210: mk(210, '', ['soul', 'time', 'plasma'], '#C5B99A', 0.32, 7.5,
|
||||||
[
|
[
|
||||||
{ type: 'elementalDamage', value: 20, desc: '+20% Time damage' },
|
{ type: 'elementalDamage', value: 15, desc: '+15% Soul damage' },
|
||||||
|
{ type: 'elementalDamage', value: 15, desc: '+15% Time damage' },
|
||||||
|
{ type: 'elementalDamage', value: 15, desc: '+15% Plasma damage' },
|
||||||
{ type: 'castingSpeed', value: 20, desc: '+20% casting speed' },
|
{ type: 'castingSpeed', value: 20, desc: '+20% casting speed' },
|
||||||
],
|
],
|
||||||
'Time spells slow enemies by 30% and reduce dodge by 20%',
|
'Astral convergence: Soul/Time/Plasma spells slow and pierce all defenses',
|
||||||
[{ type: 'slow', value: 0.3 }, { type: 'temporal_snap', value: 0.15 }],
|
[{ type: 'defense_pierce', value: 0.3 }, { type: 'slow', value: 0.2 }, { type: 'chain', value: 2 }],
|
||||||
{ barrier: 0.15, barrierRegen: 0.04, healthRegen: 9, healthRegenIsPercent: true },
|
{ barrier: 0.15, barrierRegen: 0.04, healthRegen: 9, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
220: mk(220, '', ['plasma'], '#FF6B9D', 0.28, 8.0,
|
220: mk(220, '', ['plasma'], '#FF6B9D', 0.28, 8.0,
|
||||||
|
|||||||
@@ -11,19 +11,13 @@ import { useCombatStore } from './combatStore';
|
|||||||
import { useUIStore } from './uiStore';
|
import { useUIStore } from './uiStore';
|
||||||
import * as ApplicationActions from '../crafting-actions/application-actions';
|
import * as ApplicationActions from '../crafting-actions/application-actions';
|
||||||
import * as PreparationActions from '../crafting-actions/preparation-actions';
|
import * as PreparationActions from '../crafting-actions/preparation-actions';
|
||||||
import * as CraftingEquipment from '../crafting-equipment';
|
|
||||||
import { equipItem as equipItemAction, unequipItem as unequipItemAction } from '../crafting-actions/equipment-actions';
|
import { equipItem as equipItemAction, unequipItem as unequipItemAction } from '../crafting-actions/equipment-actions';
|
||||||
import { ErrorCode } from '../utils/result';
|
import { ErrorCode } from '../utils/result';
|
||||||
import { createSafeStorage } from '../utils/safe-persist';
|
import { createSafeStorage } from '../utils/safe-persist';
|
||||||
import { createDefaultCraftingState } from './crafting-initial-state';
|
import { createDefaultCraftingState } from './crafting-initial-state';
|
||||||
import {
|
|
||||||
getFabricatorRecipe,
|
|
||||||
deductFabricatorMana,
|
|
||||||
deductMaterials,
|
|
||||||
makeFabricatorProgress,
|
|
||||||
} from '../crafting-fabricator';
|
|
||||||
import { craftMaterial as craftMaterialAction } from '../crafting-actions/crafting-material-actions';
|
import { craftMaterial as craftMaterialAction } from '../crafting-actions/crafting-material-actions';
|
||||||
import { processEquipmentCraftingTick } from './crafting-equipment-tick';
|
import { processEquipmentCraftingTick } from './crafting-equipment-tick';
|
||||||
|
import { startCraftingEquipment, cancelEquipmentCrafting, startFabricatorCrafting } from './pipelines/equipment-crafting';
|
||||||
|
|
||||||
export const useCraftingStore = create<CraftingStore>()(
|
export const useCraftingStore = create<CraftingStore>()(
|
||||||
persist(
|
persist(
|
||||||
@@ -179,65 +173,12 @@ export const useCraftingStore = create<CraftingStore>()(
|
|||||||
useCombatStore.setState({ currentAction: 'meditate' });
|
useCombatStore.setState({ currentAction: 'meditate' });
|
||||||
},
|
},
|
||||||
|
|
||||||
startCraftingEquipment: (blueprintId: string) => {
|
startCraftingEquipment: (blueprintId: string) => startCraftingEquipment(blueprintId, get, set),
|
||||||
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;
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelEquipmentCrafting: () => {
|
cancelEquipmentCrafting: () => cancelEquipmentCrafting(get, set),
|
||||||
const progress = get().equipmentCraftingProgress;
|
|
||||||
if (!progress) return;
|
|
||||||
const cancelResult = CraftingEquipment.cancelEquipmentCrafting(progress.blueprintId, progress.manaSpent);
|
|
||||||
set({ equipmentCraftingProgress: null });
|
|
||||||
useManaStore.setState((s) => ({ rawMana: s.rawMana + cancelResult.manaRefund }));
|
|
||||||
useCombatStore.setState({ currentAction: 'meditate' });
|
|
||||||
useUIStore.getState().addLog(cancelResult.logMessage);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Fabricator crafting — uses elemental mana instead of raw mana
|
// Fabricator crafting — uses elemental mana instead of raw mana
|
||||||
startFabricatorCrafting: (recipeId: string) => {
|
startFabricatorCrafting: (recipeId: string) => startFabricatorCrafting(recipeId, get, set),
|
||||||
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;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Material crafting — instant crafting of materials
|
// Material crafting — instant crafting of materials
|
||||||
craftMaterial: (recipeId: string) => {
|
craftMaterial: (recipeId: string) => {
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import { HOURS_PER_TICK, MAX_DAY } from '../constants';
|
import { HOURS_PER_TICK, MAX_DAY } from '../constants';
|
||||||
import { getGuardianForFloor } from '../data/guardian-encounters';
|
|
||||||
import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
|
|
||||||
import { computeEquipmentEffects } from '../effects';
|
import { computeEquipmentEffects } from '../effects';
|
||||||
import type { ComputedEffects } from '../effects/upgrade-effects.types';
|
import type { ComputedEffects } from '../effects/upgrade-effects.types';
|
||||||
|
|
||||||
@@ -11,6 +9,7 @@ import { computeDisciplineEffects } from '../effects/discipline-effects';
|
|||||||
import { computeMaxMana, computeRegen, getMeditationBonus, getIncursionStrength, calcInsight } from '../utils';
|
import { computeMaxMana, computeRegen, getMeditationBonus, getIncursionStrength, calcInsight } from '../utils';
|
||||||
import { mergePerElementCapBonuses } from '../utils/element-cap-bonus';
|
import { mergePerElementCapBonuses } from '../utils/element-cap-bonus';
|
||||||
import { processPactRitual } from './pipelines/pact-ritual';
|
import { processPactRitual } from './pipelines/pact-ritual';
|
||||||
|
import { buildCombatCallbacks } from './pipelines/combat-tick';
|
||||||
import { useUIStore } from './uiStore';
|
import { useUIStore } from './uiStore';
|
||||||
import { usePrestigeStore } from './prestigeStore';
|
import { usePrestigeStore } from './prestigeStore';
|
||||||
import { useManaStore } from './manaStore';
|
import { useManaStore } from './manaStore';
|
||||||
@@ -158,6 +157,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let totalConversionPerTick = 0;
|
let totalConversionPerTick = 0;
|
||||||
|
let rawManaDelta = 0;
|
||||||
let elements = { ...ctx.mana.elements };
|
let elements = { ...ctx.mana.elements };
|
||||||
Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
|
Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
|
||||||
if (!state.active) return;
|
if (!state.active) return;
|
||||||
@@ -166,6 +166,8 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
const scaledRate = getAttunementConversionRate(id, state.level || 1);
|
const scaledRate = getAttunementConversionRate(id, state.level || 1);
|
||||||
const conversionThisTick = scaledRate * HOURS_PER_TICK;
|
const conversionThisTick = scaledRate * HOURS_PER_TICK;
|
||||||
totalConversionPerTick += conversionThisTick;
|
totalConversionPerTick += conversionThisTick;
|
||||||
|
// Deduct raw mana to pay for the conversion — without this, attunements produce free element mana
|
||||||
|
rawManaDelta -= conversionThisTick;
|
||||||
if (elements[def.primaryManaType]) {
|
if (elements[def.primaryManaType]) {
|
||||||
if (!elements[def.primaryManaType].unlocked) {
|
if (!elements[def.primaryManaType].unlocked) {
|
||||||
elements[def.primaryManaType] = { ...elements[def.primaryManaType], unlocked: true };
|
elements[def.primaryManaType] = { ...elements[def.primaryManaType], unlocked: true };
|
||||||
@@ -179,7 +181,8 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
|
|
||||||
const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
|
const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
|
||||||
|
|
||||||
let rawMana = Math.min(ctx.mana.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
|
const rawAfterConversion = ctx.mana.rawMana + rawManaDelta;
|
||||||
|
let rawMana = Math.max(0, Math.min(rawAfterConversion + effectiveRegen * HOURS_PER_TICK, maxMana));
|
||||||
let totalManaGathered = ctx.mana.totalManaGathered;
|
let totalManaGathered = ctx.mana.totalManaGathered;
|
||||||
|
|
||||||
if (ctx.combat.currentAction === 'convert') {
|
if (ctx.combat.currentAction === 'convert') {
|
||||||
@@ -221,11 +224,18 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!canConvert) continue;
|
if (!canConvert) continue;
|
||||||
|
// Re-check against actual remaining mana to prevent negative values
|
||||||
|
// when multiple disciplines share the same source
|
||||||
|
for (const srcType of conv.sourceManaTypes) {
|
||||||
|
if (srcType === 'raw' && rawMana < conversionAmount) { canConvert = false; break; }
|
||||||
|
if (srcType !== 'raw' && elements[srcType] && elements[srcType].current < conversionAmount) { canConvert = false; break; }
|
||||||
|
}
|
||||||
|
if (!canConvert) continue;
|
||||||
for (const srcType of conv.sourceManaTypes) {
|
for (const srcType of conv.sourceManaTypes) {
|
||||||
if (srcType === 'raw') {
|
if (srcType === 'raw') {
|
||||||
rawMana -= conversionAmount;
|
rawMana -= conversionAmount;
|
||||||
} else if (elements[srcType]) {
|
} else if (elements[srcType]) {
|
||||||
elements[srcType] = { ...elements[srcType], current: elements[srcType].current - conversionAmount };
|
elements[srcType] = { ...elements[srcType], current: Math.max(0, elements[srcType].current - conversionAmount) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (elements[targetElem]) {
|
if (elements[targetElem]) {
|
||||||
@@ -263,81 +273,22 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
|
|
||||||
// Combat — delegate to combatStore
|
// Combat — delegate to combatStore
|
||||||
if (ctx.combat.currentAction === 'climb') {
|
if (ctx.combat.currentAction === 'climb') {
|
||||||
|
const combatCbs = buildCombatCallbacks({
|
||||||
|
ctx, effects, maxMana, addLog, useCombatStore, usePrestigeStore,
|
||||||
|
});
|
||||||
const combatResult = useCombatStore.getState().processCombatTick(
|
const combatResult = useCombatStore.getState().processCombatTick(
|
||||||
rawMana,
|
rawMana, elements, maxMana, 1,
|
||||||
elements,
|
combatCbs.onFloorCleared,
|
||||||
maxMana,
|
combatCbs.makeOnDamageDealt(() => rawMana, () => elements),
|
||||||
1,
|
|
||||||
(floor, wasGuardian) => {
|
|
||||||
if (wasGuardian) {
|
|
||||||
const defeatedGuardian = getGuardianForFloor(floor);
|
|
||||||
addLog((defeatedGuardian?.name || 'Unknown') + ' defeated! Visit the Grimoire to sign a pact.');
|
|
||||||
} else if (floor % 5 === 0) {
|
|
||||||
addLog('Floor ' + floor + ' cleared!');
|
|
||||||
}
|
|
||||||
useCombatStore.setState({ guardianShield: 0, guardianShieldMax: 0, guardianBarrier: 0, guardianBarrierMax: 0 });
|
|
||||||
},
|
|
||||||
(damage) => {
|
|
||||||
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;
|
|
||||||
let shieldMax = ctx.combat.guardianShieldMax;
|
|
||||||
let barrier = ctx.combat.guardianBarrier;
|
|
||||||
let 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 };
|
|
||||||
},
|
|
||||||
ctx.prestige.signedPacts,
|
ctx.prestige.signedPacts,
|
||||||
);
|
);
|
||||||
|
|
||||||
rawMana = combatResult.rawMana;
|
rawMana = combatResult.rawMana;
|
||||||
elements = combatResult.elements;
|
elements = combatResult.elements;
|
||||||
totalManaGathered += combatResult.totalManaGathered || 0;
|
totalManaGathered += combatResult.totalManaGathered || 0;
|
||||||
|
|
||||||
if (combatResult.logMessages) {
|
if (combatResult.logMessages) {
|
||||||
combatResult.logMessages.forEach(msg => addLog(msg));
|
combatResult.logMessages.forEach(msg => addLog(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
writes.combat = {
|
writes.combat = {
|
||||||
...(writes.combat || {}),
|
...(writes.combat || {}),
|
||||||
currentFloor: combatResult.currentFloor,
|
currentFloor: combatResult.currentFloor,
|
||||||
|
|||||||
@@ -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