From 7eeb5eb4f920cefd02df6a5033926c9fba0f7793 Mon Sep 17 00:00:00 2001 From: Aidan Blum Levine Date: Tue, 9 Apr 2024 21:13:22 -0400 Subject: [PATCH 1/5] refactor controls-bar --- client/src/app-context.tsx | 8 +- .../controls-bar/controls-bar-timeline.tsx | 11 +- .../components/controls-bar/controls-bar.tsx | 171 ++++++------------ client/src/components/game/GameController.ts | 56 ++++++ client/src/components/game/game-area.tsx | 62 ++++++- client/src/pages/main-page.tsx | 5 +- client/src/playback/Match.ts | 4 + 7 files changed, 179 insertions(+), 138 deletions(-) create mode 100644 client/src/components/game/GameController.ts diff --git a/client/src/app-context.tsx b/client/src/app-context.tsx index cea23d2f..eebe5817 100644 --- a/client/src/app-context.tsx +++ b/client/src/app-context.tsx @@ -11,8 +11,8 @@ export interface AppState { tournament: Tournament | undefined tournamentState: TournamentState loadingRemoteContent: string - updatesPerSecond: number - paused: boolean + // updatesPerSecond: number + // paused: boolean disableHotkeys: boolean config: ClientConfig } @@ -24,8 +24,8 @@ const DEFAULT_APP_STATE: AppState = { tournament: undefined, tournamentState: DEFAULT_TOURNAMENT_STATE, loadingRemoteContent: '', - updatesPerSecond: 1, - paused: true, + // updatesPerSecond: 1, + // paused: true, disableHotkeys: false, config: getDefaultConfig() } diff --git a/client/src/components/controls-bar/controls-bar-timeline.tsx b/client/src/components/controls-bar/controls-bar-timeline.tsx index c1e8583d..70b59f1b 100644 --- a/client/src/components/controls-bar/controls-bar-timeline.tsx +++ b/client/src/components/controls-bar/controls-bar-timeline.tsx @@ -3,10 +3,11 @@ import { useAppContext } from '../../app-context' const TIMELINE_WIDTH = 350 interface Props { - currentUPS: number + liveUPS: number + targetUPS: number } -export const ControlsBarTimeline: React.FC = ({ currentUPS }) => { +export const ControlsBarTimeline: React.FC = ({ liveUPS, targetUPS }) => { const appContext = useAppContext() let down = useRef(false) @@ -67,9 +68,9 @@ export const ControlsBarTimeline: React.FC = ({ currentUPS }) => { return (

- Turn: {turn}/{maxTurn}   {appContext.state.updatesPerSecond} UPS ( - {appContext.state.updatesPerSecond < 0 && '-'} - {currentUPS}) + Turn: {turn}/{maxTurn}   {targetUPS} UPS ( + {targetUPS < 0 && '-'} + {liveUPS})

void + targetUPS: number + liveUPS: number + setTargetUPS: (targetUPS: number) => void + nextMatch: () => void + closeGame: () => void +} -export const ControlsBar: React.FC = () => { +export const ControlsBar: React.FC = ({ + match, + paused, + setPaused, + targetUPS, + setTargetUPS, + liveUPS, + nextMatch, + closeGame +}) => { const { state: appState, setState: setAppState } = useAppContext() const [minimized, setMinimized] = React.useState(false) const keyboard = useKeyboard() - const [page, setPage] = usePage() - const currentUPSBuffer = React.useRef([]) - - const currentMatch = appState.activeGame?.currentMatch - const isPlayable = appState.activeGame && appState.activeGame.playable && currentMatch - const hasNextMatch = - currentMatch && appState.activeGame!.matches.indexOf(currentMatch!) + 1 < appState.activeGame!.matches.length + const forceUpdate = useForceUpdate() + useListenEvent(EventType.TURN_PROGRESS, forceUpdate) - const changePaused = (paused: boolean) => { - if (!currentMatch) return - setAppState((prevState) => ({ - ...prevState, - paused: paused, - updatesPerSecond: appState.updatesPerSecond == 0 && !paused ? 1 : appState.updatesPerSecond - })) - } + const hasNextMatch = match && match.game.matches.indexOf(match!) + 1 < match.game.matches.length const multiplyUpdatesPerSecond = (multiplier: number) => { - if (!isPlayable) return - setAppState((old) => { - const u = old.updatesPerSecond - const sign = Math.sign(u * multiplier) - const newMag = Math.max(1 / 4, Math.min(64, Math.abs(u * multiplier))) - return { ...old, updatesPerSecond: sign * newMag } - }) + if (!match?.isPlayable()) return + const sign = Math.sign(targetUPS * multiplier) + const newMag = Math.max(1 / 4, Math.min(64, Math.abs(targetUPS * multiplier))) + setTargetUPS(sign * newMag) } const stepTurn = (delta: number) => { - if (!isPlayable) return - // explicit rerender at the end so a render doesnt occur between these two steps - currentMatch!.stepTurn(delta, false) - currentMatch!.roundSimulation() - currentMatch!.rerender() + if (!match?.isPlayable()) return + match.stepTurn(delta, false) // false to not rerender here, so we can round first + match.roundSimulation() + match.rerender() } const jumpToTurn = (turn: number) => { - if (!isPlayable) return - // explicit rerender at the end so a render doesnt occur between these two steps - currentMatch!.jumpToTurn(turn, false) - currentMatch!.roundSimulation() - currentMatch!.rerender() + if (!match?.isPlayable()) return + match.jumpToTurn(turn, false) // false to not rerender here, so we can round first + match.roundSimulation() + match.rerender() } const jumpToEnd = () => { - if (!isPlayable) return - // explicit rerender at the end so a render doesnt occur between these two steps - currentMatch!.jumpToEnd(false) - currentMatch!.roundSimulation() - currentMatch!.rerender() + if (!match?.isPlayable()) return + match.jumpToEnd(false) // false to not rerender here, so we can round first + match.roundSimulation() + match.rerender() } - const nextMatch = () => { - if (!isPlayable) return - const game = appState.activeGame! - const prevMatch = game.currentMatch! - const prevMatchIndex = game.matches.indexOf(prevMatch) - if (prevMatchIndex + 1 == game.matches.length) { - closeGame() - return - } - - game.currentMatch = game.matches[prevMatchIndex + 1] - setAppState((prevState) => ({ - ...prevState, - activeGame: game, - activeMatch: game.currentMatch - })) - } - - const closeGame = () => { - setAppState((prevState) => ({ - ...prevState, - activeGame: undefined, - activeMatch: undefined - })) - if (appState.tournament) setPage(PageType.TOURNAMENT) - } - - React.useEffect(() => { - // We want to pause whenever the match changes - changePaused(true) - }, [currentMatch]) - - React.useEffect(() => { - if (!isPlayable) return - if (appState.paused) { - // Snap bots to their actual position when paused by rounding simulation - // to the true turn - currentMatch!.roundSimulation() - currentMatch!.rerender() - return - } - - const msPerUpdate = 1000 / appState.updatesPerSecond - const updatesPerInterval = SIMULATION_UPDATE_INTERVAL_MS / msPerUpdate - const stepInterval = setInterval(() => { - const prevTurn = currentMatch!.currentTurn.turnNumber - currentMatch!.stepSimulation(updatesPerInterval) - - if (prevTurn != currentMatch!.currentTurn.turnNumber) { - currentUPSBuffer.current.push(Date.now()) - while (currentUPSBuffer.current.length > 0 && currentUPSBuffer.current[0] < Date.now() - 1000) - currentUPSBuffer.current.shift() - } - - if (currentMatch!.currentTurn.isEnd() && appState.updatesPerSecond > 0) { - changePaused(true) - } - }, SIMULATION_UPDATE_INTERVAL_MS) - - return () => { - clearInterval(stepInterval) - } - }, [appState.updatesPerSecond, appState.activeGame, currentMatch, appState.paused]) - useEffect(() => { if (appState.disableHotkeys) return @@ -139,12 +75,12 @@ export const ControlsBar: React.FC = () => { // specific accessibility features that mess with these shortcuts. if (keyboard.targetElem instanceof HTMLButtonElement) keyboard.targetElem.blur() - if (keyboard.keyCode === 'Space') changePaused(!appState.paused) + if (keyboard.keyCode === 'Space' && match) setPaused(!paused) if (keyboard.keyCode === 'KeyC') setMinimized(!minimized) const applyArrows = () => { - if (appState.paused) { + if (paused) { if (keyboard.keyCode === 'ArrowRight') stepTurn(1) if (keyboard.keyCode === 'ArrowLeft') stepTurn(-1) } else { @@ -170,13 +106,10 @@ export const ControlsBar: React.FC = () => { } }, [keyboard.keyCode]) - const forceUpdate = useForceUpdate() - useListenEvent(EventType.TURN_PROGRESS, forceUpdate) - - if (!isPlayable) return null + if (!match?.isPlayable()) return null - const atStart = currentMatch.currentTurn.turnNumber == 0 - const atEnd = currentMatch.currentTurn.turnNumber == currentMatch.maxTurn + const atStart = match.currentTurn.turnNumber == 0 + const atEnd = match.currentTurn.turnNumber == match.maxTurn return (
{ ' flex bg-darkHighlight text-white p-1.5 rounded-t-md z-10 gap-1.5 relative' } > - + } tooltip="Reverse" @@ -210,7 +143,7 @@ export const ControlsBar: React.FC = () => { icon={} tooltip={'Decrease Speed'} onClick={() => multiplyUpdatesPerSecond(0.5)} - disabled={Math.abs(appState.updatesPerSecond) <= 0.25} + disabled={Math.abs(targetUPS) <= 0.25} /> } @@ -218,12 +151,12 @@ export const ControlsBar: React.FC = () => { onClick={() => stepTurn(-1)} disabled={atStart} /> - {appState.paused ? ( + {paused ? ( } tooltip="Play" onClick={() => { - changePaused(false) + setPaused(false) }} /> ) : ( @@ -231,7 +164,7 @@ export const ControlsBar: React.FC = () => { icon={} tooltip="Pause" onClick={() => { - changePaused(true) + setPaused(true) }} /> )} @@ -245,7 +178,7 @@ export const ControlsBar: React.FC = () => { icon={} tooltip={'Increase Speed'} onClick={() => multiplyUpdatesPerSecond(2)} - disabled={Math.abs(appState.updatesPerSecond) >= 64} + disabled={Math.abs(targetUPS) >= 64} /> } diff --git a/client/src/components/game/GameController.ts b/client/src/components/game/GameController.ts new file mode 100644 index 00000000..a37b10af --- /dev/null +++ b/client/src/components/game/GameController.ts @@ -0,0 +1,56 @@ +import { useEffect, useRef, useState } from 'react' +import Match from '../../playback/Match' + +const SIMULATION_UPDATE_INTERVAL_MS = 17 // About 60 fps + +export const useSimulationControl = (currentMatch: Match | undefined) => { + const [paused, setPaused] = useState(true) + const [targetUPS, setTargetUPS] = useState(1) + const [liveUPS, setLiveUPS] = useState(0) // State to keep track of liveUPS + + const currentUPSBuffer = useRef([]) + + useEffect(() => { + setPaused(true) // Pause when match changes + currentUPSBuffer.current = [] // Clear the buffer when match changes + setLiveUPS(0) // Reset liveUPS when match changes + }, [currentMatch]) + + useEffect(() => { + if (!currentMatch?.isPlayable()) return + + if (paused) { + currentMatch.roundSimulation() + currentMatch.rerender() + return + } + + const msPerUpdate = 1000 / targetUPS + const updatesPerInterval = SIMULATION_UPDATE_INTERVAL_MS / msPerUpdate + const stepInterval = setInterval(() => { + const prevTurn = currentMatch.currentTurn.turnNumber + currentMatch.stepSimulation(updatesPerInterval) + if (prevTurn !== currentMatch.currentTurn.turnNumber) { + currentUPSBuffer.current.push(Date.now()) + while (currentUPSBuffer.current.length > 0 && currentUPSBuffer.current[0] < Date.now() - 1000) { + currentUPSBuffer.current.shift() + } + setLiveUPS(currentUPSBuffer.current.length) // Update liveUPS + } + if (currentMatch.currentTurn.isEnd() && targetUPS > 0) setPaused(true) + }, SIMULATION_UPDATE_INTERVAL_MS) + + return () => { + clearInterval(stepInterval) + } + }, [targetUPS, currentMatch, paused]) + + return { + paused, + setPaused, + targetUPS, + setTargetUPS, + liveUPS + } +} + diff --git a/client/src/components/game/game-area.tsx b/client/src/components/game/game-area.tsx index 65df02cd..4e378114 100644 --- a/client/src/components/game/game-area.tsx +++ b/client/src/components/game/game-area.tsx @@ -1,20 +1,70 @@ -import React from 'react' +import React, { useEffect, useRef, useState } from 'react' import { GameRenderer } from './game-renderer' import { useAppContext } from '../../app-context' import { TournamentRenderer } from './tournament-renderer/tournament-renderer' +import { ControlsBar } from '../controls-bar/controls-bar' +import { useSimulationControl } from './GameController'; -export const GameArea: React.FC = () => { - const appContext = useAppContext() - if (appContext.state.loadingRemoteContent) { +export const GameArea = () => { + const { state: appState, setState: setAppState } = useAppContext() + const currentMatch = appState.activeGame?.currentMatch + const { paused, setPaused, targetUPS, setTargetUPS, liveUPS } = useSimulationControl(currentMatch) + + const handleNextMatch = () => { + if (!currentMatch) return + const game = currentMatch.game + const prevMatchIndex = game.matches.indexOf(currentMatch) + if (game.matches[prevMatchIndex + 1] === undefined) return + game.currentMatch = game.matches[prevMatchIndex + 1] + setAppState((prevState) => ({ + ...prevState, + activeGame: game, + activeMatch: game.currentMatch + })) + } + + const handleCloseGame = () => { + setAppState((prevState) => ({ + ...prevState, + activeGame: undefined, + activeMatch: undefined + })) + } + + return ( +
+ + +
+ ) +} + +const GameCanvasArea: React.FC<{ loadingRemoteContent: string; tournamentScreen: boolean }> = ({ + loadingRemoteContent, + tournamentScreen +}) => { + if (loadingRemoteContent) { return (
-

{`Loading remote ${appContext.state.loadingRemoteContent}...`}

+

{`Loading remote ${loadingRemoteContent}...`}

) } - if (!appContext.state.activeGame && appContext.state.tournament) { + if (tournamentScreen) { return } diff --git a/client/src/pages/main-page.tsx b/client/src/pages/main-page.tsx index 53e12986..2f61e002 100644 --- a/client/src/pages/main-page.tsx +++ b/client/src/pages/main-page.tsx @@ -10,10 +10,7 @@ export const MainPage: React.FC = () => {
-
- - -
+
) diff --git a/client/src/playback/Match.ts b/client/src/playback/Match.ts index 0ddb1fe0..d1c3f8b1 100644 --- a/client/src/playback/Match.ts +++ b/client/src/playback/Match.ts @@ -86,6 +86,10 @@ export default class Match { return match } + public isPlayable() { + return this.game.playable + } + /* * Add a new turn to the match. Used for live match replaying. */ From c9f075537b55c244f5a177e6ebcc4e19a8c434c3 Mon Sep 17 00:00:00 2001 From: Aidan Blum Levine Date: Wed, 10 Apr 2024 14:24:18 -0400 Subject: [PATCH 2/5] refactor event loop hooks and tooltip --- client/src/app-events.tsx | 10 +- .../components/controls-bar/controls-bar.tsx | 2 +- client/src/components/game/GameController.ts | 2 +- client/src/components/game/game-area.tsx | 6 +- client/src/components/game/game-renderer.tsx | 322 +++++++++--------- client/src/components/game/overlay.tsx | 262 ++++++++++++++ client/src/components/game/tooltip.tsx | 197 ----------- client/src/components/sidebar/game/game.tsx | 2 +- .../src/components/sidebar/game/histogram.tsx | 2 +- .../sidebar/game/resource-graph.tsx | 2 +- .../components/sidebar/game/team-table.tsx | 2 +- .../sidebar/map-editor/map-editor.tsx | 2 +- .../components/sidebar/runner/websocket.ts | 4 +- client/src/playback/Match.ts | 2 +- 14 files changed, 444 insertions(+), 373 deletions(-) delete mode 100644 client/src/components/game/tooltip.tsx diff --git a/client/src/app-events.tsx b/client/src/app-events.tsx index 4a8e82ff..7e2e7a39 100644 --- a/client/src/app-events.tsx +++ b/client/src/app-events.tsx @@ -1,12 +1,12 @@ import { useEffect } from 'react' export enum EventType { - TURN_PROGRESS = 'turnprogress', - TILE_CLICK = 'tileclick', + NEW_TURN = 'NEW_TURN', + TILE_CLICK = 'TILE_CLICK', TILE_DRAG = 'TILE_DRAG', CANVAS_RIGHT_CLICK = 'CANVAS_RIGHT_CLICK', - RENDER = 'render', - INITIAL_RENDER = 'initalrender' + RENDER = 'RENDER', + MAP_RENDER = 'MAP_RENDER' } export function useListenEvent( @@ -26,7 +26,7 @@ export function useListenEvent( }, deps) } -export function publishEvent(eventType: string, eventData: any) { +export function publishEvent(eventType: string, eventData: any = false) { const event = new CustomEvent(eventType as string, { detail: eventData }) document.dispatchEvent(event) } diff --git a/client/src/components/controls-bar/controls-bar.tsx b/client/src/components/controls-bar/controls-bar.tsx index fdd455df..730c4fc2 100644 --- a/client/src/components/controls-bar/controls-bar.tsx +++ b/client/src/components/controls-bar/controls-bar.tsx @@ -35,7 +35,7 @@ export const ControlsBar: React.FC = ({ const keyboard = useKeyboard() const forceUpdate = useForceUpdate() - useListenEvent(EventType.TURN_PROGRESS, forceUpdate) + useListenEvent(EventType.NEW_TURN, forceUpdate) const hasNextMatch = match && match.game.matches.indexOf(match!) + 1 < match.game.matches.length diff --git a/client/src/components/game/GameController.ts b/client/src/components/game/GameController.ts index a37b10af..ef0cfe5e 100644 --- a/client/src/components/game/GameController.ts +++ b/client/src/components/game/GameController.ts @@ -3,7 +3,7 @@ import Match from '../../playback/Match' const SIMULATION_UPDATE_INTERVAL_MS = 17 // About 60 fps -export const useSimulationControl = (currentMatch: Match | undefined) => { +export const useGameControl = (currentMatch: Match | undefined) => { const [paused, setPaused] = useState(true) const [targetUPS, setTargetUPS] = useState(1) const [liveUPS, setLiveUPS] = useState(0) // State to keep track of liveUPS diff --git a/client/src/components/game/game-area.tsx b/client/src/components/game/game-area.tsx index 4e378114..83e09d48 100644 --- a/client/src/components/game/game-area.tsx +++ b/client/src/components/game/game-area.tsx @@ -1,15 +1,15 @@ -import React, { useEffect, useRef, useState } from 'react' +import React from 'react' import { GameRenderer } from './game-renderer' import { useAppContext } from '../../app-context' import { TournamentRenderer } from './tournament-renderer/tournament-renderer' import { ControlsBar } from '../controls-bar/controls-bar' -import { useSimulationControl } from './GameController'; +import { useGameControl } from './GameController'; export const GameArea = () => { const { state: appState, setState: setAppState } = useAppContext() const currentMatch = appState.activeGame?.currentMatch - const { paused, setPaused, targetUPS, setTargetUPS, liveUPS } = useSimulationControl(currentMatch) + const { paused, setPaused, targetUPS, setTargetUPS, liveUPS } = useGameControl(currentMatch) const handleNextMatch = () => { if (!currentMatch) return diff --git a/client/src/components/game/game-renderer.tsx b/client/src/components/game/game-renderer.tsx index c3017f8b..97793993 100644 --- a/client/src/components/game/game-renderer.tsx +++ b/client/src/components/game/game-renderer.tsx @@ -1,11 +1,12 @@ -import React, { useEffect, useRef, useState } from 'react' +import React, { MutableRefObject, useEffect, useRef, useState } from 'react' import { useAppContext } from '../../app-context' import { Vector } from '../../playback/Vector' import { EventType, publishEvent, useListenEvent } from '../../app-events' -import assert from 'assert' -import { Tooltip } from './tooltip' +import { Overlay } from './overlay' import { TILE_RESOLUTION } from '../../constants' import { CurrentMap } from '../../playback/Map' +import Match from '../../playback/Match' +import { ClientConfig } from '../../client-config' export const GameRenderer: React.FC = () => { const wrapperRef = useRef(null) @@ -14,84 +15,155 @@ export const GameRenderer: React.FC = () => { const overlayCanvas = useRef(null) const appContext = useAppContext() - const { activeGame, activeMatch } = appContext.state + const { activeMatch, config } = appContext.state - const [selectedBodyID, setSelectedBodyID] = useState(undefined) - const [hoveredTile, setHoveredTile] = useState(undefined) - const [selectedSquare, setSelectedSquare] = useState(undefined) + const { + onMouseMove, + onMouseDown, + onMouseUp, + onMouseLeave, + onMouseEnter, + onCanvasClick, + selectedBodyID, + hoveredTile, + hoveredBodyID + } = useRenderEvents(activeMatch, config, backgroundCanvas, dynamicCanvas, overlayCanvas) + + return ( +
+ {!activeMatch ? ( +

Select a game from the queue

+ ) : ( + <> + + + { + e.preventDefault() + }} + /> + {overlayCanvas.current && wrapperRef.current && activeMatch && ( + + )} + + )} +
+ ) +} + +const useHoveredBody = (hoveredTile: Vector | undefined, match: Match | undefined) => { const [hoveredBodyID, setHoveredBodyID] = useState(undefined) const calculateHoveredBodyID = () => { if (!hoveredTile) return setHoveredBodyID(undefined) - const match = appContext.state.activeMatch if (!match) return - const hoveredBodyIDFound = match.currentTurn.bodies.getBodyAtLocation(hoveredTile.x, hoveredTile.y)?.id - setHoveredBodyID(hoveredBodyIDFound) + const hoveredBody = match.currentTurn.bodies.getBodyAtLocation(hoveredTile.x, hoveredTile.y) + setHoveredBodyID(hoveredBody?.id) + } + useEffect(calculateHoveredBodyID, [hoveredTile, match]) + useListenEvent(EventType.NEW_TURN, calculateHoveredBodyID) + return hoveredBodyID +} + +const useRenderEvents = ( + match: Match | undefined, + config: ClientConfig, + backgroundCanvas: MutableRefObject, + dynamicCanvas: MutableRefObject, + overlayCanvas: MutableRefObject +) => { + const [selectedBodyID, setSelectedBodyID] = useState(undefined) + const [hoveredTile, setHoveredTile] = useState(undefined) + const mouseDown = React.useRef(false) + const mouseDownRightPrev = React.useRef(false) + const lastFiredDragEvent = React.useRef({ x: -1, y: -1 }) + const hoveredBodyID = useHoveredBody(hoveredTile, match) - // always clear this so the selection is cleared when you move - setSelectedSquare(undefined) + const lastRender = useRef(0) + const queueTimeout = useRef(null) + const queueRender = () => { + if (queueTimeout.current) clearTimeout(queueTimeout.current) + if (Date.now() - lastRender.current > 1000 / 60) { + render() + } else { + queueTimeout.current = setTimeout(render, 1000 / 60) + } } - useEffect(calculateHoveredBodyID, [hoveredTile]) - useListenEvent(EventType.TURN_PROGRESS, calculateHoveredBodyID) - const render = () => { + const render = (full: boolean = false) => { const ctx = dynamicCanvas.current?.getContext('2d') const overlayCtx = overlayCanvas.current?.getContext('2d') - if (!activeMatch || !ctx || !overlayCtx) return + const staticCtx = backgroundCanvas.current?.getContext('2d') + if (!match || !ctx || !overlayCtx || !staticCtx) return + + lastRender.current = Date.now() - const currentTurn = activeMatch.currentTurn - const map = currentTurn.map + if (full) { + staticCtx.clearRect(0, 0, staticCtx.canvas.width, staticCtx.canvas.height) + match.currentTurn.map.staticMap.draw(staticCtx) + } ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) overlayCtx.clearRect(0, 0, overlayCtx.canvas.width, overlayCtx.canvas.height) - map.draw(activeMatch, ctx, appContext.state.config, selectedBodyID, hoveredBodyID) - currentTurn.bodies.draw(activeMatch, ctx, overlayCtx, appContext.state.config, selectedBodyID, hoveredBodyID) - currentTurn.actions.draw(activeMatch, ctx) + match.currentTurn.map.draw(match, ctx, config, selectedBodyID, hoveredBodyID) + match.currentTurn.bodies.draw(match, ctx, overlayCtx, config, selectedBodyID, hoveredBodyID) + match.currentTurn.actions.draw(match, ctx) } - useEffect(render, [hoveredBodyID, selectedBodyID]) - useListenEvent(EventType.RENDER, render, [render]) - - const fullRender = () => { - const match = appContext.state.activeMatch - const ctx = backgroundCanvas.current?.getContext('2d') - if (!match || !ctx) return - match.currentTurn.map.staticMap.draw(ctx) - render() - } - useListenEvent(EventType.INITIAL_RENDER, fullRender, [fullRender]) - const updateCanvasDimensions = (canvas: HTMLCanvasElement | null, dims: Vector) => { - if (!canvas) return - canvas.width = dims.x * TILE_RESOLUTION - canvas.height = dims.y * TILE_RESOLUTION - canvas.getContext('2d')?.scale(TILE_RESOLUTION, TILE_RESOLUTION) - } + useEffect(queueRender, [hoveredBodyID, selectedBodyID]) + useListenEvent(EventType.RENDER, queueRender, [render]) + useListenEvent(EventType.MAP_RENDER, () => render(true)) + useEffect(() => { - const match = appContext.state.activeMatch if (!match) return const { width, height } = match.currentTurn.map updateCanvasDimensions(backgroundCanvas.current, { x: width, y: height }) updateCanvasDimensions(dynamicCanvas.current, { x: width, y: height }) updateCanvasDimensions(overlayCanvas.current, { x: width, y: height }) - setSelectedSquare(undefined) setSelectedBodyID(undefined) setHoveredTile(undefined) - setHoveredBodyID(undefined) - publishEvent(EventType.INITIAL_RENDER, {}) - }, [appContext.state.activeMatch, backgroundCanvas.current, dynamicCanvas.current, overlayCanvas.current]) - - const eventToPoint = (e: React.MouseEvent) => { - const canvas = e.target as HTMLCanvasElement - const rect = canvas.getBoundingClientRect() - const map = activeGame!.currentMatch!.currentTurn!.map ?? assert.fail('map is null in onclick') - let x = Math.floor(((e.clientX - rect.left) / rect.width) * map.width) - let y = Math.floor((1 - (e.clientY - rect.top) / rect.height) * map.height) - x = Math.max(0, Math.min(x, map.width - 1)) - y = Math.max(0, Math.min(y, map.height - 1)) - return { x: x, y: y } - } - const mouseDown = React.useRef(false) - const mouseDownRightPrev = React.useRef(false) - const lastFiredDragEvent = React.useRef({ x: -1, y: -1 }) + render(true) + }, [match, backgroundCanvas.current, dynamicCanvas.current, overlayCanvas.current]) + const onMouseUp = (e: React.MouseEvent) => { mouseDown.current = false lastFiredDragEvent.current = { x: -1, y: -1 } @@ -102,7 +174,8 @@ export const GameRenderer: React.FC = () => { if (e.button === 2) mouseDownRight(true, e) } const onMouseMove = (e: React.MouseEvent) => { - const tile = eventToPoint(e) + if (mouseDown.current) onCanvasDrag(e) + const tile = eventToPoint(e, match?.currentTurn.map) if (tile.x !== hoveredTile?.x || tile.y !== hoveredTile?.y) setHoveredTile(tile) } const mouseDownRight = (down: boolean, e?: React.MouseEvent) => { @@ -117,117 +190,50 @@ export const GameRenderer: React.FC = () => { setHoveredTile(undefined) } const onCanvasClick = (e: React.MouseEvent) => { - const point = eventToPoint(e) - const clickedBody = activeGame?.currentMatch?.currentTurn?.bodies.getBodyAtLocation(point.x, point.y) + const point = eventToPoint(e, match?.currentTurn.map) + const clickedBody = match?.currentTurn.bodies.getBodyAtLocation(point.x, point.y) setSelectedBodyID(clickedBody ? clickedBody.id : undefined) - setSelectedSquare(clickedBody || !activeMatch?.game.playable ? undefined : point) publishEvent(EventType.TILE_CLICK, point) } const onCanvasDrag = (e: React.MouseEvent) => { - const tile = eventToPoint(e) + const tile = eventToPoint(e, match?.currentTurn.map) if (tile.x !== hoveredTile?.x || tile.y !== hoveredTile?.y) setHoveredTile(tile) if (tile.x === lastFiredDragEvent.current.x && tile.y === lastFiredDragEvent.current.y) return lastFiredDragEvent.current = tile publishEvent(EventType.TILE_DRAG, tile) } + const onMouseEnter = (e: React.MouseEvent) => { + if (e.buttons === 1) mouseDown.current = true + } - return ( -
- {!activeMatch ? ( -

Select a game from the queue

- ) : ( - <> - - - { - if (mouseDown.current) onCanvasDrag(e) - onMouseMove(e) - }} - onMouseDown={onMouseDown} - onMouseUp={onMouseUp} - onMouseLeave={onMouseLeave} - onMouseEnter={(e) => { - if (e.buttons === 1) mouseDown.current = true - }} - onContextMenu={(e) => { - e.preventDefault() - }} - /> - - - - )} -
- ) + return { + onMouseMove, + onMouseDown, + onMouseUp, + onMouseLeave, + onMouseEnter, + onCanvasClick, + onCanvasDrag, + selectedBodyID, + hoveredTile, + hoveredBodyID + } } -interface HighlightedSquareProps { - overlayCanvasRef: HTMLCanvasElement | null - wrapperRef: HTMLDivElement | null - map?: CurrentMap - hoveredTile?: Vector +const updateCanvasDimensions = (canvas: HTMLCanvasElement | null, dims: Vector) => { + if (!canvas) return + canvas.width = dims.x * TILE_RESOLUTION + canvas.height = dims.y * TILE_RESOLUTION + canvas.getContext('2d')?.scale(TILE_RESOLUTION, TILE_RESOLUTION) } -const HighlightedSquare: React.FC = ({ overlayCanvasRef, wrapperRef, map, hoveredTile }) => { - if (!hoveredTile || !map || !wrapperRef || !overlayCanvasRef) return <> - const overlayCanvasRect = overlayCanvasRef.getBoundingClientRect() - const wrapperRect = wrapperRef.getBoundingClientRect() - const mapLeft = overlayCanvasRect.left - wrapperRect.left - const mapTop = overlayCanvasRect.top - wrapperRect.top - const tileWidth = overlayCanvasRect.width / map.width - const tileHeight = overlayCanvasRect.height / map.height - const tileLeft = mapLeft + tileWidth * hoveredTile.x - const tileTop = mapTop + tileHeight * (map.height - hoveredTile.y - 1) - return ( -
- ) + +const eventToPoint = (e: React.MouseEvent, map: CurrentMap | undefined) => { + if (!map) throw new Error('Map is undefined in eventToPoint function') + const canvas = e.target as HTMLCanvasElement + const rect = canvas.getBoundingClientRect() + let x = Math.floor(((e.clientX - rect.left) / rect.width) * map.width) + let y = Math.floor((1 - (e.clientY - rect.top) / rect.height) * map.height) + x = Math.max(0, Math.min(x, map.width - 1)) + y = Math.max(0, Math.min(y, map.height - 1)) + return { x: x, y: y } } diff --git a/client/src/components/game/overlay.tsx b/client/src/components/game/overlay.tsx index e69de29b..d35413c0 100644 --- a/client/src/components/game/overlay.tsx +++ b/client/src/components/game/overlay.tsx @@ -0,0 +1,262 @@ +import React, { useEffect } from 'react' +import { useAppContext } from '../../app-context' +import { useListenEvent, EventType } from '../../app-events' +import { useForceUpdate } from '../../util/react-util' +import { ThreeBarsIcon } from '../../icons/three-bars' +import { getRenderCoords } from '../../util/RenderUtil' +import { Vector } from '../../playback/Vector' +import Match from '../../playback/Match' +import { CurrentMap } from '../../playback/Map' +import { Body } from '../../playback/Bodies' +import { Vec } from 'battlecode-schema/js/battlecode/schema' + +type OverlayProps = { + match: Match + overlayCanvas: HTMLCanvasElement + selectedBodyID: number | undefined + hoveredBodyID: number | undefined + hoveredTile: Vector | undefined + wrapperRef: HTMLDivElement +} + +export const Overlay = ({ + match, + overlayCanvas, + selectedBodyID, + hoveredBodyID, + hoveredTile, + wrapperRef +}: OverlayProps) => { + const appContext = useAppContext() + const forceUpdate = useForceUpdate() + useListenEvent(EventType.NEW_TURN, forceUpdate) // update tooltip content + + const selectedBody = selectedBodyID !== undefined ? match.currentTurn.bodies.bodies.get(selectedBodyID) : undefined + const hoveredBody = hoveredBodyID !== undefined ? match.currentTurn.bodies.bodies.get(hoveredBodyID) : undefined + const map = match.currentTurn.map + + const wrapperRect = wrapperRef.getBoundingClientRect() + + const floatingTooltipContent = hoveredBody + ? hoveredBody.onHoverInfo() + : hoveredTile + ? map.getTooltipInfo(hoveredTile, match!) + : [] + const floatingTooltipFocus = hoveredBody ? hoveredBody.pos : hoveredTile + const showFloatingTooltip = !!( + ((hoveredBody && hoveredBody != selectedBody) || hoveredTile) && + floatingTooltipFocus && + floatingTooltipContent.length > 0 + ) + + return ( + <> +
+ {showFloatingTooltip && ( + + {floatingTooltipContent.map((v, i) => ( +

{v}

+ ))} +
+ )} + + + + {appContext.state.config.showMapXY && hoveredTile && ( +
+ {`(X: ${hoveredTile.x}, Y: ${hoveredTile.y})`} +
+ )} +
+ {hoveredTile && ( + + )} + + ) +} + +interface FloatingTooltipProps { + children: React.ReactNode + mapPosition: Vector + wrapperRect: DOMRect + mapRect: DOMRect + map: CurrentMap +} + +const FloatingTooltip = ({ children, mapPosition, wrapperRect, mapRect, map }: FloatingTooltipProps) => { + const tooltipRef = React.useRef(null) + const position = getRenderCoords(mapPosition.x, mapPosition.y, map.dimension, true) + + const [tooltipSize, setTooltipSize] = React.useState(undefined) + useEffect(() => { + const observer = new ResizeObserver((entries) => { + if (entries[0]) { + const borderBox = entries[0].borderBoxSize[0] + setTooltipSize({ x: borderBox.inlineSize, y: borderBox.blockSize }) + } + }) + if (tooltipRef.current) observer.observe(tooltipRef.current) + return () => { + if (tooltipRef.current) observer.unobserve(tooltipRef.current) + } + }) + + const getTooltipStyle = () => { + if (!tooltipSize) return {} + + const tileWidth = mapRect.width / map.width + const tileHeight = mapRect.height / map.height + const mapLeft = mapRect.left - wrapperRect.left + const mapTop = mapRect.top - wrapperRect.top + + let tooltipStyle: React.CSSProperties = {} + + const distanceFromBotCenterX = 0.75 * tileWidth + const distanceFromBotCenterY = 0.75 * tileHeight + const clearanceLeft = mapLeft + position.x * tileWidth - distanceFromBotCenterX + const clearanceRight = wrapperRect.width - clearanceLeft - 2 * distanceFromBotCenterX + const clearanceTop = mapTop + position.y * tileHeight - distanceFromBotCenterY + + if (clearanceTop > tooltipSize.y) { + tooltipStyle.top = mapTop + position.y * tileHeight - tooltipSize.y - distanceFromBotCenterY + 'px' + } else { + tooltipStyle.top = mapTop + position.y * tileHeight + distanceFromBotCenterY + 'px' + } + if (clearanceLeft < tooltipSize.x / 2) { + tooltipStyle.left = mapLeft + position.x * tileWidth + distanceFromBotCenterX + 'px' + } else if (clearanceRight < tooltipSize.x / 2) { + tooltipStyle.left = mapLeft + position.x * tileWidth - tooltipSize.x - distanceFromBotCenterX + 'px' + } else { + tooltipStyle.left = mapLeft + position.x * tileWidth - tooltipSize.x / 2 + 'px' + } + + return tooltipStyle + } + + return ( +
+ {children} +
+ ) +} +interface DraggableTooltipProps { + areaWidth: number + areaHeight: number + selectedBody?: Body +} +const DraggableTooltip = ({ areaWidth, areaHeight, selectedBody }: DraggableTooltipProps) => { + return ( + + {selectedBody && ( +
+ {selectedBody.onHoverInfo().map((v, i) => ( +

{v}

+ ))} +
+ +
+
+ )} +
+ ) +} + +interface DraggableProps { + children: React.ReactNode + width: number + height: number + margin?: number +} + +const Draggable = ({ children, width, height, margin = 0 }: DraggableProps) => { + const [dragging, setDragging] = React.useState(false) + const [pos, setPos] = React.useState({ x: 20, y: 20 }) + const [offset, setOffset] = React.useState({ x: 0, y: 0 }) + const ref = React.useRef(null) + + const mouseDown = (e: React.MouseEvent) => { + setDragging(true) + setOffset({ x: e.clientX - pos.x, y: e.clientY - pos.y }) + } + + const mouseUp = () => { + setDragging(false) + } + + const mouseMove = (e: React.MouseEvent) => { + if (dragging && ref.current) { + const targetX = e.clientX - offset.x + const targetY = e.clientY - offset.y + const realX = Math.min(Math.max(targetX, margin), width - ref.current.clientWidth - margin) + const realY = Math.min(Math.max(targetY, margin), height - ref.current.clientHeight - margin) + setPos({ x: realX, y: realY }) + } + } + + return ( +
{ + if (e.buttons === 1) mouseDown(e) + }} + onMouseMove={mouseMove} + className="absolute z-20" + style={{ + left: pos.x + 'px', + top: pos.y + 'px' + }} + > + {children} +
+ ) +} + +interface HighlightedSquareProps { + overlayCanvasRef: HTMLCanvasElement + wrapperRef: HTMLDivElement + map: CurrentMap + hoveredTile: Vector +} +const HighlightedSquare: React.FC = ({ overlayCanvasRef, wrapperRef, map, hoveredTile }) => { + const overlayCanvasRect = overlayCanvasRef.getBoundingClientRect() + const wrapperRect = wrapperRef.getBoundingClientRect() + const mapLeft = overlayCanvasRect.left - wrapperRect.left + const mapTop = overlayCanvasRect.top - wrapperRect.top + const tileWidth = overlayCanvasRect.width / map.width + const tileHeight = overlayCanvasRect.height / map.height + const tileLeft = mapLeft + tileWidth * hoveredTile.x + const tileTop = mapTop + tileHeight * (map.height - hoveredTile.y - 1) + return ( +
+ ) +} diff --git a/client/src/components/game/tooltip.tsx b/client/src/components/game/tooltip.tsx deleted file mode 100644 index 726fc6c7..00000000 --- a/client/src/components/game/tooltip.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import React, { MutableRefObject, useEffect } from 'react' -import { useAppContext } from '../../app-context' -import { useListenEvent, EventType } from '../../app-events' -import { useForceUpdate } from '../../util/react-util' -import { ThreeBarsIcon } from '../../icons/three-bars' -import { getRenderCoords } from '../../util/RenderUtil' -import { Vector } from '../../playback/Vector' - -type TooltipProps = { - overlayCanvas: HTMLCanvasElement | null - selectedBodyID: number | undefined - hoveredBodyID: number | undefined - hoveredSquare: Vector | undefined - selectedSquare: Vector | undefined - wrapperRef: HTMLDivElement | null -} - -export const Tooltip = ({ - overlayCanvas, - selectedBodyID, - hoveredBodyID, - hoveredSquare, - selectedSquare, - wrapperRef -}: TooltipProps) => { - const appContext = useAppContext() - const forceUpdate = useForceUpdate() - useListenEvent(EventType.TURN_PROGRESS, forceUpdate) - useListenEvent(EventType.INITIAL_RENDER, forceUpdate) - - const selectedBody = - selectedBodyID !== undefined - ? appContext.state.activeMatch?.currentTurn.bodies.bodies.get(selectedBodyID) - : undefined - const hoveredBody = - hoveredBodyID !== undefined - ? appContext.state.activeMatch?.currentTurn.bodies.bodies.get(hoveredBodyID) - : undefined - - const tooltipRef = React.useRef(null) - const [tooltipSize, setTooltipSize] = React.useState({ width: 0, height: 0 }) - useEffect(() => { - const observer = new ResizeObserver((entries) => { - if (entries[0]) { - const borderBox = entries[0].borderBoxSize[0] - setTooltipSize({ width: borderBox.inlineSize, height: borderBox.blockSize }) - } - }) - if (tooltipRef.current) observer.observe(tooltipRef.current) - return () => { - if (tooltipRef.current) observer.unobserve(tooltipRef.current) - } - }, [hoveredBody, hoveredSquare]) - - const map = appContext.state.activeMatch?.currentTurn.map - if (!overlayCanvas || !wrapperRef || !map) return <> - - const wrapperRect = wrapperRef.getBoundingClientRect() - - const getTooltipStyle = () => { - const overlayCanvasRect = overlayCanvas.getBoundingClientRect() - const tileWidth = overlayCanvasRect.width / map.width - const tileHeight = overlayCanvasRect.height / map.height - const mapLeft = overlayCanvasRect.left - wrapperRect.left - const mapTop = overlayCanvasRect.top - wrapperRect.top - - let tooltipStyle: React.CSSProperties = {} - - if (!hoveredBody && !hoveredSquare) return tooltipStyle - - let tipPos: Vector - if (hoveredBody) { - tipPos = getRenderCoords(hoveredBody.pos.x, hoveredBody.pos.y, map.dimension, true) - } else { - tipPos = getRenderCoords(hoveredSquare!.x, hoveredSquare!.y, map.dimension, true) - } - const distanceFromBotCenterX = 0.75 * tileWidth - const distanceFromBotCenterY = 0.75 * tileHeight - const clearanceLeft = mapLeft + tipPos.x * tileWidth - distanceFromBotCenterX - const clearanceRight = wrapperRect.width - clearanceLeft - 2 * distanceFromBotCenterX - const clearanceTop = mapTop + tipPos.y * tileHeight - distanceFromBotCenterY - - if (clearanceTop > tooltipSize.height) { - tooltipStyle.top = mapTop + tipPos.y * tileHeight - tooltipSize.height - distanceFromBotCenterY + 'px' - } else { - tooltipStyle.top = mapTop + tipPos.y * tileHeight + distanceFromBotCenterY + 'px' - } - if (clearanceLeft < tooltipSize.width / 2) { - tooltipStyle.left = mapLeft + tipPos.x * tileWidth + distanceFromBotCenterX + 'px' - } else if (clearanceRight < tooltipSize.width / 2) { - tooltipStyle.left = mapLeft + tipPos.x * tileWidth - tooltipSize.width - distanceFromBotCenterX + 'px' - } else { - tooltipStyle.left = mapLeft + tipPos.x * tileWidth - tooltipSize.width / 2 + 'px' - } - - return tooltipStyle - } - - let showFloatingTooltip = !!((hoveredBody && hoveredBody != selectedBody) || hoveredSquare) - const tooltipContent = hoveredBody - ? hoveredBody.onHoverInfo() - : hoveredSquare - ? map.getTooltipInfo(hoveredSquare, appContext.state.activeMatch!) - : [] - - if (tooltipContent.length === 0) showFloatingTooltip = false - - // Check for the default empty size and don't show before the resize observer - // has updated - if (tooltipSize.width == 16 || tooltipSize.height == 16) showFloatingTooltip = false - - return ( -
-
- {tooltipContent.map((v, i) => ( -

{v}

- ))} -
- - - {selectedBody && ( -
- {selectedBody.onHoverInfo().map((v, i) => ( -

{v}

- ))} -
- -
-
- )} -
- - {appContext.state.config.showMapXY && hoveredSquare && ( -
- {`(X: ${hoveredSquare.x}, Y: ${hoveredSquare.y})`} -
- )} -
- ) -} - -interface DraggableProps { - children: React.ReactNode - width: number - height: number - margin?: number -} - -const Draggable = ({ children, width, height, margin = 0 }: DraggableProps) => { - const [dragging, setDragging] = React.useState(false) - const [pos, setPos] = React.useState({ x: 20, y: 20 }) - const [offset, setOffset] = React.useState({ x: 0, y: 0 }) - const ref = React.useRef(null) - - const mouseDown = (e: React.MouseEvent) => { - setDragging(true) - setOffset({ x: e.clientX - pos.x, y: e.clientY - pos.y }) - } - - const mouseUp = () => { - setDragging(false) - } - - const mouseMove = (e: React.MouseEvent) => { - if (dragging && ref.current) { - const targetX = e.clientX - offset.x - const targetY = e.clientY - offset.y - const realX = Math.min(Math.max(targetX, margin), width - ref.current.clientWidth - margin) - const realY = Math.min(Math.max(targetY, margin), height - ref.current.clientHeight - margin) - setPos({ x: realX, y: realY }) - } - } - - return ( -
{ - if (e.buttons === 1) mouseDown(e) - }} - onMouseMove={mouseMove} - className="absolute z-20" - style={{ - left: pos.x + 'px', - top: pos.y + 'px' - }} - > - {children} -
- ) -} diff --git a/client/src/components/sidebar/game/game.tsx b/client/src/components/sidebar/game/game.tsx index 35517a9f..11d6cd3d 100644 --- a/client/src/components/sidebar/game/game.tsx +++ b/client/src/components/sidebar/game/game.tsx @@ -26,7 +26,7 @@ export const GamePage: React.FC = React.memo((props) => { const [showStats, setShowStats] = useSearchParamBool('showStats', true) const forceUpdate = useForceUpdate() - useListenEvent(EventType.TURN_PROGRESS, forceUpdate) + useListenEvent(EventType.NEW_TURN, forceUpdate) if (!props.open) return null diff --git a/client/src/components/sidebar/game/histogram.tsx b/client/src/components/sidebar/game/histogram.tsx index ea192586..d5b4402d 100644 --- a/client/src/components/sidebar/game/histogram.tsx +++ b/client/src/components/sidebar/game/histogram.tsx @@ -33,7 +33,7 @@ interface SpecialtyHistogramProps { export const SpecialtyHistogram: React.FC = (props) => { const appContext = useAppContext() const forceUpdate = useForceUpdate() - useListenEvent(EventType.TURN_PROGRESS, () => { + useListenEvent(EventType.NEW_TURN, () => { if (props.active) forceUpdate() }) diff --git a/client/src/components/sidebar/game/resource-graph.tsx b/client/src/components/sidebar/game/resource-graph.tsx index d0e777c2..11d02562 100644 --- a/client/src/components/sidebar/game/resource-graph.tsx +++ b/client/src/components/sidebar/game/resource-graph.tsx @@ -41,7 +41,7 @@ export const ResourceGraph: React.FC = (props: Props) => { const appContext = useAppContext() const forceUpdate = useForceUpdate() - useListenEvent(EventType.TURN_PROGRESS, () => { + useListenEvent(EventType.NEW_TURN, () => { if (props.active) forceUpdate() }) diff --git a/client/src/components/sidebar/game/team-table.tsx b/client/src/components/sidebar/game/team-table.tsx index 29f59475..87c1fb4a 100644 --- a/client/src/components/sidebar/game/team-table.tsx +++ b/client/src/components/sidebar/game/team-table.tsx @@ -32,7 +32,7 @@ interface TeamTableProps { export const TeamTable: React.FC = (props: TeamTableProps) => { const context = useAppContext() const forceUpdate = useForceUpdate() - useListenEvent(EventType.TURN_PROGRESS, forceUpdate) + useListenEvent(EventType.NEW_TURN, forceUpdate) const match = context.state.activeMatch const teamStat = match?.currentTurn?.stat.getTeamStat(match.game.teams[props.teamIdx]) diff --git a/client/src/components/sidebar/map-editor/map-editor.tsx b/client/src/components/sidebar/map-editor/map-editor.tsx index 2f6d9d70..c5874b7e 100644 --- a/client/src/components/sidebar/map-editor/map-editor.tsx +++ b/client/src/components/sidebar/map-editor/map-editor.tsx @@ -51,7 +51,7 @@ export const MapEditorPage: React.FC = (props) => { if (!openBrush) return openBrush.apply(point.x, point.y, openBrush.fields) - publishEvent(EventType.INITIAL_RENDER, {}) + publishEvent(EventType.MAP_RENDER) setCleared(mapEmpty()) } diff --git a/client/src/components/sidebar/runner/websocket.ts b/client/src/components/sidebar/runner/websocket.ts index 461dae9c..aed19b32 100644 --- a/client/src/components/sidebar/runner/websocket.ts +++ b/client/src/components/sidebar/runner/websocket.ts @@ -67,7 +67,7 @@ export default class WebSocketListener { this.lastSetTurn = match.currentTurn.turnNumber } else { // Publish anyways so the control bar updates - publishEvent(EventType.TURN_PROGRESS, {}) + publishEvent(EventType.NEW_TURN, {}) } } @@ -110,7 +110,7 @@ export default class WebSocketListener { break } case schema.Event.GameFooter: { - publishEvent(EventType.TURN_PROGRESS, {}) + publishEvent(EventType.NEW_TURN, {}) this.onGameComplete(this.activeGame!) this.reset() diff --git a/client/src/playback/Match.ts b/client/src/playback/Match.ts index d1c3f8b1..bf9b2b18 100644 --- a/client/src/playback/Match.ts +++ b/client/src/playback/Match.ts @@ -213,7 +213,7 @@ export default class Match { } this.currentTurn = updatingTurn - publishEvent(EventType.TURN_PROGRESS, {}) + publishEvent(EventType.NEW_TURN, {}) if (rerender) this.rerender() } } From f0ed7a31e6437c50f02417b7fac4e1a5f9ac9a16 Mon Sep 17 00:00:00 2001 From: Aidan Blum Levine Date: Wed, 10 Apr 2024 16:47:45 -0400 Subject: [PATCH 3/5] Move files around --- client/src/app-context.tsx | 10 +++------- .../components/controls-bar/controls-bar.tsx | 2 +- client/src/components/game/GameController.ts | 2 +- client/src/components/game/game-renderer.tsx | 8 ++++---- client/src/components/game/overlay.tsx | 10 +++++----- .../game/tournament-renderer}/Tournament.ts | 0 .../tournament-renderer/tournament-game.tsx | 4 ++-- .../tournament-renderer.tsx | 2 +- client/src/components/sidebar/help/help.tsx | 2 +- .../sidebar/map-editor/MapEditorBrush.ts | 2 +- .../sidebar/map-editor/map-editor-field.tsx | 2 +- .../sidebar/map-editor/map-editor.tsx | 12 ++++++------ .../components/sidebar/queue/queue-game.tsx | 4 ++-- client/src/components/sidebar/queue/queue.tsx | 4 ++-- .../src/components/sidebar/runner/scaffold.ts | 6 +++--- .../src/components/sidebar/runner/websocket.ts | 4 ++-- client/src/components/sidebar/sidebar.tsx | 10 +++++----- .../{tournament.tsx => tournament-page.tsx} | 2 +- .../src/components/sidebar/update-warning.tsx | 2 +- .../src/{playback => current-game}/Actions.ts | 6 +++--- .../src/{playback => current-game}/Bodies.ts | 8 ++++---- .../src/{playback => current-game}/Brushes.ts | 0 .../Constants.ts} | 0 client/src/{playback => current-game}/Game.ts | 2 +- client/src/{playback => current-game}/Map.ts | 10 +++++----- .../MapGenerator.ts | 14 +++++++------- client/src/{playback => current-game}/Match.ts | 0 client/src/{playback => current-game}/Turn.ts | 0 .../src/{playback => current-game}/TurnStat.ts | 0 .../sidebar-game-tab}/game.tsx | 18 +++++++++--------- .../sidebar-game-tab/graphs}/d3-histogram.tsx | 2 +- .../sidebar-game-tab/graphs}/d3-line-chart.tsx | 2 +- .../graphs}/quick-histogram.tsx | 0 .../graphs}/quick-line-chart.tsx | 2 +- .../sidebar-game-tab}/histogram.tsx | 10 +++++----- .../sidebar-game-tab}/resource-graph.tsx | 8 ++++---- .../sidebar-game-tab}/team-table.tsx | 16 ++++++++-------- client/src/pages/main-page.tsx | 2 +- .../util/{ImageLoader.ts => image-loader.ts} | 0 .../src/util/{RenderUtil.ts => render-util.ts} | 10 +++++----- .../SchemaHelpers.ts => util/schema-util.ts} | 2 +- .../src/{playback/Vector.ts => util/vector.ts} | 0 42 files changed, 98 insertions(+), 102 deletions(-) rename client/src/{playback => components/game/tournament-renderer}/Tournament.ts (100%) rename client/src/components/sidebar/tournament/{tournament.tsx => tournament-page.tsx} (98%) rename client/src/{playback => current-game}/Actions.ts (98%) rename client/src/{playback => current-game}/Bodies.ts (99%) rename client/src/{playback => current-game}/Brushes.ts (100%) rename client/src/{constants.ts => current-game/Constants.ts} (100%) rename client/src/{playback => current-game}/Game.ts (99%) rename client/src/{playback => current-game}/Map.ts (98%) rename client/src/{components/sidebar/map-editor => current-game}/MapGenerator.ts (94%) rename client/src/{playback => current-game}/Match.ts (100%) rename client/src/{playback => current-game}/Turn.ts (100%) rename client/src/{playback => current-game}/TurnStat.ts (100%) rename client/src/{components/sidebar/game => current-game/sidebar-game-tab}/game.tsx (91%) rename client/src/{components/sidebar/game => current-game/sidebar-game-tab/graphs}/d3-histogram.tsx (97%) rename client/src/{components/sidebar/game => current-game/sidebar-game-tab/graphs}/d3-line-chart.tsx (98%) rename client/src/{components/sidebar/game => current-game/sidebar-game-tab/graphs}/quick-histogram.tsx (100%) rename client/src/{components/sidebar/game => current-game/sidebar-game-tab/graphs}/quick-line-chart.tsx (97%) rename client/src/{components/sidebar/game => current-game/sidebar-game-tab}/histogram.tsx (89%) rename client/src/{components/sidebar/game => current-game/sidebar-game-tab}/resource-graph.tsx (87%) rename client/src/{components/sidebar/game => current-game/sidebar-game-tab}/team-table.tsx (93%) rename client/src/util/{ImageLoader.ts => image-loader.ts} (100%) rename client/src/util/{RenderUtil.ts => render-util.ts} (97%) rename client/src/{playback/SchemaHelpers.ts => util/schema-util.ts} (95%) rename client/src/{playback/Vector.ts => util/vector.ts} (100%) diff --git a/client/src/app-context.tsx b/client/src/app-context.tsx index eebe5817..62071b4b 100644 --- a/client/src/app-context.tsx +++ b/client/src/app-context.tsx @@ -1,8 +1,8 @@ import React from 'react' -import Game from './playback/Game' -import Match from './playback/Match' -import Tournament, { DEFAULT_TOURNAMENT_STATE, TournamentState } from './playback/Tournament' +import Game from './current-game/Game' +import Match from './current-game/Match' import { ClientConfig, getDefaultConfig } from './client-config' +import Tournament, { DEFAULT_TOURNAMENT_STATE, TournamentState } from './components/game/tournament-renderer/Tournament' export interface AppState { queue: Game[] @@ -11,8 +11,6 @@ export interface AppState { tournament: Tournament | undefined tournamentState: TournamentState loadingRemoteContent: string - // updatesPerSecond: number - // paused: boolean disableHotkeys: boolean config: ClientConfig } @@ -24,8 +22,6 @@ const DEFAULT_APP_STATE: AppState = { tournament: undefined, tournamentState: DEFAULT_TOURNAMENT_STATE, loadingRemoteContent: '', - // updatesPerSecond: 1, - // paused: true, disableHotkeys: false, config: getDefaultConfig() } diff --git a/client/src/components/controls-bar/controls-bar.tsx b/client/src/components/controls-bar/controls-bar.tsx index 730c4fc2..b52e59c6 100644 --- a/client/src/components/controls-bar/controls-bar.tsx +++ b/client/src/components/controls-bar/controls-bar.tsx @@ -7,7 +7,7 @@ import { ControlsBarTimeline } from './controls-bar-timeline' import { EventType, useListenEvent } from '../../app-events' import { useForceUpdate } from '../../util/react-util' import Tooltip from '../tooltip' -import Match from '../../playback/Match' +import Match from '../../current-game/Match' type ControlsBarProps = { match: Match | undefined diff --git a/client/src/components/game/GameController.ts b/client/src/components/game/GameController.ts index ef0cfe5e..65f00797 100644 --- a/client/src/components/game/GameController.ts +++ b/client/src/components/game/GameController.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react' -import Match from '../../playback/Match' +import Match from '../../current-game/Match' const SIMULATION_UPDATE_INTERVAL_MS = 17 // About 60 fps diff --git a/client/src/components/game/game-renderer.tsx b/client/src/components/game/game-renderer.tsx index 97793993..64e40497 100644 --- a/client/src/components/game/game-renderer.tsx +++ b/client/src/components/game/game-renderer.tsx @@ -1,11 +1,11 @@ import React, { MutableRefObject, useEffect, useRef, useState } from 'react' import { useAppContext } from '../../app-context' -import { Vector } from '../../playback/Vector' +import { Vector } from '../../util/vector' import { EventType, publishEvent, useListenEvent } from '../../app-events' import { Overlay } from './overlay' -import { TILE_RESOLUTION } from '../../constants' -import { CurrentMap } from '../../playback/Map' -import Match from '../../playback/Match' +import { TILE_RESOLUTION } from '../../current-game/Constants' +import { CurrentMap } from '../../current-game/Map' +import Match from '../../current-game/Match' import { ClientConfig } from '../../client-config' export const GameRenderer: React.FC = () => { diff --git a/client/src/components/game/overlay.tsx b/client/src/components/game/overlay.tsx index d35413c0..03a4df0f 100644 --- a/client/src/components/game/overlay.tsx +++ b/client/src/components/game/overlay.tsx @@ -3,11 +3,11 @@ import { useAppContext } from '../../app-context' import { useListenEvent, EventType } from '../../app-events' import { useForceUpdate } from '../../util/react-util' import { ThreeBarsIcon } from '../../icons/three-bars' -import { getRenderCoords } from '../../util/RenderUtil' -import { Vector } from '../../playback/Vector' -import Match from '../../playback/Match' -import { CurrentMap } from '../../playback/Map' -import { Body } from '../../playback/Bodies' +import { getRenderCoords } from '../../util/render-util' +import { Vector } from '../../util/vector' +import Match from '../../current-game/Match' +import { CurrentMap } from '../../current-game/Map' +import { Body } from '../../current-game/Bodies' import { Vec } from 'battlecode-schema/js/battlecode/schema' type OverlayProps = { diff --git a/client/src/playback/Tournament.ts b/client/src/components/game/tournament-renderer/Tournament.ts similarity index 100% rename from client/src/playback/Tournament.ts rename to client/src/components/game/tournament-renderer/Tournament.ts diff --git a/client/src/components/game/tournament-renderer/tournament-game.tsx b/client/src/components/game/tournament-renderer/tournament-game.tsx index e978eb49..635d2f3e 100644 --- a/client/src/components/game/tournament-renderer/tournament-game.tsx +++ b/client/src/components/game/tournament-renderer/tournament-game.tsx @@ -1,11 +1,11 @@ import React from 'react' import { useAppContext } from '../../../app-context' -import { TournamentGame } from '../../../playback/Tournament' -import Game from '../../../playback/Game' +import Game from '../../../current-game/Game' import { PageType, usePage } from '../../../app-search-params' import { Pressable } from 'react-zoomable-ui' import { Crown } from '../../../icons/crown' import Tooltip from '../../tooltip' +import { TournamentGame } from './Tournament'; interface Props { game: TournamentGame diff --git a/client/src/components/game/tournament-renderer/tournament-renderer.tsx b/client/src/components/game/tournament-renderer/tournament-renderer.tsx index c29e647b..0a6de02d 100644 --- a/client/src/components/game/tournament-renderer/tournament-renderer.tsx +++ b/client/src/components/game/tournament-renderer/tournament-renderer.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useRef, useState } from 'react' import { useAppContext } from '../../../app-context' import { TournamentGameElement } from './tournament-game' -import Tournament, { TournamentGame, TournamentState } from '../../../playback/Tournament' import { Space } from 'react-zoomable-ui' import { useSearchParamNumber } from '../../../app-search-params' +import Tournament, { TournamentState, TournamentGame } from './Tournament'; export const TournamentRenderer: React.FC = () => { const appContext = useAppContext() diff --git a/client/src/components/sidebar/help/help.tsx b/client/src/components/sidebar/help/help.tsx index 1dc83b53..aa51a083 100644 --- a/client/src/components/sidebar/help/help.tsx +++ b/client/src/components/sidebar/help/help.tsx @@ -1,6 +1,6 @@ import React from 'react' import { SectionHeader } from '../../section-header' -import { BATTLECODE_YEAR } from '../../../constants' +import { BATTLECODE_YEAR } from '../../../current-game/Constants' enum TabType { NONE = '', diff --git a/client/src/components/sidebar/map-editor/MapEditorBrush.ts b/client/src/components/sidebar/map-editor/MapEditorBrush.ts index 5f8c05fb..d7962590 100644 --- a/client/src/components/sidebar/map-editor/MapEditorBrush.ts +++ b/client/src/components/sidebar/map-editor/MapEditorBrush.ts @@ -1,4 +1,4 @@ -import { StaticMap, CurrentMap } from '../../../playback/Map' +import { StaticMap, CurrentMap } from '../../../current-game/Map' export abstract class MapEditorBrush { abstract name: string diff --git a/client/src/components/sidebar/map-editor/map-editor-field.tsx b/client/src/components/sidebar/map-editor/map-editor-field.tsx index 953b5cc9..e0aa4342 100644 --- a/client/src/components/sidebar/map-editor/map-editor-field.tsx +++ b/client/src/components/sidebar/map-editor/map-editor-field.tsx @@ -1,6 +1,6 @@ import React from 'react' import { MapEditorBrushField, MapEditorBrushFieldType } from './MapEditorBrush' -import { TEAM_COLORS, TEAM_COLOR_NAMES } from '../../../constants' +import { TEAM_COLORS, TEAM_COLOR_NAMES } from '../../../current-game/Constants' import { Toggle } from '../../toggle' import { Select, NumInput } from '../../forms' diff --git a/client/src/components/sidebar/map-editor/map-editor.tsx b/client/src/components/sidebar/map-editor/map-editor.tsx index c5874b7e..158b01f0 100644 --- a/client/src/components/sidebar/map-editor/map-editor.tsx +++ b/client/src/components/sidebar/map-editor/map-editor.tsx @@ -1,16 +1,16 @@ import React, { useEffect } from 'react' -import { CurrentMap, StaticMap } from '../../../playback/Map' +import { CurrentMap, StaticMap } from '../../../current-game/Map' import { MapEditorBrushRow } from './map-editor-brushes' -import Bodies from '../../../playback/Bodies' -import Game from '../../../playback/Game' +import Bodies from '../../../current-game/Bodies' +import Game from '../../../current-game/Game' import { Button, BrightButton, SmallButton } from '../../button' import { NumInput, Select } from '../../forms' import { useAppContext } from '../../../app-context' -import Match from '../../../playback/Match' +import Match from '../../../current-game/Match' import { EventType, publishEvent, useListenEvent } from '../../../app-events' import { MapEditorBrush } from './MapEditorBrush' -import { exportMap, loadFileAsMap } from './MapGenerator' -import { MAP_SIZE_RANGE } from '../../../constants' +import { exportMap, loadFileAsMap } from '../../../current-game/MapGenerator' +import { MAP_SIZE_RANGE } from '../../../current-game/Constants' import { InputDialog } from '../../input-dialog' import { ConfirmDialog } from '../../confirm-dialog' diff --git a/client/src/components/sidebar/queue/queue-game.tsx b/client/src/components/sidebar/queue/queue-game.tsx index c0710b85..e99876d8 100644 --- a/client/src/components/sidebar/queue/queue-game.tsx +++ b/client/src/components/sidebar/queue/queue-game.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react' -import Game from '../../../playback/Game' -import Match from '../../../playback/Match' +import Game from '../../../current-game/Game' +import Match from '../../../current-game/Match' import { useAppContext } from '../../../app-context' import { IconContext } from 'react-icons' import { IoCloseCircle, IoCloseCircleOutline } from 'react-icons/io5' diff --git a/client/src/components/sidebar/queue/queue.tsx b/client/src/components/sidebar/queue/queue.tsx index bc172134..243b588a 100644 --- a/client/src/components/sidebar/queue/queue.tsx +++ b/client/src/components/sidebar/queue/queue.tsx @@ -1,10 +1,10 @@ import React from 'react' import { useAppContext } from '../../../app-context' import { useKeyboard } from '../../../util/keyboard' -import { BATTLECODE_YEAR } from '../../../constants' +import { BATTLECODE_YEAR } from '../../../current-game/Constants' import { Button } from '../../button' import { FiUpload } from 'react-icons/fi' -import Game from '../../../playback/Game' +import Game from '../../../current-game/Game' import { QueuedGame } from './queue-game' interface Props { diff --git a/client/src/components/sidebar/runner/scaffold.ts b/client/src/components/sidebar/runner/scaffold.ts index df99530b..43490ea4 100644 --- a/client/src/components/sidebar/runner/scaffold.ts +++ b/client/src/components/sidebar/runner/scaffold.ts @@ -1,12 +1,12 @@ import { useEffect, useRef, useState } from 'react' -import { BATTLECODE_YEAR, ENGINE_BUILTIN_MAP_NAMES } from '../../../constants' +import { BATTLECODE_YEAR, ENGINE_BUILTIN_MAP_NAMES } from '../../../current-game/Constants' import { NativeAPI, nativeAPI } from './native-api-wrapper' import { ConsoleLine } from './runner' import { useForceUpdate } from '../../../util/react-util' import WebSocketListener from './websocket' import { useAppContext } from '../../../app-context' -import Game from '../../../playback/Game' -import Match from '../../../playback/Match' +import Game from '../../../current-game/Game' +import Match from '../../../current-game/Match' import { RingBuffer } from '../../../util/ring-buffer' export type JavaInstall = { diff --git a/client/src/components/sidebar/runner/websocket.ts b/client/src/components/sidebar/runner/websocket.ts index aed19b32..78fb4c22 100644 --- a/client/src/components/sidebar/runner/websocket.ts +++ b/client/src/components/sidebar/runner/websocket.ts @@ -1,6 +1,6 @@ import { schema, flatbuffers } from 'battlecode-schema' -import Game from '../../../playback/Game' -import Match from '../../../playback/Match' +import Game from '../../../current-game/Game' +import Match from '../../../current-game/Match' import assert from 'assert' import { EventType, publishEvent } from '../../../app-events' diff --git a/client/src/components/sidebar/sidebar.tsx b/client/src/components/sidebar/sidebar.tsx index 33c07c69..5fcb2ca6 100644 --- a/client/src/components/sidebar/sidebar.tsx +++ b/client/src/components/sidebar/sidebar.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { BATTLECODE_YEAR, GAME_VERSION } from '../../constants' +import { BATTLECODE_YEAR, GAME_VERSION } from '../../current-game/Constants' import { ThreeBarsIcon } from '../../icons/three-bars' -import { GamePage } from './game/game' +import { GamePage } from '../../current-game/sidebar-game-tab/game' import { QueuePage } from './queue/queue' import { BsChevronLeft } from 'react-icons/bs' import { HelpPage } from './help/help' @@ -11,13 +11,13 @@ import { usePage, PageType, useSearchParamBool, useSearchParamString } from '../ import { useKeyboard } from '../../util/keyboard' import { Scrollbars } from 'react-custom-scrollbars-2' import useWindowDimensions from '../../util/window-size' -import { TournamentPage } from './tournament/tournament' -import Tournament, { JsonTournamentGame } from '../../playback/Tournament' import { useAppContext } from '../../app-context' import { useScaffold } from './runner/scaffold' import { ConfigPage } from '../../client-config' import { UpdateWarning } from './update-warning' -import Game from '../../playback/Game' +import Game from '../../current-game/Game' +import Tournament, { JsonTournamentGame } from '../game/tournament-renderer/Tournament' +import { TournamentPage } from './tournament/tournament-page'; export const Sidebar: React.FC = () => { const { width, height } = useWindowDimensions() diff --git a/client/src/components/sidebar/tournament/tournament.tsx b/client/src/components/sidebar/tournament/tournament-page.tsx similarity index 98% rename from client/src/components/sidebar/tournament/tournament.tsx rename to client/src/components/sidebar/tournament/tournament-page.tsx index e23006ce..0cb43549 100644 --- a/client/src/components/sidebar/tournament/tournament.tsx +++ b/client/src/components/sidebar/tournament/tournament-page.tsx @@ -2,7 +2,7 @@ import React from 'react' import { useAppContext } from '../../../app-context' import { Button } from '../../button' import { FiEye, FiEyeOff, FiUpload } from 'react-icons/fi' -import Tournament, { JsonTournamentGame } from '../../../playback/Tournament' +import Tournament, { JsonTournamentGame } from '../../game/tournament-renderer/Tournament' import { NumInput } from '../../forms' import { BsLock, BsUnlock } from 'react-icons/bs' import { useSearchParamBool, useSearchParamNumber } from '../../../app-search-params' diff --git a/client/src/components/sidebar/update-warning.tsx b/client/src/components/sidebar/update-warning.tsx index caac34f7..3c4c7f6f 100644 --- a/client/src/components/sidebar/update-warning.tsx +++ b/client/src/components/sidebar/update-warning.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { BATTLECODE_YEAR, GAME_VERSION } from '../../constants' +import { BATTLECODE_YEAR, GAME_VERSION } from '../../current-game/Constants' import { nativeAPI } from './runner/native-api-wrapper' const UPDATE_CHECK_MINUTES = 5 diff --git a/client/src/playback/Actions.ts b/client/src/current-game/Actions.ts similarity index 98% rename from client/src/playback/Actions.ts rename to client/src/current-game/Actions.ts index ad7d615a..b150423c 100644 --- a/client/src/playback/Actions.ts +++ b/client/src/current-game/Actions.ts @@ -1,11 +1,11 @@ import Turn from './Turn' import { schema } from 'battlecode-schema' import assert from 'assert' -import * as renderUtils from '../util/RenderUtil' -import { vectorAdd, vectorLength, vectorMultiply, vectorSub, vectorMultiplyInPlace, Vector } from './Vector' +import * as renderUtils from '../util/render-util' +import { vectorAdd, vectorLength, vectorMultiply, vectorSub, vectorMultiplyInPlace, Vector } from '../util/vector' import Match from './Match' import { Body } from './Bodies' -import { ATTACK_COLOR, GRASS_COLOR, HEAL_COLOR, TEAM_COLORS, WATER_COLOR } from '../constants' +import { ATTACK_COLOR, GRASS_COLOR, HEAL_COLOR, TEAM_COLORS, WATER_COLOR } from './Constants' export default class Actions { actions: Action[] = [] diff --git a/client/src/playback/Bodies.ts b/client/src/current-game/Bodies.ts similarity index 99% rename from client/src/playback/Bodies.ts rename to client/src/current-game/Bodies.ts index f0c09da9..7304316c 100644 --- a/client/src/playback/Bodies.ts +++ b/client/src/current-game/Bodies.ts @@ -3,11 +3,11 @@ import assert from 'assert' import Game, { Team } from './Game' import Turn from './Turn' import TurnStat from './TurnStat' -import { getImageIfLoaded } from '../util/ImageLoader' -import * as renderUtils from '../util/RenderUtil' +import { getImageIfLoaded } from '../util/image-loader' +import * as renderUtils from '../util/render-util' import { MapEditorBrush } from '../components/sidebar/map-editor/MapEditorBrush' import { StaticMap } from './Map' -import { Vector } from './Vector' +import { Vector } from '../util/vector' import { ATTACK_COLOR, BUILD_COLOR, @@ -18,7 +18,7 @@ import { TOOLTIP_PATH_DECAY_R, TOOLTIP_PATH_INIT_R, TOOLTIP_PATH_LENGTH -} from '../constants' +} from './Constants' import Match from './Match' import { ClientConfig } from '../client-config' diff --git a/client/src/playback/Brushes.ts b/client/src/current-game/Brushes.ts similarity index 100% rename from client/src/playback/Brushes.ts rename to client/src/current-game/Brushes.ts diff --git a/client/src/constants.ts b/client/src/current-game/Constants.ts similarity index 100% rename from client/src/constants.ts rename to client/src/current-game/Constants.ts diff --git a/client/src/playback/Game.ts b/client/src/current-game/Game.ts similarity index 99% rename from client/src/playback/Game.ts rename to client/src/current-game/Game.ts index 4823ecff..24646ef5 100644 --- a/client/src/playback/Game.ts +++ b/client/src/current-game/Game.ts @@ -2,7 +2,7 @@ import Match from './Match' import { flatbuffers, schema } from 'battlecode-schema' import { ungzip } from 'pako' import assert from 'assert' -import { SPEC_VERSION, TEAM_COLORS, TEAM_COLOR_NAMES } from '../constants' +import { SPEC_VERSION, TEAM_COLORS, TEAM_COLOR_NAMES } from './Constants' import { FakeGameWrapper } from '../components/sidebar/runner/websocket' let nextID = 0 diff --git a/client/src/playback/Map.ts b/client/src/current-game/Map.ts similarity index 98% rename from client/src/playback/Map.ts rename to client/src/current-game/Map.ts index e96e397c..b7188717 100644 --- a/client/src/playback/Map.ts +++ b/client/src/current-game/Map.ts @@ -1,9 +1,9 @@ import { flatbuffers, schema } from 'battlecode-schema' import assert from 'assert' -import { Vector } from './Vector' +import { Vector } from '../util/vector' import Match from './Match' import { MapEditorBrush, Symmetry } from '../components/sidebar/map-editor/MapEditorBrush' -import { packVecTable, parseVecTable } from './SchemaHelpers' +import { packVecTable, parseVecTable } from '../util/schema-util' import { DividerBrush, ResourcePileBrush, SpawnZoneBrush, WallsBrush, WaterBrush } from './Brushes' import { DIVIDER_COLOR, @@ -13,9 +13,9 @@ import { TEAM_COLORS, BUILD_NAMES, TEAM_COLOR_NAMES -} from '../constants' -import * as renderUtils from '../util/RenderUtil' -import { getImageIfLoaded } from '../util/ImageLoader' +} from './Constants' +import * as renderUtils from '../util/render-util' +import { getImageIfLoaded } from '../util/image-loader' import { ClientConfig } from '../client-config' export type Dimension = { diff --git a/client/src/components/sidebar/map-editor/MapGenerator.ts b/client/src/current-game/MapGenerator.ts similarity index 94% rename from client/src/components/sidebar/map-editor/MapGenerator.ts rename to client/src/current-game/MapGenerator.ts index 98d19387..e404cdbc 100644 --- a/client/src/components/sidebar/map-editor/MapGenerator.ts +++ b/client/src/current-game/MapGenerator.ts @@ -1,11 +1,11 @@ import { schema, flatbuffers } from 'battlecode-schema' -import Game from '../../../playback/Game' -import Match from '../../../playback/Match' -import { CurrentMap, StaticMap } from '../../../playback/Map' -import Turn from '../../../playback/Turn' -import Bodies from '../../../playback/Bodies' -import { BATTLECODE_YEAR, DIRECTIONS } from '../../../constants' -import { nativeAPI } from '../runner/native-api-wrapper' +import Game from './Game' +import Match from './Match' +import { CurrentMap, StaticMap } from './Map' +import Turn from './Turn' +import Bodies from './Bodies' +import { BATTLECODE_YEAR, DIRECTIONS } from './Constants' +import { nativeAPI } from '../components/sidebar/runner/native-api-wrapper' export function loadFileAsMap(file: File): Promise { return new Promise((resolve, reject) => { diff --git a/client/src/playback/Match.ts b/client/src/current-game/Match.ts similarity index 100% rename from client/src/playback/Match.ts rename to client/src/current-game/Match.ts diff --git a/client/src/playback/Turn.ts b/client/src/current-game/Turn.ts similarity index 100% rename from client/src/playback/Turn.ts rename to client/src/current-game/Turn.ts diff --git a/client/src/playback/TurnStat.ts b/client/src/current-game/TurnStat.ts similarity index 100% rename from client/src/playback/TurnStat.ts rename to client/src/current-game/TurnStat.ts diff --git a/client/src/components/sidebar/game/game.tsx b/client/src/current-game/sidebar-game-tab/game.tsx similarity index 91% rename from client/src/components/sidebar/game/game.tsx rename to client/src/current-game/sidebar-game-tab/game.tsx index 11d6cd3d..adf4ccc7 100644 --- a/client/src/components/sidebar/game/game.tsx +++ b/client/src/current-game/sidebar-game-tab/game.tsx @@ -2,16 +2,16 @@ import React from 'react' import { TeamTable } from './team-table' import { ResourceGraph } from './resource-graph' import { SpecialtyHistogram } from './histogram' -import { useSearchParamBool } from '../../../app-search-params' -import { useAppContext } from '../../../app-context' -import { SectionHeader } from '../../section-header' -import { Crown } from '../../../icons/crown' +import { useSearchParamBool } from '../../app-search-params' +import { useAppContext } from '../../app-context' +import { SectionHeader } from '../../components/section-header' +import { Crown } from '../../icons/crown' import { BiMedal } from 'react-icons/bi' -import { EventType, useListenEvent } from '../../../app-events' -import Tooltip from '../../tooltip' -import { useForceUpdate } from '../../../util/react-util' -import Match from '../../../playback/Match' -import { Team } from '../../../playback/Game' +import { EventType, useListenEvent } from '../../app-events' +import Tooltip from '../../components/tooltip' +import { useForceUpdate } from '../../util/react-util' +import Match from '../Match' +import { Team } from '../Game' const NO_GAME_TEAM_NAME = '?????' diff --git a/client/src/components/sidebar/game/d3-histogram.tsx b/client/src/current-game/sidebar-game-tab/graphs/d3-histogram.tsx similarity index 97% rename from client/src/components/sidebar/game/d3-histogram.tsx rename to client/src/current-game/sidebar-game-tab/graphs/d3-histogram.tsx index 73cadc21..4645bab1 100644 --- a/client/src/components/sidebar/game/d3-histogram.tsx +++ b/client/src/current-game/sidebar-game-tab/graphs/d3-histogram.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react' -import { TEAM_WHITE, TEAM_BROWN } from '../../../constants' +import { TEAM_WHITE, TEAM_BROWN } from '../../Constants' import * as d3 from 'd3' interface HistogramProps { diff --git a/client/src/components/sidebar/game/d3-line-chart.tsx b/client/src/current-game/sidebar-game-tab/graphs/d3-line-chart.tsx similarity index 98% rename from client/src/components/sidebar/game/d3-line-chart.tsx rename to client/src/current-game/sidebar-game-tab/graphs/d3-line-chart.tsx index 2f90f41f..93db7a8e 100644 --- a/client/src/components/sidebar/game/d3-line-chart.tsx +++ b/client/src/current-game/sidebar-game-tab/graphs/d3-line-chart.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react' -import { TEAM_WHITE, TEAM_BROWN } from '../../../constants' +import { TEAM_WHITE, TEAM_BROWN } from '../../Constants' import * as d3 from 'd3' export interface LineChartDataPoint { diff --git a/client/src/components/sidebar/game/quick-histogram.tsx b/client/src/current-game/sidebar-game-tab/graphs/quick-histogram.tsx similarity index 100% rename from client/src/components/sidebar/game/quick-histogram.tsx rename to client/src/current-game/sidebar-game-tab/graphs/quick-histogram.tsx diff --git a/client/src/components/sidebar/game/quick-line-chart.tsx b/client/src/current-game/sidebar-game-tab/graphs/quick-line-chart.tsx similarity index 97% rename from client/src/components/sidebar/game/quick-line-chart.tsx rename to client/src/current-game/sidebar-game-tab/graphs/quick-line-chart.tsx index 44b09f54..0c8f6f4f 100644 --- a/client/src/components/sidebar/game/quick-line-chart.tsx +++ b/client/src/current-game/sidebar-game-tab/graphs/quick-line-chart.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react' -import { TEAM_WHITE, TEAM_BROWN } from '../../../constants' +import { TEAM_WHITE, TEAM_BROWN } from '../../Constants' import { drawAxes, getAxes, setCanvasResolution } from '../../../util/graph-util' export interface LineChartDataPoint { diff --git a/client/src/components/sidebar/game/histogram.tsx b/client/src/current-game/sidebar-game-tab/histogram.tsx similarity index 89% rename from client/src/components/sidebar/game/histogram.tsx rename to client/src/current-game/sidebar-game-tab/histogram.tsx index d5b4402d..eb09be1e 100644 --- a/client/src/components/sidebar/game/histogram.tsx +++ b/client/src/current-game/sidebar-game-tab/histogram.tsx @@ -1,9 +1,9 @@ import React from 'react' -import { AppContext, useAppContext } from '../../../app-context' -import { useListenEvent, EventType } from '../../../app-events' -import { useForceUpdate } from '../../../util/react-util' -import { CanvasHistogram } from './quick-histogram' -import { ATTACK_COLOR, SPECIALTY_COLORS, TEAM_COLORS } from '../../../constants' +import { AppContext, useAppContext } from '../../app-context' +import { useListenEvent, EventType } from '../../app-events' +import { useForceUpdate } from '../../util/react-util' +import { CanvasHistogram } from './graphs/quick-histogram' +import { ATTACK_COLOR, SPECIALTY_COLORS, TEAM_COLORS } from '../Constants' function getChartData(appContext: AppContext): number[][][] { const match = appContext.state.activeMatch diff --git a/client/src/components/sidebar/game/resource-graph.tsx b/client/src/current-game/sidebar-game-tab/resource-graph.tsx similarity index 87% rename from client/src/components/sidebar/game/resource-graph.tsx rename to client/src/current-game/sidebar-game-tab/resource-graph.tsx index 11d02562..024bad83 100644 --- a/client/src/components/sidebar/game/resource-graph.tsx +++ b/client/src/current-game/sidebar-game-tab/resource-graph.tsx @@ -1,8 +1,8 @@ import React from 'react' -import { AppContext, useAppContext } from '../../../app-context' -import { useListenEvent, EventType } from '../../../app-events' -import { useForceUpdate } from '../../../util/react-util' -import { D3LineChart, LineChartDataPoint } from './d3-line-chart' +import { AppContext, useAppContext } from '../../app-context' +import { useListenEvent, EventType } from '../../app-events' +import { useForceUpdate } from '../../util/react-util' +import { D3LineChart, LineChartDataPoint } from './graphs/d3-line-chart' import assert from 'assert' interface Props { diff --git a/client/src/components/sidebar/game/team-table.tsx b/client/src/current-game/sidebar-game-tab/team-table.tsx similarity index 93% rename from client/src/components/sidebar/game/team-table.tsx rename to client/src/current-game/sidebar-game-tab/team-table.tsx index 87c1fb4a..f01657f4 100644 --- a/client/src/components/sidebar/game/team-table.tsx +++ b/client/src/current-game/sidebar-game-tab/team-table.tsx @@ -1,13 +1,13 @@ import React, { useEffect } from 'react' -import { useAppContext } from '../../../app-context' -import { useForceUpdate } from '../../../util/react-util' -import { useListenEvent, EventType } from '../../../app-events' -import { getImageIfLoaded, imageSource, removeTriggerOnImageLoad, triggerOnImageLoad } from '../../../util/ImageLoader' -import { TEAM_COLOR_NAMES } from '../../../constants' +import { useAppContext } from '../../app-context' +import { useForceUpdate } from '../../util/react-util' +import { useListenEvent, EventType } from '../../app-events' +import { getImageIfLoaded, imageSource, removeTriggerOnImageLoad, triggerOnImageLoad } from '../../util/image-loader' +import { TEAM_COLOR_NAMES } from '../Constants' import { schema } from 'battlecode-schema' -import { TeamTurnStat } from '../../../playback/TurnStat' -import { DoubleChevronUpIcon } from '../../../icons/chevron' -import { CurrentMap } from '../../../playback/Map' +import { TeamTurnStat } from '../TurnStat' +import { DoubleChevronUpIcon } from '../../icons/chevron' +import { CurrentMap } from '../Map' interface UnitsIconProps { teamIdx: 0 | 1 diff --git a/client/src/pages/main-page.tsx b/client/src/pages/main-page.tsx index 2f61e002..6525fbb4 100644 --- a/client/src/pages/main-page.tsx +++ b/client/src/pages/main-page.tsx @@ -3,7 +3,7 @@ import { AppContextProvider } from '../app-context' import { ControlsBar } from '../components/controls-bar/controls-bar' import { Sidebar } from '../components/sidebar/sidebar' import { GameArea } from '../components/game/game-area' -import { GAMEAREA_BACKGROUND } from '../constants' +import { GAMEAREA_BACKGROUND } from '../current-game/Constants' export const MainPage: React.FC = () => { return ( diff --git a/client/src/util/ImageLoader.ts b/client/src/util/image-loader.ts similarity index 100% rename from client/src/util/ImageLoader.ts rename to client/src/util/image-loader.ts diff --git a/client/src/util/RenderUtil.ts b/client/src/util/render-util.ts similarity index 97% rename from client/src/util/RenderUtil.ts rename to client/src/util/render-util.ts index fb914aef..acf6ba68 100644 --- a/client/src/util/RenderUtil.ts +++ b/client/src/util/render-util.ts @@ -1,8 +1,8 @@ -import * as cst from '../constants' -import { Team } from '../playback/Game' -import { CurrentMap, Dimension, StaticMap } from '../playback/Map' -import { Vector } from '../playback/Vector' -import { Body } from '../playback/Bodies' +import * as cst from '../current-game/Constants' +import { Team } from '../current-game/Game' +import { CurrentMap, Dimension, StaticMap } from '../current-game/Map' +import { Vector } from './vector' +import { Body } from '../current-game/Bodies' export const getRenderCoords = (cellX: number, cellY: number, dims: Dimension, centered: boolean = false) => { const cx = dims.minCorner.x + cellX diff --git a/client/src/playback/SchemaHelpers.ts b/client/src/util/schema-util.ts similarity index 95% rename from client/src/playback/SchemaHelpers.ts rename to client/src/util/schema-util.ts index 2d6f1381..b04fbbc6 100644 --- a/client/src/playback/SchemaHelpers.ts +++ b/client/src/util/schema-util.ts @@ -1,5 +1,5 @@ import { flatbuffers, schema } from 'battlecode-schema' -import { Vector } from './Vector' +import { Vector } from './vector' export const parseVecTable = (value: schema.VecTable) => { const result: Vector[] = [] diff --git a/client/src/playback/Vector.ts b/client/src/util/vector.ts similarity index 100% rename from client/src/playback/Vector.ts rename to client/src/util/vector.ts From bd9edf8e2b7a99ff5580c248f08a040cc711dca1 Mon Sep 17 00:00:00 2001 From: Aidan Blum Levine Date: Wed, 10 Apr 2024 17:31:44 -0400 Subject: [PATCH 4/5] more rearranging --- client/src/components/basic-dialog.tsx | 24 ++++++++++++++----- .../sidebar}/graphs/d3-histogram.tsx | 2 +- .../sidebar}/graphs/d3-line-chart.tsx | 2 +- .../sidebar}/graphs/quick-histogram.tsx | 0 .../sidebar}/graphs/quick-line-chart.tsx | 2 +- client/src/current-game/Colors.ts | 1 + .../sidebar-game-tab/histogram.tsx | 2 +- .../sidebar-game-tab/resource-graph.tsx | 2 +- client/src/util/hotkeys.ts | 15 ++++++++++++ 9 files changed, 39 insertions(+), 11 deletions(-) rename client/src/{current-game/sidebar-game-tab => components/sidebar}/graphs/d3-histogram.tsx (97%) rename client/src/{current-game/sidebar-game-tab => components/sidebar}/graphs/d3-line-chart.tsx (98%) rename client/src/{current-game/sidebar-game-tab => components/sidebar}/graphs/quick-histogram.tsx (100%) rename client/src/{current-game/sidebar-game-tab => components/sidebar}/graphs/quick-line-chart.tsx (96%) create mode 100644 client/src/current-game/Colors.ts create mode 100644 client/src/util/hotkeys.ts diff --git a/client/src/components/basic-dialog.tsx b/client/src/components/basic-dialog.tsx index 03ef20e4..fc556672 100644 --- a/client/src/components/basic-dialog.tsx +++ b/client/src/components/basic-dialog.tsx @@ -1,6 +1,7 @@ import React, { PropsWithChildren } from 'react' import { useAppContext } from '../app-context' import { useKeyboard } from '../util/keyboard' +import { useHotkeys } from '../util/hotkeys' interface Props { open: boolean @@ -20,6 +21,17 @@ export const BasicDialog: React.FC> = (props) => { const context = useAppContext() const keyboard = useKeyboard() + useHotkeys( + { + EscapeDialog: () => { + if (props.open && props.onCancel) { + props.onCancel() + } + } + }, + [props.open, props.onCancel] + ) + React.useEffect(() => { if (!props.open) return @@ -41,12 +53,12 @@ export const BasicDialog: React.FC> = (props) => { widthType == 'sm' ? 'w-4/6 md:w-3/5 lg:w-6/12' : widthType == 'md' - ? 'w-5/6 md:w-3/4 lg:w-7/12' - : widthType == 'lg' - ? 'w-5/6 md:w-4/5 lg:w-9/12' - : widthType == 'full' - ? 'w-5/6 md:w-5/6 lg:w-11/12' - : '' + ? 'w-5/6 md:w-3/4 lg:w-7/12' + : widthType == 'lg' + ? 'w-5/6 md:w-4/5 lg:w-9/12' + : widthType == 'full' + ? 'w-5/6 md:w-5/6 lg:w-11/12' + : '' return (
void>) => { + const key = useKeyboard() + useEffect(() => { + if (hotkeys[key.keyCode]) { + hotkeys[key.keyCode]() + } + }, [key.keyCode]) +} From c16023c5a33e021a9d3f2b059d6469e6ce419634 Mon Sep 17 00:00:00 2001 From: Aidan Blum Levine Date: Sat, 20 Apr 2024 14:52:50 -0400 Subject: [PATCH 5/5] hotkeys done --- client/src/components/basic-dialog.tsx | 26 ++---- .../components/controls-bar/controls-bar.tsx | 61 +++++++------ client/src/components/sidebar/sidebar.tsx | 19 ++-- client/src/hotkeys.ts | 0 client/src/util/hotkeys.ts | 15 ---- client/src/util/keyboard.ts | 86 +++++++++++++++---- 6 files changed, 112 insertions(+), 95 deletions(-) create mode 100644 client/src/hotkeys.ts delete mode 100644 client/src/util/hotkeys.ts diff --git a/client/src/components/basic-dialog.tsx b/client/src/components/basic-dialog.tsx index fc556672..b616ffbf 100644 --- a/client/src/components/basic-dialog.tsx +++ b/client/src/components/basic-dialog.tsx @@ -1,7 +1,6 @@ import React, { PropsWithChildren } from 'react' import { useAppContext } from '../app-context' -import { useKeyboard } from '../util/keyboard' -import { useHotkeys } from '../util/hotkeys' +import { Hotkeys, useHotkey, useKeyboard } from '../util/keyboard' interface Props { open: boolean @@ -21,25 +20,16 @@ export const BasicDialog: React.FC> = (props) => { const context = useAppContext() const keyboard = useKeyboard() - useHotkeys( - { - EscapeDialog: () => { - if (props.open && props.onCancel) { - props.onCancel() - } - } + useHotkey( + context.state, + keyboard, + Hotkeys.EscapeDialog, + () => { + if (props.open && props.onCancel) props.onCancel() }, - [props.open, props.onCancel] + [props.onCancel, props.open] ) - React.useEffect(() => { - if (!props.open) return - - if (props.onCancel && keyboard.keyCode === 'Escape') { - props.onCancel() - } - }, [props.open, keyboard.keyCode]) - React.useEffect(() => { context.setState((prevState) => ({ ...prevState, disableHotkeys: props.open })) }, [props.open]) diff --git a/client/src/components/controls-bar/controls-bar.tsx b/client/src/components/controls-bar/controls-bar.tsx index b52e59c6..fac456d4 100644 --- a/client/src/components/controls-bar/controls-bar.tsx +++ b/client/src/components/controls-bar/controls-bar.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react' import * as ControlIcons from '../../icons/controls' import { ControlsBarButton } from './controls-bar-button' import { useAppContext } from '../../app-context' -import { useKeyboard } from '../../util/keyboard' +import { Hotkeys, useHotkey, useKeyboard } from '../../util/keyboard' import { ControlsBarTimeline } from './controls-bar-timeline' import { EventType, useListenEvent } from '../../app-events' import { useForceUpdate } from '../../util/react-util' @@ -33,6 +33,33 @@ export const ControlsBar: React.FC = ({ const { state: appState, setState: setAppState } = useAppContext() const [minimized, setMinimized] = React.useState(false) const keyboard = useKeyboard() + + useHotkey(appState, keyboard, Hotkeys.MinimizeControlBar, () => setMinimized(!minimized)) + useHotkey(appState, keyboard, Hotkeys.Pause, () => setPaused(!paused)) + useHotkey( + appState, + keyboard, + Hotkeys.ControlsNext, + () => { + if (paused) stepTurn(1) + else multiplyUpdatesPerSecond(2) + }, + [paused], + true + ) + useHotkey( + appState, + keyboard, + Hotkeys.ControlsPrev, + () => { + if (paused) stepTurn(-1) + else multiplyUpdatesPerSecond(0.5) + }, + [paused], + true + ) + useHotkey(appState, keyboard, Hotkeys.JumpToStart, () => jumpToTurn(0)) + useHotkey(appState, keyboard, Hotkeys.JumpToEnd, () => jumpToEnd()) const forceUpdate = useForceUpdate() useListenEvent(EventType.NEW_TURN, forceUpdate) @@ -74,37 +101,7 @@ export const ControlsBar: React.FC = ({ // control bar before using a shortcut, unselect it; Most browsers have // specific accessibility features that mess with these shortcuts. if (keyboard.targetElem instanceof HTMLButtonElement) keyboard.targetElem.blur() - - if (keyboard.keyCode === 'Space' && match) setPaused(!paused) - - if (keyboard.keyCode === 'KeyC') setMinimized(!minimized) - - const applyArrows = () => { - if (paused) { - if (keyboard.keyCode === 'ArrowRight') stepTurn(1) - if (keyboard.keyCode === 'ArrowLeft') stepTurn(-1) - } else { - if (keyboard.keyCode === 'ArrowRight') multiplyUpdatesPerSecond(2) - if (keyboard.keyCode === 'ArrowLeft') multiplyUpdatesPerSecond(0.5) - } - } - applyArrows() - - if (keyboard.keyCode === 'Comma') jumpToTurn(0) - if (keyboard.keyCode === 'Period') jumpToEnd() - - const initalDelay = 250 - const repeatDelay = 100 - const timeouts: { initialTimeout: NodeJS.Timeout; repeatedFire?: NodeJS.Timeout } = { - initialTimeout: setTimeout(() => { - timeouts.repeatedFire = setInterval(applyArrows, repeatDelay) - }, initalDelay) - } - return () => { - clearTimeout(timeouts.initialTimeout) - clearInterval(timeouts.repeatedFire) - } - }, [keyboard.keyCode]) + }, [appState, keyboard]) if (!match?.isPlayable()) return null diff --git a/client/src/components/sidebar/sidebar.tsx b/client/src/components/sidebar/sidebar.tsx index 5fcb2ca6..58707540 100644 --- a/client/src/components/sidebar/sidebar.tsx +++ b/client/src/components/sidebar/sidebar.tsx @@ -8,7 +8,7 @@ import { HelpPage } from './help/help' import { MapEditorPage } from './map-editor/map-editor' import { RunnerPage } from './runner/runner' import { usePage, PageType, useSearchParamBool, useSearchParamString } from '../../app-search-params' -import { useKeyboard } from '../../util/keyboard' +import { Hotkeys, useHotkey, useKeyboard } from '../../util/keyboard' import { Scrollbars } from 'react-custom-scrollbars-2' import useWindowDimensions from '../../util/window-size' import { useAppContext } from '../../app-context' @@ -17,13 +17,17 @@ import { ConfigPage } from '../../client-config' import { UpdateWarning } from './update-warning' import Game from '../../current-game/Game' import Tournament, { JsonTournamentGame } from '../game/tournament-renderer/Tournament' -import { TournamentPage } from './tournament/tournament-page'; +import { TournamentPage } from './tournament/tournament-page' export const Sidebar: React.FC = () => { const { width, height } = useWindowDimensions() const [page, setPage] = usePage() const context = useAppContext() const keyboard = useKeyboard() + + useHotkey(context.state, keyboard, Hotkeys.BackSidebarPage, () => setPage(getNextPage(page, true))) + useHotkey(context.state, keyboard, Hotkeys.ForwardSidebarPage, () => setPage(getNextPage(page, false))) + useHotkey(context.state, keyboard, Hotkeys.JumpToQueue, () => setPage(PageType.QUEUE)) // scaffold is created at this level so it is never re-created const scaffold = useScaffold() @@ -129,17 +133,6 @@ export const Sidebar: React.FC = () => { return hotkeyPageLoop[nextIndex] } - React.useEffect(() => { - if (context.state.disableHotkeys) return - - if (keyboard.keyCode === 'Backquote') setPage(getNextPage(page, true)) - if (keyboard.keyCode === 'Digit1') setPage(getNextPage(page, false)) - - if (keyboard.keyCode == 'KeyO' && (keyboard.ctrlKey || keyboard.metaKey)) { - setPage(PageType.QUEUE) - } - }, [keyboard]) - const activeSidebarButtons = React.useMemo(() => { if (showTournamentFeatures) { return [ diff --git a/client/src/hotkeys.ts b/client/src/hotkeys.ts new file mode 100644 index 00000000..e69de29b diff --git a/client/src/util/hotkeys.ts b/client/src/util/hotkeys.ts deleted file mode 100644 index bb4ae0fe..00000000 --- a/client/src/util/hotkeys.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useEffect } from 'react' -import { useKeyboard } from './keyboard' - -enum Hotkeys { - EscapeDialog = 'Escape', -} - -export const useHotkeys = (hotkeys: Record void>) => { - const key = useKeyboard() - useEffect(() => { - if (hotkeys[key.keyCode]) { - hotkeys[key.keyCode]() - } - }, [key.keyCode]) -} diff --git a/client/src/util/keyboard.ts b/client/src/util/keyboard.ts index ad8bdf02..fdfabef3 100644 --- a/client/src/util/keyboard.ts +++ b/client/src/util/keyboard.ts @@ -1,25 +1,54 @@ import { useEffect, useState } from 'react' +import { AppState } from '../app-context' -interface KeyState { - keyCode: string - repeat: boolean - targetElem: EventTarget | null - ctrlKey: boolean - metaKey: boolean - shiftKey: boolean +export enum Hotkeys { + EscapeDialog, + MinimizeControlBar, + Pause, + ControlsNext, + ControlsPrev, + JumpToStart, + JumpToEnd, + BackSidebarPage, + ForwardSidebarPage, + JumpToQueue } -const DEFAULT_KEY_STATE: KeyState = { - keyCode: '', - repeat: false, - targetElem: null, - ctrlKey: false, - metaKey: false, - shiftKey: false +const HotkeyDefs: Record = { + [Hotkeys.EscapeDialog]: { key: 'Escape' }, + [Hotkeys.MinimizeControlBar]: { key: 'KeyC' }, + [Hotkeys.Pause]: { key: 'Space' }, + [Hotkeys.ControlsNext]: { key: 'ArrowRight' }, + [Hotkeys.ControlsPrev]: { key: 'ArrowLeft' }, + [Hotkeys.JumpToStart]: { key: 'Comma' }, + [Hotkeys.JumpToEnd]: { key: 'Period' }, + [Hotkeys.BackSidebarPage]: { key: 'Backquote' }, + [Hotkeys.ForwardSidebarPage]: { key: 'Digit1' }, + [Hotkeys.JumpToQueue]: { key: 'KeyO', ctrlOrMeta: true } } -export function useKeyboard() { - const [key, setKey] = useState(DEFAULT_KEY_STATE) +export function useHotkey( + state: AppState, + keyboard: KeyState, + hotkeyName: Hotkeys, + callback: () => void, + defs: any[] = [], + repeat: boolean = false +) { + const deps = [state, keyboard.keyCode, keyboard.ctrlKey, keyboard.metaKey, hotkeyName, ...defs] + if (repeat) deps.push(keyboard) // keyboard is recreated on repeat fire, so this makes it refire on native key repeat + + useEffect(() => { + if (state.disableHotkeys) return + const hotkey = HotkeyDefs[hotkeyName] + if (keyboard.keyCode !== hotkey.key) return + if (hotkey.ctrlOrMeta && !keyboard.ctrlKey && !keyboard.metaKey) return + callback() + }, deps) +} + +export function useKeyboard(): KeyState { + const [pressedKey, setKey] = useState(DEFAULT_KEY_STATE) useEffect(() => { const pressedCallback = (e: KeyboardEvent) => @@ -42,5 +71,28 @@ export function useKeyboard() { } }, []) - return key + return pressedKey +} + +interface Hotkey { + key: string + ctrlOrMeta?: boolean +} + +interface KeyState { + keyCode: string + repeat: boolean + targetElem: EventTarget | null + ctrlKey: boolean + metaKey: boolean + shiftKey: boolean +} + +const DEFAULT_KEY_STATE: KeyState = { + keyCode: '', + repeat: false, + targetElem: null, + ctrlKey: false, + metaKey: false, + shiftKey: false }