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

View File

@ -10,6 +10,8 @@ interface HeaderProps {
playerCount: number;
isConcluded?: boolean;
onOpenScoreboard?: () => void;
showPopups?: boolean;
onTogglePopups?: (value?: boolean) => void;
}
export const Header: React.FC<HeaderProps> = ({
@ -22,6 +24,8 @@ export const Header: React.FC<HeaderProps> = ({
playerCount,
isConcluded,
onOpenScoreboard,
showPopups = true,
onTogglePopups,
}) => {
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">
@ -108,6 +112,33 @@ export const Header: React.FC<HeaderProps> = ({
</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
href="/docs"
target="_blank"

View File

@ -13,6 +13,8 @@ interface MovementControlsProps {
onStepBot: () => void;
isAutoPlaying: boolean;
onToggleAutoPlay: () => void;
showPopups?: boolean;
onTogglePopups?: (value?: boolean) => void;
}
export const MovementControls: React.FC<MovementControlsProps> = ({
@ -26,6 +28,8 @@ export const MovementControls: React.FC<MovementControlsProps> = ({
onStepBot,
isAutoPlaying,
onToggleAutoPlay,
showPopups = true,
onTogglePopups,
}) => {
const isStarted = Boolean(boardState.turn?.game_started);
const currentTurnId = boardState.turn.current_player_id;
@ -382,6 +386,28 @@ export const MovementControls: React.FC<MovementControlsProps> = ({
</button>
</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">
{isStarted ? 'WASD / Arrows / Numpad to move' : 'Bots can join & depart in Lobby'}
</div>

View File

@ -51,6 +51,7 @@ export function useGameSocket() {
const [activeBattle, setActiveBattle] = useState<BattleResult | null>(null);
const [activeWizardChallenge, setActiveWizardChallenge] = useState<WizardChallengeResult | null>(null);
const [showScoreboard, setShowScoreboard] = useState(false);
const [showPopups, setShowPopups] = useState(true);
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<number | null>(null);
@ -59,6 +60,20 @@ export function useGameSocket() {
activeBattleRef.current = activeBattle;
const activeWizardChallengeRef = useRef<WizardChallengeResult | null>(null);
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 () => {
try {
@ -205,7 +220,9 @@ export function useGameSocket() {
turn: data.turn ?? prev.turn,
}));
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}!`);
} else if (data.event === 'wizard_challenge_resolved') {
setBoardState((prev) => ({
@ -215,7 +232,9 @@ export function useGameSocket() {
turn: data.turn ?? prev.turn,
}));
const c: WizardChallengeResult = data.challenge_result || data.challenge;
setActiveWizardChallenge(c);
if (showPopupsRef.current) {
setActiveWizardChallenge(c);
}
const wizName = c.wizard_name || 'Gary the Wizard';
let rewardLabel = `+${c.score_change} score`;
if (c.reward_chosen === 'strength') {
@ -413,7 +432,9 @@ export function useGameSocket() {
throw new Error(err.detail || 'Battle failed');
}
const result: BattleResult = await res.json();
setActiveBattle(result);
if (showPopupsRef.current) {
setActiveBattle(result);
}
return result;
};
@ -432,7 +453,9 @@ export function useGameSocket() {
setSelectedPlayer((curr) => (curr?.id === data.player.id ? data.player : curr));
}
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) {
setIsAutoPlaying(false);
@ -490,7 +513,9 @@ export function useGameSocket() {
throw new Error(err.detail || 'Failed to challenge wizard');
}
const data: WizardChallengeResult = await res.json();
setActiveWizardChallenge(data);
if (showPopupsRef.current) {
setActiveWizardChallenge(data);
}
return data;
};
@ -515,11 +540,15 @@ export function useGameSocket() {
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}!`);
} 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}!`);
} else if (data.action_taken === 'challenged_wizard' && 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';
setLastEventMessage(
wcr.player_won
@ -529,7 +558,9 @@ export function useGameSocket() {
} 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)!`);
} 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) {
@ -575,6 +606,8 @@ export function useGameSocket() {
setActiveWizardChallenge,
showScoreboard,
setShowScoreboard,
showPopups,
setShowPopups: updateShowPopups,
registerPlayer,
removePlayer,
formParty,

View File

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