a8fab1eb86
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m37s
- 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
156 lines
6.5 KiB
TypeScript
156 lines
6.5 KiB
TypeScript
// ─── 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 10–140) 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;
|
||
}
|