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
+27 -27
View File
@@ -3,7 +3,7 @@ import {
computePactMultiplier,
computePactInsightMultiplier,
} from '../utils/pact-utils';
import { GUARDIANS } from '../constants';
import { getGuardianForFloor } from '../data/guardian-encounters';
// Helper: compute actual multiplier values
function getDamageMult(mult: number, extraPacts: number, mitigation: number): number {
const numAdditional = extraPacts;
@@ -41,14 +41,14 @@ describe('computePactMultiplier', () => {
});
it('should return guardian damage multiplier for a single guardian pact', () => {
const floor10 = GUARDIANS[10];
const floor10 = getGuardianForFloor(10)!;
const result = computePactMultiplier({ signedPacts: [10] });
expect(result).toBe(floor10.damageMultiplier);
});
it('should multiply damage multipliers for multiple guardian pacts', () => {
const floor10 = GUARDIANS[10];
const floor20 = GUARDIANS[20];
const floor10 = getGuardianForFloor(10)!;
const floor20 = getGuardianForFloor(20)!;
const result = computePactMultiplier({ signedPacts: [10, 20] });
// With 2 pacts: baseMult = f10 * f20, then penalty = 0.5 * 1 = 0.5
// effectivePenalty = max(0, 0.5 - 0) = 0.5
@@ -58,8 +58,8 @@ describe('computePactMultiplier', () => {
});
it('should apply interference mitigation to reduce penalty', () => {
const floor10 = GUARDIANS[10];
const floor20 = GUARDIANS[20];
const floor10 = getGuardianForFloor(10)!;
const floor20 = getGuardianForFloor(20)!;
const baseMult = floor10.damageMultiplier * floor20.damageMultiplier;
// No mitigation: penalty = 0.5, result = baseMult * 0.5
@@ -79,8 +79,8 @@ describe('computePactMultiplier', () => {
});
it('should apply synergy bonus when mitigation exceeds 5', () => {
const floor10 = GUARDIANS[10];
const floor20 = GUARDIANS[20];
const floor10 = getGuardianForFloor(10)!;
const floor20 = getGuardianForFloor(20)!;
const baseMult = floor10.damageMultiplier * floor20.damageMultiplier;
// 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', () => {
const floor10 = GUARDIANS[10];
const floor20 = GUARDIANS[20];
const floor10 = getGuardianForFloor(10)!;
const floor20 = getGuardianForFloor(20)!;
const baseMult = floor10.damageMultiplier * floor20.damageMultiplier;
const mit5 = computePactMultiplier({
@@ -115,9 +115,9 @@ describe('computePactMultiplier', () => {
});
it('should handle three pacts with penalty', () => {
const floor10 = GUARDIANS[10];
const floor20 = GUARDIANS[20];
const floor30 = GUARDIANS[30];
const floor10 = getGuardianForFloor(10)!;
const floor20 = getGuardianForFloor(20)!;
const floor30 = getGuardianForFloor(30)!;
const baseMult = floor10.damageMultiplier * floor20.damageMultiplier * floor30.damageMultiplier;
// 3 pacts: numAdditional = 2, basePenalty = 1.0, effectivePenalty = 1.0
@@ -130,9 +130,9 @@ describe('computePactMultiplier', () => {
});
it('should handle three pacts with partial mitigation', () => {
const floor10 = GUARDIANS[10];
const floor20 = GUARDIANS[20];
const floor30 = GUARDIANS[30];
const floor10 = getGuardianForFloor(10)!;
const floor20 = getGuardianForFloor(20)!;
const floor30 = getGuardianForFloor(30)!;
const baseMult = floor10.damageMultiplier * floor20.damageMultiplier * floor30.damageMultiplier;
// 3 pacts: numAdditional = 2, basePenalty = 1.0
@@ -146,7 +146,7 @@ describe('computePactMultiplier', () => {
});
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)
const result = computePactMultiplier({ signedPacts: [5, 10] });
// Only floor 10 counts: baseMult = floor10.damageMultiplier
@@ -181,22 +181,22 @@ describe('computePactInsightMultiplier', () => {
});
it('should return guardian insight multiplier for a single guardian pact', () => {
const floor10 = GUARDIANS[10];
const floor10 = getGuardianForFloor(10)!;
const result = computePactInsightMultiplier({ signedPacts: [10] });
expect(result).toBe(floor10.insightMultiplier);
});
it('should multiply insight multipliers for multiple guardian pacts', () => {
const floor10 = GUARDIANS[10];
const floor20 = GUARDIANS[20];
const floor10 = getGuardianForFloor(10)!;
const floor20 = getGuardianForFloor(20)!;
const result = computePactInsightMultiplier({ signedPacts: [10, 20] });
const expected = floor10.insightMultiplier * floor20.insightMultiplier * 0.5;
expect(result).toBe(expected);
});
it('should apply interference mitigation to reduce penalty', () => {
const floor10 = GUARDIANS[10];
const floor20 = GUARDIANS[20];
const floor10 = getGuardianForFloor(10)!;
const floor20 = getGuardianForFloor(20)!;
const baseMult = floor10.insightMultiplier * floor20.insightMultiplier;
const noMitigation = computePactInsightMultiplier({
@@ -213,8 +213,8 @@ describe('computePactInsightMultiplier', () => {
});
it('should apply synergy bonus when mitigation exceeds 5', () => {
const floor10 = GUARDIANS[10];
const floor20 = GUARDIANS[20];
const floor10 = getGuardianForFloor(10)!;
const floor20 = getGuardianForFloor(20)!;
const baseMult = floor10.insightMultiplier * floor20.insightMultiplier;
const result = computePactInsightMultiplier({
@@ -226,9 +226,9 @@ describe('computePactInsightMultiplier', () => {
});
it('should handle three pacts with full penalty', () => {
const floor10 = GUARDIANS[10];
const floor20 = GUARDIANS[20];
const floor30 = GUARDIANS[30];
const floor10 = getGuardianForFloor(10)!;
const floor20 = getGuardianForFloor(20)!;
const floor30 = getGuardianForFloor(30)!;
const baseMult = floor10.insightMultiplier * floor20.insightMultiplier * floor30.insightMultiplier;
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,
getDodgeChance,
getEnemyBarrier,
generateFloorState,
getPuzzleProgressSpeed,
} from '../utils/room-utils';
import { GUARDIANS, FLOOR_ELEM_CYCLE, PUZZLE_ROOMS, SWARM_CONFIG, SPEED_ROOM_CONFIG, FLOOR_ARMOR_CONFIG } from '../constants';
import type { EnemyState, FloorState } from '../types';
import { getFloorMaxHP, getFloorElement, getEnemyName } from '../utils/floor-utils';
import { FLOOR_ELEM_CYCLE, PUZZLE_ROOMS, SWARM_CONFIG, SPEED_ROOM_CONFIG, FLOOR_ARMOR_CONFIG } from '../constants';
import { getAllGuardianFloors } from '../data/guardian-encounters';
// ─── generateRoomType ─────────────────────────────────────────────────────────
@@ -21,8 +19,8 @@ describe('generateRoomType', () => {
});
it('should return "guardian" for guardian floors', () => {
// Get all guardian floors from GUARDIANS object
for (const floor of Object.keys(GUARDIANS).map(Number)) {
// Get all guardian floors from the unified system
for (const floor of getAllGuardianFloors().filter(f => f <= 100)) {
// Override and restore Math.random to ensure consistent result
const originalRandom = Math.random;
Math.random = () => 0.1; // Anything < PUZZLE_ROOM_CHANCE ensures non-puzzle
@@ -163,7 +161,7 @@ describe('getFloorArmor', () => {
});
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', () => {
@@ -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 ───────────────────────────────────────────────────
describe('getPuzzleProgressSpeed', () => {