feat: Recreate Spire Combat Page — full spire climbing experience
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:
2026-05-20 09:28:05 +02:00
parent 1c7fc8c551
commit 7d56fc368f
17 changed files with 2004 additions and 5 deletions
@@ -0,0 +1,72 @@
'use client';
import { useManaStore, fmt, fmtDec } from '@/lib/game/stores';
import { Card, CardContent } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { ELEMENTS } from '@/lib/game/constants';
interface SpireManaDisplayProps {
maxMana: number;
effectiveRegen: number;
}
export function SpireManaDisplay({ maxMana, effectiveRegen }: SpireManaDisplayProps) {
const rawMana = useManaStore((s) => s.rawMana);
const elements = useManaStore((s) => s.elements);
const unlockedElements = Object.entries(elements)
.filter(([, state]) => state.unlocked && state.current > 0)
.sort((a, b) => b[1].current - a[1].current)
.slice(0, 6); // Show max 6 in compact view
const manaPercent = maxMana > 0 ? (rawMana / maxMana) * 100 : 0;
return (
<Card className="bg-gray-900/80 border-gray-700">
<CardContent className="py-3 space-y-2">
{/* Raw Mana */}
<div>
<div className="flex items-baseline gap-1">
<span className="text-xl font-bold game-mono" style={{ color: '#60A5FA' }}>
{fmt(rawMana)}
</span>
<span className="text-xs text-gray-500">/ {fmt(maxMana)}</span>
</div>
<div className="text-[10px] text-gray-500">
+{fmtDec(effectiveRegen)}/hr
</div>
</div>
<Progress
value={manaPercent}
className="h-1.5 bg-gray-800"
style={{ '--progress-bg': '#60A5FA' } as React.CSSProperties}
/>
{/* Elemental pools (compact) */}
{unlockedElements.length > 0 && (
<div className="grid grid-cols-3 gap-1">
{unlockedElements.map(([id, state]) => {
const elem = ELEMENTS[id];
if (!elem) return null;
const pct = state.max > 0 ? (state.current / state.max) * 100 : 0;
return (
<div key={id} className="text-center">
<div className="text-[10px]" style={{ color: elem.color }}>
{elem.sym} {fmt(state.current)}
</div>
<div className="h-1 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full rounded-full"
style={{ width: `${pct}%`, backgroundColor: elem.color }}
/>
</div>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
);
}