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:
@@ -1,4 +1,4 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-30T20:28:32.289Z
|
Generated: 2026-05-30T20:28:51.293Z
|
||||||
|
|
||||||
No circular dependencies found. ✅
|
No circular dependencies found. ✅
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_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.",
|
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function LeftPanel() {
|
|||||||
const currentAction = useCombatStore((s) => s.currentAction);
|
const currentAction = useCombatStore((s) => s.currentAction);
|
||||||
const designProgress = useCraftingStore((s) => s.designProgress);
|
const designProgress = useCraftingStore((s) => s.designProgress);
|
||||||
const designProgress2 = useCraftingStore((s) => s.designProgress2);
|
const designProgress2 = useCraftingStore((s) => s.designProgress2);
|
||||||
|
const cancelDesign = useCraftingStore((s) => s.cancelDesign);
|
||||||
const preparationProgress = useCraftingStore((s) => s.preparationProgress);
|
const preparationProgress = useCraftingStore((s) => s.preparationProgress);
|
||||||
const applicationProgress = useCraftingStore((s) => s.applicationProgress);
|
const applicationProgress = useCraftingStore((s) => s.applicationProgress);
|
||||||
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
||||||
@@ -101,6 +102,7 @@ export function LeftPanel() {
|
|||||||
preparationProgress={preparationProgress}
|
preparationProgress={preparationProgress}
|
||||||
applicationProgress={applicationProgress}
|
applicationProgress={applicationProgress}
|
||||||
equipmentCraftingProgress={equipmentCraftingProgress}
|
equipmentCraftingProgress={equipmentCraftingProgress}
|
||||||
|
cancelDesign={cancelDesign}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface ActionButtonsProps {
|
|||||||
preparationProgress: { progress: number; required: number } | null;
|
preparationProgress: { progress: number; required: number } | null;
|
||||||
applicationProgress: { progress: number; required: number } | null;
|
applicationProgress: { progress: number; required: number } | null;
|
||||||
equipmentCraftingProgress: { progress: number; required: number } | null;
|
equipmentCraftingProgress: { progress: number; required: number } | null;
|
||||||
}
|
cancelDesign?: (slot: 1 | 2) => void;
|
||||||
|
|
||||||
// Map action IDs to labels and icons
|
// Map action IDs to labels and icons
|
||||||
const ACTION_CONFIG: Record<string, { label: string; icon: typeof Sparkles; color: string }> = {
|
const ACTION_CONFIG: Record<string, { label: string; icon: typeof Sparkles; color: string }> = {
|
||||||
@@ -50,6 +50,7 @@ export function ActionButtons({
|
|||||||
preparationProgress,
|
preparationProgress,
|
||||||
applicationProgress,
|
applicationProgress,
|
||||||
equipmentCraftingProgress,
|
equipmentCraftingProgress,
|
||||||
|
cancelDesign,
|
||||||
}: ActionButtonsProps) {
|
}: ActionButtonsProps) {
|
||||||
const config = ACTION_CONFIG[currentAction] || { label: currentAction, icon: Sparkles, color: 'text-gray-400' };
|
const config = ACTION_CONFIG[currentAction] || { label: currentAction, icon: Sparkles, color: 'text-gray-400' };
|
||||||
const Icon = config.icon;
|
const Icon = config.icon;
|
||||||
@@ -135,9 +136,19 @@ export function ActionButtons({
|
|||||||
{/* Show second design slot if active */}
|
{/* Show second design slot if active */}
|
||||||
{designProgress2 && (
|
{designProgress2 && (
|
||||||
<div className="mt-2 pt-2 border-t border-gray-700">
|
<div className="mt-2 pt-2 border-t border-gray-700">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<Target className="w-3 h-3 text-purple-400" />
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-gray-400">Second Design Slot</span>
|
<Target className="w-3 h-3 text-purple-400" />
|
||||||
|
<span className="text-xs text-gray-400">Second Design Slot</span>
|
||||||
|
</div>
|
||||||
|
{cancelDesign && (
|
||||||
|
<button
|
||||||
|
onClick={() => cancelDesign(2)}
|
||||||
|
className="text-xs text-red-400 hover:text-red-300 cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
progress={designProgress2.progress}
|
progress={designProgress2.progress}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function EquipmentTypeSelector({
|
|||||||
/>
|
/>
|
||||||
<div className="flex justify-between text-xs text-[var(--text-muted)]">
|
<div className="flex justify-between text-xs text-[var(--text-muted)]">
|
||||||
<span>{designProgress.progress.toFixed(1)}h / {designProgress.required.toFixed(1)}h</span>
|
<span>{designProgress.progress.toFixed(1)}h / {designProgress.required.toFixed(1)}h</span>
|
||||||
<ActionButton size="sm" variant="ghost" onClick={cancelDesign}>Cancel</ActionButton>
|
<ActionButton size="sm" variant="ghost" onClick={() => cancelDesign(1)}>Cancel</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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', () => {
|
describe('Issue 78 / #225 — cancelDesign slot targeting', () => {
|
||||||
it('should cancel designProgress first when both slots are active', async () => {
|
it('should cancel slot 1 when slot=1 is specified', async () => {
|
||||||
const { useCraftingStore } = await import('../stores/craftingStore');
|
const { useCraftingStore } = await import('../stores/craftingStore');
|
||||||
const store = useCraftingStore.getState();
|
const store = useCraftingStore.getState();
|
||||||
|
|
||||||
// Set both slots active
|
|
||||||
store.setDesignProgress({
|
store.setDesignProgress({
|
||||||
designId: 'test-1',
|
designId: 'test-1',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
@@ -160,15 +159,70 @@ describe('Issue 78 — cancelDesign logic', () => {
|
|||||||
effects: [],
|
effects: [],
|
||||||
});
|
});
|
||||||
store.setDesignProgress2({
|
store.setDesignProgress2({
|
||||||
design: 'test-2',
|
designId: 'test-2',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
required: 100,
|
required: 100,
|
||||||
name: 'Design 2',
|
name: 'Design 2',
|
||||||
equipmentType: 'basicStaff',
|
equipmentType: 'basicStaff',
|
||||||
effects: [],
|
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();
|
store.cancelDesign();
|
||||||
|
|
||||||
const state = useCraftingStore.getState();
|
const state = useCraftingStore.getState();
|
||||||
@@ -176,11 +230,10 @@ describe('Issue 78 — cancelDesign logic', () => {
|
|||||||
expect(state.designProgress2).not.toBeNull();
|
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 { useCraftingStore } = await import('../stores/craftingStore');
|
||||||
const store = useCraftingStore.getState();
|
const store = useCraftingStore.getState();
|
||||||
|
|
||||||
// Only slot 2 active
|
|
||||||
store.setDesignProgress(null);
|
store.setDesignProgress(null);
|
||||||
store.setDesignProgress2({
|
store.setDesignProgress2({
|
||||||
designId: 'test-2',
|
designId: 'test-2',
|
||||||
|
|||||||
@@ -126,13 +126,20 @@ describe('Tick Integration', () => {
|
|||||||
expect(useManaStore.getState().rawMana).toBeLessThanOrEqual(100);
|
expect(useManaStore.getState().rawMana).toBeLessThanOrEqual(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not decrease totalManaGathered on tick', () => {
|
it('should increase totalManaGathered from meditation regen', () => {
|
||||||
// Note: passive regen in tick() updates rawMana directly, not via addRawMana,
|
// Passive meditation regen should count toward totalManaGathered
|
||||||
// so totalManaGathered only increases from gatherMana or combat loot.
|
|
||||||
// This is expected behavior — totalManaGathered tracks active gathering.
|
|
||||||
useManaStore.setState({ rawMana: 50, totalManaGathered: 5 });
|
useManaStore.setState({ rawMana: 50, totalManaGathered: 5 });
|
||||||
useGameStore.getState().tick();
|
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);
|
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 });
|
useManaStore.setState({ rawMana: 10, totalManaGathered: 42 });
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
useGameStore.getState().tick();
|
useGameStore.getState().tick();
|
||||||
}
|
}
|
||||||
// totalManaGathered should stay at 42 (passive regen doesn't change it)
|
// Passive meditation regen should now count toward totalManaGathered
|
||||||
expect(useManaStore.getState().totalManaGathered).toBe(42);
|
expect(useManaStore.getState().totalManaGathered).toBeGreaterThan(42);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import type { CraftingState } from '../stores/craftingStore.types';
|
import type { CraftingState } from '../stores/craftingStore.types';
|
||||||
import type { GameAction } from '../types';
|
import type { GameAction } from '../types';
|
||||||
import * as CraftingApply from '../crafting-apply';
|
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.
|
* Start applying an enchantment design to an equipment instance.
|
||||||
@@ -66,8 +68,25 @@ export function resumeApplication(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function cancelApplication(
|
export function cancelApplication(
|
||||||
|
get: () => CraftingState,
|
||||||
set: (partial: Partial<CraftingState>) => void
|
set: (partial: Partial<CraftingState>) => 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({
|
set({
|
||||||
applicationProgress: null,
|
applicationProgress: null,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import type { CraftingState } from '../stores/craftingStore.types';
|
import type { CraftingState } from '../stores/craftingStore.types';
|
||||||
import * as CraftingPrep from '../crafting-prep';
|
import * as CraftingPrep from '../crafting-prep';
|
||||||
|
import { useManaStore } from '../stores/manaStore';
|
||||||
|
import { useUIStore } from '../stores/uiStore';
|
||||||
|
|
||||||
export function startPreparing(
|
export function startPreparing(
|
||||||
equipmentInstanceId: string,
|
equipmentInstanceId: string,
|
||||||
@@ -35,8 +37,25 @@ export function startPreparing(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function cancelPreparation(
|
export function cancelPreparation(
|
||||||
|
get: () => CraftingState,
|
||||||
set: (partial: Partial<CraftingState>) => void
|
set: (partial: Partial<CraftingState>) => 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({
|
set({
|
||||||
preparationProgress: null,
|
preparationProgress: null,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { SPELLS_DEF, HOURS_PER_TICK } from '../constants';
|
|||||||
import { getGuardianForFloor } from '../data/guardian-encounters';
|
import { getGuardianForFloor } from '../data/guardian-encounters';
|
||||||
import type { CombatStore, CombatState } from './combat-state.types';
|
import type { CombatStore, CombatState } from './combat-state.types';
|
||||||
import type { SpellState } from '../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';
|
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,14 +95,21 @@ export function processCombatTick(
|
|||||||
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
||||||
rawMana = afterCost.rawMana;
|
rawMana = afterCost.rawMana;
|
||||||
elements = afterCost.elements;
|
elements = afterCost.elements;
|
||||||
// Calculate base damage
|
// Calculate base damage (without elemental bonus first)
|
||||||
const floorElement = getFloorElement(currentFloor);
|
const floorElement = getFloorElement(currentFloor);
|
||||||
const damage = calcDamage(
|
const baseDamage = calcDamage(
|
||||||
{ signedPacts },
|
{ signedPacts },
|
||||||
spellId,
|
spellId,
|
||||||
floorElement,
|
undefined,
|
||||||
disciplineEffects,
|
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)
|
// Let gameStore apply damage modifiers (executioner, berserker)
|
||||||
const result = onDamageDealt(damage);
|
const result = onDamageDealt(damage);
|
||||||
@@ -125,7 +132,6 @@ export function processCombatTick(
|
|||||||
if (floorHP <= 0) {
|
if (floorHP <= 0) {
|
||||||
const guardian = getGuardianForFloor(currentFloor);
|
const guardian = getGuardianForFloor(currentFloor);
|
||||||
onFloorCleared(currentFloor, !!guardian);
|
onFloorCleared(currentFloor, !!guardian);
|
||||||
|
|
||||||
currentFloor = Math.min(currentFloor + 1, 100);
|
currentFloor = Math.min(currentFloor + 1, 100);
|
||||||
floorMaxHP = getFloorMaxHP(currentFloor);
|
floorMaxHP = getFloorMaxHP(currentFloor);
|
||||||
floorHP = floorMaxHP;
|
floorHP = floorMaxHP;
|
||||||
@@ -159,14 +165,20 @@ export function processCombatTick(
|
|||||||
const eAfterCost = deductSpellCost(eSpellDef.cost, rawMana, elements);
|
const eAfterCost = deductSpellCost(eSpellDef.cost, rawMana, elements);
|
||||||
rawMana = eAfterCost.rawMana;
|
rawMana = eAfterCost.rawMana;
|
||||||
elements = eAfterCost.elements;
|
elements = eAfterCost.elements;
|
||||||
// Calculate damage
|
// Calculate damage — for multi-element guardians, use all elements
|
||||||
const eFloorElement = getFloorElement(currentFloor);
|
const eFloorElement = getFloorElement(currentFloor);
|
||||||
const eDamage = calcDamage(
|
const eBaseDamage = calcDamage(
|
||||||
{ signedPacts },
|
{ signedPacts },
|
||||||
eSpell.spellId,
|
eSpell.spellId,
|
||||||
eFloorElement,
|
undefined,
|
||||||
disciplineEffects,
|
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);
|
const eResult = onDamageDealt(eDamage);
|
||||||
rawMana = eResult.rawMana;
|
rawMana = eResult.rawMana;
|
||||||
@@ -182,7 +194,20 @@ export function processCombatTick(
|
|||||||
eCastProgress -= 1;
|
eCastProgress -= 1;
|
||||||
eSafetyCounter++;
|
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
|
// Update equipment spell state
|
||||||
|
|||||||
@@ -82,9 +82,18 @@ export const useCraftingStore = create<CraftingStore>()(
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelDesign: () => {
|
cancelDesign: (slot?: 1 | 2) => {
|
||||||
const state = get();
|
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 });
|
set({ designProgress: null });
|
||||||
useCombatStore.setState({ currentAction: 'meditate' });
|
useCombatStore.setState({ currentAction: 'meditate' });
|
||||||
} else if (state.designProgress2) {
|
} else if (state.designProgress2) {
|
||||||
@@ -136,7 +145,7 @@ export const useCraftingStore = create<CraftingStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
cancelApplication: () => {
|
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' });
|
useCombatStore.setState({ currentAction: 'meditate' });
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -169,7 +178,7 @@ export const useCraftingStore = create<CraftingStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
cancelPreparation: () => {
|
cancelPreparation: () => {
|
||||||
PreparationActions.cancelPreparation(set);
|
PreparationActions.cancelPreparation(get, set);
|
||||||
useCombatStore.setState({ currentAction: 'meditate' });
|
useCombatStore.setState({ currentAction: 'meditate' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export interface CraftingActions {
|
|||||||
setApplicationProgress: (progress: ApplicationProgress | null) => void;
|
setApplicationProgress: (progress: ApplicationProgress | null) => void;
|
||||||
setEquipmentCraftingProgress: (progress: EquipmentCraftingProgress | null) => void;
|
setEquipmentCraftingProgress: (progress: EquipmentCraftingProgress | null) => void;
|
||||||
startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => boolean;
|
startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => boolean;
|
||||||
cancelDesign: () => void;
|
cancelDesign: (slot?: 1 | 2) => void;
|
||||||
saveDesign: (design: EnchantmentDesign) => void;
|
saveDesign: (design: EnchantmentDesign) => void;
|
||||||
deleteDesign: (designId: string) => void;
|
deleteDesign: (designId: string) => void;
|
||||||
startApplying: (equipmentInstanceId: string, designId: string) => boolean;
|
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 effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
|
||||||
|
|
||||||
const rawAfterConversion = ctx.mana.rawMana + rawManaDelta;
|
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 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') {
|
if (ctx.combat.currentAction === 'convert') {
|
||||||
const convertResult = useManaStore.getState().processConvertAction(rawMana);
|
const convertResult = useManaStore.getState().processConvertAction(rawMana);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
deductFabricatorMana,
|
deductFabricatorMana,
|
||||||
deductMaterials,
|
deductMaterials,
|
||||||
makeFabricatorProgress,
|
makeFabricatorProgress,
|
||||||
|
refundFabricatorMana,
|
||||||
} from '../../crafting-fabricator';
|
} from '../../crafting-fabricator';
|
||||||
import { useManaStore } from '../manaStore';
|
import { useManaStore } from '../manaStore';
|
||||||
import { useCombatStore } from '../combatStore';
|
import { useCombatStore } from '../combatStore';
|
||||||
@@ -50,33 +51,71 @@ export function startCraftingEquipment(
|
|||||||
export function cancelEquipmentCrafting(get: GetFn, set: SetFn): void {
|
export function cancelEquipmentCrafting(get: GetFn, set: SetFn): void {
|
||||||
const progress = get().equipmentCraftingProgress;
|
const progress = get().equipmentCraftingProgress;
|
||||||
if (!progress) return;
|
if (!progress) return;
|
||||||
const cancelResult = CraftingEquipment.cancelEquipmentCrafting(
|
|
||||||
progress.blueprintId,
|
const isFabricator = progress.blueprintId.startsWith('fabricator-');
|
||||||
progress.manaSpent,
|
|
||||||
progress.progress,
|
if (isFabricator) {
|
||||||
progress.required,
|
// Fabricator recipe cancel: refund elemental/raw mana and materials
|
||||||
);
|
const recipeId = progress.blueprintId.replace('fabricator-', '');
|
||||||
// Refund materials proportionally to remaining progress
|
const recipe = getFabricatorRecipe(recipeId);
|
||||||
const recipe = CraftingEquipment.getRecipe(progress.blueprintId);
|
if (recipe) {
|
||||||
if (recipe) {
|
const remainingFraction = progress.required > 0
|
||||||
const remainingFraction = progress.required > 0
|
? Math.max(0, (progress.required - progress.progress) / progress.required)
|
||||||
? Math.max(0, (progress.required - progress.progress) / progress.required)
|
: 1;
|
||||||
: 1;
|
// Full refund for unspent progress, 50% for spent progress
|
||||||
const currentMaterials = get().lootInventory.materials;
|
const refundRate = remainingFraction + (1 - remainingFraction) * 0.5;
|
||||||
const refundedMaterials = { ...currentMaterials };
|
const manaRefund = Math.floor(progress.manaSpent * refundRate);
|
||||||
for (const [matId, amount] of Object.entries(recipe.materials)) {
|
|
||||||
const refundAmount = Math.floor(amount * remainingFraction);
|
// Refund the correct mana type
|
||||||
if (refundAmount > 0) {
|
const rawMana = useManaStore.getState().rawMana;
|
||||||
refundedMaterials[matId] = (refundedMaterials[matId] || 0) + refundAmount;
|
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 {
|
} 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' });
|
useCombatStore.setState({ currentAction: 'meditate' });
|
||||||
useUIStore.getState().addLog(cancelResult.logMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startFabricatorCrafting(recipeId: string, get: GetFn, set: SetFn): boolean {
|
export function startFabricatorCrafting(recipeId: string, get: GetFn, set: SetFn): boolean {
|
||||||
|
|||||||
@@ -50,6 +50,17 @@ export function getElementalBonus(spellElem: string, floorElem: string): number
|
|||||||
return 1.0; // Neutral
|
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 ─────────────────────────────────────────────────────────────
|
// ─── Boon Bonuses ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface BoonBonuses {
|
export interface BoonBonuses {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export {
|
|||||||
// computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades
|
// computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades
|
||||||
export {
|
export {
|
||||||
getElementalBonus,
|
getElementalBonus,
|
||||||
|
getMultiElementBonus,
|
||||||
getBoonBonuses,
|
getBoonBonuses,
|
||||||
calcDamage,
|
calcDamage,
|
||||||
calcInsight,
|
calcInsight,
|
||||||
|
|||||||
Reference in New Issue
Block a user