Files
Mana-Loop/src/lib/game/data/guardian-encounters.ts
T
n8n-gitea a8fab1eb86
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m37s
fix: make guardian names deterministic per floor instead of using Math.random()
- generateGuardianName() now takes a floor parameter and uses floor % prefixes.length for deterministic prefix selection
- generateComboGuardianName() now takes a floor parameter and uses (floor + i) % prefixes.length for each element
- getGuardianForFloor() passes floor to generateGuardianName for static guardians with empty names
- getExtendedGuardian() passes floor to generateComboGuardianName for combo guardians
- Removes dependency on Math.random() → names are stable across ticks/refreshes

Fixes #161
2026-05-27 11:26:28 +02:00

156 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ─── 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 ──────────────────────────────────────────────────────────
const GUARDIAN_PREFIXES: Record<string, string[]> = {
fire: ['Ignis', 'Pyra', 'Sol', 'Vulcan', 'Ember'],
water: ['Aqua', 'Marina', 'Thal', 'Pelag', 'Coral'],
air: ['Ventus', 'Zephyr', 'Aero', 'Nimbus', 'Gale'],
earth: ['Terra', 'Petra', 'Mont', 'Gaia', 'Ore'],
light: ['Lux', 'Solaris', 'Radi', 'Lumin', 'Aur'],
dark: ['Umbra', 'Noct', 'Teneb', 'Ereb', 'Nyx'],
death: ['Mors', 'Necro', 'Than', 'Mort', 'Skull'],
transference: ['Link', 'Arcana', 'Vinc', 'Bind', 'Chain'],
metal: ['Ferr', 'Chroma', 'Steel', 'Arg', 'Ore'],
sand: ['Arena', 'Dune', 'Siroc', 'Erg', 'Sah'],
lightning: ['Volt', 'Fulg', 'Electr', 'Spark', 'Storm'],
crystal: ['Prism', 'Gemma', 'Crystal', 'Shard', 'Facet'],
stellar: ['Astro', 'Stella', 'Nova', 'Cosmo', 'Lumin'],
void: ['Void', 'Abyss', 'Null', 'Nihil', 'Obliv'],
};
const GUARDIAN_TITLES: string[] = [
'Warden', 'Keeper', 'Lord', 'Titan', 'Sovereign',
'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 {
const parts = elements.map((el, i) => {
const prefixes = GUARDIAN_PREFIXES[el] || ['Unknown'];
return prefixes[(floor + i) % prefixes.length];
});
const title = GUARDIAN_TITLES[Math.floor(floor / 10) % GUARDIAN_TITLES.length];
return `${parts.join('-')} the ${title}`;
}
// ─── Guardian HP Scaling ──────────────────────────────────────────────────────
export function getGuardianHP(floor: number): number {
const base = 5000;
const exponent = 1.1 + (floor / 200);
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.
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
];
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);
return {
name: '',
element: `${el1}+${el2}`,
hp,
pact: 6.0 + (floor - 150) * 0.05,
color: '#E8D5F5',
armor,
boons: [
{ type: 'elementalDamage', value: 10, desc: `+10% ${el1} damage` },
{ type: 'elementalDamage', value: 10, desc: `+10% ${el2} damage` },
],
pactCost: Math.floor(hp * 0.5),
pactTime: 20 + Math.floor((floor - 150) / 10),
uniquePerk: `Dual-aspect: ${el1} and ${el2} spells gain +20% effectiveness`,
power: Math.floor(hp * 0.5),
effects: [
{ type: `${el1}_boost`, value: 0.2 },
{ type: `${el2}_boost`, value: 0.2 },
],
signingCost: { mana: Math.floor(hp * 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,
};
}
// ─── Procedural Guardian Lookup (Floor 150+) ──────────────────────────────────
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;
}
// ─── Unified Guardian System ─────────────────────────────────────────────────
// Merges static guardians (floors 10140) with procedural combo guardians (150+).
/** 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 (!g.name) g.name = generateGuardianName(g.element, floor);
return g;
}
return getExtendedGuardian(floor);
}
/** All guardian floors — merged from static + extended. */
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]);
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). */
export function isGuardianFloor(floor: number): boolean {
return floor % 10 === 0;
}