Files
Mana-Loop/src/components/game/tabs/SpireSummaryTab.helpers.tsx
T
n8n-gitea fa78c7a93a
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
fix: bugs #238,#240,#244,#246 + docs #248 update
- #238: Fix spire tab inconsistent state (Max Floor 1 but Floors Cleared 0) by not inflating maxFloorReached on enterSpireMode and preserving it on exitSpireMode
- #240: Fix guardian armor display stray text by extracting stat formatters in SpireSummaryTab
- #244: Improve discipline auto-pause UX with log messages and visual feedback on DisciplineCard
- #246: Fix raw mana exceeding max cap by recomputing maxMana after discipline XP gains
- #248: Update AGENTS.md (remove gitea_get_project_boards, add gitea_start_session, 22 mana types, 8 stores, updated guardian tiers)
- #248: Update README.md (remove Prisma/SQLite refs, update mana types/guardian tiers/discipline counts)
- #248: Update GAME_BRIEFING.md (8 stores, 22 mana types, 64 disciplines, 8-tier guardians, correct code architecture)
2026-06-01 13:54:28 +02:00

250 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useMemo } from 'react';
import { fmt } from '@/lib/game/stores';
import { ELEMENT_OPPOSITES, FLOOR_ELEM_CYCLE } from '@/lib/game/constants';
import { getGuardianForFloor, getAllGuardianFloors } from '@/lib/game/data/guardian-encounters';
import type { GuardianDef } from '@/lib/game/types';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { SectionHeader } from '@/components/ui/section-header';
const GUARDIAN_FLOORS = getAllGuardianFloors();
// ─── Helper: Get Counter Element ─────────────────────────────────────────────
export function getCounterElement(element: string): string | null {
return ELEMENT_OPPOSITES[element] || null;
}
export function getElementColor(element: string): string {
const colors: Record<string, string> = {
fire: '#FF6B35',
water: '#4ECDC4',
air: '#00D4FF',
earth: '#F4A261',
light: '#FFD700',
dark: '#9B59B6',
death: '#778CA3',
void: '#4A235A',
stellar: '#F0E68C',
};
return colors[element] || '#9CA3AF';
}
// ─── Guardian Stat Formatters ────────────────────────────────────────────────
export function fmtArmor(armor: number | undefined): React.ReactNode {
if (!armor || armor <= 0) return null;
return (
<span className="text-xs text-gray-500">
{'Armor: '}{Math.round(armor * 100)}{'%'}
</span>
);
}
export function fmtShield(shield: number | undefined): React.ReactNode {
if (!shield || shield <= 0) return null;
return (
<span className="text-xs text-cyan-400">
{'Shield: '}{fmt(shield)}
</span>
);
}
export function fmtBarrier(barrier: number | undefined): React.ReactNode {
if (!barrier || barrier <= 0) return null;
return (
<span className="text-xs text-blue-400">
{'Barrier: '}{Math.round(barrier * 100)}{'%'}
</span>
);
}
export function fmtRegen(regen: number | undefined, isPercent: boolean | undefined): React.ReactNode {
if (!regen || regen <= 0) return null;
return (
<span className="text-xs text-green-400">
{'Regen: '}{isPercent ? `${regen}%/tick` : `${regen}/tick`}
</span>
);
}
// ─── Preparation Tips ────────────────────────────────────────────────────────
export function PreparationTips({ counterElement, nextFloorElement, hasHighArmor }: {
counterElement: string | null; nextFloorElement: string | null; hasHighArmor: boolean;
}) {
return (
<div className="bg-gray-800/50 rounded-lg p-3 space-y-2">
<div className="text-xs font-medium text-gray-300">Recommended Preparation:</div>
<div className="text-xs text-gray-400 space-y-1">
{counterElement && (
<div className="flex items-center gap-2">
<span className="text-emerald-400"></span>
<span>
Use <span style={{ color: getElementColor(counterElement) }} className="font-medium">{counterElement}</span> spells for super effective damage (+50%)
</span>
</div>
)}
{nextFloorElement && (
<div className="flex items-center gap-2">
<span className="text-blue-400">🔄</span>
<span>
Floor element: <span style={{ color: getElementColor(nextFloorElement) }} className="font-medium">{nextFloorElement}</span>
</span>
</div>
)}
{hasHighArmor && (
<div className="flex items-center gap-2">
<span className="text-red-400">🛡</span>
<span>High armor consider armor-piercing or raw damage spells</span>
</div>
)}
<div className="flex items-center gap-2">
<span className="text-amber-400">💡</span>
<span>Ensure mana pools are full before attempting</span>
</div>
</div>
</div>
);
}
// ─── Guardian Roster ─────────────────────────────────────────────────────────
export function GuardianRoster({ clearedFloors }: { clearedFloors: Record<number, boolean> }) {
return (
<Card className="bg-gray-900/60 border-gray-700">
<SectionHeader title="🏛️ Guardian Roster" />
<CardContent className="pt-0">
<div className="space-y-2">
{GUARDIAN_FLOORS.map((floor) => {
const guardian = getGuardianForFloor(floor);
return guardian ? (
<GuardianRosterItem key={floor} floor={floor} guardian={guardian} isDefeated={!!clearedFloors[floor]} />
) : null;
})}
</div>
</CardContent>
</Card>
);
}
function GuardianRosterItem({ floor, guardian, isDefeated }: { floor: number; guardian: GuardianDef; isDefeated: boolean }) {
return (
<div
className={`flex items-center justify-between p-2 rounded border ${
isDefeated
? 'bg-emerald-900/20 border-emerald-800/40'
: 'bg-gray-800/40 border-gray-700/50'
}`}
>
<div className="flex items-center gap-2">
<div
className="w-7 h-7 rounded flex items-center justify-center text-xs font-bold"
style={{
backgroundColor: isDefeated ? `${guardian.color}30` : '#374151',
color: isDefeated ? guardian.color : '#6B7280',
}}
>
{floor}
</div>
<div>
<div className={`text-sm font-medium ${isDefeated ? 'text-gray-100' : 'text-gray-400'}`}>
{guardian.name}
</div>
<div className="flex items-center gap-1.5">
<span
className="text-[10px] px-1.5 py-0.5 rounded"
style={{
backgroundColor: `${guardian.color}15`,
color: guardian.color,
}}
>
{guardian.element.join(' + ')}
</span>
<span className="text-[10px] text-gray-500">Health: {fmt(guardian.hp)}</span>
</div>
</div>
</div>
<div>
{isDefeated ? (
<Badge variant="outline" className="border-emerald-600 text-emerald-400 text-xs">
Defeated
</Badge>
) : (
<Badge variant="outline" className="border-gray-600 text-gray-500 text-xs">
Undefeated
</Badge>
)}
</div>
</div>
);
}
// ─── Floor Progress Bar ────────────────────────────────────────────────────────
export function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clearedFloors: Record<number, boolean> }) {
const totalFloors = Math.min(maxFloor, 100);
const clearedSet = new Set(Object.entries(clearedFloors).filter(([, v]) => v).map(([k]) => Number(k)));
const rows: number[][] = [];
for (let i = 0; i < totalFloors; i += 10) {
rows.push(Array.from({ length: 10 }, (_, j) => i + j + 1).filter((f) => f <= totalFloors));
}
return (
<div className="space-y-1">
{rows.reverse().map((row) => (
<div key={row[0]} className="flex gap-1">
{row.map((floor) => {
const isCleared = clearedSet.has(floor);
const isGuardian = !!getGuardianForFloor(floor);
const isCurrent = floor === maxFloor;
let bgClass = 'bg-gray-800';
if (isCleared) bgClass = 'bg-emerald-600/60';
else if (isCurrent) bgClass = 'bg-amber-600/60';
const borderClass = isGuardian ? 'border-amber-500' : isCurrent ? 'border-amber-400' : 'border-gray-700';
return (
<div
key={floor}
className={`w-7 h-7 flex items-center justify-center text-[9px] rounded border ${bgClass} ${borderClass} ${isGuardian ? 'font-bold' : ''}`}
title={getGuardianForFloor(floor) ? `Floor ${floor}${getGuardianForFloor(floor)!.name} (${getGuardianForFloor(floor)!.element.join(' + ')})` : `Floor ${floor}${isCleared ? ' (cleared)' : ''}`}
>
{floor}
</div>
);
})}
</div>
))}
<FloorLegend />
</div>
);
}
function FloorLegend() {
return (
<div className="flex items-center gap-3 mt-2 text-[10px] text-gray-500">
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-emerald-600/60 border border-gray-700" />
<span>Cleared</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-gray-800 border border-gray-700" />
<span>Uncleared</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-gray-800 border border-amber-500" />
<span>Guardian</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-amber-600/60 border border-amber-400" />
<span>Current</span>
</div>
</div>
);
}