feat: Recreate Spire Combat Page — full spire climbing experience
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
- Add guardian-encounters.ts: Extended guardian definitions for all mana types (compound, exotic, combo) with dynamic name generation - Add spire-utils.ts: Spire-specific utilities (room generation, enemy stat scaling, insight calculation) - Add enemy-generator.ts: Enemy generation with combinable modifiers (mage, shield, armored, swarm, agile) - Add SpireCombatPage/ directory with modular sub-components: - SpireHeader.tsx: Floor info, climb controls, exit button, HP/room progress bars - RoomDisplay.tsx: Current room info with enemies, barriers, armor, dodge stats - SpireCombatControls.tsx: Spell selection panel, golem status panel - SpireActivityLog.tsx: Combat activity log - SpireManaDisplay.tsx: Compact mana display with elemental pools - Modify page.tsx: Conditionally render SpireCombatPage when spireMode is true - Add comprehensive tests (49 tests) for spire utilities, guardian encounters, and enemy generation
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
// ─── 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.
|
||||
|
||||
import type { GuardianDef } from '../types';
|
||||
|
||||
// ─── 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): string {
|
||||
const prefixes = GUARDIAN_PREFIXES[element] || ['Unknown'];
|
||||
const prefix = prefixes[Math.floor(Math.random() * prefixes.length)];
|
||||
const title = GUARDIAN_TITLES[Math.floor(Math.random() * GUARDIAN_TITLES.length)];
|
||||
return `${prefix} the ${title}`;
|
||||
}
|
||||
|
||||
export function generateComboGuardianName(elements: string[]): string {
|
||||
const parts = elements.map((el) => {
|
||||
const prefixes = GUARDIAN_PREFIXES[el] || ['Unknown'];
|
||||
return prefixes[Math.floor(Math.random() * prefixes.length)];
|
||||
});
|
||||
const title = GUARDIAN_TITLES[Math.floor(Math.random() * GUARDIAN_TITLES.length)];
|
||||
return `${parts.join('-')} the ${title}`;
|
||||
}
|
||||
|
||||
// ─── 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<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+) ───────────────────────────────────────
|
||||
|
||||
const COMBO_PAIRS: [string, string][] = [
|
||||
['fire', 'water'], // Steam
|
||||
['fire', 'air'], // Already lightning but different flavor
|
||||
['water', 'earth'], // Already sand but different flavor
|
||||
['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,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Guardian Lookup ──────────────────────────────────────────────────────────
|
||||
|
||||
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) {
|
||||
const elements = g.element.split('+');
|
||||
g.name = generateComboGuardianName(elements);
|
||||
}
|
||||
return g;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// All guardian floors (extended)
|
||||
export const ALL_GUARDIAN_FLOORS: number[] = [
|
||||
10, 20, 30, 40, 50, 60, 80, 100, // Original
|
||||
90, 110, // Compound
|
||||
120, 130, 140, // Exotic
|
||||
...Array.from({ length: 10 }, (_, i) => 150 + i * 10), // Combo
|
||||
].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;
|
||||
}
|
||||
Reference in New Issue
Block a user