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:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -10,9 +10,6 @@ export { getStudySpeedMultiplier, getStudyCostMultiplier } from './core';
|
||||
export { MANA_PER_ELEMENT, rawCost, elemCost, ELEMENTS, FLOOR_ELEM_CYCLE } from './elements';
|
||||
export { ELEMENT_OPPOSITES, ELEMENT_ICON_NAMES, BASE_UNLOCKED_ELEMENTS } from './elements';
|
||||
|
||||
// Guardian constants
|
||||
export { GUARDIANS } from './guardians';
|
||||
|
||||
// Spell constants
|
||||
export { SPELLS_DEF } from './spells';
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// ─── Guardians ────────────────────────────────────────────────────────────────
|
||||
// ─── Base Guardian Definitions (floors 10–100) ────────────────────────────────
|
||||
// Static canonical guardian data for the first 100 floors.
|
||||
|
||||
import type { GuardianDef } from '../types';
|
||||
|
||||
// All guardians have armor - damage reduction percentage
|
||||
export const GUARDIANS: Record<number, GuardianDef> = {
|
||||
export const BASE_GUARDIANS: Record<number, GuardianDef> = {
|
||||
10: {
|
||||
name: "Ignis Prime", element: "fire", hp: 5000, pact: 1.5, color: "#FF6B35",
|
||||
armor: 0.10,
|
||||
@@ -159,4 +160,4 @@ export const GUARDIANS: Record<number, GuardianDef> = {
|
||||
damageMultiplier: 2.5,
|
||||
insightMultiplier: 2.0,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -265,27 +265,24 @@ export function getExtendedGuardian(floor: number): GuardianDef | null {
|
||||
}
|
||||
|
||||
// ─── Unified Guardian System ─────────────────────────────────────────────────
|
||||
// Merges the static GUARDIANS constant (floors 10–100) with the extended
|
||||
// procedural system (compound 110, exotic 120–140, combo 150+).
|
||||
// For floors 90–100 the static definitions take precedence (canonical names).
|
||||
// Merges the base guardians (floors 10–100) with the extended procedural system
|
||||
// (compound 110, exotic 120–140, combo 150+).
|
||||
|
||||
import { GUARDIANS } from '../constants/guardians';
|
||||
import { BASE_GUARDIANS } from './guardian-data';
|
||||
|
||||
/** Get the guardian for any floor, merging static and extended systems. */
|
||||
export function getGuardianForFloor(floor: number): GuardianDef | null {
|
||||
// Static GUARDIANS take precedence for floors 10–100 (canonical definitions)
|
||||
if (GUARDIANS[floor]) {
|
||||
return { ...GUARDIANS[floor] };
|
||||
if (BASE_GUARDIANS[floor]) {
|
||||
return { ...BASE_GUARDIANS[floor] };
|
||||
}
|
||||
// Extended system for floors beyond 100 (and compound floors not in GUARDIANS)
|
||||
return getExtendedGuardian(floor);
|
||||
}
|
||||
|
||||
/** All guardian floors — merged from static + extended. */
|
||||
/** All guardian floors — merged from base + extended. */
|
||||
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 all = new Set([...staticFloors, ...extendedFloors]);
|
||||
const all = new Set([...baseFloors, ...extendedFloors]);
|
||||
return Array.from(all).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
getElementalBonus,
|
||||
} from '../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';
|
||||
|
||||
/**
|
||||
@@ -123,12 +124,12 @@ export function useCombatStats() {
|
||||
);
|
||||
|
||||
const isGuardianFloor = useMemo(
|
||||
() => !!GUARDIANS[currentFloor],
|
||||
() => !!getGuardianForFloor(currentFloor),
|
||||
[currentFloor]
|
||||
);
|
||||
|
||||
const currentGuardian = useMemo(
|
||||
() => GUARDIANS[currentFloor],
|
||||
() => getGuardianForFloor(currentFloor),
|
||||
[currentFloor]
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// Pure combat logic — no cross-store getState() calls.
|
||||
// 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 { SpellState } from '../types';
|
||||
import { getFloorMaxHP, getFloorElement, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
|
||||
@@ -114,16 +115,16 @@ export function processCombatTick(
|
||||
|
||||
// Check if floor is cleared
|
||||
if (floorHP <= 0) {
|
||||
const wasGuardian = GUARDIANS[currentFloor];
|
||||
onFloorCleared(currentFloor, !!wasGuardian);
|
||||
const guardian = getGuardianForFloor(currentFloor);
|
||||
onFloorCleared(currentFloor, !!guardian);
|
||||
|
||||
currentFloor = Math.min(currentFloor + 1, 100);
|
||||
floorMaxHP = getFloorMaxHP(currentFloor);
|
||||
floorHP = floorMaxHP;
|
||||
castProgress = 0;
|
||||
|
||||
if (wasGuardian) {
|
||||
logMessages.push(`⚔️ ${wasGuardian.name} defeated!`);
|
||||
if (guardian) {
|
||||
logMessages.push(`\u2694\ufe0f ${guardian.name} defeated!`);
|
||||
} else if (currentFloor % 5 === 0) {
|
||||
logMessages.push(`🏰 Floor ${currentFloor - 1} cleared!`);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
import { create } from 'zustand';
|
||||
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 { computeEquipmentEffects } from '../effects';
|
||||
import type { ComputedEffects } from '../effects/upgrade-effects.types';
|
||||
@@ -234,7 +235,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
|
||||
// Pact ritual progress
|
||||
if (ctx.prestige.pactRitualFloor !== null) {
|
||||
const guardian = GUARDIANS[ctx.prestige.pactRitualFloor];
|
||||
const guardian = getGuardianForFloor(ctx.prestige.pactRitualFloor);
|
||||
if (guardian) {
|
||||
const pactAffinityBonus = 1 - (ctx.prestige.prestigeUpgrades.pactAffinity || 0) * 0.1;
|
||||
const requiredTime = guardian.pactTime * pactAffinityBonus;
|
||||
@@ -275,7 +276,8 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
1,
|
||||
(floor, 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) {
|
||||
addLog(`🏰 Floor ${floor} cleared!`);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { createSafeStorage } from '../utils/safe-persist';
|
||||
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 type { Result } from '../utils/result';
|
||||
|
||||
@@ -151,7 +152,7 @@ export const usePrestigeStore = create<PrestigeStore>()(
|
||||
|
||||
startPactRitual: (floor: number, rawMana: number) => {
|
||||
const state = get();
|
||||
const guardian = GUARDIANS[floor];
|
||||
const guardian = getGuardianForFloor(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`);
|
||||
@@ -178,7 +179,7 @@ export const usePrestigeStore = create<PrestigeStore>()(
|
||||
const state = get();
|
||||
if (state.pactRitualFloor === null) return;
|
||||
|
||||
const guardian = GUARDIANS[state.pactRitualFloor];
|
||||
const guardian = getGuardianForFloor(state.pactRitualFloor);
|
||||
if (!guardian) return;
|
||||
|
||||
set({
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import type { SpellCost, EquipmentInstance } from '../types';
|
||||
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 { getGuardianForFloor } from '../data/guardian-encounters';
|
||||
import { BASE_GUARDIANS } from '../data/guardian-data';
|
||||
|
||||
// ─── Damage Calculation Params ──────────────────────────────────────────────
|
||||
|
||||
@@ -86,7 +88,7 @@ export function getBoonBonuses(signedPacts: number[]): BoonBonuses {
|
||||
};
|
||||
|
||||
for (const floor of signedPacts) {
|
||||
const guardian = GUARDIANS[floor];
|
||||
const guardian = getGuardianForFloor(floor);
|
||||
if (!guardian) continue;
|
||||
|
||||
for (const boon of guardian.boons) {
|
||||
@@ -158,7 +160,7 @@ export function calcDamage(
|
||||
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
|
||||
|
||||
// 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
|
||||
? 1 + (skills.guardianBane || 0) * 0.2
|
||||
: 1;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// ─── 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 {
|
||||
if (GUARDIANS[floor]) return GUARDIANS[floor].hp;
|
||||
const guardian = getGuardianForFloor(floor);
|
||||
if (guardian) return guardian.hp;
|
||||
// Improved scaling: slower early game, faster late game
|
||||
const baseHP = 100;
|
||||
const floorScaling = floor * 50;
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// Moved from store-modules/room-utils.ts to eliminate legacy dependencies
|
||||
|
||||
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 { getFloorElement } from './floor-utils';
|
||||
import { getEnemyName, generateSwarmEnemies } from './enemy-utils';
|
||||
@@ -10,7 +11,7 @@ import { getEnemyName, generateSwarmEnemies } from './enemy-utils';
|
||||
// Generate room type for a floor
|
||||
export function generateRoomType(floor: number): RoomType {
|
||||
// Guardian floors are always guardian type
|
||||
if (GUARDIANS[floor]) {
|
||||
if (getGuardianForFloor(floor)) {
|
||||
return 'guardian';
|
||||
}
|
||||
|
||||
@@ -35,8 +36,9 @@ export function generateRoomType(floor: number): RoomType {
|
||||
|
||||
// Get armor for a non-guardian floor
|
||||
export function getFloorArmor(floor: number): number {
|
||||
if (GUARDIANS[floor]) {
|
||||
return GUARDIANS[floor].armor || 0;
|
||||
const guardian = getGuardianForFloor(floor);
|
||||
if (guardian) {
|
||||
return guardian.armor || 0;
|
||||
}
|
||||
|
||||
// Armor becomes more common on higher floors
|
||||
@@ -84,10 +86,11 @@ export function generateFloorState(floor: number): FloorState {
|
||||
const roomType = generateRoomType(floor);
|
||||
const element = getFloorElement(floor);
|
||||
const baseHP = getFloorMaxHP(floor);
|
||||
const guardian = GUARDIANS[floor];
|
||||
const guardian = getGuardianForFloor(floor);
|
||||
|
||||
switch (roomType) {
|
||||
case 'guardian':
|
||||
if (!guardian) return { roomType: 'combat', enemies: [] };
|
||||
return {
|
||||
roomType: 'guardian',
|
||||
enemies: [{
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// Spire-specific utility functions for room generation, enemy stat scaling, etc.
|
||||
|
||||
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 { getEnemyName } from './enemy-utils';
|
||||
import { isGuardianFloor, getExtendedGuardian } from '../data/guardian-encounters';
|
||||
import { getGuardianForFloor, isGuardianFloor } from '../data/guardian-encounters';
|
||||
|
||||
// ─── Spire Room Configuration ─────────────────────────────────────────────────
|
||||
|
||||
@@ -85,7 +85,7 @@ export function generateSpireFloorState(floor: number, roomIndex: number, totalR
|
||||
|
||||
switch (roomType) {
|
||||
case 'guardian': {
|
||||
const guardian = GUARDIANS[floor] || getExtendedGuardian(floor);
|
||||
const guardian = getGuardianForFloor(floor);
|
||||
if (guardian) {
|
||||
return {
|
||||
roomType: 'guardian',
|
||||
|
||||
Reference in New Issue
Block a user