botWebWars/frontend/src/components/PartyModal.tsx

252 lines
9.4 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import type { BoardState, Player } from '../types';
interface PartyModalProps {
isOpen: boolean;
boardState: BoardState;
onClose: () => void;
onFormParty: (memberIds: string[], leaderId: string, name?: string) => Promise<unknown>;
}
export const PartyModal: React.FC<PartyModalProps> = ({
isOpen,
boardState,
onClose,
onFormParty,
}) => {
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [leaderId, setLeaderId] = useState<string>('');
const [partyName, setPartyName] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
if (!isOpen) return null;
const { players } = boardState;
// Check if two players are adjacent (Chebyshev distance <= 1)
const areAdjacent = (p1: Player, p2: Player) => {
return Math.max(Math.abs(p1.x - p2.x), Math.abs(p1.y - p2.y)) <= 1;
};
// Find any adjacent pairs in the game for quick selection
const findAdjacentPairs = () => {
for (let i = 0; i < players.length; i++) {
for (let j = i + 1; j < players.length; j++) {
if (areAdjacent(players[i], players[j])) {
return [players[i], players[j]];
}
}
}
return null;
};
2026-09-05 23:20:54 +00:00
const pickStrongestLeader = (ids: string[]) => {
if (ids.length === 0) return '';
const selectedBots = players.filter((p) => ids.includes(p.id));
selectedBots.sort((a, b) => (b.strength || 1) - (a.strength || 1));
return selectedBots[0]?.id || ids[0];
};
const handleQuickPair = () => {
const pair = findAdjacentPairs();
if (pair) {
2026-09-05 23:20:54 +00:00
const ids = [pair[0].id, pair[1].id];
setSelectedIds(ids);
const chosenLeader = pickStrongestLeader(ids);
setLeaderId(chosenLeader);
const leadBot = players.find((p) => p.id === chosenLeader) || pair[0];
setPartyName(`Squad ${leadBot.name}`);
setError(null);
} else {
setError('No adjacent bots found! Move bots within 1 distance of each other to link them.');
}
};
const toggleSelect = (id: string) => {
setError(null);
setSelectedIds((prev) => {
const next = prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id];
2026-09-05 23:20:54 +00:00
setLeaderId(pickStrongestLeader(next));
return next;
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (selectedIds.length < 2) {
setError('Select at least 2 bots to form a party');
return;
}
if (!leaderId) {
setError('You must agree on a party leader');
return;
}
setLoading(true);
setError(null);
try {
await onFormParty(selectedIds, leaderId, partyName.trim() || undefined);
onClose();
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('Failed to form party');
}
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-sm p-4">
<div className="bg-slate-900 border border-slate-700 rounded-2xl w-full max-w-md shadow-2xl p-6 relative">
<div className="flex items-center justify-between mb-4 border-b border-slate-800 pb-3">
<div className="flex items-center gap-2">
<span className="text-xl">🤝</span>
<h2 className="text-lg font-bold text-slate-100">Form a Bot Party</h2>
</div>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-200 transition-colors text-sm"
>
</button>
</div>
<div className="text-xs text-slate-400 mb-4 leading-relaxed">
2026-09-05 23:20:54 +00:00
The goal of a bot without a party is to form a party! Bots insist on being leader if they consider the other bot less than them (higher strength), and desire to join a bot that is equal or stronger.
</div>
<div className="mb-4">
<button
type="button"
onClick={handleQuickPair}
className="w-full bg-slate-800 hover:bg-slate-700 border border-sky-500/40 text-sky-300 text-xs font-mono py-2 rounded-lg transition-colors flex items-center justify-center gap-2"
>
<span>🎯</span> Auto-Select Nearest Adjacent Bots
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Party Name */}
<div>
<label className="block text-xs font-mono text-slate-400 mb-1">Party Name (Optional)</label>
<input
type="text"
value={partyName}
onChange={(e) => setPartyName(e.target.value)}
placeholder="e.g. NeonVipers"
className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-100 focus:outline-none focus:border-sky-500"
/>
</div>
{/* Member Selection */}
<div>
<label className="block text-xs font-mono text-slate-400 mb-1.5">
Select Members (At least 2):
</label>
<div className="max-h-48 overflow-y-auto space-y-1.5 border border-slate-800 rounded-xl p-2 bg-slate-950/50">
{players.map((p) => {
const isChecked = selectedIds.includes(p.id);
return (
<div
key={p.id}
onClick={() => toggleSelect(p.id)}
className={`flex items-center justify-between p-2 rounded-lg cursor-pointer border text-xs transition-colors ${
isChecked
? 'bg-sky-950/60 border-sky-500/70 text-sky-200'
: 'bg-slate-900/60 border-slate-800 text-slate-400 hover:border-slate-700'
}`}
>
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: p.color }}
/>
<span className="font-semibold text-slate-200">{p.name}</span>
2026-09-05 23:20:54 +00:00
<span className="text-amber-400 font-mono text-[11px]">{p.strength || 1}</span>
<span className="font-mono text-[10px] text-slate-500">
({p.x}, {p.y})
</span>
</div>
<input
type="checkbox"
checked={isChecked}
onChange={() => {}}
className="rounded border-slate-700 text-sky-500"
/>
</div>
);
})}
</div>
</div>
{/* Agreed Leader Selection */}
{selectedIds.length > 0 && (
<div>
2026-09-05 23:20:54 +00:00
<div className="flex items-center justify-between mb-1.5">
<label className="text-xs font-mono text-slate-400">
👑 Agreed Party Leader:
</label>
<span className="text-[10px] font-mono text-amber-400/90">
(Stronger bots insist on leading)
</span>
</div>
<div className="grid grid-cols-2 gap-2">
{selectedIds.map((id) => {
const bot = players.find((p) => p.id === id);
if (!bot) return null;
const isLeader = leaderId === id;
return (
<button
type="button"
key={id}
onClick={() => setLeaderId(id)}
2026-09-05 23:20:54 +00:00
className={`p-2 rounded-lg border text-xs font-mono flex items-center justify-between gap-1 transition-all ${
isLeader
? 'bg-amber-950/80 border-amber-500 text-amber-200 shadow'
: 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700'
}`}
>
2026-09-05 23:20:54 +00:00
<div className="flex items-center gap-1.5 truncate">
<span>{isLeader ? '👑' : '🛡️'}</span>
<span className="truncate">{bot.name}</span>
</div>
<span className="text-amber-400 text-[10px]">{bot.strength || 1}</span>
</button>
);
})}
</div>
</div>
)}
{error && (
<div className="p-2.5 rounded-lg bg-red-950/70 border border-red-800/80 text-red-300 text-xs font-mono">
{error}
</div>
)}
<div className="flex gap-2 pt-2">
<button
type="button"
onClick={onClose}
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 py-2 rounded-xl text-xs font-mono transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={loading || selectedIds.length < 2 || !leaderId}
className="flex-1 bg-sky-600 hover:bg-sky-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-bold py-2 rounded-xl text-xs font-mono transition-colors shadow-lg shadow-sky-950/50"
>
{loading ? 'Forming...' : 'Confirm Party'}
</button>
</div>
</form>
</div>
</div>
);
};