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
+2 -2
View File
@@ -1,8 +1,8 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-05-23T12:53:16.980Z Generated: 2026-05-23T14:09:24.244Z
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.4s) (3 warnings) 1. Processed 129 files (1.5s) (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
+14 -7
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-05-23T12:53:15.385Z", "generated": "2026-05-23T14:09:22.569Z",
"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."
}, },
@@ -12,13 +12,9 @@
"constants/elements.ts": [ "constants/elements.ts": [
"types.ts" "types.ts"
], ],
"constants/guardians.ts": [
"types.ts"
],
"constants/index.ts": [ "constants/index.ts": [
"constants/core.ts", "constants/core.ts",
"constants/elements.ts", "constants/elements.ts",
"constants/guardians.ts",
"constants/prestige.ts", "constants/prestige.ts",
"constants/rooms.ts", "constants/rooms.ts",
"constants/spells.ts", "constants/spells.ts",
@@ -358,8 +354,11 @@
"data/golems/golems-data.ts", "data/golems/golems-data.ts",
"data/golems/types.ts" "data/golems/types.ts"
], ],
"data/guardian-data.ts": [
"types.ts"
],
"data/guardian-encounters.ts": [ "data/guardian-encounters.ts": [
"constants/guardians.ts", "data/guardian-data.ts",
"types.ts" "types.ts"
], ],
"data/loot-drops.ts": [ "data/loot-drops.ts": [
@@ -392,6 +391,7 @@
"effects/upgrade-effects.types.ts": [], "effects/upgrade-effects.types.ts": [],
"hooks/useGameDerived.ts": [ "hooks/useGameDerived.ts": [
"constants.ts", "constants.ts",
"data/guardian-encounters.ts",
"effects/special-effects.ts", "effects/special-effects.ts",
"effects/upgrade-effects.ts", "effects/upgrade-effects.ts",
"stores/combatStore.ts", "stores/combatStore.ts",
@@ -408,6 +408,7 @@
], ],
"stores/combat-actions.ts": [ "stores/combat-actions.ts": [
"constants.ts", "constants.ts",
"data/guardian-encounters.ts",
"effects/discipline-effects.ts", "effects/discipline-effects.ts",
"stores/combat-state.types.ts", "stores/combat-state.types.ts",
"types.ts", "types.ts",
@@ -497,6 +498,7 @@
"stores/gameStore.ts": [ "stores/gameStore.ts": [
"constants.ts", "constants.ts",
"data/attunements.ts", "data/attunements.ts",
"data/guardian-encounters.ts",
"effects.ts", "effects.ts",
"effects/discipline-effects.ts", "effects/discipline-effects.ts",
"effects/special-effects.ts", "effects/special-effects.ts",
@@ -537,6 +539,7 @@
], ],
"stores/prestigeStore.ts": [ "stores/prestigeStore.ts": [
"constants.ts", "constants.ts",
"data/guardian-encounters.ts",
"types.ts", "types.ts",
"utils/result.ts", "utils/result.ts",
"utils/safe-persist.ts" "utils/safe-persist.ts"
@@ -593,6 +596,8 @@
"utils/combat-utils.ts": [ "utils/combat-utils.ts": [
"constants.ts", "constants.ts",
"data/enchantment-effects.ts", "data/enchantment-effects.ts",
"data/guardian-data.ts",
"data/guardian-encounters.ts",
"types.ts", "types.ts",
"utils/mana-utils.ts" "utils/mana-utils.ts"
], ],
@@ -610,7 +615,8 @@
"utils/floor-utils.ts" "utils/floor-utils.ts"
], ],
"utils/floor-utils.ts": [ "utils/floor-utils.ts": [
"constants.ts" "constants.ts",
"data/guardian-encounters.ts"
], ],
"utils/formatting.ts": [], "utils/formatting.ts": [],
"utils/index.ts": [ "utils/index.ts": [
@@ -633,6 +639,7 @@
"utils/result.ts": [], "utils/result.ts": [],
"utils/room-utils.ts": [ "utils/room-utils.ts": [
"constants.ts", "constants.ts",
"data/guardian-encounters.ts",
"types.ts", "types.ts",
"utils/enemy-utils.ts", "utils/enemy-utils.ts",
"utils/floor-utils.ts" "utils/floor-utils.ts"
@@ -28,24 +28,42 @@ describe('Tab barrel export', () => {
// ─── Test: Guardian data ─────────────────────────────────────────────────────── // ─── Test: Guardian data ───────────────────────────────────────────────────────
describe('Guardian data', () => { describe('Guardian data', () => {
it('has 9 static guardians plus extended guardians', async () => { it('has 14 static guardians plus combo guardians', async () => {
const { getAllGuardianFloors } = await import('@/lib/game/data/guardian-encounters'); const { getAllGuardianFloors } = await import('@/lib/game/data/guardian-encounters');
const floors = getAllGuardianFloors(); const floors = getAllGuardianFloors();
// 9 static + compound (110) + exotic (120,130,140) + combo (150-240) = 23 total // 14 static (10-140) + 10 combo (150-240) = 24 total
expect(floors.length).toBeGreaterThanOrEqual(9); expect(floors.length).toBe(24);
}); });
it('guardians are at expected base floors', async () => { it('guardians are at expected floors for all tiers', async () => {
const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters'); const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters');
const expectedFloors = [10, 20, 30, 40, 50, 60, 80, 90, 100]; // Base: 10-70, Utility: 80, Compound: 90-110, Exotic: 120-140
for (const floor of expectedFloors) { for (const floor of [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140]) {
expect(getGuardianForFloor(floor)).not.toBeNull(); expect(getGuardianForFloor(floor)).not.toBeNull();
} }
}); });
it('all base guardians have required fields', async () => { it('follows elemental → compound → exotic progression', async () => {
const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters'); const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters');
for (const floor of [10, 20, 30, 40, 50, 60, 80, 90, 100]) { expect(getGuardianForFloor(10)!.element).toBe('fire');
expect(getGuardianForFloor(20)!.element).toBe('water');
expect(getGuardianForFloor(30)!.element).toBe('air');
expect(getGuardianForFloor(40)!.element).toBe('earth');
expect(getGuardianForFloor(50)!.element).toBe('light');
expect(getGuardianForFloor(60)!.element).toBe('dark');
expect(getGuardianForFloor(70)!.element).toBe('death');
expect(getGuardianForFloor(80)!.element).toBe('transference');
expect(getGuardianForFloor(90)!.element).toBe('metal');
expect(getGuardianForFloor(100)!.element).toBe('sand');
expect(getGuardianForFloor(110)!.element).toBe('lightning');
expect(getGuardianForFloor(120)!.element).toBe('crystal');
expect(getGuardianForFloor(130)!.element).toBe('stellar');
expect(getGuardianForFloor(140)!.element).toBe('void');
});
it('all static guardians have required fields', async () => {
const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters');
for (const floor of [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140]) {
const def = getGuardianForFloor(floor)!; const def = getGuardianForFloor(floor)!;
expect(def.name).toBeTruthy(); expect(def.name).toBeTruthy();
expect(def.element).toBeTruthy(); expect(def.element).toBeTruthy();
@@ -56,9 +74,10 @@ describe('Guardian data', () => {
} }
}); });
it('all base guardians have unique elements', async () => { it('all static guardians have unique elements', async () => {
const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters'); const { getGuardianForFloor } = await import('@/lib/game/data/guardian-encounters');
const elements = [10, 20, 30, 40, 50, 60, 80, 90, 100].map((f) => getGuardianForFloor(f)!.element); const floors = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140];
const elements = floors.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);
}); });
+45 -28
View File
@@ -9,7 +9,7 @@ import {
getSpireRoomTypeDisplay, getSpireRoomTypeDisplay,
SPIRE_CONFIG, SPIRE_CONFIG,
} from '../utils/spire-utils'; } from '../utils/spire-utils';
import { isGuardianFloor, getExtendedGuardian, getGuardianHP, generateGuardianName, generateComboGuardianName, ALL_GUARDIAN_FLOORS } from '../data/guardian-encounters'; import { isGuardianFloor, getExtendedGuardian, getGuardianForFloor, getGuardianHP, generateGuardianName, generateComboGuardianName, ALL_GUARDIAN_FLOORS } from '../data/guardian-encounters';
// ─── Spire Utils ───────────────────────────────────────────────────────────── // ─── Spire Utils ─────────────────────────────────────────────────────────────
@@ -182,42 +182,59 @@ describe('isGuardianFloor', () => {
}); });
}); });
describe('getExtendedGuardian', () => { describe('getExtendedGuardian (procedural combo guardians)', () => {
it('should return compound guardians for floors 90, 110', () => {
const g90 = getExtendedGuardian(90);
expect(g90).not.toBeNull();
expect(g90!.element).toBe('metal');
expect(g90!.name).toBeTruthy();
const g110 = getExtendedGuardian(110);
expect(g110).not.toBeNull();
expect(g110!.element).toBe('lightning');
});
it('should return exotic guardians for floors 120, 130, 140', () => {
const g120 = getExtendedGuardian(120);
expect(g120).not.toBeNull();
expect(g120!.element).toBe('crystal');
const g130 = getExtendedGuardian(130);
expect(g130).not.toBeNull();
expect(g130!.element).toBe('stellar');
const g140 = getExtendedGuardian(140);
expect(g140).not.toBeNull();
expect(g140!.element).toBe('void');
});
it('should return combo guardians for floors 150+', () => { it('should return combo guardians for floors 150+', () => {
const g150 = getExtendedGuardian(150); const g150 = getExtendedGuardian(150);
expect(g150).not.toBeNull(); expect(g150).not.toBeNull();
expect(g150!.element).toContain('+'); expect(g150!.element).toContain('+');
}); });
it('should return null for non-guardian floors', () => { it('should return null for floors below 150', () => {
expect(getExtendedGuardian(1)).toBeNull(); expect(getExtendedGuardian(1)).toBeNull();
expect(getExtendedGuardian(15)).toBeNull(); expect(getExtendedGuardian(15)).toBeNull();
expect(getExtendedGuardian(95)).toBeNull(); expect(getExtendedGuardian(95)).toBeNull();
expect(getExtendedGuardian(100)).toBeNull();
expect(getExtendedGuardian(140)).toBeNull();
});
});
describe('getGuardianForFloor (unified lookup)', () => {
it('should return base element guardians for floors 10-70', () => {
expect(getGuardianForFloor(10)!.element).toBe('fire');
expect(getGuardianForFloor(20)!.element).toBe('water');
expect(getGuardianForFloor(30)!.element).toBe('air');
expect(getGuardianForFloor(40)!.element).toBe('earth');
expect(getGuardianForFloor(50)!.element).toBe('light');
expect(getGuardianForFloor(60)!.element).toBe('dark');
expect(getGuardianForFloor(70)!.element).toBe('death');
});
it('should return utility guardian for floor 80', () => {
expect(getGuardianForFloor(80)!.element).toBe('transference');
});
it('should return compound guardians for floors 90-110', () => {
expect(getGuardianForFloor(90)!.element).toBe('metal');
expect(getGuardianForFloor(100)!.element).toBe('sand');
expect(getGuardianForFloor(110)!.element).toBe('lightning');
});
it('should return exotic guardians for floors 120-140', () => {
expect(getGuardianForFloor(120)!.element).toBe('crystal');
expect(getGuardianForFloor(130)!.element).toBe('stellar');
expect(getGuardianForFloor(140)!.element).toBe('void');
});
it('should return combo guardians for floors 150+', () => {
const g150 = getGuardianForFloor(150);
expect(g150).not.toBeNull();
expect(g150!.element).toContain('+');
});
it('should return null for non-guardian floors', () => {
expect(getGuardianForFloor(1)).toBeNull();
expect(getGuardianForFloor(15)).toBeNull();
expect(getGuardianForFloor(95)).toBeNull();
}); });
}); });
+163 -82
View File
@@ -1,163 +1,244 @@
// ─── Base Guardian Definitions (floors 10100) ──────────────────────────────── // ─── Static Guardian Definitions ──────────────────────────────────────────────
// Static canonical guardian data for the first 100 floors. // Ordered by floor: base → utility → compound → exotic.
//
// Floors 10-80: Base elements (Fire, Water, Air, Earth, Light, Dark, Death)
// + Utility element (Transference) — 8 guardians total
// Floors 90-110: Compound elements (Metal, Sand, Lightning) — 3 guardians
// Floors 120-140: Exotic elements (Crystal, Stellar, Void) — 3 guardians
//
// Floor 150+: Procedural combination guardians (see getComboGuardian in guardian-encounters.ts)
import type { GuardianDef } from '../types'; import type { GuardianDef } from '../types';
export const BASE_GUARDIANS: Record<number, GuardianDef> = { // Helper: HP scales exponentially with floor
10: { function hp(floor: number): number {
name: "Ignis Prime", element: "fire", hp: 5000, pact: 1.5, color: "#FF6B35", const base = 5000;
const exponent = 1.1 + (floor / 200);
return Math.floor(base * Math.pow(floor / 10, exponent));
}
// ─── Base Elements (Floors 1070) ────────────────────────────────────────────
const BASE_GUARDIANS: Record<number, GuardianDef> = {
// -- Base elements --
10: {
name: 'Ignis Prime', element: 'fire', hp: hp(10), pact: 1.5, color: '#FF6B35',
armor: 0.10, armor: 0.10,
boons: [ boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Fire damage' }, { type: 'elementalDamage', value: 5, desc: '+5% Fire damage' },
{ type: 'maxMana', value: 50, desc: '+50 max mana' }, { type: 'maxMana', value: 50, desc: '+50 max mana' },
], ],
pactCost: 500, pactCost: 500, pactTime: 2,
pactTime: 2, uniquePerk: 'Fire spells cast 10% faster',
uniquePerk: "Fire spells cast 10% faster",
power: 50, power: 50,
effects: [{ type: 'burn', value: 0.1 }], effects: [{ type: 'burn', value: 0.1 }],
signingCost: { mana: 500, time: 2 }, signingCost: { mana: 500, time: 2 },
unlocksMana: ['fire', 'lightning'], unlocksMana: ['fire', 'lightning'],
damageMultiplier: 1.1, damageMultiplier: 1.1, insightMultiplier: 1.05,
insightMultiplier: 1.05,
}, },
20: { 20: {
name: "Aqua Regia", element: "water", hp: 15000, pact: 1.75, color: "#4ECDC4", name: 'Aqua Regia', element: 'water', hp: hp(20), pact: 1.75, color: '#4ECDC4',
armor: 0.15, armor: 0.15,
boons: [ boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Water damage' }, { type: 'elementalDamage', value: 5, desc: '+5% Water damage' },
{ type: 'manaRegen', value: 0.5, desc: '+0.5 mana regen' }, { type: 'manaRegen', value: 0.5, desc: '+0.5 mana regen' },
], ],
pactCost: 1000, pactCost: 1000, pactTime: 4,
pactTime: 4, uniquePerk: 'Water spells deal +15% damage',
uniquePerk: "Water spells deal +15% damage",
power: 150, power: 150,
effects: [{ type: 'armor_pierce', value: 0.15 }], effects: [{ type: 'armor_pierce', value: 0.15 }],
signingCost: { mana: 1000, time: 4 }, signingCost: { mana: 1000, time: 4 },
unlocksMana: ['water', 'transference'], unlocksMana: ['water', 'sand'],
damageMultiplier: 1.2, damageMultiplier: 1.2, insightMultiplier: 1.1,
insightMultiplier: 1.1,
}, },
30: { 30: {
name: "Ventus Rex", element: "air", hp: 30000, pact: 2.0, color: "#00D4FF", name: 'Ventus Rex', element: 'air', hp: hp(30), pact: 2.0, color: '#00D4FF',
armor: 0.18, armor: 0.18,
boons: [ boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Air damage' }, { type: 'elementalDamage', value: 5, desc: '+5% Air damage' },
{ type: 'castingSpeed', value: 5, desc: '+5% cast speed' }, { type: 'castingSpeed', value: 5, desc: '+5% casting speed' },
], ],
pactCost: 2000, pactCost: 2000, pactTime: 6,
pactTime: 6, uniquePerk: 'Air spells have 15% crit chance',
uniquePerk: "Air spells have 15% crit chance",
power: 300, power: 300,
effects: [{ type: 'cast_speed', value: 0.05 }], effects: [{ type: 'cast_speed', value: 0.05 }],
signingCost: { mana: 2000, time: 6 }, signingCost: { mana: 2000, time: 6 },
unlocksMana: ['air', 'sand'], unlocksMana: ['air'],
damageMultiplier: 1.3, damageMultiplier: 1.3, insightMultiplier: 1.15,
insightMultiplier: 1.15,
}, },
40: { 40: {
name: "Terra Firma", element: "earth", hp: 50000, pact: 2.25, color: "#F4A261", name: 'Terra Firma', element: 'earth', hp: hp(40), pact: 2.25, color: '#F4A261',
armor: 0.25, armor: 0.25,
boons: [ boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Earth damage' }, { type: 'elementalDamage', value: 5, desc: '+5% Earth damage' },
{ type: 'maxMana', value: 100, desc: '+100 max mana' }, { type: 'maxMana', value: 100, desc: '+100 max mana' },
], ],
pactCost: 4000, pactCost: 4000, pactTime: 8,
pactTime: 8, uniquePerk: 'Earth spells deal +25% damage to guardians',
uniquePerk: "Earth spells deal +25% damage to guardians",
power: 500, power: 500,
effects: [{ type: 'armor_pierce', value: 0.2 }], effects: [{ type: 'armor_pierce', value: 0.2 }],
signingCost: { mana: 4000, time: 8 }, signingCost: { mana: 4000, time: 8 },
unlocksMana: ['earth', 'metal'], unlocksMana: ['earth', 'metal'],
damageMultiplier: 1.4, damageMultiplier: 1.4, insightMultiplier: 1.2,
insightMultiplier: 1.2,
}, },
50: { 50: {
name: "Lux Aeterna", element: "light", hp: 80000, pact: 2.5, color: "#FFD700", name: 'Lux Aeterna', element: 'light', hp: hp(50), pact: 2.5, color: '#FFD700',
armor: 0.20, armor: 0.20,
boons: [ boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Light damage' }, { type: 'elementalDamage', value: 10, desc: '+10% Light damage' },
{ type: 'insightGain', value: 10, desc: '+10% insight gain' }, { type: 'insightGain', value: 10, desc: '+10% insight gain' },
], ],
pactCost: 8000, pactCost: 8000, pactTime: 10,
pactTime: 10, uniquePerk: 'Light spells reveal enemy weaknesses (+20% damage)',
uniquePerk: "Light spells reveal enemy weaknesses (+20% damage)",
power: 800, power: 800,
effects: [{ type: 'crit_chance', value: 0.1 }], effects: [{ type: 'crit_chance', value: 0.1 }],
signingCost: { mana: 8000, time: 10 }, signingCost: { mana: 8000, time: 10 },
unlocksMana: ['light', 'crystal'], unlocksMana: ['light', 'crystal'],
damageMultiplier: 1.5, damageMultiplier: 1.5, insightMultiplier: 1.3,
insightMultiplier: 1.3,
}, },
60: { 60: {
name: "Umbra Mortis", element: "dark", hp: 120000, pact: 2.75, color: "#9B59B6", name: 'Umbra Mortis', element: 'dark', hp: hp(60), pact: 2.75, color: '#9B59B6',
armor: 0.22, armor: 0.22,
boons: [ boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Dark damage' }, { type: 'elementalDamage', value: 10, desc: '+10% Dark damage' },
{ type: 'critDamage', value: 15, desc: '+15% crit damage' }, { type: 'critDamage', value: 15, desc: '+15% crit damage' },
], ],
pactCost: 15000, pactCost: 15000, pactTime: 12,
pactTime: 12, uniquePerk: 'Dark spells deal +25% damage to armored enemies',
uniquePerk: "Dark spells deal +25% damage to armored enemies",
power: 1200, power: 1200,
effects: [{ type: 'crit_damage', value: 0.15 }], effects: [{ type: 'crit_damage', value: 0.15 }],
signingCost: { mana: 15000, time: 12 }, signingCost: { mana: 15000, time: 12 },
unlocksMana: ['dark', 'void'], unlocksMana: ['dark', 'void'],
damageMultiplier: 1.6, damageMultiplier: 1.6, insightMultiplier: 1.4,
insightMultiplier: 1.4,
}, },
80: { 70: {
name: "Mors Ultima", element: "death", hp: 250000, pact: 3.25, color: "#778CA3", name: 'Mors Ultima', element: 'death', hp: hp(70), pact: 3.0, color: '#778CA3',
armor: 0.25, armor: 0.25,
boons: [ boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Death damage' }, { type: 'elementalDamage', value: 10, desc: '+10% Death damage' },
{ type: 'rawDamage', value: 10, desc: '+10% raw damage' }, { type: 'rawDamage', value: 10, desc: '+10% raw damage' },
], ],
pactCost: 40000, pactCost: 25000, pactTime: 14,
pactTime: 16, uniquePerk: 'Death spells execute enemies below 20% HP',
uniquePerk: "Death spells execute enemies below 20% HP",
power: 2500, power: 2500,
effects: [{ type: 'raw_damage', value: 0.1 }], effects: [{ type: 'raw_damage', value: 0.1 }],
signingCost: { mana: 40000, time: 16 }, signingCost: { mana: 25000, time: 14 },
unlocksMana: ['death'], unlocksMana: ['death'],
damageMultiplier: 1.8, damageMultiplier: 1.8, insightMultiplier: 1.5,
insightMultiplier: 1.5,
}, },
90: {
name: "Primordialis", element: "void", hp: 400000, pact: 4.0, color: "#4A235A", // -- Utility element --
80: {
name: 'Vinculum Arcana', element: 'transference', hp: hp(80), pact: 3.25, color: '#1ABC9C',
armor: 0.20,
boons: [
{ type: 'maxMana', value: 150, desc: '+150 max mana' },
{ type: 'manaRegen', value: 1.0, desc: '+1.0 mana regen' },
],
pactCost: 35000, pactTime: 16,
uniquePerk: 'Transference spells have 25% reduced cost',
power: 3500,
effects: [{ type: 'cost_reduction', value: 0.25 }],
signingCost: { mana: 35000, time: 16 },
unlocksMana: ['transference'],
damageMultiplier: 1.85, insightMultiplier: 1.55,
},
// -- Compound Elements (Floors 90110) ───────────────────────────────────────
90: {
name: '', element: 'metal', hp: hp(90), pact: 3.5, color: '#BDC3C7',
armor: 0.30, armor: 0.30,
boons: [ boons: [
{ type: 'elementalDamage', value: 15, desc: '+15% Void damage' }, { type: 'elementalDamage', value: 15, desc: '+15% Metal damage' },
{ type: 'maxMana', value: 200, desc: '+200 max mana' }, { type: 'maxMana', value: 150, desc: '+150 max mana' },
{ type: 'manaRegen', value: 1, desc: '+1 mana regen' },
], ],
pactCost: 75000, pactCost: 60000, pactTime: 18,
pactTime: 20, uniquePerk: 'Metal spells pierce 20% armor',
uniquePerk: "Void spells ignore 30% of enemy resistance", power: 6000,
power: 4000, effects: [{ type: 'armor_pierce', value: 0.2 }],
effects: [{ type: 'void_resist', value: 0.3 }], signingCost: { mana: 60000, time: 18 },
signingCost: { mana: 75000, time: 20 }, unlocksMana: ['metal'],
unlocksMana: ['void', 'stellar'], damageMultiplier: 1.9, insightMultiplier: 1.6,
damageMultiplier: 2.0,
insightMultiplier: 1.7,
}, },
100: { 100: {
name: "The Awakened One", element: "stellar", hp: 1000000, pact: 5.0, color: "#F0E68C", name: '', element: 'sand', hp: hp(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: hp(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,
},
// -- Exotic Elements (Floors 120140) ────────────────────────────────────────
120: {
name: '', element: 'crystal', hp: hp(120), pact: 4.5, color: '#85C1E9',
armor: 0.35, armor: 0.35,
boons: [ boons: [
{ type: 'elementalDamage', value: 20, desc: '+20% Stellar damage' }, { type: 'elementalDamage', value: 20, desc: '+20% Crystal damage' },
{ type: 'maxMana', value: 500, desc: '+500 max mana' }, { type: 'maxMana', value: 300, desc: '+300 max mana' },
{ type: 'manaRegen', value: 2, desc: '+2 mana regen' }, { type: 'manaRegen', value: 2, desc: '+2 mana regen' },
{ type: 'insightGain', value: 25, desc: '+25% insight gain' },
], ],
pactCost: 150000, pactCost: 150000, pactTime: 26,
pactTime: 24, uniquePerk: 'Crystal spells reflect 15% damage back to attackers',
uniquePerk: "All spells deal +50% damage and cast 25% faster", power: 15000,
power: 10000, effects: [{ type: 'reflect', value: 0.15 }],
effects: [{ type: 'all_damage', value: 0.5 }], signingCost: { mana: 150000, time: 26 },
signingCost: { mana: 150000, time: 24 }, unlocksMana: ['crystal'],
damageMultiplier: 2.3, insightMultiplier: 1.9,
},
130: {
name: '', element: 'stellar', hp: hp(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'], unlocksMana: ['stellar'],
damageMultiplier: 2.5, damageMultiplier: 2.5, insightMultiplier: 2.0,
insightMultiplier: 2.0, },
140: {
name: '', element: 'void', hp: hp(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,
}, },
}; };
export { BASE_GUARDIANS };
+29 -174
View File
@@ -1,8 +1,16 @@
// ─── Extended Guardian Encounters ───────────────────────────────────────────── // ─── Guardian Encounters ───────────────────────────────────────────────────────
// Full guardian definitions for all mana types across all spire floors. // Procedural guardian generation and unified lookup.
// Guardians at floors 10-80: base types, 90-110: compound, 120+: exotic/combination. //
// 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 type { GuardianDef } from '../types';
import { BASE_GUARDIANS } from './guardian-data';
// ─── Name Generation ────────────────────────────────────────────────────────── // ─── Name Generation ──────────────────────────────────────────────────────────
@@ -47,159 +55,19 @@ export function generateComboGuardianName(elements: string[]): string {
// ─── Guardian HP Scaling ────────────────────────────────────────────────────── // ─── Guardian HP Scaling ──────────────────────────────────────────────────────
export function getGuardianHP(floor: number): number { export function getGuardianHP(floor: number): number {
// Base scaling: exponential growth per floor
const base = 5000; const base = 5000;
const exponent = 1.1 + (floor / 200); const exponent = 1.1 + (floor / 200);
return Math.floor(base * Math.pow(floor / 10, exponent)); 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+) ─────────────────────────────────────── // ─── 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][] = [ const COMBO_PAIRS: [string, string][] = [
['fire', 'water'], // Steam ['fire', 'water'], // Steam
['fire', 'air'], // Already lightning but different flavor ['fire', 'air'], // Smoke
['water', 'earth'], // Already sand but different flavor ['water', 'earth'], // Mud
['light', 'dark'], // Twilight ['light', 'dark'], // Twilight
['death', 'light'], // Undeath ['death', 'light'], // Undeath
['fire', 'death'], // Hellfire ['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 { 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) { if (floor >= 150 && floor % 10 === 0) {
const g = getComboGuardian(floor); const g = getComboGuardian(floor);
if (!g.name) { if (!g.name) {
@@ -265,36 +123,33 @@ export function getExtendedGuardian(floor: number): GuardianDef | null {
} }
// ─── Unified Guardian System ───────────────────────────────────────────────── // ─── Unified Guardian System ─────────────────────────────────────────────────
// Merges the base guardians (floors 10100) with the extended procedural system // Merges static guardians (floors 10140) with procedural combo guardians (150+).
// (compound 110, exotic 120140, combo 150+).
import { BASE_GUARDIANS } from './guardian-data'; /** Get the guardian for any floor. Returns null if no guardian at that floor. */
/** 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 {
if (BASE_GUARDIANS[floor]) { 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); return getExtendedGuardian(floor);
} }
/** All guardian floors — merged from base + extended. */ /** All guardian floors — merged from static + extended. */
export function getAllGuardianFloors(): number[] { export function getAllGuardianFloors(): number[] {
const baseFloors = Object.keys(BASE_GUARDIANS).map(Number); const staticFloors = Object.keys(BASE_GUARDIANS).map(Number);
const extendedFloors = [110, 120, 130, 140, ...Array.from({ length: 10 }, (_, i) => 150 + i * 10)]; const comboFloors = Array.from({ length: 10 }, (_, i) => 150 + i * 10);
const all = new Set([...baseFloors, ...extendedFloors]); const all = new Set([...staticFloors, ...comboFloors]);
return Array.from(all).sort((a, b) => a - b); 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[] = [ export const ALL_GUARDIAN_FLOORS: number[] = [
10, 20, 30, 40, 50, 60, 80, 90, 100, // Original (all static floors) ...Object.keys(BASE_GUARDIANS).map(Number),
110, // Compound (90,100 already in static) ...Array.from({ length: 10 }, (_, i) => 150 + i * 10),
120, 130, 140, // Exotic
...Array.from({ length: 10 }, (_, i) => 150 + i * 10), // Combo
].sort((a, b) => a - b); ].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 { export function isGuardianFloor(floor: number): boolean {
return floor % 10 === 0; return floor % 10 === 0;
} }