botWebWars/frontend/src/components/BattleModal.tsx

195 lines
8.1 KiB
TypeScript
Raw Normal View History

2026-09-06 00:32:44 +00:00
import React, { useEffect, useRef, useState } from 'react';
import type { BattleResult } from '../types';
interface BattleModalProps {
battle: BattleResult | null;
onClose: () => void;
}
export const BattleModal: React.FC<BattleModalProps> = ({ battle, onClose }) => {
2026-09-05 23:50:27 +00:00
const [countdown, setCountdown] = useState(5);
2026-09-06 00:32:44 +00:00
const [progressPercent, setProgressPercent] = useState(100);
2026-09-05 23:50:27 +00:00
2026-09-06 00:32:44 +00:00
// Keep onClose in a ref to decouple the timer effect from callback identity changes
const onCloseRef = useRef(onClose);
2026-09-05 23:50:27 +00:00
useEffect(() => {
2026-09-06 00:32:44 +00:00
onCloseRef.current = onClose;
});
2026-09-05 23:50:27 +00:00
2026-09-06 00:32:44 +00:00
// Unique battle key so the 5s timer only restarts when a genuinely new battle arrives
const battleKey = battle
? `${battle.winner_party_id}_${battle.defeated_party_id}_${battle.party1_total_score}_${battle.party2_total_score}_${battle.killed_leader_id}`
: null;
useEffect(() => {
if (!battleKey) {
setCountdown(5);
setProgressPercent(100);
return;
}
const DURATION_MS = 5000;
const targetEndTime = Date.now() + DURATION_MS;
2026-09-05 23:50:27 +00:00
setCountdown(5);
2026-09-06 00:32:44 +00:00
setProgressPercent(100);
2026-09-05 23:50:27 +00:00
const timer = window.setInterval(() => {
2026-09-06 00:32:44 +00:00
const now = Date.now();
const remainingMs = targetEndTime - now;
if (remainingMs <= 0) {
window.clearInterval(timer);
setCountdown(0);
setProgressPercent(0);
onCloseRef.current();
return;
}
const seconds = Math.ceil(remainingMs / 1000);
const percent = Math.min(100, Math.max(0, (remainingMs / DURATION_MS) * 100));
setCountdown(seconds);
setProgressPercent(percent);
}, 100);
2026-09-05 23:50:27 +00:00
return () => {
window.clearInterval(timer);
};
2026-09-06 00:32:44 +00:00
}, [battleKey]);
2026-09-05 23:50:27 +00:00
if (!battle) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-md p-4 animate-in fade-in duration-200">
<div className="bg-slate-900 border-2 border-amber-500/80 rounded-2xl w-full max-w-xl shadow-2xl p-6 relative overflow-hidden">
{/* Glowing cyber header banner */}
<div className="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-amber-500 via-rose-500 to-sky-500" />
2026-09-05 23:50:27 +00:00
{/* 5-second automatic progress bar */}
<div className="absolute top-2 left-0 right-0 h-1 bg-slate-800">
<div
2026-09-06 00:32:44 +00:00
className="h-full bg-amber-400 transition-all duration-100 ease-linear"
style={{ width: `${progressPercent}%` }}
2026-09-05 23:50:27 +00:00
/>
</div>
<div className="flex items-center justify-between mb-4 pb-2 border-b border-slate-800 mt-1">
<div className="flex items-center gap-2">
<span className="text-2xl"></span>
<div>
<h2 className="text-base font-bold text-slate-100 font-mono tracking-wide">
PARTY ENGAGEMENT: 3-BOUT D20 CLASH
</h2>
<p className="text-xs text-slate-400">
{battle.party1_name} vs {battle.party2_name}
</p>
</div>
</div>
<button
2026-09-06 00:32:44 +00:00
onClick={() => onCloseRef.current()}
className="text-slate-400 hover:text-slate-200 p-1 text-sm font-mono"
2026-09-05 23:50:27 +00:00
title="Dismiss now"
>
</button>
</div>
{/* 3 Bouts Breakdown */}
<div className="space-y-3 mb-5">
{battle.bouts.map((bout) => (
<div
key={bout.bout_number}
className="bg-slate-950/80 border border-slate-800 rounded-xl p-3 text-xs font-mono"
>
<div className="flex justify-between items-center mb-2 pb-1 border-b border-slate-800/80">
<span className="text-amber-400 font-bold">BOUT #{bout.bout_number}</span>
<span className="text-slate-300">
Winner: <strong className="text-emerald-400">{bout.winner_name || 'Tied'}</strong>
</span>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Party 1 */}
<div className="p-2 rounded-lg bg-slate-900/90 border border-slate-800">
<div className="text-slate-400 font-semibold truncate mb-1">
{battle.party1_name}
</div>
<div className="flex items-center justify-between text-slate-300">
<span>
Str: {bout.party1_strength} × 🎲 {bout.party1_roll} =
</span>
<strong className="text-sm text-sky-400">{bout.party1_score}</strong>
</div>
</div>
{/* Party 2 */}
<div className="p-2 rounded-lg bg-slate-900/90 border border-slate-800">
<div className="text-slate-400 font-semibold truncate mb-1">
{battle.party2_name}
</div>
<div className="flex items-center justify-between text-slate-300">
<span>
Str: {bout.party2_strength} × 🎲 {bout.party2_roll} =
</span>
<strong className="text-sm text-sky-400">{bout.party2_score}</strong>
</div>
</div>
</div>
</div>
))}
</div>
{/* Battle Resolution Summary */}
<div className="bg-amber-950/20 border border-amber-500/40 rounded-xl p-4 text-xs font-mono space-y-2 mb-5">
<div className="flex items-center justify-between text-sm font-bold">
<span className="text-amber-300 flex items-center gap-1.5">
<span>🏆</span> VICTORY: {battle.winner_party_name}
</span>
<span className="text-slate-400 text-xs font-normal">
Bouts: {battle.party1_bouts_won} - {battle.party2_bouts_won}
</span>
</div>
<div className="text-slate-300 leading-relaxed">
Winning Leader <strong className="text-amber-300">{battle.winner_leader_name}</strong> receives{' '}
<span className="text-emerald-400 font-bold">+2 points</span>, and all other winning squad members receive{' '}
<span className="text-emerald-400 font-bold">+1 point</span>!
</div>
<div className="text-slate-300 leading-relaxed">
Defeated Leader <strong className="text-rose-400">{battle.killed_leader_name}</strong> receives{' '}
2026-09-05 23:50:27 +00:00
<span className="text-rose-400 font-bold">-1 point</span>.
{battle.absorbed_members.includes(battle.killed_leader_id) ? (
<span> Resistance broken! Surrendered and <strong className="text-sky-300">joined {battle.winner_party_name}</strong>.</span>
) : (
<span> Respawned at ({battle.killed_leader_respawn_position.x}, {battle.killed_leader_respawn_position.y}).</span>
)}
</div>
2026-09-05 23:50:27 +00:00
{battle.absorbed_members.length > 0 && !battle.absorbed_members.includes(battle.killed_leader_id) && (
<div className="text-slate-300 leading-relaxed">
<strong className="text-emerald-400">{battle.absorbed_members.length} surviving bot(s)</strong> from the
defeated party surrendered and <strong className="text-sky-300">joined {battle.winner_party_name}</strong>!
</div>
)}
<div className="text-slate-400 text-[11px] pt-1">
New Squad Size: <strong className="text-slate-200">{battle.new_party_size} bots</strong> Total Strength:{' '}
<strong className="text-amber-400">{battle.new_party_strength}</strong>
</div>
</div>
2026-09-05 23:50:27 +00:00
{/* Continue Button with 5s Auto-close Countdown */}
<button
2026-09-06 00:32:44 +00:00
onClick={() => onCloseRef.current()}
2026-09-05 23:50:27 +00:00
className="w-full bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold py-2.5 rounded-xl text-xs font-mono transition-all shadow-lg shadow-amber-500/20 flex items-center justify-center gap-2"
>
2026-09-05 23:50:27 +00:00
<span>Acknowledge & Continue Battle</span>
<span className="bg-amber-950 text-amber-300 px-2 py-0.5 rounded-full text-[11px] font-mono">
Auto-closing in {countdown}s
</span>
</button>
</div>
</div>
);
};