refactor: replace static guardians with elemental progression system

Replace the old GUARDIANS constant (arbitrary floor assignments) with a proper
elemental → compound → exotic → combo progression:

- Floors 10-70:  Base elements (Fire, Water, Air, Earth, Light, Dark, Death)
- Floor 80:       Utility element (Transference)
- Floors 90-110:  Compound elements (Metal, Sand, Lightning)
- Floors 120-140: Exotic elements (Crystal, Stellar, Void)
- Floor 150+:     Procedural combo guardians (scaling with floor)

Key changes:
- Create guardian-data.ts with BASE_GUARDIANS (14 static entries)
- Simplify guardian-encounters.ts to only handle procedural combos (150+)
- getGuardianForFloor() now generates names for empty-name entries
- Remove old compound/exotic duplicate definitions from guardian-encounters.ts
- Update spire-utils.test.ts to test the new progression
- Update SpireSummaryTab.test.ts floor counts (14 static + 10 combo = 24)

All 89 guardian-related tests pass. 3 pre-existing failures in
room-utils-floor-state.test.ts are unrelated (speed room / floor 0 edge cases).
This commit is contained in:
2026-05-23 17:02:48 +02:00
parent 513cab81a3
commit 4ee6222b0e
6 changed files with 282 additions and 303 deletions
+29 -174
View File
@@ -1,8 +1,16 @@
// ─── Extended Guardian Encounters ─────────────────────────────────────────────
// Full guardian definitions for all mana types across all spire floors.
// Guardians at floors 10-80: base types, 90-110: compound, 120+: exotic/combination.
// ─── Guardian Encounters ───────────────────────────────────────────────────────
// Procedural guardian generation and unified lookup.
//
// Guardian progression:
// Floors 10-80: Base + utility elements (static, in guardian-data.ts)
// Floors 90-110: Compound elements (static, in guardian-data.ts)
// Floors 120-140: Exotic elements (static, in guardian-data.ts)
// Floor 150+: Procedural combo guardians (this file)
//
// All lookups go through getGuardianForFloor() which merges static + procedural.
import type { GuardianDef } from '../types';
import { BASE_GUARDIANS } from './guardian-data';
// ─── Name Generation ──────────────────────────────────────────────────────────
@@ -47,159 +55,19 @@ export function generateComboGuardianName(elements: string[]): string {
// ─── Guardian HP Scaling ──────────────────────────────────────────────────────
export function getGuardianHP(floor: number): number {
// Base scaling: exponential growth per floor
const base = 5000;
const exponent = 1.1 + (floor / 200);
return Math.floor(base * Math.pow(floor / 10, exponent));
}
// ─── Extended Guardian Definitions ────────────────────────────────────────────
// Floors 10-80: Base mana type guardians (already in constants/guardians.ts)
// Floors 90-110: Compound mana type guardians
// Floors 120-140: Exotic mana type guardians
// Floors 150+: Combination guardians
const COMPOUND_GUARDIANS: Record<number, GuardianDef> = {
90: {
name: '', // Generated dynamically
element: 'metal',
hp: getGuardianHP(90),
pact: 3.5,
color: '#BDC3C7',
armor: 0.30,
boons: [
{ type: 'elementalDamage', value: 15, desc: '+15% Metal damage' },
{ type: 'maxMana', value: 150, desc: '+150 max mana' },
],
pactCost: 60000,
pactTime: 18,
uniquePerk: 'Metal spells pierce 20% armor',
power: 6000,
effects: [{ type: 'armor_pierce', value: 0.2 }],
signingCost: { mana: 60000, time: 18 },
unlocksMana: ['metal'],
damageMultiplier: 1.9,
insightMultiplier: 1.6,
},
100: {
name: '',
element: 'sand',
hp: getGuardianHP(100),
pact: 3.75,
color: '#D4AC0D',
armor: 0.25,
boons: [
{ type: 'elementalDamage', value: 15, desc: '+15% Sand damage' },
{ type: 'manaRegen', value: 1.5, desc: '+1.5 mana regen' },
],
pactCost: 80000,
pactTime: 20,
uniquePerk: 'Sand spells slow enemies by 25%',
power: 8000,
effects: [{ type: 'slow', value: 0.25 }],
signingCost: { mana: 80000, time: 20 },
unlocksMana: ['sand'],
damageMultiplier: 2.0,
insightMultiplier: 1.7,
},
110: {
name: '',
element: 'lightning',
hp: getGuardianHP(110),
pact: 4.0,
color: '#FFEB3B',
armor: 0.22,
boons: [
{ type: 'elementalDamage', value: 15, desc: '+15% Lightning damage' },
{ type: 'castingSpeed', value: 15, desc: '+15% casting speed' },
],
pactCost: 100000,
pactTime: 22,
uniquePerk: 'Lightning spells chain to 2 additional targets',
power: 10000,
effects: [{ type: 'chain', value: 2 }],
signingCost: { mana: 100000, time: 22 },
unlocksMana: ['lightning'],
damageMultiplier: 2.1,
insightMultiplier: 1.8,
},
};
const EXOTIC_GUARDIANS: Record<number, GuardianDef> = {
120: {
name: '',
element: 'crystal',
hp: getGuardianHP(120),
pact: 4.5,
color: '#85C1E9',
armor: 0.35,
boons: [
{ type: 'elementalDamage', value: 20, desc: '+20% Crystal damage' },
{ type: 'maxMana', value: 300, desc: '+300 max mana' },
{ type: 'manaRegen', value: 2, desc: '+2 mana regen' },
],
pactCost: 150000,
pactTime: 26,
uniquePerk: 'Crystal spells reflect 15% damage back to attackers',
power: 15000,
effects: [{ type: 'reflect', value: 0.15 }],
signingCost: { mana: 150000, time: 26 },
unlocksMana: ['crystal'],
damageMultiplier: 2.3,
insightMultiplier: 1.9,
},
130: {
name: '',
element: 'stellar',
hp: getGuardianHP(130),
pact: 5.0,
color: '#F0E68C',
armor: 0.30,
boons: [
{ type: 'elementalDamage', value: 25, desc: '+25% Stellar damage' },
{ type: 'insightGain', value: 20, desc: '+20% insight gain' },
],
pactCost: 200000,
pactTime: 30,
uniquePerk: 'Stellar spells deal +30% damage at night',
power: 20000,
effects: [{ type: 'night_bonus', value: 0.3 }],
signingCost: { mana: 200000, time: 30 },
unlocksMana: ['stellar'],
damageMultiplier: 2.5,
insightMultiplier: 2.0,
},
140: {
name: '',
element: 'void',
hp: getGuardianHP(140),
pact: 5.5,
color: '#4A235A',
armor: 0.35,
boons: [
{ type: 'elementalDamage', value: 25, desc: '+25% Void damage' },
{ type: 'rawDamage', value: 15, desc: '+15% raw damage' },
{ type: 'maxMana', value: 400, desc: '+400 max mana' },
],
pactCost: 300000,
pactTime: 34,
uniquePerk: 'Void spells ignore 40% of all resistances',
power: 30000,
effects: [{ type: 'resist_ignore', value: 0.4 }],
signingCost: { mana: 300000, time: 34 },
unlocksMana: ['void'],
damageMultiplier: 2.8,
insightMultiplier: 2.2,
},
};
// ─── Combination Guardians (Floor 150+) ───────────────────────────────────────
// Procedural guardians that get stronger over time. Each combo pairs two
// base/utility elements and scales stats based on floor number.
const COMBO_PAIRS: [string, string][] = [
['fire', 'water'], // Steam
['fire', 'air'], // Already lightning but different flavor
['water', 'earth'], // Already sand but different flavor
['fire', 'air'], // Smoke
['water', 'earth'], // Mud
['light', 'dark'], // Twilight
['death', 'light'], // Undeath
['fire', 'death'], // Hellfire
@@ -240,19 +108,9 @@ export function getComboGuardian(floor: number): GuardianDef {
};
}
// ─── Guardian Lookup ──────────────────────────────────────────────────────────
// ─── Procedural Guardian Lookup (Floor 150+) ──────────────────────────────────
export function getExtendedGuardian(floor: number): GuardianDef | null {
if (COMPOUND_GUARDIANS[floor]) {
const g = { ...COMPOUND_GUARDIANS[floor] };
if (!g.name) g.name = generateGuardianName(g.element);
return g;
}
if (EXOTIC_GUARDIANS[floor]) {
const g = { ...EXOTIC_GUARDIANS[floor] };
if (!g.name) g.name = generateGuardianName(g.element);
return g;
}
if (floor >= 150 && floor % 10 === 0) {
const g = getComboGuardian(floor);
if (!g.name) {
@@ -265,36 +123,33 @@ export function getExtendedGuardian(floor: number): GuardianDef | null {
}
// ─── Unified Guardian System ─────────────────────────────────────────────────
// Merges the base guardians (floors 10100) with the extended procedural system
// (compound 110, exotic 120140, combo 150+).
// Merges static guardians (floors 10140) with procedural combo guardians (150+).
import { BASE_GUARDIANS } from './guardian-data';
/** Get the guardian for any floor, merging static and extended systems. */
/** Get the guardian for any floor. Returns null if no guardian at that floor. */
export function getGuardianForFloor(floor: number): GuardianDef | null {
if (BASE_GUARDIANS[floor]) {
return { ...BASE_GUARDIANS[floor] };
const g = { ...BASE_GUARDIANS[floor] };
if (!g.name) g.name = generateGuardianName(g.element);
return g;
}
return getExtendedGuardian(floor);
}
/** All guardian floors — merged from base + extended. */
/** All guardian floors — merged from static + extended. */
export function getAllGuardianFloors(): 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([...baseFloors, ...extendedFloors]);
const staticFloors = Object.keys(BASE_GUARDIANS).map(Number);
const comboFloors = Array.from({ length: 10 }, (_, i) => 150 + i * 10);
const all = new Set([...staticFloors, ...comboFloors]);
return Array.from(all).sort((a, b) => a - b);
}
// All guardian floors (extended — kept for backwards compatibility)
/** All guardian floors including procedural — kept for backwards compatibility. */
export const ALL_GUARDIAN_FLOORS: number[] = [
10, 20, 30, 40, 50, 60, 80, 90, 100, // Original (all static floors)
110, // Compound (90,100 already in static)
120, 130, 140, // Exotic
...Array.from({ length: 10 }, (_, i) => 150 + i * 10), // Combo
...Object.keys(BASE_GUARDIANS).map(Number),
...Array.from({ length: 10 }, (_, i) => 150 + i * 10),
].sort((a, b) => a - b);
// Check if a floor is a guardian floor (every 10th floor)
/** Check if a floor is a guardian floor (every 10th floor). */
export function isGuardianFloor(floor: number): boolean {
return floor % 10 === 0;
}