botWebWars/frontend/src/components/WizardChallengeModal.tsx

231 lines
9.2 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 { WizardChallengeResult } from '../types';
interface WizardChallengeModalProps {
challenge: WizardChallengeResult | null;
onClose: () => void;
}
export const WizardChallengeModal: React.FC<WizardChallengeModalProps> = ({
challenge,
onClose,
}) => {
const [countdown, setCountdown] = useState(5);
const [progressPercent, setProgressPercent] = useState(100);
const onCloseRef = useRef(onClose);
useEffect(() => {
onCloseRef.current = onClose;
});
const challengeKey = challenge
? `${challenge.challenger_id}_${challenge.player_bouts_won}_${challenge.wizard_bouts_won}_${challenge.reward_chosen}_${challenge.score_change}_${challenge.strength_change}_${challenge.health_change}_${challenge.wizard_respawn_position?.x}_${challenge.wizard_respawn_position?.y}`
: null;
useEffect(() => {
if (!challengeKey) {
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);
};
}, [challengeKey]);
if (!challenge) return null;
const playerName = challenge.challenger_name;
const wizardName = challenge.wizard_name || 'Gary the Wizard';
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-purple-500/80 rounded-2xl w-full max-w-xl shadow-2xl p-6 relative overflow-hidden">
{/* Glowing cyber wizard header banner */}
<div className="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-purple-500 via-fuchsia-500 to-indigo-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-purple-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 flex items-center gap-2">
<span>WIZARD NPC DUEL: 3-BOUT D20 CHALLENGE</span>
</h2>
<p className="text-xs text-slate-400">
<span className="text-sky-300 font-semibold">{playerName}</span> vs{' '}
<span className="text-purple-300 font-semibold">{wizardName}</span>
</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">
{challenge.bouts.map((bout) => (
<div
key={bout.bout_number}
className={`p-3 rounded-xl border flex items-center justify-between transition-colors ${
bout.winner === 'player'
? 'bg-sky-950/30 border-sky-500/50'
: bout.winner === 'wizard'
? 'bg-purple-950/30 border-purple-500/50'
: 'bg-slate-800/40 border-slate-700'
}`}
>
{/* Bout Index */}
<div className="w-16 font-mono text-xs font-bold text-slate-400">
Bout #{bout.bout_number}
</div>
{/* Player Bout Score */}
<div className="flex-1 text-left px-2">
<div className="text-xs font-bold text-slate-200 truncate">
{playerName}
</div>
<div className="text-[11px] font-mono text-slate-400 flex items-center gap-1">
<span>🎲 D20({bout.player_roll})</span>
<span>×</span>
<span>Str({bout.player_strength})</span>
<span>=</span>
<strong className="text-sky-400 text-xs">{bout.player_score}</strong>
</div>
</div>
{/* VS Divider */}
<div className="px-2 text-slate-600 font-mono text-xs font-black">VS</div>
{/* Wizard Bout Score */}
<div className="flex-1 text-right px-2">
<div className="text-xs font-bold text-purple-300 truncate">
{wizardName}
</div>
<div className="text-[11px] font-mono text-slate-400 flex items-center justify-end gap-1">
<strong className="text-purple-400 text-xs">{bout.wizard_score}</strong>
<span>=</span>
<span>Str({bout.wizard_strength})</span>
<span>×</span>
<span>🎲 D20({bout.wizard_roll})</span>
</div>
</div>
{/* Bout Outcome Badge */}
<div className="w-20 text-right">
<span
className={`text-[10px] font-mono px-2 py-0.5 rounded font-bold uppercase ${
bout.winner === 'player'
? 'bg-sky-900/80 text-sky-200 border border-sky-600'
: bout.winner === 'wizard'
? 'bg-purple-900/80 text-purple-200 border border-purple-600'
: 'bg-slate-700 text-slate-300'
}`}
>
{bout.winner === 'player'
? 'Player'
: bout.winner === 'wizard'
? 'Wizard'
: 'Tie'}
</span>
</div>
</div>
))}
</div>
{/* Overall Match Outcome Banner */}
<div
className={`p-4 rounded-xl border mb-4 text-center ${
challenge.player_won
? 'bg-emerald-950/40 border-emerald-500/60 text-emerald-200'
: 'bg-rose-950/40 border-rose-500/60 text-rose-200'
}`}
>
<div className="text-xs font-mono uppercase tracking-wider mb-1">
Duel Outcome: {challenge.player_bouts_won} - {challenge.wizard_bouts_won}
</div>
<div className="text-base font-extrabold flex items-center justify-center gap-2">
<span>{challenge.player_won ? '✨ CHALLENGE VICTORY ✨' : '💀 CHALLENGE DEFEAT 💀'}</span>
</div>
<div className="mt-2 text-xs font-mono space-y-1">
{challenge.player_won ? (
<div className="text-emerald-300 font-bold space-y-0.5">
{challenge.reward_chosen === 'strength' ? (
<div> Victory Reward: +{challenge.strength_change || 2} Strength! (Total Strength: {challenge.new_strength ?? '?'})</div>
) : challenge.reward_chosen === 'health' ? (
<div> Victory Reward: +{challenge.health_change || 2} Health! (Total HP: {challenge.new_health})</div>
) : (
<div>🏆 Victory Reward: +{challenge.score_change || 2} Score Points! (Total Score: {challenge.new_score})</div>
)}
</div>
) : (
<div className="text-rose-300 font-bold">
{challenge.health_change < 0 ? (
<span>
💔 Lost {Math.abs(challenge.health_change)} Health! (Remaining HP: {challenge.new_health})
</span>
) : (
<span>
📉 Out of HP! Lost {Math.abs(challenge.score_change)} Score points! (Score: {challenge.new_score})
</span>
)}
</div>
)}
<div className="text-purple-300/80 text-[11px] pt-1">
🔮 The Wizard vanished in a puff of smoke and teleported to ({challenge.wizard_respawn_position?.x}, {challenge.wizard_respawn_position?.y}).
</div>
</div>
</div>
{/* Modal Controls / Auto-close countdown */}
<div className="flex items-center justify-between text-xs text-slate-400 font-mono">
<span>Auto-closing in {countdown}s...</span>
<button
onClick={() => onCloseRef.current()}
className="px-4 py-1.5 rounded-lg bg-slate-800 hover:bg-purple-700 text-slate-200 hover:text-white border border-slate-700 transition-colors font-semibold"
>
Acknowledge & Close
</button>
</div>
</div>
</div>
);
};