feat: restructure guardian progression system with dynamic element support
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s

This commit is contained in:
2026-05-29 17:18:13 +02:00
parent 644b76f16d
commit 71c68443c4
19 changed files with 757 additions and 446 deletions
+199 -185
View File
@@ -1,249 +1,263 @@
// ─── Static Guardian Definitions ──────────────────────────────────────────────
// Ordered by floor: base → utility → compound → exotic.
// ─── Static Guardian Definitions ─────────────────────────────────────────────────
// New 9-tier progression for guardians:
//
// 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
// Tier 1: Base Elements (floors 1080)
// Fire, Water, Air, Earth, Light, Dark, Death, Transference
// Tier 2: Composite Elements (floors 90120)
// Metal, Sand, Lightning
// Tier 3: Composite + Components (floors 130160)
// Tier 4: Exotic Elements (floors 170200)
//
// Floor 150+: Procedural combination guardians (see getComboGuardian in guardian-encounters.ts)
// Floors 210+ are procedurally generated in guardian-encounters.ts.
import type { GuardianDef } from '../types';
import { resolveMultiUnlockChain } from '../utils/guardian-utils';
// ─── Shared Helpers ─────────────────────────────────────────────────────────────
// 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));
}
// Helper: pact cost scales with guardian HP, power, and armor
function pactCost(hpVal: number, power: number, armor: number): number {
return Math.floor(hpVal * 0.3 + power * 5 + hpVal * armor * 0.5);
}
// ─── Base Elements (Floors 1070) ────────────────────────────────────────────
function mk(
floor: number,
name: string,
element: string[],
color: string,
armor: number,
pactMult: number,
boons: GuardianDef['boons'],
uniquePerk: string,
effects: GuardianDef['effects'],
): GuardianDef {
const hpVal = hp(floor);
const power = Math.floor(hpVal * 0.5);
const arm = armor;
const pc = pactCost(hpVal, power, arm);
const pt = 2 + Math.floor(floor / 10);
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,
boons: [
return {
name,
element,
hp: hpVal,
pact: pactMult,
color,
armor: arm,
boons,
pactCost: pc,
pactTime: pt,
uniquePerk,
power,
effects,
signingCost: { mana: pc, time: pt },
unlocksMana: resolveMultiUnlockChain(element),
damageMultiplier: 1.0 + floor * 0.01,
insightMultiplier: 1.0 + floor * 0.005,
};
}
// ═══════════════════════════════════════════════════════════════════════════════
// TIER 1: Base Elements (Floors 1080)
// ═══════════════════════════════════════════════════════════════════════════════
const TIER1: Record<number, GuardianDef> = {
10: mk(10, 'Ignis Prime', ['fire'], '#FF6B35', 0.10, 1.5,
[
{ type: 'elementalDamage', value: 5, desc: '+5% Fire damage' },
{ type: 'maxMana', value: 50, desc: '+50 max mana' },
],
pactCost: pactCost(hp(10), 50, 0.10), 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,
},
20: {
name: 'Aqua Regia', element: 'water', hp: hp(20), pact: 1.75, color: '#4ECDC4',
armor: 0.15,
boons: [
'Fire spells cast 10% faster',
[{ type: 'burn', value: 0.1 }],
),
20: mk(20, 'Aqua Regia', ['water'], '#4ECDC4', 0.15, 1.75,
[
{ type: 'elementalDamage', value: 5, desc: '+5% Water damage' },
{ type: 'manaRegen', value: 0.5, desc: '+0.5 mana regen' },
],
pactCost: pactCost(hp(20), 150, 0.15), pactTime: 4,
uniquePerk: 'Water spells deal +15% damage',
power: 150,
effects: [{ type: 'armor_pierce', value: 0.15 }],
signingCost: { mana: 1000, time: 4 },
unlocksMana: ['water', 'sand'],
damageMultiplier: 1.2, insightMultiplier: 1.1,
},
30: {
name: 'Ventus Rex', element: 'air', hp: hp(30), pact: 2.0, color: '#00D4FF',
armor: 0.18,
boons: [
'Water spells deal +15% damage',
[{ type: 'armor_pierce', value: 0.15 }],
),
30: mk(30, 'Ventus Rex', ['air'], '#00D4FF', 0.18, 2.0,
[
{ type: 'elementalDamage', value: 5, desc: '+5% Air damage' },
{ type: 'castingSpeed', value: 5, desc: '+5% casting speed' },
],
pactCost: pactCost(hp(30), 300, 0.18), 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'],
damageMultiplier: 1.3, insightMultiplier: 1.15,
},
40: {
name: 'Terra Firma', element: 'earth', hp: hp(40), pact: 2.25, color: '#F4A261',
armor: 0.25,
boons: [
'Air spells have 15% crit chance',
[{ type: 'cast_speed', value: 0.05 }],
),
40: mk(40, 'Terra Firma', ['earth'], '#F4A261', 0.25, 2.25,
[
{ type: 'elementalDamage', value: 5, desc: '+5% Earth damage' },
{ type: 'maxMana', value: 100, desc: '+100 max mana' },
],
pactCost: pactCost(hp(40), 500, 0.25), 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,
},
50: {
name: 'Lux Aeterna', element: 'light', hp: hp(50), pact: 2.5, color: '#FFD700',
armor: 0.20,
boons: [
'Earth spells deal +25% damage to guardians',
[{ type: 'armor_pierce', value: 0.2 }],
),
50: mk(50, 'Lux Aeterna', ['light'], '#FFD700', 0.20, 2.5,
[
{ type: 'elementalDamage', value: 10, desc: '+10% Light damage' },
{ type: 'insightGain', value: 10, desc: '+10% insight gain' },
],
pactCost: pactCost(hp(50), 800, 0.20), 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,
},
60: {
name: 'Umbra Mortis', element: 'dark', hp: hp(60), pact: 2.75, color: '#9B59B6',
armor: 0.22,
boons: [
'Light spells reveal enemy weaknesses (+20% damage)',
[{ type: 'crit_chance', value: 0.1 }],
),
60: mk(60, 'Umbra Mortis', ['dark'], '#9B59B6', 0.22, 2.75,
[
{ type: 'elementalDamage', value: 10, desc: '+10% Dark damage' },
{ type: 'critDamage', value: 15, desc: '+15% crit damage' },
],
pactCost: pactCost(hp(60), 1200, 0.22), 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,
},
70: {
name: 'Mors Ultima', element: 'death', hp: hp(70), pact: 3.0, color: '#778CA3',
armor: 0.25,
boons: [
'Dark spells deal +25% damage to armored enemies',
[{ type: 'crit_damage', value: 0.15 }],
),
70: mk(70, 'Mors Ultima', ['death'], '#778CA3', 0.25, 3.0,
[
{ type: 'elementalDamage', value: 10, desc: '+10% Death damage' },
{ type: 'rawDamage', value: 10, desc: '+10% raw damage' },
],
pactCost: pactCost(hp(70), 2500, 0.25), pactTime: 14,
uniquePerk: 'Death spells execute enemies below 20% HP',
power: 2500,
effects: [{ type: 'raw_damage', value: 0.1 }],
signingCost: { mana: 25000, time: 14 },
unlocksMana: ['death'],
damageMultiplier: 1.8, insightMultiplier: 1.5,
},
// -- Utility element --
80: {
name: 'Vinculum Arcana', element: 'transference', hp: hp(80), pact: 3.25, color: '#1ABC9C',
armor: 0.20,
boons: [
'Death spells execute enemies below 20% HP',
[{ type: 'raw_damage', value: 0.1 }],
),
80: mk(80, 'Vinculum Arcana', ['transference'], '#1ABC9C', 0.20, 3.25,
[
{ type: 'maxMana', value: 150, desc: '+150 max mana' },
{ type: 'manaRegen', value: 1.0, desc: '+1.0 mana regen' },
],
pactCost: pactCost(hp(80), 3500, 0.20), 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,
},
'Transference spells have 25% reduced cost',
[{ type: 'cost_reduction', value: 0.25 }],
),
};
// -- Compound Elements (Floors 90110) ───────────────────────────────────────
90: {
name: '', element: 'metal', hp: hp(90), pact: 3.5, color: '#BDC3C7',
armor: 0.30,
boons: [
// ═══════════════════════════════════════════════════════════════════════════════
// TIER 2: Composite Elements (Floors 90120)
// ═══════════════════════════════════════════════════════════════════════════════
const TIER2: Record<number, GuardianDef> = {
90: mk(90, '', ['metal'], '#BDC3C7', 0.30, 3.5,
[
{ type: 'elementalDamage', value: 15, desc: '+15% Metal damage' },
{ type: 'maxMana', value: 150, desc: '+150 max mana' },
],
pactCost: pactCost(hp(90), 6000, 0.30), 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: hp(100), pact: 3.75, color: '#D4AC0D',
armor: 0.25,
boons: [
'Metal spells pierce 20% armor',
[{ type: 'armor_pierce', value: 0.2 }],
),
100: mk(100, '', ['sand'], '#D4AC0D', 0.25, 3.75,
[
{ type: 'elementalDamage', value: 15, desc: '+15% Sand damage' },
{ type: 'manaRegen', value: 1.5, desc: '+1.5 mana regen' },
],
pactCost: pactCost(hp(100), 8000, 0.25), 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: [
'Sand spells slow enemies by 25%',
[{ type: 'slow', value: 0.25 }],
),
110: mk(110, '', ['lightning'], '#FFEB3B', 0.22, 4.0,
[
{ type: 'elementalDamage', value: 15, desc: '+15% Lightning damage' },
{ type: 'castingSpeed', value: 15, desc: '+15% casting speed' },
],
pactCost: pactCost(hp(110), 10000, 0.22), 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,
},
'Lightning spells chain to 2 additional targets',
[{ type: 'chain', value: 2 }],
),
};
// -- Exotic Elements (Floors 120140) ────────────────────────────────────────
120: {
name: '', element: 'crystal', hp: hp(120), pact: 4.5, color: '#85C1E9',
armor: 0.35,
boons: [
// ═══════════════════════════════════════════════════════════════════════════════
// TIER 3: Composite + Their Components (Floors 130160)
// ═══════════════════════════════════════════════════════════════════════════════
const TIER3: Record<number, GuardianDef> = {
130: mk(130, '', ['metal', 'fire', 'earth'], '#D4A574', 0.35, 4.5,
[
{ type: 'elementalDamage', value: 20, desc: '+20% Metal damage' },
{ type: 'elementalDamage', value: 10, desc: '+10% Fire damage' },
{ type: 'elementalDamage', value: 10, desc: '+10% Earth damage' },
],
'Tri-aspect: Metal, Fire, and Earth spells gain +10% effectiveness',
[{ type: 'armor_pierce', value: 0.25 }, { type: 'burn', value: 0.1 }],
),
140: mk(140, '', ['sand', 'earth', 'water'], '#C9B896', 0.30, 4.75,
[
{ type: 'elementalDamage', value: 20, desc: '+20% Sand damage' },
{ type: 'elementalDamage', value: 10, desc: '+10% Earth damage' },
{ type: 'elementalDamage', value: 10, desc: '+10% Water damage' },
],
'Tri-aspect: Sand, Earth, and Water spells gain +10% effectiveness',
[{ type: 'slow', value: 0.3 }, { type: 'armor_pierce', value: 0.15 }],
),
150: mk(150, '', ['lightning', 'fire', 'air'], '#FFE066', 0.28, 5.0,
[
{ type: 'elementalDamage', value: 20, desc: '+20% Lightning damage' },
{ type: 'elementalDamage', value: 10, desc: '+10% Fire damage' },
{ type: 'elementalDamage', value: 10, desc: '+10% Air damage' },
],
'Tri-aspect: Lightning, Fire, and Air spells gain +10% effectiveness',
[{ type: 'chain', value: 2 }, { type: 'cast_speed', value: 0.1 }],
),
160: mk(160, '', ['metal', 'lightning', 'fire', 'earth', 'air'], '#E8C872', 0.35, 5.25,
[
{ type: 'elementalDamage', value: 15, desc: '+15% Metal damage' },
{ type: 'elementalDamage', value: 15, desc: '+15% Lightning damage' },
{ type: 'rawDamage', value: 10, desc: '+10% raw damage' },
],
'Fused aspects: Lightning spells gain +20% armor pierce; Metal spells chain once',
[{ type: 'armor_pierce', value: 0.3 }, { type: 'chain', value: 1 }],
),
};
// ═══════════════════════════════════════════════════════════════════════════════
// TIER 4: Exotic Elements (Floors 170200)
// ═══════════════════════════════════════════════════════════════════════════════
const TIER4: Record<number, GuardianDef> = {
170: mk(170, '', ['crystal'], '#85C1E9', 0.35, 5.5,
[
{ 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: pactCost(hp(120), 15000, 0.35), 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: [
'Crystal spells reflect 15% damage back to attackers',
[{ type: 'reflect', value: 0.15 }],
),
180: mk(180, '', ['stellar'], '#F0E68C', 0.30, 6.0,
[
{ type: 'elementalDamage', value: 25, desc: '+25% Stellar damage' },
{ type: 'insightGain', value: 20, desc: '+20% insight gain' },
],
pactCost: pactCost(hp(130), 20000, 0.30), 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: hp(140), pact: 5.5, color: '#4A235A',
armor: 0.35,
boons: [
'Stellar spells deal +30% damage at night',
[{ type: 'night_bonus', value: 0.3 }],
),
190: mk(190, '', ['void'], '#4A235A', 0.35, 6.5,
[
{ 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: pactCost(hp(140), 30000, 0.35), 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,
},
'Void spells ignore 40% of all resistances',
[{ type: 'resist_ignore', value: 0.4 }],
),
200: mk(200, '', ['crystal', 'stellar', 'void'], '#B39DDB', 0.40, 7.0,
[
{ type: 'elementalDamage', value: 15, desc: '+15% Crystal damage' },
{ type: 'elementalDamage', value: 15, desc: '+15% Stellar damage' },
{ type: 'elementalDamage', value: 15, desc: '+15% Void damage' },
],
'Exotic convergence: All exotic spells gain +15% effectiveness',
[{ type: 'reflect', value: 0.1 }, { type: 'resist_ignore', value: 0.1 }],
),
};
export { BASE_GUARDIANS };
// ═══════════════════════════════════════════════════════════════════════════════
// COMBINED STATIC GUARDIANS
// ═══════════════════════════════════════════════════════════════════════════════
const STATIC_GUARDIANS: Record<number, GuardianDef> = {
...TIER1,
...TIER2,
...TIER3,
...TIER4,
};
export { STATIC_GUARDIANS, TIER1, TIER2, TIER3, TIER4 };
+304 -81
View File
@@ -1,18 +1,24 @@
// ─── Guardian Encounters ───────────────────────────────────────────────────────
// ─── 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)
// Guardian progression (9 tiers):
// Tier 1: Base Elements (static, floors 1080, guardian-data.ts)
// Tier 2: Composite Elements (static, floors 90120, guardian-data.ts)
// Tier 3: Composite+Components (static, floors 130160, guardian-data.ts)
// Tier 4: Exotic Elements (static, floors 170200, guardian-data.ts)
// Tier 5: Dual Element Pairs (dynamic, floors 210240, this file)
// Tier 6: Dual Comp+Components (dynamic, floors 250290, this file)
// Tier 7: Exotic+Components (dynamic, floors 300340, this file)
// Tier 8: Exotic+Comp+Components (dynamic, floors 350390, this file)
// Tier 9: Full Fusion (dynamic, floors 400+, this file)
//
// All lookups go through getGuardianForFloor() which merges static + procedural.
import type { GuardianDef } from '../types';
import { BASE_GUARDIANS } from './guardian-data';
import type { GuardianDef, GuardianBoon } from '../types';
import { STATIC_GUARDIANS } from './guardian-data';
import { resolveMultiUnlockChain } from '../utils/guardian-utils';
// ─── Name Generation ──────────────────────────────────────────────────────────
// ─── Name Generation ────────────────────────────────────────────────────────────
const GUARDIAN_PREFIXES: Record<string, string[]> = {
fire: ['Ignis', 'Pyra', 'Sol', 'Vulcan', 'Ember'],
@@ -36,14 +42,7 @@ const GUARDIAN_TITLES: string[] = [
'Guardian', 'Sentinel', 'Champion', 'Overlord', 'Archon',
];
export function generateGuardianName(element: string, floor: number = 0): string {
const prefixes = GUARDIAN_PREFIXES[element] || ['Unknown'];
const prefix = prefixes[floor % prefixes.length];
const title = GUARDIAN_TITLES[Math.floor(floor / 10) % GUARDIAN_TITLES.length];
return `${prefix} the ${title}`;
}
export function generateComboGuardianName(elements: string[], floor: number = 0): string {
export function generateGuardianName(elements: string[], floor: number = 0): string {
const parts = elements.map((el, i) => {
const prefixes = GUARDIAN_PREFIXES[el] || ['Unknown'];
return prefixes[(floor + i) % prefixes.length];
@@ -52,7 +51,12 @@ export function generateComboGuardianName(elements: string[], floor: number = 0)
return `${parts.join('-')} the ${title}`;
}
// ─── Guardian HP Scaling ──────────────────────────────────────────────────────
/** @deprecated Use generateGuardianName(elements[], floor) instead. Kept for backward compat. */
export function generateGuardianNameSingle(element: string, floor: number = 0): string {
return generateGuardianName([element], floor);
}
// ─── Guardian HP Scaling ───────────────────────────────────────────────────────
export function getGuardianHP(floor: number): number {
const base = 5000;
@@ -60,96 +64,315 @@ export function getGuardianHP(floor: number): number {
return Math.floor(base * Math.pow(floor / 10, exponent));
}
// ─── 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.
// ─── Tier Helpers ───────────────────────────────────────────────────────────────
const COMBO_PAIRS: [string, string][] = [
['fire', 'water'], // Steam
['fire', 'air'], // Smoke
['water', 'earth'], // Mud
['light', 'dark'], // Twilight
['death', 'light'], // Undeath
['fire', 'death'], // Hellfire
['water', 'dark'], // Abyssal
['air', 'light'], // Radiant wind
['earth', 'death'], // Fossil
const BASE_ELEMENTS = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'];
const COMPOSITE_ELEMENTS = ['metal', 'sand', 'lightning'];
const EXOTIC_ELEMENTS = ['crystal', 'stellar', 'void'];
/** Generate boon elements (max 3) */
function makeBoons(elements: string[], floor: number): GuardianBoon[] {
return elements.slice(0, 3).map((el) => ({
type: 'elementalDamage' as const,
value: 10 + Math.floor(floor / 20) * 5,
desc: `+${10 + Math.floor(floor / 20) * 5}% ${el} damage`,
}));
}
function getTier(floor: number): number {
if (floor >= 400) return 9;
if (floor >= 350) return 8;
if (floor >= 300) return 7;
if (floor >= 250) return 6;
if (floor >= 210) return 5;
return 0; // Static tiers
}
// ═══════════════════════════════════════════════════════════════════════════════
// TIER 5: Dual Element Pairs (Floors 210240)
// ═══════════════════════════════════════════════════════════════════════════════
const DUAL_PAIRS: [string, string][] = [
['fire', 'water'],
['fire', 'air'],
['water', 'earth'],
['light', 'dark'],
['death', 'light'],
['fire', 'death'],
['water', 'dark'],
['air', 'light'],
['earth', 'death'],
];
export function getComboGuardian(floor: number): GuardianDef {
const comboIndex = Math.floor((floor - 150) / 10) % COMBO_PAIRS.length;
const [el1, el2] = COMBO_PAIRS[comboIndex];
const hp = getGuardianHP(floor);
const armor = Math.min(0.5, 0.25 + (floor - 150) * 0.002);
function getTier5Guardian(floor: number): GuardianDef {
const idx = Math.floor((floor - 210) / 10) % DUAL_PAIRS.length;
const [el1, el2] = DUAL_PAIRS[idx];
const elements = [el1, el2];
const hpVal = getGuardianHP(floor);
const armor = Math.min(0.5, 0.30 + (floor - 210) * 0.003);
return {
name: '',
element: `${el1}+${el2}`,
hp,
pact: 6.0 + (floor - 150) * 0.05,
color: '#E8D5F5',
element: elements,
hp: hpVal,
pact: 7.5 + (floor - 210) * 0.05,
color: blendColors(el1, el2),
armor,
boons: [
{ type: 'elementalDamage', value: 10, desc: `+10% ${el1} damage` },
{ type: 'elementalDamage', value: 10, desc: `+10% ${el2} damage` },
],
pactCost: Math.floor(hp * 0.3 + Math.floor(hp * 0.5) * 5 + hp * armor * 0.5),
pactTime: 20 + Math.floor((floor - 150) / 10),
boons: makeBoons(elements, floor),
pactCost: Math.floor(hpVal * 0.3 + hpVal * armor * 0.5),
pactTime: 20 + Math.floor((floor - 210) / 10),
uniquePerk: `Dual-aspect: ${el1} and ${el2} spells gain +20% effectiveness`,
power: Math.floor(hp * 0.5),
power: Math.floor(hpVal * 0.5),
effects: [
{ type: `${el1}_boost`, value: 0.2 },
{ type: `${el2}_boost`, value: 0.2 },
{ type: `${el1}_boost`, value: 0.15 },
{ type: `${el2}_boost`, value: 0.15 },
],
signingCost: { mana: Math.floor(hp * 0.3 + Math.floor(hp * 0.5) * 5 + hp * armor * 0.5), time: 20 + Math.floor((floor - 150) / 10) },
unlocksMana: [el1, el2],
damageMultiplier: 3.0 + (floor - 150) * 0.02,
insightMultiplier: 2.5 + (floor - 150) * 0.01,
signingCost: {
mana: Math.floor(hpVal * 0.3),
time: 20 + Math.floor((floor - 210) / 10),
},
unlocksMana: resolveMultiUnlockChain(elements),
damageMultiplier: 3.5 + (floor - 210) * 0.02,
insightMultiplier: 3.0 + (floor - 210) * 0.01,
};
}
// ─── Procedural Guardian Lookup (Floor 150+) ──────────────────────────────────
// ═══════════════════════════════════════════════════════════════════════════════
// TIER 6: Dual Composite + Components (Floors 250290)
// Pairs of composites with all their base components.
// ═══════════════════════════════════════════════════════════════════════════════
export function getExtendedGuardian(floor: number): GuardianDef | null {
if (floor >= 150 && floor % 10 === 0) {
const g = getComboGuardian(floor);
if (!g.name) {
const elements = g.element.split('+');
g.name = generateComboGuardianName(elements, floor);
}
return g;
}
return null;
const DUAL_COMP_PAIRS: [string, string][] = [
['metal', 'sand'],
['metal', 'lightning'],
['sand', 'lightning'],
];
function getTier6Guardian(floor: number): GuardianDef {
const idx = Math.floor((floor - 250) / 10) % DUAL_COMP_PAIRS.length;
const [comp1, comp2] = DUAL_COMP_PAIRS[idx];
const elements = resolveMultiUnlockChain([comp1, comp2]);
const hpVal = getGuardianHP(floor);
const armor = Math.min(0.55, 0.35 + (floor - 250) * 0.003);
return {
name: '',
element: elements,
hp: hpVal,
pact: 9.0 + (floor - 250) * 0.05,
color: blendColors(comp1, comp2),
armor,
boons: makeBoons([comp1, comp2], floor),
pactCost: Math.floor(hpVal * 0.3 + hpVal * armor * 0.5),
pactTime: 24 + Math.floor((floor - 250) / 10),
uniquePerk: `Fusion twin-aspect: ${comp1} and ${comp2} spells gain +25% effectiveness`,
power: Math.floor(hpVal * 0.55),
effects: [
{ type: 'armor_pierce', value: 0.2 },
{ type: 'chain', value: 1 },
],
signingCost: {
mana: Math.floor(hpVal * 0.35),
time: 24 + Math.floor((floor - 250) / 10),
},
unlocksMana: resolveMultiUnlockChain(elements),
damageMultiplier: 4.0 + (floor - 250) * 0.02,
insightMultiplier: 3.5 + (floor - 250) * 0.01,
};
}
// ─── Unified Guardian System ─────────────────────────────────────────────────
// Merges static guardians (floors 10140) with procedural combo guardians (150+).
// ═══════════════════════════════════════════════════════════════════════════════
// TIER 7: Exotic + Components (Floors 300340)
// Each exotic element paired with all its component base elements.
// ═══════════════════════════════════════════════════════════════════════════════
function getTier7Guardian(floor: number): GuardianDef {
const exoticIdx = Math.floor((floor - 300) / 10) % EXOTIC_ELEMENTS.length;
const exoticEl = EXOTIC_ELEMENTS[exoticIdx];
const chain = resolveMultiUnlockChain([exoticEl]);
const hpVal = getGuardianHP(floor);
const armor = Math.min(0.6, 0.40 + (floor - 300) * 0.003);
return {
name: '',
element: chain,
hp: hpVal,
pact: 10.5 + (floor - 300) * 0.05,
color: '#B8A9C9',
armor,
boons: [makeBoons([exoticEl], floor)[0]],
pactCost: Math.floor(hpVal * 0.35 + hpVal * armor * 0.5),
pactTime: 28 + Math.floor((floor - 300) / 10),
uniquePerk: `Exotic resonance: ${exoticEl} spells gain +30% effectiveness`,
power: Math.floor(hpVal * 0.6),
effects: [
{ type: 'resist_ignore', value: 0.2 },
{ type: 'reflect', value: 0.1 },
],
signingCost: {
mana: Math.floor(hpVal * 0.35),
time: 28 + Math.floor((floor - 300) / 10),
},
unlocksMana: chain,
damageMultiplier: 4.5 + (floor - 300) * 0.02,
insightMultiplier: 4.0 + (floor - 300) * 0.01,
};
}
// ═══════════════════════════════════════════════════════════════════════════════
// TIER 8: Exotic + Composite + Components (Floors 350390)
// One exotic + one composite + all base elements.
// ═══════════════════════════════════════════════════════════════════════════════
function getTier8Guardian(floor: number): GuardianDef {
const exoticIdx = Math.floor((floor - 350) / 10) % EXOTIC_ELEMENTS.length;
const compIdx = Math.floor((floor - 350) / 10) % COMPOSITE_ELEMENTS.length;
const exoticEl = EXOTIC_ELEMENTS[exoticIdx];
const compEl = COMPOSITE_ELEMENTS[compIdx];
const elements = resolveMultiUnlockChain([exoticEl, compEl]);
const hpVal = getGuardianHP(floor);
const armor = Math.min(0.65, 0.45 + (floor - 350) * 0.003);
return {
name: '',
element: elements,
hp: hpVal,
pact: 12.0 + (floor - 350) * 0.05,
color: '#9B72AA',
armor,
boons: makeBoons([exoticEl, compEl], floor),
pactCost: Math.floor(hpVal * 0.4 + hpVal * armor * 0.5),
pactTime: 32 + Math.floor((floor - 350) / 10),
uniquePerk: `Primordial fusion: ${exoticEl} and ${compEl} spells gain +25% effectiveness`,
power: Math.floor(hpVal * 0.65),
effects: [
{ type: 'resist_ignore', value: 0.25 },
{ type: 'armor_pierce', value: 0.2 },
{ type: 'chain', value: 1 },
],
signingCost: {
mana: Math.floor(hpVal * 0.4),
time: 32 + Math.floor((floor - 350) / 10),
},
unlocksMana: elements,
damageMultiplier: 5.0 + (floor - 350) * 0.02,
insightMultiplier: 4.5 + (floor - 350) * 0.01,
};
}
// ═══════════════════════════════════════════════════════════════════════════════
// TIER 9: Full Fusion — 1 Exotic + 2 Composite + All Components (Floors 400+)
// ═══════════════════════════════════════════════════════════════════════════════
function getTier9Guardian(floor: number): GuardianDef {
const exoticIdx = (Math.floor((floor - 400) / 10)) % EXOTIC_ELEMENTS.length;
const exoticEl = EXOTIC_ELEMENTS[exoticIdx];
// Pick 2 different composites
const comp1 = COMPOSITE_ELEMENTS[Math.floor((floor - 400) / 10) % COMPOSITE_ELEMENTS.length];
const comp2 = COMPOSITE_ELEMENTS[(Math.floor((floor - 400) / 10) + 1) % COMPOSITE_ELEMENTS.length];
const elements = resolveMultiUnlockChain([exoticEl, comp1, comp2]);
const hpVal = getGuardianHP(floor);
const armor = Math.min(0.7, 0.50 + (floor - 400) * 0.002);
return {
name: '',
element: elements,
hp: hpVal,
pact: 14.0 + (floor - 400) * 0.05,
color: '#7B5E9A',
armor,
boons: makeBoons([exoticEl, comp1, comp2], floor),
pactCost: Math.floor(hpVal * 0.45 + hpVal * armor * 0.5),
pactTime: 36 + Math.floor((floor - 400) / 10),
uniquePerk: `Cosmic convergence: All exotic, composite, and base spells gain +15% effectiveness`,
power: Math.floor(hpVal * 0.7),
effects: [
{ type: 'resist_ignore', value: 0.3 },
{ type: 'armor_pierce', value: 0.25 },
{ type: 'chain', value: 2 },
{ type: 'reflect', value: 0.1 },
],
signingCost: {
mana: Math.floor(hpVal * 0.45),
time: 36 + Math.floor((floor - 400) / 10),
},
unlocksMana: elements,
damageMultiplier: 5.5 + (floor - 400) * 0.02,
insightMultiplier: 5.0 + (floor - 400) * 0.01,
};
}
// ─── Blending Colors for Dual/Multi-Element Guardians ──────────────────────────
const ELEMENT_COLORS: Record<string, string> = {
fire: '#FF6B35', water: '#4ECDC4', air: '#00D4FF', earth: '#F4A261',
light: '#FFD700', dark: '#9B59B6', death: '#778CA3', transference: '#1ABC9C',
metal: '#BDC3C7', sand: '#D4AC0D', lightning: '#FFEB3B',
crystal: '#85C1E9', stellar: '#F0E68C', void: '#4A235A',
};
function blendColors(el1: string, el2: string): string {
const c1 = ELEMENT_COLORS[el1] || '#888888';
const c2 = ELEMENT_COLORS[el2] || '#888888';
try {
const r = Math.floor((parseInt(c1.slice(1, 3), 16) + parseInt(c2.slice(1, 3), 16)) / 2);
const g = Math.floor((parseInt(c1.slice(3, 5), 16) + parseInt(c2.slice(3, 5), 16)) / 2);
const b = Math.floor((parseInt(c1.slice(5, 7), 16) + parseInt(c2.slice(5, 7), 16)) / 2);
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
} catch {
return '#888888';
}
}
// ─── Procedural Guardian Generator ─────────────────────────────────────────────
function getProceduralGuardian(floor: number): GuardianDef | null {
if (floor < 210 || floor % 10 !== 0) return null;
const tier = getTier(floor);
let g: GuardianDef;
switch (tier) {
case 5: g = getTier5Guardian(floor); break;
case 6: g = getTier6Guardian(floor); break;
case 7: g = getTier7Guardian(floor); break;
case 8: g = getTier8Guardian(floor); break;
case 9: g = getTier9Guardian(floor); break;
default: return null;
}
if (!g.name) {
g.name = generateGuardianName(g.element, floor);
}
return g;
}
// ─── Unified Guardian System ────────────────────────────────────────────────────
/** 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]) {
const g = { ...BASE_GUARDIANS[floor] };
if (STATIC_GUARDIANS[floor]) {
const g = { ...STATIC_GUARDIANS[floor] };
if (!g.name) g.name = generateGuardianName(g.element, floor);
return g;
}
return getExtendedGuardian(floor);
return getProceduralGuardian(floor);
}
/** All guardian floors — merged from static + extended. */
/** All guardian floors — dynamically computed. */
export function getAllGuardianFloors(): number[] {
const staticFloors = Object.keys(BASE_GUARDIANS).map(Number);
const comboFloors = Array.from({ length: 10 }, (_, i) => 150 + i * 10);
const all = new Set([...staticFloors, ...comboFloors]);
// Static floors from guardian-data.ts
const staticFloors = Object.keys(STATIC_GUARDIANS).map(Number);
// Procedural floors: every 10th floor from 210 to 450
const proceduralFloors: number[] = [];
for (let f = 210; f <= 450; f += 10) {
proceduralFloors.push(f);
}
const all = new Set([...staticFloors, ...proceduralFloors]);
return Array.from(all).sort((a, b) => a - b);
}
/** All guardian floors including procedural — kept for backwards compatibility. */
export const ALL_GUARDIAN_FLOORS: number[] = [
...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 with a defined guardian). */
export function isGuardianFloor(floor: number): boolean {
return floor % 10 === 0;
return floor % 10 === 0 && floor >= 10;
}