From 4ee6222b0ec1b03a917a6c22c7328a48179fd299 Mon Sep 17 00:00:00 2001 From: n8n-gitea Date: Sat, 23 May 2026 17:02:48 +0200 Subject: [PATCH] refactor: replace static guardians with elemental progression system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- docs/circular-deps.txt | 4 +- docs/dependency-graph.json | 21 +- .../game/tabs/SpireSummaryTab.test.ts | 39 ++- src/lib/game/__tests__/spire-utils.test.ts | 73 ++++-- src/lib/game/data/guardian-data.ts | 245 ++++++++++++------ src/lib/game/data/guardian-encounters.ts | 203 +++------------ 6 files changed, 282 insertions(+), 303 deletions(-) diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index bcbb627..b51f555 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,8 +1,8 @@ # 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. -1. Processed 129 files (1.4s) (3 warnings) +1. Processed 129 files (1.5s) (3 warnings) 2. 1) stores/gameStore.ts > stores/gameActions.ts 3. 2) stores/gameStore.ts > stores/gameLoopActions.ts 4. 3) stores/gameStore.ts > stores/tick-pipeline.ts diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 4ded6b5..9541652 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_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.", "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": [ "types.ts" ], - "constants/guardians.ts": [ - "types.ts" - ], "constants/index.ts": [ "constants/core.ts", "constants/elements.ts", - "constants/guardians.ts", "constants/prestige.ts", "constants/rooms.ts", "constants/spells.ts", @@ -358,8 +354,11 @@ "data/golems/golems-data.ts", "data/golems/types.ts" ], + "data/guardian-data.ts": [ + "types.ts" + ], "data/guardian-encounters.ts": [ - "constants/guardians.ts", + "data/guardian-data.ts", "types.ts" ], "data/loot-drops.ts": [ @@ -392,6 +391,7 @@ "effects/upgrade-effects.types.ts": [], "hooks/useGameDerived.ts": [ "constants.ts", + "data/guardian-encounters.ts", "effects/special-effects.ts", "effects/upgrade-effects.ts", "stores/combatStore.ts", @@ -408,6 +408,7 @@ ], "stores/combat-actions.ts": [ "constants.ts", + "data/guardian-encounters.ts", "effects/discipline-effects.ts", "stores/combat-state.types.ts", "types.ts", @@ -497,6 +498,7 @@ "stores/gameStore.ts": [ "constants.ts", "data/attunements.ts", + "data/guardian-encounters.ts", "effects.ts", "effects/discipline-effects.ts", "effects/special-effects.ts", @@ -537,6 +539,7 @@ ], "stores/prestigeStore.ts": [ "constants.ts", + "data/guardian-encounters.ts", "types.ts", "utils/result.ts", "utils/safe-persist.ts" @@ -593,6 +596,8 @@ "utils/combat-utils.ts": [ "constants.ts", "data/enchantment-effects.ts", + "data/guardian-data.ts", + "data/guardian-encounters.ts", "types.ts", "utils/mana-utils.ts" ], @@ -610,7 +615,8 @@ "utils/floor-utils.ts" ], "utils/floor-utils.ts": [ - "constants.ts" + "constants.ts", + "data/guardian-encounters.ts" ], "utils/formatting.ts": [], "utils/index.ts": [ @@ -633,6 +639,7 @@ "utils/result.ts": [], "utils/room-utils.ts": [ "constants.ts", + "data/guardian-encounters.ts", "types.ts", "utils/enemy-utils.ts", "utils/floor-utils.ts" diff --git a/src/components/game/tabs/SpireSummaryTab.test.ts b/src/components/game/tabs/SpireSummaryTab.test.ts index 056d4cc..fe7eac9 100644 --- a/src/components/game/tabs/SpireSummaryTab.test.ts +++ b/src/components/game/tabs/SpireSummaryTab.test.ts @@ -28,24 +28,42 @@ describe('Tab barrel export', () => { // ─── Test: 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 floors = getAllGuardianFloors(); - // 9 static + compound (110) + exotic (120,130,140) + combo (150-240) = 23 total - expect(floors.length).toBeGreaterThanOrEqual(9); + // 14 static (10-140) + 10 combo (150-240) = 24 total + 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 expectedFloors = [10, 20, 30, 40, 50, 60, 80, 90, 100]; - for (const floor of expectedFloors) { + // Base: 10-70, Utility: 80, Compound: 90-110, Exotic: 120-140 + for (const floor of [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140]) { 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'); - 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)!; expect(def.name).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 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); expect(uniqueElements.size).toBe(elements.length); }); diff --git a/src/lib/game/__tests__/spire-utils.test.ts b/src/lib/game/__tests__/spire-utils.test.ts index 57f7a1b..cd440bc 100644 --- a/src/lib/game/__tests__/spire-utils.test.ts +++ b/src/lib/game/__tests__/spire-utils.test.ts @@ -9,7 +9,7 @@ import { getSpireRoomTypeDisplay, SPIRE_CONFIG, } 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 ───────────────────────────────────────────────────────────── @@ -182,42 +182,59 @@ describe('isGuardianFloor', () => { }); }); -describe('getExtendedGuardian', () => { - 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'); - }); - +describe('getExtendedGuardian (procedural combo guardians)', () => { it('should return combo guardians for floors 150+', () => { const g150 = getExtendedGuardian(150); expect(g150).not.toBeNull(); 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(15)).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(); }); }); diff --git a/src/lib/game/data/guardian-data.ts b/src/lib/game/data/guardian-data.ts index 497b3cd..a12ccd8 100644 --- a/src/lib/game/data/guardian-data.ts +++ b/src/lib/game/data/guardian-data.ts @@ -1,163 +1,244 @@ -// ─── Base Guardian Definitions (floors 10–100) ──────────────────────────────── -// Static canonical guardian data for the first 100 floors. +// ─── Static Guardian Definitions ────────────────────────────────────────────── +// 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'; -export const BASE_GUARDIANS: Record = { - 10: { - name: "Ignis Prime", element: "fire", hp: 5000, pact: 1.5, color: "#FF6B35", +// Helper: HP scales exponentially with floor +function hp(floor: number): number { + const base = 5000; + const exponent = 1.1 + (floor / 200); + return Math.floor(base * Math.pow(floor / 10, exponent)); +} + +// ─── Base Elements (Floors 10–70) ──────────────────────────────────────────── + +const BASE_GUARDIANS: Record = { + // -- Base elements -- + 10: { + name: 'Ignis Prime', element: 'fire', hp: hp(10), pact: 1.5, color: '#FF6B35', armor: 0.10, boons: [ { type: 'elementalDamage', value: 5, desc: '+5% Fire damage' }, { type: 'maxMana', value: 50, desc: '+50 max mana' }, ], - pactCost: 500, - pactTime: 2, - uniquePerk: "Fire spells cast 10% faster", + pactCost: 500, pactTime: 2, + uniquePerk: 'Fire spells cast 10% faster', power: 50, effects: [{ type: 'burn', value: 0.1 }], signingCost: { mana: 500, time: 2 }, unlocksMana: ['fire', 'lightning'], - damageMultiplier: 1.1, - insightMultiplier: 1.05, + damageMultiplier: 1.1, insightMultiplier: 1.05, }, - 20: { - name: "Aqua Regia", element: "water", hp: 15000, pact: 1.75, color: "#4ECDC4", + 20: { + name: 'Aqua Regia', element: 'water', hp: hp(20), pact: 1.75, color: '#4ECDC4', armor: 0.15, boons: [ { type: 'elementalDamage', value: 5, desc: '+5% Water damage' }, { type: 'manaRegen', value: 0.5, desc: '+0.5 mana regen' }, ], - pactCost: 1000, - pactTime: 4, - uniquePerk: "Water spells deal +15% damage", + pactCost: 1000, pactTime: 4, + uniquePerk: 'Water spells deal +15% damage', power: 150, effects: [{ type: 'armor_pierce', value: 0.15 }], signingCost: { mana: 1000, time: 4 }, - unlocksMana: ['water', 'transference'], - damageMultiplier: 1.2, - insightMultiplier: 1.1, + unlocksMana: ['water', 'sand'], + damageMultiplier: 1.2, insightMultiplier: 1.1, }, - 30: { - name: "Ventus Rex", element: "air", hp: 30000, pact: 2.0, color: "#00D4FF", + 30: { + name: 'Ventus Rex', element: 'air', hp: hp(30), pact: 2.0, color: '#00D4FF', armor: 0.18, boons: [ { 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, - pactTime: 6, - uniquePerk: "Air spells have 15% crit chance", + pactCost: 2000, pactTime: 6, + uniquePerk: 'Air spells have 15% crit chance', power: 300, effects: [{ type: 'cast_speed', value: 0.05 }], signingCost: { mana: 2000, time: 6 }, - unlocksMana: ['air', 'sand'], - damageMultiplier: 1.3, - insightMultiplier: 1.15, + unlocksMana: ['air'], + damageMultiplier: 1.3, insightMultiplier: 1.15, }, - 40: { - name: "Terra Firma", element: "earth", hp: 50000, pact: 2.25, color: "#F4A261", + 40: { + name: 'Terra Firma', element: 'earth', hp: hp(40), pact: 2.25, color: '#F4A261', armor: 0.25, boons: [ { type: 'elementalDamage', value: 5, desc: '+5% Earth damage' }, { type: 'maxMana', value: 100, desc: '+100 max mana' }, ], - pactCost: 4000, - pactTime: 8, - uniquePerk: "Earth spells deal +25% damage to guardians", + pactCost: 4000, pactTime: 8, + uniquePerk: 'Earth spells deal +25% damage to guardians', power: 500, effects: [{ type: 'armor_pierce', value: 0.2 }], signingCost: { mana: 4000, time: 8 }, unlocksMana: ['earth', 'metal'], - damageMultiplier: 1.4, - insightMultiplier: 1.2, + damageMultiplier: 1.4, insightMultiplier: 1.2, }, - 50: { - name: "Lux Aeterna", element: "light", hp: 80000, pact: 2.5, color: "#FFD700", + 50: { + name: 'Lux Aeterna', element: 'light', hp: hp(50), pact: 2.5, color: '#FFD700', armor: 0.20, boons: [ { type: 'elementalDamage', value: 10, desc: '+10% Light damage' }, { type: 'insightGain', value: 10, desc: '+10% insight gain' }, ], - pactCost: 8000, - pactTime: 10, - uniquePerk: "Light spells reveal enemy weaknesses (+20% damage)", + pactCost: 8000, pactTime: 10, + uniquePerk: 'Light spells reveal enemy weaknesses (+20% damage)', power: 800, effects: [{ type: 'crit_chance', value: 0.1 }], signingCost: { mana: 8000, time: 10 }, unlocksMana: ['light', 'crystal'], - damageMultiplier: 1.5, - insightMultiplier: 1.3, + damageMultiplier: 1.5, insightMultiplier: 1.3, }, - 60: { - name: "Umbra Mortis", element: "dark", hp: 120000, pact: 2.75, color: "#9B59B6", + 60: { + name: 'Umbra Mortis', element: 'dark', hp: hp(60), pact: 2.75, color: '#9B59B6', armor: 0.22, boons: [ { type: 'elementalDamage', value: 10, desc: '+10% Dark damage' }, { type: 'critDamage', value: 15, desc: '+15% crit damage' }, ], - pactCost: 15000, - pactTime: 12, - uniquePerk: "Dark spells deal +25% damage to armored enemies", + pactCost: 15000, pactTime: 12, + uniquePerk: 'Dark spells deal +25% damage to armored enemies', power: 1200, effects: [{ type: 'crit_damage', value: 0.15 }], signingCost: { mana: 15000, time: 12 }, unlocksMana: ['dark', 'void'], - damageMultiplier: 1.6, - insightMultiplier: 1.4, + damageMultiplier: 1.6, insightMultiplier: 1.4, }, - 80: { - name: "Mors Ultima", element: "death", hp: 250000, pact: 3.25, color: "#778CA3", + 70: { + name: 'Mors Ultima', element: 'death', hp: hp(70), pact: 3.0, color: '#778CA3', armor: 0.25, boons: [ { type: 'elementalDamage', value: 10, desc: '+10% Death damage' }, { type: 'rawDamage', value: 10, desc: '+10% raw damage' }, ], - pactCost: 40000, - pactTime: 16, - uniquePerk: "Death spells execute enemies below 20% HP", + pactCost: 25000, pactTime: 14, + uniquePerk: 'Death spells execute enemies below 20% HP', power: 2500, effects: [{ type: 'raw_damage', value: 0.1 }], - signingCost: { mana: 40000, time: 16 }, + signingCost: { mana: 25000, time: 14 }, unlocksMana: ['death'], - damageMultiplier: 1.8, - insightMultiplier: 1.5, + damageMultiplier: 1.8, 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 90–110) ─────────────────────────────────────── + 90: { + name: '', element: 'metal', hp: hp(90), pact: 3.5, color: '#BDC3C7', armor: 0.30, boons: [ - { type: 'elementalDamage', value: 15, desc: '+15% Void damage' }, - { type: 'maxMana', value: 200, desc: '+200 max mana' }, - { type: 'manaRegen', value: 1, desc: '+1 mana regen' }, + { type: 'elementalDamage', value: 15, desc: '+15% Metal damage' }, + { type: 'maxMana', value: 150, desc: '+150 max mana' }, ], - pactCost: 75000, - pactTime: 20, - uniquePerk: "Void spells ignore 30% of enemy resistance", - power: 4000, - effects: [{ type: 'void_resist', value: 0.3 }], - signingCost: { mana: 75000, time: 20 }, - unlocksMana: ['void', 'stellar'], - damageMultiplier: 2.0, - insightMultiplier: 1.7, + 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: "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 120–140) ──────────────────────────────────────── + 120: { + name: '', element: 'crystal', hp: hp(120), pact: 4.5, color: '#85C1E9', armor: 0.35, boons: [ - { type: 'elementalDamage', value: 20, desc: '+20% Stellar damage' }, - { type: 'maxMana', value: 500, desc: '+500 max mana' }, + { type: 'elementalDamage', value: 20, desc: '+20% Crystal damage' }, + { type: 'maxMana', value: 300, desc: '+300 max mana' }, { type: 'manaRegen', value: 2, desc: '+2 mana regen' }, - { type: 'insightGain', value: 25, desc: '+25% insight gain' }, ], - pactCost: 150000, - pactTime: 24, - uniquePerk: "All spells deal +50% damage and cast 25% faster", - power: 10000, - effects: [{ type: 'all_damage', value: 0.5 }], - signingCost: { mana: 150000, time: 24 }, + 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: 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'], - damageMultiplier: 2.5, - insightMultiplier: 2.0, + damageMultiplier: 2.5, 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 }; diff --git a/src/lib/game/data/guardian-encounters.ts b/src/lib/game/data/guardian-encounters.ts index d096ebb..e44e170 100644 --- a/src/lib/game/data/guardian-encounters.ts +++ b/src/lib/game/data/guardian-encounters.ts @@ -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 = { - 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 = { - 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 10–100) with the extended procedural system -// (compound 110, exotic 120–140, combo 150+). +// Merges static guardians (floors 10–140) 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; }