refactor: remove GUARDIANS constant, consolidate into guardian-data.ts

- Delete src/lib/game/constants/guardians.ts (the old static GUARDIANS constant)
- Create src/lib/game/data/guardian-data.ts with BASE_GUARDIANS (same data, new home)
- Remove GUARDIANS export from constants/index.ts
- Update all 11 files that imported GUARDIANS to use getGuardianForFloor() or BASE_GUARDIANS:
  - useGameDerived.ts, combat-actions.ts, gameStore.ts, prestigeStore.ts
  - combat-utils.ts, room-utils.ts, floor-utils.ts, spire-utils.ts
  - SpireCombatPage.tsx, SpireHeader.tsx
- Update 4 test files to use getGuardianForFloor() instead of GUARDIANS constant
- guardian-encounters.ts now imports BASE_GUARDIANS from guardian-data.ts
- Split room-utils.test.ts (505 lines) into room-utils.test.ts + room-utils-floor-state.test.ts
This commit is contained in:
2026-05-23 16:09:19 +02:00
parent d7b822d965
commit 513cab81a3
21 changed files with 228 additions and 217 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-05-23T11:46:22.135Z Generated: 2026-05-23T12:53:16.980Z
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files. Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
1. Processed 129 files (1.5s) (3 warnings) 1. Processed 129 files (1.4s) (3 warnings)
2. 1) stores/gameStore.ts > stores/gameActions.ts 2. 1) stores/gameStore.ts > stores/gameActions.ts
3. 2) stores/gameStore.ts > stores/gameLoopActions.ts 3. 2) stores/gameStore.ts > stores/gameLoopActions.ts
4. 3) stores/gameStore.ts > stores/tick-pipeline.ts 4. 3) stores/gameStore.ts > stores/tick-pipeline.ts
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-05-23T11:46:20.479Z", "generated": "2026-05-23T12:53:15.385Z",
"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."
}, },
@@ -628,7 +628,7 @@
"types.ts" "types.ts"
], ],
"utils/pact-utils.ts": [ "utils/pact-utils.ts": [
"constants.ts" "data/guardian-encounters.ts"
], ],
"utils/result.ts": [], "utils/result.ts": [],
"utils/room-utils.ts": [ "utils/room-utils.ts": [
+2 -1
View File
@@ -208,6 +208,7 @@ Mana-Loop/
│ │ │ ├── mana-utils.test.ts │ │ │ ├── mana-utils.test.ts
│ │ │ ├── pact-utils.test.ts │ │ │ ├── pact-utils.test.ts
│ │ │ ├── regression-fixes.test.ts │ │ │ ├── regression-fixes.test.ts
│ │ │ ├── room-utils-floor-state.test.ts
│ │ │ ├── room-utils.test.ts │ │ │ ├── room-utils.test.ts
│ │ │ ├── spire-utils.test.ts │ │ │ ├── spire-utils.test.ts
│ │ │ ├── store-actions-combat-prestige.test.ts │ │ │ ├── store-actions-combat-prestige.test.ts
@@ -229,7 +230,6 @@ Mana-Loop/
│ │ │ │ └── utility-spells.ts │ │ │ │ └── utility-spells.ts
│ │ │ ├── core.ts │ │ │ ├── core.ts
│ │ │ ├── elements.ts │ │ │ ├── elements.ts
│ │ │ ├── guardians.ts
│ │ │ ├── index.ts │ │ │ ├── index.ts
│ │ │ ├── prestige.ts │ │ │ ├── prestige.ts
│ │ │ ├── rooms.ts │ │ │ ├── rooms.ts
@@ -296,6 +296,7 @@ Mana-Loop/
│ │ │ ├── enchantment-effects.ts │ │ │ ├── enchantment-effects.ts
│ │ │ ├── enchantment-types.ts │ │ │ ├── enchantment-types.ts
│ │ │ ├── fabricator-recipes.ts │ │ │ ├── fabricator-recipes.ts
│ │ │ ├── guardian-data.ts
│ │ │ ├── guardian-encounters.ts │ │ │ ├── guardian-encounters.ts
│ │ │ └── loot-drops.ts │ │ │ └── loot-drops.ts
│ │ ├── effects/ │ │ ├── effects/
@@ -29,8 +29,9 @@ describe('Tab barrel export', () => {
describe('Guardian data', () => { describe('Guardian data', () => {
it('all guardians have required fields', async () => { it('all guardians have required fields', async () => {
const { GUARDIANS } = await import('@/lib/game/constants/guardians'); const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters');
for (const [floor, def] of Object.entries(GUARDIANS)) { for (const floor of [10, 20, 30, 40, 50, 60, 80, 90, 100]) {
const def = getGuardianForFloor(floor)!;
expect(def.name).toBeTruthy(); expect(def.name).toBeTruthy();
expect(def.element).toBeTruthy(); expect(def.element).toBeTruthy();
expect(def.hp).toBeGreaterThan(0); expect(def.hp).toBeGreaterThan(0);
@@ -49,10 +50,10 @@ describe('Guardian data', () => {
}); });
it('guardians are defined at expected floors', async () => { it('guardians are defined at expected floors', async () => {
const { GUARDIANS } = await import('@/lib/game/constants/guardians'); const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters');
const expectedFloors = [10, 20, 30, 40, 50, 60, 80, 90, 100]; const expectedFloors = [10, 20, 30, 40, 50, 60, 80, 90, 100];
for (const floor of expectedFloors) { for (const floor of expectedFloors) {
expect(GUARDIANS[floor]).toBeDefined(); expect(getGuardianForFloor(floor)).not.toBeNull();
} }
}); });
@@ -62,8 +63,9 @@ describe('Guardian data', () => {
'critChance', 'critDamage', 'spellEfficiency', 'manaGain', 'insightGain', 'critChance', 'critDamage', 'spellEfficiency', 'manaGain', 'insightGain',
'studySpeed', 'prestigeInsight', 'studySpeed', 'prestigeInsight',
]; ];
const { GUARDIANS } = await import('@/lib/game/constants/guardians'); const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters');
for (const def of Object.values(GUARDIANS)) { for (const floor of [10, 20, 30, 40, 50, 60, 80, 90, 100]) {
const def = getGuardianForFloor(floor)!;
for (const boon of def.boons) { for (const boon of def.boons) {
expect(validBoonTypes).toContain(boon.type); expect(validBoonTypes).toContain(boon.type);
expect(boon.value).toBeGreaterThan(0); expect(boon.value).toBeGreaterThan(0);
@@ -6,8 +6,7 @@ import { useCombatStore, useManaStore, usePrestigeStore, fmt, computeMaxMana, co
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects'; import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
import { getUnifiedEffects } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects';
import { useCraftingStore } from '@/lib/game/stores/craftingStore'; import { useCraftingStore } from '@/lib/game/stores/craftingStore';
import { GUARDIANS } from '@/lib/game/constants'; import { getGuardianForFloor, isGuardianFloor } from '@/lib/game/data/guardian-encounters';
import { getExtendedGuardian, isGuardianFloor } from '@/lib/game/data/guardian-encounters';
import { getRoomsForFloor, generateSpireFloorState } from '@/lib/game/utils/spire-utils'; import { getRoomsForFloor, generateSpireFloorState } from '@/lib/game/utils/spire-utils';
import { SpireHeader } from './SpireHeader'; import { SpireHeader } from './SpireHeader';
import { RoomDisplay } from './RoomDisplay'; import { RoomDisplay } from './RoomDisplay';
@@ -122,7 +121,7 @@ export function SpireCombatPage() {
setClearedFloor(currentFloor, true); setClearedFloor(currentFloor, true);
if (wasGuardian) { if (wasGuardian) {
const guardian = GUARDIANS[currentFloor] || getExtendedGuardian(currentFloor); const guardian = getGuardianForFloor(currentFloor);
if (guardian) { if (guardian) {
addActivityLog('enemy_defeated', `⚔️ ${guardian.name} defeated!`, { addActivityLog('enemy_defeated', `⚔️ ${guardian.name} defeated!`, {
enemyName: guardian.name, enemyName: guardian.name,
@@ -5,8 +5,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { Mountain, ArrowUp, ArrowDown, LogOut } from 'lucide-react'; import { Mountain, ArrowUp, ArrowDown, LogOut } from 'lucide-react';
import { GUARDIANS } from '@/lib/game/constants'; import { getGuardianForFloor, isGuardianFloor } from '@/lib/game/data/guardian-encounters';
import { isGuardianFloor, getExtendedGuardian } from '@/lib/game/data/guardian-encounters';
interface SpireHeaderProps { interface SpireHeaderProps {
currentFloor: number; currentFloor: number;
@@ -34,7 +33,7 @@ export function SpireHeader({
const maxFloorReached = useCombatStore((s) => s.maxFloorReached); const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
const { insight } = usePrestigeStore((s) => ({ insight: s.insight })); const { insight } = usePrestigeStore((s) => ({ insight: s.insight }));
const guardian = GUARDIANS[currentFloor] || getExtendedGuardian(currentFloor); const guardian = getGuardianForFloor(currentFloor);
const isGuardian = isGuardianFloor(currentFloor); const isGuardian = isGuardianFloor(currentFloor);
const hpPercent = floorMaxHP > 0 ? (floorHP / floorMaxHP) * 100 : 100; const hpPercent = floorMaxHP > 0 ? (floorHP / floorMaxHP) * 100 : 100;
const roomProgress = totalRooms > 0 ? ((roomsCleared) / totalRooms) * 100 : 0; const roomProgress = totalRooms > 0 ? ((roomsCleared) / totalRooms) * 100 : 0;
@@ -27,23 +27,26 @@ describe('Tab barrel export', () => {
// ─── Test: Guardian data ─────────────────────────────────────────────────────── // ─── Test: Guardian data ───────────────────────────────────────────────────────
describe('Guardian constants', () => { describe('Guardian data', () => {
it('has 9 guardians defined', async () => { it('has 9 static guardians plus extended guardians', async () => {
const { GUARDIANS } = await import('@/lib/game/constants'); const { getAllGuardianFloors } = await import('@/lib/game/data/guardian-encounters');
expect(Object.keys(GUARDIANS).length).toBe(9); const floors = getAllGuardianFloors();
// 9 static + compound (110) + exotic (120,130,140) + combo (150-240) = 23 total
expect(floors.length).toBeGreaterThanOrEqual(9);
}); });
it('guardians are at expected floors', async () => { it('guardians are at expected base floors', async () => {
const { GUARDIANS } = await import('@/lib/game/constants'); const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters');
const expectedFloors = [10, 20, 30, 40, 50, 60, 80, 90, 100]; const expectedFloors = [10, 20, 30, 40, 50, 60, 80, 90, 100];
for (const floor of expectedFloors) { for (const floor of expectedFloors) {
expect(GUARDIANS[floor]).toBeDefined(); expect(getGuardianForFloor(floor)).not.toBeNull();
} }
}); });
it('all guardians have required fields', async () => { it('all base guardians have required fields', async () => {
const { GUARDIANS } = await import('@/lib/game/constants'); const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters');
for (const [, def] of Object.entries(GUARDIANS)) { for (const floor of [10, 20, 30, 40, 50, 60, 80, 90, 100]) {
const def = getGuardianForFloor(floor)!;
expect(def.name).toBeTruthy(); expect(def.name).toBeTruthy();
expect(def.element).toBeTruthy(); expect(def.element).toBeTruthy();
expect(def.hp).toBeGreaterThan(0); expect(def.hp).toBeGreaterThan(0);
@@ -53,9 +56,9 @@ describe('Guardian constants', () => {
} }
}); });
it('all guardians have unique elements', async () => { it('all base guardians have unique elements', async () => {
const { GUARDIANS } = await import('@/lib/game/constants'); const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters');
const elements = Object.values(GUARDIANS).map((g) => g.element); const elements = [10, 20, 30, 40, 50, 60, 80, 90, 100].map((f) => getGuardianForFloor(f)!.element);
const uniqueElements = new Set(elements); const uniqueElements = new Set(elements);
expect(uniqueElements.size).toBe(elements.length); expect(uniqueElements.size).toBe(elements.length);
}); });
+27 -27
View File
@@ -3,7 +3,7 @@ import {
computePactMultiplier, computePactMultiplier,
computePactInsightMultiplier, computePactInsightMultiplier,
} from '../utils/pact-utils'; } from '../utils/pact-utils';
import { GUARDIANS } from '../constants'; import { getGuardianForFloor } from '../data/guardian-encounters';
// Helper: compute actual multiplier values // Helper: compute actual multiplier values
function getDamageMult(mult: number, extraPacts: number, mitigation: number): number { function getDamageMult(mult: number, extraPacts: number, mitigation: number): number {
const numAdditional = extraPacts; const numAdditional = extraPacts;
@@ -41,14 +41,14 @@ describe('computePactMultiplier', () => {
}); });
it('should return guardian damage multiplier for a single guardian pact', () => { it('should return guardian damage multiplier for a single guardian pact', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const result = computePactMultiplier({ signedPacts: [10] }); const result = computePactMultiplier({ signedPacts: [10] });
expect(result).toBe(floor10.damageMultiplier); expect(result).toBe(floor10.damageMultiplier);
}); });
it('should multiply damage multipliers for multiple guardian pacts', () => { it('should multiply damage multipliers for multiple guardian pacts', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const floor20 = GUARDIANS[20]; const floor20 = getGuardianForFloor(20)!;
const result = computePactMultiplier({ signedPacts: [10, 20] }); const result = computePactMultiplier({ signedPacts: [10, 20] });
// With 2 pacts: baseMult = f10 * f20, then penalty = 0.5 * 1 = 0.5 // With 2 pacts: baseMult = f10 * f20, then penalty = 0.5 * 1 = 0.5
// effectivePenalty = max(0, 0.5 - 0) = 0.5 // effectivePenalty = max(0, 0.5 - 0) = 0.5
@@ -58,8 +58,8 @@ describe('computePactMultiplier', () => {
}); });
it('should apply interference mitigation to reduce penalty', () => { it('should apply interference mitigation to reduce penalty', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const floor20 = GUARDIANS[20]; const floor20 = getGuardianForFloor(20)!;
const baseMult = floor10.damageMultiplier * floor20.damageMultiplier; const baseMult = floor10.damageMultiplier * floor20.damageMultiplier;
// No mitigation: penalty = 0.5, result = baseMult * 0.5 // No mitigation: penalty = 0.5, result = baseMult * 0.5
@@ -79,8 +79,8 @@ describe('computePactMultiplier', () => {
}); });
it('should apply synergy bonus when mitigation exceeds 5', () => { it('should apply synergy bonus when mitigation exceeds 5', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const floor20 = GUARDIANS[20]; const floor20 = getGuardianForFloor(20)!;
const baseMult = floor10.damageMultiplier * floor20.damageMultiplier; const baseMult = floor10.damageMultiplier * floor20.damageMultiplier;
// mitigation = 6: synergyBonus = (6-5)*0.1 = 0.1, result = baseMult * 1.1 // mitigation = 6: synergyBonus = (6-5)*0.1 = 0.1, result = baseMult * 1.1
@@ -92,8 +92,8 @@ describe('computePactMultiplier', () => {
}); });
it('should scale synergy bonus with higher mitigation', () => { it('should scale synergy bonus with higher mitigation', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const floor20 = GUARDIANS[20]; const floor20 = getGuardianForFloor(20)!;
const baseMult = floor10.damageMultiplier * floor20.damageMultiplier; const baseMult = floor10.damageMultiplier * floor20.damageMultiplier;
const mit5 = computePactMultiplier({ const mit5 = computePactMultiplier({
@@ -115,9 +115,9 @@ describe('computePactMultiplier', () => {
}); });
it('should handle three pacts with penalty', () => { it('should handle three pacts with penalty', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const floor20 = GUARDIANS[20]; const floor20 = getGuardianForFloor(20)!;
const floor30 = GUARDIANS[30]; const floor30 = getGuardianForFloor(30)!;
const baseMult = floor10.damageMultiplier * floor20.damageMultiplier * floor30.damageMultiplier; const baseMult = floor10.damageMultiplier * floor20.damageMultiplier * floor30.damageMultiplier;
// 3 pacts: numAdditional = 2, basePenalty = 1.0, effectivePenalty = 1.0 // 3 pacts: numAdditional = 2, basePenalty = 1.0, effectivePenalty = 1.0
@@ -130,9 +130,9 @@ describe('computePactMultiplier', () => {
}); });
it('should handle three pacts with partial mitigation', () => { it('should handle three pacts with partial mitigation', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const floor20 = GUARDIANS[20]; const floor20 = getGuardianForFloor(20)!;
const floor30 = GUARDIANS[30]; const floor30 = getGuardianForFloor(30)!;
const baseMult = floor10.damageMultiplier * floor20.damageMultiplier * floor30.damageMultiplier; const baseMult = floor10.damageMultiplier * floor20.damageMultiplier * floor30.damageMultiplier;
// 3 pacts: numAdditional = 2, basePenalty = 1.0 // 3 pacts: numAdditional = 2, basePenalty = 1.0
@@ -146,7 +146,7 @@ describe('computePactMultiplier', () => {
}); });
it('should handle mix of guardian and non-guardian floors', () => { it('should handle mix of guardian and non-guardian floors', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
// Non-guardian floors contribute nothing (no entry in GUARDIANS) // Non-guardian floors contribute nothing (no entry in GUARDIANS)
const result = computePactMultiplier({ signedPacts: [5, 10] }); const result = computePactMultiplier({ signedPacts: [5, 10] });
// Only floor 10 counts: baseMult = floor10.damageMultiplier // Only floor 10 counts: baseMult = floor10.damageMultiplier
@@ -181,22 +181,22 @@ describe('computePactInsightMultiplier', () => {
}); });
it('should return guardian insight multiplier for a single guardian pact', () => { it('should return guardian insight multiplier for a single guardian pact', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const result = computePactInsightMultiplier({ signedPacts: [10] }); const result = computePactInsightMultiplier({ signedPacts: [10] });
expect(result).toBe(floor10.insightMultiplier); expect(result).toBe(floor10.insightMultiplier);
}); });
it('should multiply insight multipliers for multiple guardian pacts', () => { it('should multiply insight multipliers for multiple guardian pacts', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const floor20 = GUARDIANS[20]; const floor20 = getGuardianForFloor(20)!;
const result = computePactInsightMultiplier({ signedPacts: [10, 20] }); const result = computePactInsightMultiplier({ signedPacts: [10, 20] });
const expected = floor10.insightMultiplier * floor20.insightMultiplier * 0.5; const expected = floor10.insightMultiplier * floor20.insightMultiplier * 0.5;
expect(result).toBe(expected); expect(result).toBe(expected);
}); });
it('should apply interference mitigation to reduce penalty', () => { it('should apply interference mitigation to reduce penalty', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const floor20 = GUARDIANS[20]; const floor20 = getGuardianForFloor(20)!;
const baseMult = floor10.insightMultiplier * floor20.insightMultiplier; const baseMult = floor10.insightMultiplier * floor20.insightMultiplier;
const noMitigation = computePactInsightMultiplier({ const noMitigation = computePactInsightMultiplier({
@@ -213,8 +213,8 @@ describe('computePactInsightMultiplier', () => {
}); });
it('should apply synergy bonus when mitigation exceeds 5', () => { it('should apply synergy bonus when mitigation exceeds 5', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const floor20 = GUARDIANS[20]; const floor20 = getGuardianForFloor(20)!;
const baseMult = floor10.insightMultiplier * floor20.insightMultiplier; const baseMult = floor10.insightMultiplier * floor20.insightMultiplier;
const result = computePactInsightMultiplier({ const result = computePactInsightMultiplier({
@@ -226,9 +226,9 @@ describe('computePactInsightMultiplier', () => {
}); });
it('should handle three pacts with full penalty', () => { it('should handle three pacts with full penalty', () => {
const floor10 = GUARDIANS[10]; const floor10 = getGuardianForFloor(10)!;
const floor20 = GUARDIANS[20]; const floor20 = getGuardianForFloor(20)!;
const floor30 = GUARDIANS[30]; const floor30 = getGuardianForFloor(30)!;
const baseMult = floor10.insightMultiplier * floor20.insightMultiplier * floor30.insightMultiplier; const baseMult = floor10.insightMultiplier * floor20.insightMultiplier * floor30.insightMultiplier;
const result = computePactInsightMultiplier({ const result = computePactInsightMultiplier({
@@ -0,0 +1,110 @@
import { describe, it, expect } from 'vitest';
import { generateFloorState } from '../utils/room-utils';
import { PUZZLE_ROOMS, SWARM_CONFIG, SPEED_ROOM_CONFIG } from '../constants';
import { getGuardianForFloor } from '../data/guardian-encounters';
import { getFloorMaxHP, getDodgeChance } from '../utils/floor-utils';
// ─── generateFloorState ───────────────────────────────────────────────────────
describe('generateFloorState', () => {
it('should return a FloorState object', () => {
const state = generateFloorState(1);
expect(typeof state).toBe('object');
expect(state).toHaveProperty('roomType');
expect(state).toHaveProperty('enemies');
});
it('should generate guardian state for guardian floor', () => {
const state = generateFloorState(10);
expect(state.roomType).toBe('guardian');
expect(state.enemies.length).toBe(1);
const g10 = getGuardianForFloor(10)!;
expect(state.enemies[0].name).toBe(g10.name);
expect(state.enemies[0].hp).toBe(g10.hp);
expect(state.enemies[0].element).toBe(g10.element);
});
it('should generate combat state for non-guardian floor with combat', () => {
const originalRandom = Math.random;
Math.random = () => 0.5; // Won't trigger special rooms
const state = generateFloorState(5);
expect(state.roomType).toBe('combat');
expect(state.enemies.length).toBe(1);
expect(state.enemies[0].hp).toBe(getFloorMaxHP(5));
Math.random = originalRandom;
});
it('should generate swarm state for swarm room', () => {
const originalRandom = Math.random;
Math.random = () => 0.14; // < SWARM_ROOM_CHANCE (0.15)
const state = generateFloorState(5);
expect(state.roomType).toBe('swarm');
expect(Array.isArray(state.enemies)).toBe(true);
expect(state.enemies.length).toBeGreaterThanOrEqual(SWARM_CONFIG.minEnemies);
Math.random = originalRandom;
});
it('should generate speed state for speed room', () => {
const originalRandom = Math.random;
Math.random = () => 0.09; // < SPEED_ROOM_CHANCE (0.10)
const state = generateFloorState(5);
expect(state.roomType).toBe('speed');
expect(state.enemies.length).toBe(1);
Math.random = originalRandom;
});
it('should generate puzzle state for puzzle room', () => {
const originalRandom = Math.random;
Math.random = () => 0.19; // < PUZZLE_ROOM_CHANCE (0.20)
const state = generateFloorState(7);
expect(state.roomType).toBe('puzzle');
expect(Array.isArray(state.enemies)).toBe(true);
expect(state.enemies).toEqual([]);
expect(state.puzzleProgress).toBe(0);
expect(typeof state.puzzleRequired).toBe('number');
expect(typeof state.puzzleId).toBe('string');
expect(typeof state.puzzleAttunements).toBe('object');
Math.random = originalRandom;
});
it('should fill puzzle attunements from PUZZLE_ROOMS', () => {
const originalRandom = Math.random;
Math.random = () => 0.19;
const state = generateFloorState(7);
expect(state.roomType).toBe('puzzle');
expect(state.puzzleAttunements.length).toBeGreaterThan(0);
expect(typeof state.puzzleAttunements[0]).toBe('string');
Math.random = originalRandom;
});
it('should use correct element for floor', () => {
const state = generateFloorState(1);
expect(state.enemies[0].element).toBe('fire');
const state2 = generateFloorState(2);
expect(state2.enemies[0].element).toBe('water');
});
it('combat enemy HP should match floor max HP', () => {
const state = generateFloorState(50);
expect(state.enemies[0].hp).toBe(getFloorMaxHP(50));
});
it('speed room should have correct dodge chance', () => {
const originalRandom = Math.random;
Math.random = () => 0.09; // Speed room
const speedState = generateFloorState(50);
expect(speedState.roomType).toBe('speed');
expect(speedState.enemies[0].dodgeChance).toBe(getDodgeChance(50));
Math.random = originalRandom;
});
it('should handle very high floor number', () => {
const state = generateFloorState(1000);
expect(state.roomType).toBe('guardian');
});
it('should handle floor 0', () => {
const state = generateFloorState(0);
expect(state.roomType).toBe('combat');
});
});
+5 -115
View File
@@ -4,12 +4,10 @@ import {
getFloorArmor, getFloorArmor,
getDodgeChance, getDodgeChance,
getEnemyBarrier, getEnemyBarrier,
generateFloorState,
getPuzzleProgressSpeed, getPuzzleProgressSpeed,
} from '../utils/room-utils'; } from '../utils/room-utils';
import { GUARDIANS, FLOOR_ELEM_CYCLE, PUZZLE_ROOMS, SWARM_CONFIG, SPEED_ROOM_CONFIG, FLOOR_ARMOR_CONFIG } from '../constants'; import { FLOOR_ELEM_CYCLE, PUZZLE_ROOMS, SWARM_CONFIG, SPEED_ROOM_CONFIG, FLOOR_ARMOR_CONFIG } from '../constants';
import type { EnemyState, FloorState } from '../types'; import { getAllGuardianFloors } from '../data/guardian-encounters';
import { getFloorMaxHP, getFloorElement, getEnemyName } from '../utils/floor-utils';
// ─── generateRoomType ───────────────────────────────────────────────────────── // ─── generateRoomType ─────────────────────────────────────────────────────────
@@ -21,8 +19,8 @@ describe('generateRoomType', () => {
}); });
it('should return "guardian" for guardian floors', () => { it('should return "guardian" for guardian floors', () => {
// Get all guardian floors from GUARDIANS object // Get all guardian floors from the unified system
for (const floor of Object.keys(GUARDIANS).map(Number)) { for (const floor of getAllGuardianFloors().filter(f => f <= 100)) {
// Override and restore Math.random to ensure consistent result // Override and restore Math.random to ensure consistent result
const originalRandom = Math.random; const originalRandom = Math.random;
Math.random = () => 0.1; // Anything < PUZZLE_ROOM_CHANCE ensures non-puzzle Math.random = () => 0.1; // Anything < PUZZLE_ROOM_CHANCE ensures non-puzzle
@@ -163,7 +161,7 @@ describe('getFloorArmor', () => {
}); });
it('should return 0 for guardian floors', () => { it('should return 0 for guardian floors', () => {
expect(getFloorArmor(10)).toBe(0); // Ignis Prime has armor 0.10 in GUARDIANS expect(getFloorArmor(10)).toBe(0); // Floor 10 is a guardian floor
}); });
it('should return armor between min and max for non-guardian floor with armor', () => { it('should return armor between min and max for non-guardian floor with armor', () => {
@@ -310,114 +308,6 @@ describe('getEnemyBarrier', () => {
}); });
}); });
// ─── generateFloorState ───────────────────────────────────────────────────────
describe('generateFloorState', () => {
it('should return a FloorState object', () => {
const state = generateFloorState(1);
expect(typeof state).toBe('object');
expect(state).toHaveProperty('roomType');
expect(state).toHaveProperty('enemies');
});
it('should generate guardian state for guardian floor', () => {
const state = generateFloorState(10);
expect(state.roomType).toBe('guardian');
expect(state.enemies.length).toBe(1);
expect(state.enemies[0].name).toBe(GUARDIANS[10].name);
expect(state.enemies[0].hp).toBe(GUARDIANS[10].hp);
expect(state.enemies[0].element).toBe(GUARDIANS[10].element);
});
it('should generate combat state for non-guardian floor with combat', () => {
const originalRandom = Math.random;
Math.random = () => 0.5; // Won't trigger special rooms
const state = generateFloorState(5);
expect(state.roomType).toBe('combat');
expect(state.enemies.length).toBe(1);
expect(state.enemies[0].hp).toBe(getFloorMaxHP(5));
Math.random = originalRandom;
});
it('should generate swarm state for swarm room', () => {
const originalRandom = Math.random;
Math.random = () => 0.14; // < SWARM_ROOM_CHANCE (0.15)
const state = generateFloorState(5);
expect(state.roomType).toBe('swarm');
expect(Array.isArray(state.enemies)).toBe(true);
expect(state.enemies.length).toBeGreaterThanOrEqual(SWARM_CONFIG.minEnemies);
Math.random = originalRandom;
});
it('should generate speed state for speed room', () => {
const originalRandom = Math.random;
Math.random = () => 0.09; // < SPEED_ROOM_CHANCE (0.10)
const state = generateFloorState(5);
expect(state.roomType).toBe('speed');
expect(state.enemies.length).toBe(1);
Math.random = originalRandom;
});
it('should generate puzzle state for puzzle room', () => {
const originalRandom = Math.random;
Math.random = () => 0.19; // < PUZZLE_ROOM_CHANCE (0.20)
const state = generateFloorState(7);
expect(state.roomType).toBe('puzzle');
expect(Array.isArray(state.enemies)).toBe(true); // Array, even if empty
expect(state.enemies).toEqual([]);
expect(state.puzzleProgress).toBe(0); // Empty array is truthy, empty is falsy
expect(typeof state.puzzleRequired).toBe('number');
expect(typeof state.puzzleId).toBe('string');
expect(typeof state.puzzleAttunements).toBe('object');
Math.random = originalRandom;
});
it('should fill puzzle attunements from PUZZLE_ROOMS', () => {
const originalRandom = Math.random;
Math.random = () => 0.19;
const state = generateFloorState(7);
expect(state.roomType).toBe('puzzle');
expect(state.puzzleAttunements.length).toBeGreaterThan(0);
expect(typeof state.puzzleAttunements[0]).toBe('string');
Math.random = originalRandom;
});
it('should use correct element for floor', () => {
// Test multiple floors to verify element cycle
const state = generateFloorState(1);
expect(state.enemies[0].element).toBe('fire');
const state2 = generateFloorState(2);
expect(state2.enemies[0].element).toBe('water');
});
it('combat enemy HP should match floor max HP', () => {
const state = generateFloorState(50);
// Non-guardian floor 50 returns combat
expect(state.enemies[0].hp).toBe(getFloorMaxHP(50));
});
it('speed room should have correct dodge chance', () => {
const state = generateFloorState(50);
const originalRandom = Math.random;
Math.random = () => 0.09; // Speed room
const speedState = generateFloorState(50);
expect(speedState.roomType).toBe('speed');
expect(speedState.enemies[0].dodgeChance).toBe(getDodgeChance(50));
Math.random = originalRandom;
});
it('should handle very high floor number', () => {
const state = generateFloorState(1000);
expect(state.roomType).toBe('guardian');
});
it('should handle floor 0', () => {
const state = generateFloorState(0);
expect(state.roomType).toBe('combat'); // Guardian? No. Special room? No. Default combat.
});
});
// ─── getPuzzleProgressSpeed ─────────────────────────────────────────────────── // ─── getPuzzleProgressSpeed ───────────────────────────────────────────────────
describe('getPuzzleProgressSpeed', () => { describe('getPuzzleProgressSpeed', () => {
-3
View File
@@ -10,9 +10,6 @@ export { getStudySpeedMultiplier, getStudyCostMultiplier } from './core';
export { MANA_PER_ELEMENT, rawCost, elemCost, ELEMENTS, FLOOR_ELEM_CYCLE } from './elements'; export { MANA_PER_ELEMENT, rawCost, elemCost, ELEMENTS, FLOOR_ELEM_CYCLE } from './elements';
export { ELEMENT_OPPOSITES, ELEMENT_ICON_NAMES, BASE_UNLOCKED_ELEMENTS } from './elements'; export { ELEMENT_OPPOSITES, ELEMENT_ICON_NAMES, BASE_UNLOCKED_ELEMENTS } from './elements';
// Guardian constants
export { GUARDIANS } from './guardians';
// Spell constants // Spell constants
export { SPELLS_DEF } from './spells'; export { SPELLS_DEF } from './spells';
@@ -1,8 +1,9 @@
// ─── Guardians ──────────────────────────────────────────────────────────────── // ─── Base Guardian Definitions (floors 10100) ────────────────────────────────
// Static canonical guardian data for the first 100 floors.
import type { GuardianDef } from '../types'; import type { GuardianDef } from '../types';
// All guardians have armor - damage reduction percentage export const BASE_GUARDIANS: Record<number, GuardianDef> = {
export const GUARDIANS: Record<number, GuardianDef> = {
10: { 10: {
name: "Ignis Prime", element: "fire", hp: 5000, pact: 1.5, color: "#FF6B35", name: "Ignis Prime", element: "fire", hp: 5000, pact: 1.5, color: "#FF6B35",
armor: 0.10, armor: 0.10,
@@ -159,4 +160,4 @@ export const GUARDIANS: Record<number, GuardianDef> = {
damageMultiplier: 2.5, damageMultiplier: 2.5,
insightMultiplier: 2.0, insightMultiplier: 2.0,
}, },
}; };
+8 -11
View File
@@ -265,27 +265,24 @@ export function getExtendedGuardian(floor: number): GuardianDef | null {
} }
// ─── Unified Guardian System ───────────────────────────────────────────────── // ─── Unified Guardian System ─────────────────────────────────────────────────
// Merges the static GUARDIANS constant (floors 10100) with the extended // Merges the base guardians (floors 10100) with the extended procedural system
// procedural system (compound 110, exotic 120140, combo 150+). // (compound 110, exotic 120140, combo 150+).
// For floors 90100 the static definitions take precedence (canonical names).
import { GUARDIANS } from '../constants/guardians'; import { BASE_GUARDIANS } from './guardian-data';
/** Get the guardian for any floor, merging static and extended systems. */ /** Get the guardian for any floor, merging static and extended systems. */
export function getGuardianForFloor(floor: number): GuardianDef | null { export function getGuardianForFloor(floor: number): GuardianDef | null {
// Static GUARDIANS take precedence for floors 10100 (canonical definitions) if (BASE_GUARDIANS[floor]) {
if (GUARDIANS[floor]) { return { ...BASE_GUARDIANS[floor] };
return { ...GUARDIANS[floor] };
} }
// Extended system for floors beyond 100 (and compound floors not in GUARDIANS)
return getExtendedGuardian(floor); return getExtendedGuardian(floor);
} }
/** All guardian floors — merged from static + extended. */ /** All guardian floors — merged from base + extended. */
export function getAllGuardianFloors(): number[] { export function getAllGuardianFloors(): number[] {
const staticFloors = Object.keys(GUARDIANS).map(Number); const baseFloors = Object.keys(BASE_GUARDIANS).map(Number);
const extendedFloors = [110, 120, 130, 140, ...Array.from({ length: 10 }, (_, i) => 150 + i * 10)]; const extendedFloors = [110, 120, 130, 140, ...Array.from({ length: 10 }, (_, i) => 150 + i * 10)];
const all = new Set([...staticFloors, ...extendedFloors]); const all = new Set([...baseFloors, ...extendedFloors]);
return Array.from(all).sort((a, b) => a - b); return Array.from(all).sort((a, b) => a - b);
} }
+4 -3
View File
@@ -18,7 +18,8 @@ import {
getElementalBonus, getElementalBonus,
} from '../utils'; } from '../utils';
import { computePactMultiplier, computePactInsightMultiplier } from '../utils/pact-utils'; import { computePactMultiplier, computePactInsightMultiplier } from '../utils/pact-utils';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier, HOURS_PER_TICK, TICK_MS } from '../constants'; import { ELEMENTS, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier, HOURS_PER_TICK, TICK_MS } from '../constants';
import { getGuardianForFloor } from '../data/guardian-encounters';
import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects'; import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
/** /**
@@ -123,12 +124,12 @@ export function useCombatStats() {
); );
const isGuardianFloor = useMemo( const isGuardianFloor = useMemo(
() => !!GUARDIANS[currentFloor], () => !!getGuardianForFloor(currentFloor),
[currentFloor] [currentFloor]
); );
const currentGuardian = useMemo( const currentGuardian = useMemo(
() => GUARDIANS[currentFloor], () => getGuardianForFloor(currentFloor),
[currentFloor] [currentFloor]
); );
+6 -5
View File
@@ -2,7 +2,8 @@
// Pure combat logic — no cross-store getState() calls. // Pure combat logic — no cross-store getState() calls.
// All external data (signedPacts, etc.) is passed in as parameters. // All external data (signedPacts, etc.) is passed in as parameters.
import { SPELLS_DEF, GUARDIANS, HOURS_PER_TICK } from '../constants'; import { SPELLS_DEF, HOURS_PER_TICK } from '../constants';
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, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
@@ -114,16 +115,16 @@ export function processCombatTick(
// Check if floor is cleared // Check if floor is cleared
if (floorHP <= 0) { if (floorHP <= 0) {
const wasGuardian = GUARDIANS[currentFloor]; const guardian = getGuardianForFloor(currentFloor);
onFloorCleared(currentFloor, !!wasGuardian); 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;
castProgress = 0; castProgress = 0;
if (wasGuardian) { if (guardian) {
logMessages.push(`⚔️ ${wasGuardian.name} defeated!`); logMessages.push(`\u2694\ufe0f ${guardian.name} defeated!`);
} else if (currentFloor % 5 === 0) { } else if (currentFloor % 5 === 0) {
logMessages.push(`🏰 Floor ${currentFloor - 1} cleared!`); logMessages.push(`🏰 Floor ${currentFloor - 1} cleared!`);
} }
+5 -3
View File
@@ -4,7 +4,8 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { TICK_MS, HOURS_PER_TICK, MAX_DAY, GUARDIANS, getStudySpeedMultiplier } from '../constants'; import { TICK_MS, HOURS_PER_TICK, MAX_DAY, getStudySpeedMultiplier } from '../constants';
import { getGuardianForFloor } from '../data/guardian-encounters';
import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects'; 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';
@@ -234,7 +235,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
// Pact ritual progress // Pact ritual progress
if (ctx.prestige.pactRitualFloor !== null) { if (ctx.prestige.pactRitualFloor !== null) {
const guardian = GUARDIANS[ctx.prestige.pactRitualFloor]; const guardian = getGuardianForFloor(ctx.prestige.pactRitualFloor);
if (guardian) { if (guardian) {
const pactAffinityBonus = 1 - (ctx.prestige.prestigeUpgrades.pactAffinity || 0) * 0.1; const pactAffinityBonus = 1 - (ctx.prestige.prestigeUpgrades.pactAffinity || 0) * 0.1;
const requiredTime = guardian.pactTime * pactAffinityBonus; const requiredTime = guardian.pactTime * pactAffinityBonus;
@@ -275,7 +276,8 @@ export const useGameStore = create<GameCoordinatorStore>()(
1, 1,
(floor, wasGuardian) => { (floor, wasGuardian) => {
if (wasGuardian) { if (wasGuardian) {
addLog(`⚔️ ${GUARDIANS[floor]?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`); const defeatedGuardian = getGuardianForFloor(floor);
addLog(`\u2694\ufe0f ${defeatedGuardian?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`);
} else if (floor % 5 === 0) { } else if (floor % 5 === 0) {
addLog(`🏰 Floor ${floor} cleared!`); addLog(`🏰 Floor ${floor} cleared!`);
} }
+4 -3
View File
@@ -5,7 +5,8 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { createSafeStorage } from '../utils/safe-persist'; import { createSafeStorage } from '../utils/safe-persist';
import type { Memory } from '../types'; import type { Memory } from '../types';
import { GUARDIANS, PRESTIGE_DEF } from '../constants'; import { PRESTIGE_DEF } from '../constants';
import { getGuardianForFloor } from '../data/guardian-encounters';
import { ok, okVoid, fail, ErrorCode } from '../utils/result'; import { ok, okVoid, fail, ErrorCode } from '../utils/result';
import type { Result } from '../utils/result'; import type { Result } from '../utils/result';
@@ -151,7 +152,7 @@ export const usePrestigeStore = create<PrestigeStore>()(
startPactRitual: (floor: number, rawMana: number) => { startPactRitual: (floor: number, rawMana: number) => {
const state = get(); const state = get();
const guardian = GUARDIANS[floor]; const guardian = getGuardianForFloor(floor);
if (!guardian) return fail(ErrorCode.INVALID_INPUT, `No guardian at floor ${floor}`); if (!guardian) return fail(ErrorCode.INVALID_INPUT, `No guardian at floor ${floor}`);
if (!state.defeatedGuardians.includes(floor)) return fail(ErrorCode.GUARDIAN_NOT_DEFEATED, `Guardian at floor ${floor} has not been defeated`); if (!state.defeatedGuardians.includes(floor)) return fail(ErrorCode.GUARDIAN_NOT_DEFEATED, `Guardian at floor ${floor} has not been defeated`);
@@ -178,7 +179,7 @@ export const usePrestigeStore = create<PrestigeStore>()(
const state = get(); const state = get();
if (state.pactRitualFloor === null) return; if (state.pactRitualFloor === null) return;
const guardian = GUARDIANS[state.pactRitualFloor]; const guardian = getGuardianForFloor(state.pactRitualFloor);
if (!guardian) return; if (!guardian) return;
set({ set({
+5 -3
View File
@@ -2,8 +2,10 @@
import type { SpellCost, EquipmentInstance } from '../types'; import type { SpellCost, EquipmentInstance } from '../types';
import type { DisciplineBonuses } from './mana-utils'; import type { DisciplineBonuses } from './mana-utils';
import { GUARDIANS, SPELLS_DEF, ELEMENT_OPPOSITES, INCURSION_START_DAY, MAX_DAY } from '../constants'; import { SPELLS_DEF, ELEMENT_OPPOSITES, INCURSION_START_DAY, MAX_DAY } from '../constants';
import { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects'; import { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects';
import { getGuardianForFloor } from '../data/guardian-encounters';
import { BASE_GUARDIANS } from '../data/guardian-data';
// ─── Damage Calculation Params ────────────────────────────────────────────── // ─── Damage Calculation Params ──────────────────────────────────────────────
@@ -86,7 +88,7 @@ export function getBoonBonuses(signedPacts: number[]): BoonBonuses {
}; };
for (const floor of signedPacts) { for (const floor of signedPacts) {
const guardian = GUARDIANS[floor]; const guardian = getGuardianForFloor(floor);
if (!guardian) continue; if (!guardian) continue;
for (const boon of guardian.boons) { for (const boon of guardian.boons) {
@@ -158,7 +160,7 @@ export function calcDamage(
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15; const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
// Guardian bane bonus // Guardian bane bonus
const isGuardianFloor = floorElem && Object.values(GUARDIANS).some(g => g.element === floorElem); const isGuardianFloor = floorElem && Object.values(BASE_GUARDIANS).some(g => g.element === floorElem);
const guardianBonus = isGuardianFloor const guardianBonus = isGuardianFloor
? 1 + (skills.guardianBane || 0) * 0.2 ? 1 + (skills.guardianBane || 0) * 0.2
: 1; : 1;
+4 -2
View File
@@ -1,9 +1,11 @@
// ─── Floor Helpers ──────────────────────────────────────────────────────────── // ─── Floor Helpers ────────────────────────────────────────────────────────────
import { GUARDIANS, FLOOR_ELEM_CYCLE } from '../constants'; import { FLOOR_ELEM_CYCLE } from '../constants';
import { getGuardianForFloor } from '../data/guardian-encounters';
export function getFloorMaxHP(floor: number): number { export function getFloorMaxHP(floor: number): number {
if (GUARDIANS[floor]) return GUARDIANS[floor].hp; const guardian = getGuardianForFloor(floor);
if (guardian) return guardian.hp;
// Improved scaling: slower early game, faster late game // Improved scaling: slower early game, faster late game
const baseHP = 100; const baseHP = 100;
const floorScaling = floor * 50; const floorScaling = floor * 50;
+8 -5
View File
@@ -2,7 +2,8 @@
// Moved from store-modules/room-utils.ts to eliminate legacy dependencies // Moved from store-modules/room-utils.ts to eliminate legacy dependencies
import type { RoomType, FloorState, EnemyState } from '../types'; import type { RoomType, FloorState, EnemyState } from '../types';
import { GUARDIANS, FLOOR_ELEM_CYCLE, PUZZLE_ROOMS, PUZZLE_ROOM_INTERVAL, PUZZLE_ROOM_CHANCE, SWARM_ROOM_CHANCE, SPEED_ROOM_CHANCE, FLOOR_ARMOR_CONFIG, SWARM_CONFIG, SPEED_ROOM_CONFIG } from '../constants'; import { FLOOR_ELEM_CYCLE, PUZZLE_ROOMS, PUZZLE_ROOM_INTERVAL, PUZZLE_ROOM_CHANCE, SWARM_ROOM_CHANCE, SPEED_ROOM_CHANCE, FLOOR_ARMOR_CONFIG, SWARM_CONFIG, SPEED_ROOM_CONFIG } from '../constants';
import { getGuardianForFloor } from '../data/guardian-encounters';
import { getFloorMaxHP } from './floor-utils'; import { getFloorMaxHP } from './floor-utils';
import { getFloorElement } from './floor-utils'; import { getFloorElement } from './floor-utils';
import { getEnemyName, generateSwarmEnemies } from './enemy-utils'; import { getEnemyName, generateSwarmEnemies } from './enemy-utils';
@@ -10,7 +11,7 @@ import { getEnemyName, generateSwarmEnemies } from './enemy-utils';
// Generate room type for a floor // Generate room type for a floor
export function generateRoomType(floor: number): RoomType { export function generateRoomType(floor: number): RoomType {
// Guardian floors are always guardian type // Guardian floors are always guardian type
if (GUARDIANS[floor]) { if (getGuardianForFloor(floor)) {
return 'guardian'; return 'guardian';
} }
@@ -35,8 +36,9 @@ export function generateRoomType(floor: number): RoomType {
// Get armor for a non-guardian floor // Get armor for a non-guardian floor
export function getFloorArmor(floor: number): number { export function getFloorArmor(floor: number): number {
if (GUARDIANS[floor]) { const guardian = getGuardianForFloor(floor);
return GUARDIANS[floor].armor || 0; if (guardian) {
return guardian.armor || 0;
} }
// Armor becomes more common on higher floors // Armor becomes more common on higher floors
@@ -84,10 +86,11 @@ export function generateFloorState(floor: number): FloorState {
const roomType = generateRoomType(floor); const roomType = generateRoomType(floor);
const element = getFloorElement(floor); const element = getFloorElement(floor);
const baseHP = getFloorMaxHP(floor); const baseHP = getFloorMaxHP(floor);
const guardian = GUARDIANS[floor]; const guardian = getGuardianForFloor(floor);
switch (roomType) { switch (roomType) {
case 'guardian': case 'guardian':
if (!guardian) return { roomType: 'combat', enemies: [] };
return { return {
roomType: 'guardian', roomType: 'guardian',
enemies: [{ enemies: [{
+3 -3
View File
@@ -2,10 +2,10 @@
// Spire-specific utility functions for room generation, enemy stat scaling, etc. // Spire-specific utility functions for room generation, enemy stat scaling, etc.
import type { RoomType, FloorState, EnemyState } from '../types'; import type { RoomType, FloorState, EnemyState } from '../types';
import { GUARDIANS, FLOOR_ELEM_CYCLE, PUZZLE_ROOMS } from '../constants'; import { FLOOR_ELEM_CYCLE, PUZZLE_ROOMS } from '../constants';
import { getFloorMaxHP, getFloorElement } from './floor-utils'; import { getFloorMaxHP, getFloorElement } from './floor-utils';
import { getEnemyName } from './enemy-utils'; import { getEnemyName } from './enemy-utils';
import { isGuardianFloor, getExtendedGuardian } from '../data/guardian-encounters'; import { getGuardianForFloor, isGuardianFloor } from '../data/guardian-encounters';
// ─── Spire Room Configuration ───────────────────────────────────────────────── // ─── Spire Room Configuration ─────────────────────────────────────────────────
@@ -85,7 +85,7 @@ export function generateSpireFloorState(floor: number, roomIndex: number, totalR
switch (roomType) { switch (roomType) {
case 'guardian': { case 'guardian': {
const guardian = GUARDIANS[floor] || getExtendedGuardian(floor); const guardian = getGuardianForFloor(floor);
if (guardian) { if (guardian) {
return { return {
roomType: 'guardian', roomType: 'guardian',