2026-09-09 22:11:54 +00:00
|
|
|
|
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
|
2026-09-10 00:32:22 +00:00
|
|
|
|
? `${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}`
|
2026-09-09 22:11:54 +00:00
|
|
|
|
: 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 || 'Grand 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 ? (
|
2026-09-10 00:32:22 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
2026-09-09 22:11:54 +00:00
|
|
|
|
</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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|