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,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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user