1.5 - option to hide popups for auto playing
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 1m6s Details

This commit is contained in:
Isaac Johnson 2026-09-14 07:30:52 -05:00
parent 65f76a00ac
commit 4c69a86726
5 changed files with 117 additions and 17 deletions

View File

@ -47,6 +47,8 @@ export function App() {
setActiveWizardChallenge, setActiveWizardChallenge,
showScoreboard, showScoreboard,
setShowScoreboard, setShowScoreboard,
showPopups,
setShowPopups,
registerPlayer, registerPlayer,
removePlayer, removePlayer,
formParty, formParty,
@ -202,6 +204,8 @@ export function App() {
playerCount={boardState.player_count} playerCount={boardState.player_count}
isConcluded={isConcluded} isConcluded={isConcluded}
onOpenScoreboard={() => setShowScoreboard(true)} onOpenScoreboard={() => setShowScoreboard(true)}
showPopups={showPopups}
onTogglePopups={setShowPopups}
/> />
{/* Main Content Area */} {/* Main Content Area */}
@ -233,6 +237,8 @@ export function App() {
onStepBot={stepActiveBotTurn} onStepBot={stepActiveBotTurn}
isAutoPlaying={isAutoPlaying} isAutoPlaying={isAutoPlaying}
onToggleAutoPlay={() => setIsAutoPlaying((prev) => !prev)} onToggleAutoPlay={() => setIsAutoPlaying((prev) => !prev)}
showPopups={showPopups}
onTogglePopups={setShowPopups}
/> />
{/* Sidebar Player Roster with Turn Order & Parties */} {/* Sidebar Player Roster with Turn Order & Parties */}
@ -278,10 +284,12 @@ export function App() {
/> />
{/* 3-Bout D20 Battle Modal */} {/* 3-Bout D20 Battle Modal */}
<BattleModal {showPopups && (
battle={activeBattle} <BattleModal
onClose={handleCloseBattle} battle={activeBattle}
/> onClose={handleCloseBattle}
/>
)}
{/* Wizard Challenge Reward Selector Prompt Modal */} {/* Wizard Challenge Reward Selector Prompt Modal */}
<WizardPromptModal <WizardPromptModal
@ -293,10 +301,12 @@ export function App() {
/> />
{/* 3-Bout D20 Wizard Challenge Modal */} {/* 3-Bout D20 Wizard Challenge Modal */}
<WizardChallengeModal {showPopups && (
challenge={activeWizardChallenge} <WizardChallengeModal
onClose={handleCloseWizardChallenge} challenge={activeWizardChallenge}
/> onClose={handleCloseWizardChallenge}
/>
)}
{/* Game Conclusion Scoreboard Modal */} {/* Game Conclusion Scoreboard Modal */}
{showScoreboard && boardState.conclusion && boardState.conclusion.concluded && ( {showScoreboard && boardState.conclusion && boardState.conclusion.concluded && (

View File

@ -10,6 +10,8 @@ interface HeaderProps {
playerCount: number; playerCount: number;
isConcluded?: boolean; isConcluded?: boolean;
onOpenScoreboard?: () => void; onOpenScoreboard?: () => void;
showPopups?: boolean;
onTogglePopups?: (value?: boolean) => void;
} }
export const Header: React.FC<HeaderProps> = ({ export const Header: React.FC<HeaderProps> = ({
@ -22,6 +24,8 @@ export const Header: React.FC<HeaderProps> = ({
playerCount, playerCount,
isConcluded, isConcluded,
onOpenScoreboard, onOpenScoreboard,
showPopups = true,
onTogglePopups,
}) => { }) => {
return ( return (
<header className="h-16 border-b border-slate-800 bg-slate-900/90 backdrop-blur px-6 flex items-center justify-between z-10"> <header className="h-16 border-b border-slate-800 bg-slate-900/90 backdrop-blur px-6 flex items-center justify-between z-10">
@ -108,6 +112,33 @@ export const Header: React.FC<HeaderProps> = ({
</button> </button>
)} )}
{/* Battle & Wizard Result Popups Toggle */}
<label
className={`text-xs font-mono font-medium px-2.5 py-1.5 rounded-lg border transition-all flex items-center gap-2 cursor-pointer select-none ${
showPopups
? 'bg-slate-800/90 text-sky-300 border-sky-500/50 shadow-sm shadow-sky-950/50 hover:bg-slate-800'
: 'bg-slate-950/60 text-slate-400 border-slate-800 hover:text-slate-300 hover:border-slate-700'
}`}
title={
showPopups
? 'Result popups enabled: Click to hide battle and wizard popups'
: 'Result popups disabled: Click to show battle and wizard popups'
}
>
<input
type="checkbox"
checked={showPopups}
onChange={(e) => onTogglePopups?.(e.target.checked)}
className="w-3.5 h-3.5 rounded accent-sky-500 cursor-pointer"
/>
<span className="flex items-center gap-1.5">
<span>Popups:</span>
<strong className={showPopups ? 'text-sky-300' : 'text-slate-500'}>
{showPopups ? 'ON' : 'OFF'}
</strong>
</span>
</label>
<a <a
href="/docs" href="/docs"
target="_blank" target="_blank"

View File

@ -13,6 +13,8 @@ interface MovementControlsProps {
onStepBot: () => void; onStepBot: () => void;
isAutoPlaying: boolean; isAutoPlaying: boolean;
onToggleAutoPlay: () => void; onToggleAutoPlay: () => void;
showPopups?: boolean;
onTogglePopups?: (value?: boolean) => void;
} }
export const MovementControls: React.FC<MovementControlsProps> = ({ export const MovementControls: React.FC<MovementControlsProps> = ({
@ -26,6 +28,8 @@ export const MovementControls: React.FC<MovementControlsProps> = ({
onStepBot, onStepBot,
isAutoPlaying, isAutoPlaying,
onToggleAutoPlay, onToggleAutoPlay,
showPopups = true,
onTogglePopups,
}) => { }) => {
const isStarted = Boolean(boardState.turn?.game_started); const isStarted = Boolean(boardState.turn?.game_started);
const currentTurnId = boardState.turn.current_player_id; const currentTurnId = boardState.turn.current_player_id;
@ -382,6 +386,28 @@ export const MovementControls: React.FC<MovementControlsProps> = ({
</button> </button>
</div> </div>
{/* Result Popups Toggle */}
<div className="pt-2 border-t border-slate-800/80 flex items-center justify-between px-0.5">
<label
className="flex items-center gap-2 cursor-pointer select-none text-xs font-mono text-slate-400 hover:text-slate-200 transition-colors"
title={
showPopups
? 'Result popups enabled: Click to hide battle and wizard popups'
: 'Result popups disabled: Click to show battle and wizard popups'
}
>
<input
type="checkbox"
checked={showPopups}
onChange={(e) => onTogglePopups?.(e.target.checked)}
className="w-3.5 h-3.5 rounded accent-sky-500 cursor-pointer"
/>
<span className="text-[11px]">
Popups: <strong className={showPopups ? 'text-sky-300' : 'text-slate-500'}>{showPopups ? 'ON' : 'OFF'}</strong>
</span>
</label>
</div>
<div className="text-[10px] text-slate-500 font-mono text-center"> <div className="text-[10px] text-slate-500 font-mono text-center">
{isStarted ? 'WASD / Arrows / Numpad to move' : 'Bots can join & depart in Lobby'} {isStarted ? 'WASD / Arrows / Numpad to move' : 'Bots can join & depart in Lobby'}
</div> </div>

View File

@ -51,6 +51,7 @@ export function useGameSocket() {
const [activeBattle, setActiveBattle] = useState<BattleResult | null>(null); const [activeBattle, setActiveBattle] = useState<BattleResult | null>(null);
const [activeWizardChallenge, setActiveWizardChallenge] = useState<WizardChallengeResult | null>(null); const [activeWizardChallenge, setActiveWizardChallenge] = useState<WizardChallengeResult | null>(null);
const [showScoreboard, setShowScoreboard] = useState(false); const [showScoreboard, setShowScoreboard] = useState(false);
const [showPopups, setShowPopups] = useState(true);
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<number | null>(null); const reconnectTimeoutRef = useRef<number | null>(null);
@ -59,6 +60,20 @@ export function useGameSocket() {
activeBattleRef.current = activeBattle; activeBattleRef.current = activeBattle;
const activeWizardChallengeRef = useRef<WizardChallengeResult | null>(null); const activeWizardChallengeRef = useRef<WizardChallengeResult | null>(null);
activeWizardChallengeRef.current = activeWizardChallenge; activeWizardChallengeRef.current = activeWizardChallenge;
const showPopupsRef = useRef(true);
showPopupsRef.current = showPopups;
const updateShowPopups = useCallback((val?: boolean | ((prev: boolean) => boolean)) => {
setShowPopups((prev) => {
const next = typeof val === 'function' ? val(prev) : typeof val === 'boolean' ? val : !prev;
showPopupsRef.current = next;
if (!next) {
setActiveBattle(null);
setActiveWizardChallenge(null);
}
return next;
});
}, []);
const fetchBoard = useCallback(async () => { const fetchBoard = useCallback(async () => {
try { try {
@ -205,7 +220,9 @@ export function useGameSocket() {
turn: data.turn ?? prev.turn, turn: data.turn ?? prev.turn,
})); }));
const b: BattleResult = data.battle; const b: BattleResult = data.battle;
setActiveBattle(b); if (showPopupsRef.current) {
setActiveBattle(b);
}
setLastEventMessage(`⚔️ 3-Bout D20 Battle: ${b.winner_party_name} defeated ${b.defeated_party_name}!`); setLastEventMessage(`⚔️ 3-Bout D20 Battle: ${b.winner_party_name} defeated ${b.defeated_party_name}!`);
} else if (data.event === 'wizard_challenge_resolved') { } else if (data.event === 'wizard_challenge_resolved') {
setBoardState((prev) => ({ setBoardState((prev) => ({
@ -215,7 +232,9 @@ export function useGameSocket() {
turn: data.turn ?? prev.turn, turn: data.turn ?? prev.turn,
})); }));
const c: WizardChallengeResult = data.challenge_result || data.challenge; const c: WizardChallengeResult = data.challenge_result || data.challenge;
setActiveWizardChallenge(c); if (showPopupsRef.current) {
setActiveWizardChallenge(c);
}
const wizName = c.wizard_name || 'Gary the Wizard'; const wizName = c.wizard_name || 'Gary the Wizard';
let rewardLabel = `+${c.score_change} score`; let rewardLabel = `+${c.score_change} score`;
if (c.reward_chosen === 'strength') { if (c.reward_chosen === 'strength') {
@ -413,7 +432,9 @@ export function useGameSocket() {
throw new Error(err.detail || 'Battle failed'); throw new Error(err.detail || 'Battle failed');
} }
const result: BattleResult = await res.json(); const result: BattleResult = await res.json();
setActiveBattle(result); if (showPopupsRef.current) {
setActiveBattle(result);
}
return result; return result;
}; };
@ -432,7 +453,9 @@ export function useGameSocket() {
setSelectedPlayer((curr) => (curr?.id === data.player.id ? data.player : curr)); setSelectedPlayer((curr) => (curr?.id === data.player.id ? data.player : curr));
} }
if (data.battle_triggered && data.battle_result) { if (data.battle_triggered && data.battle_result) {
setActiveBattle(data.battle_result); if (showPopupsRef.current) {
setActiveBattle(data.battle_result);
}
} }
if (data.game_concluded && data.game_concluded.concluded) { if (data.game_concluded && data.game_concluded.concluded) {
setIsAutoPlaying(false); setIsAutoPlaying(false);
@ -490,7 +513,9 @@ export function useGameSocket() {
throw new Error(err.detail || 'Failed to challenge wizard'); throw new Error(err.detail || 'Failed to challenge wizard');
} }
const data: WizardChallengeResult = await res.json(); const data: WizardChallengeResult = await res.json();
setActiveWizardChallenge(data); if (showPopupsRef.current) {
setActiveWizardChallenge(data);
}
return data; return data;
}; };
@ -515,11 +540,15 @@ export function useGameSocket() {
if (data.action_taken === 'formed_party' && data.formed_party) { if (data.action_taken === 'formed_party' && data.formed_party) {
setLastEventMessage(`🤝 ${data.player_name} formed party "${data.formed_party.name}" under leader ${data.formed_party.leader_name}!`); setLastEventMessage(`🤝 ${data.player_name} formed party "${data.formed_party.name}" under leader ${data.formed_party.leader_name}!`);
} else if (data.action_taken === 'battled' && data.battle_result) { } else if (data.action_taken === 'battled' && data.battle_result) {
setActiveBattle(data.battle_result); if (showPopupsRef.current) {
setActiveBattle(data.battle_result);
}
setLastEventMessage(`⚔️ Battle clash: ${data.battle_result.winner_party_name} defeated ${data.battle_result.defeated_party_name}!`); setLastEventMessage(`⚔️ Battle clash: ${data.battle_result.winner_party_name} defeated ${data.battle_result.defeated_party_name}!`);
} else if (data.action_taken === 'challenged_wizard' && data.wizard_challenge_result) { } else if (data.action_taken === 'challenged_wizard' && data.wizard_challenge_result) {
const wcr = data.wizard_challenge_result; const wcr = data.wizard_challenge_result;
setActiveWizardChallenge(wcr); if (showPopupsRef.current) {
setActiveWizardChallenge(wcr);
}
const wizName = wcr.wizard_name || 'Gary the Wizard'; const wizName = wcr.wizard_name || 'Gary the Wizard';
setLastEventMessage( setLastEventMessage(
wcr.player_won wcr.player_won
@ -529,7 +558,9 @@ export function useGameSocket() {
} else if (data.action_taken === 'slept' && data.sleep_result) { } else if (data.action_taken === 'slept' && data.sleep_result) {
setLastEventMessage(`💤 ${data.player_name} took a restful sleep (+${data.sleep_result.health_gained} HP -> ${data.sleep_result.new_health} HP)!`); setLastEventMessage(`💤 ${data.player_name} took a restful sleep (+${data.sleep_result.health_gained} HP -> ${data.sleep_result.new_health} HP)!`);
} else if (data.move_result?.battle_result) { } else if (data.move_result?.battle_result) {
setActiveBattle(data.move_result.battle_result); if (showPopupsRef.current) {
setActiveBattle(data.move_result.battle_result);
}
} }
if (data.game_concluded && data.game_concluded.concluded) { if (data.game_concluded && data.game_concluded.concluded) {
@ -575,6 +606,8 @@ export function useGameSocket() {
setActiveWizardChallenge, setActiveWizardChallenge,
showScoreboard, showScoreboard,
setShowScoreboard, setShowScoreboard,
showPopups,
setShowPopups: updateShowPopups,
registerPlayer, registerPlayer,
removePlayer, removePlayer,
formParty, formParty,

View File

@ -1,2 +1,2 @@
[metadata] [metadata]
version = 1.4 version = 1.5