botWebWars/frontend/src/components/BattleModal.tsx

195 lines
8.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.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 }) => {
const [countdown, setCountdown] = useState(5);
const [progressPercent, setProgressPercent] = useState(100);
// Keep onClose in a ref to decouple the timer effect from callback identity changes
const onCloseRef = useRef(onClose);
useEffect(() => {
onCloseRef.current = onClose;
});
// 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;
setCountdown(5);
setProgressPercent(100);
const timer = window.setInterval(() => {
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);
return () => {
window.clearInterval(timer);
};
}, [battleKey]);
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" />
{/* 5-second automatic progress bar */}
<div className="absolute top-2 left-0 right-0 h-1 bg-slate-800">
<div
className="h-full bg-amber-400 transition-all duration-100 ease-linear"
style={{ width: `${progressPercent}%` }}
/>
</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
onClick={() => onCloseRef.current()}
className="text-slate-400 hover:text-slate-200 p-1 text-sm font-mono"
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{' '}
<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>
{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>
{/* Continue Button with 5s Auto-close Countdown */}
<button
onClick={() => onCloseRef.current()}
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"
>
<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>
);
};