Skip to content

Commit e0d244b

Browse files
Merge branch 'master' into public-release
2 parents 384d5e2 + d9b549f commit e0d244b

File tree

14 files changed

+130
-47
lines changed

14 files changed

+130
-47
lines changed

client/src/app-context.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react'
22
import Game from './playback/Game'
33
import Tournament, { DEFAULT_TOURNAMENT_STATE, TournamentState } from './playback/Tournament'
44
import { ClientConfig, getDefaultConfig } from './client-config'
5+
import { GameRenderer } from './playback/GameRenderer'
56

67
export interface TimelineMarker {
78
round: number
@@ -28,6 +29,7 @@ const DEFAULT_APP_STATE: AppState = {
2829
export interface AppContext {
2930
state: AppState
3031
setState: (value: React.SetStateAction<AppState>) => void
32+
updateConfigValue: (key: keyof ClientConfig, newVal: any) => void
3133
}
3234

3335
interface Props {
@@ -37,9 +39,25 @@ interface Props {
3739
const appContext = React.createContext({} as AppContext)
3840
export const AppContextProvider: React.FC<Props> = (props) => {
3941
const [appState, setAppState] = React.useState(DEFAULT_APP_STATE)
42+
4043
GameConfig.config = appState.config
44+
45+
const updateConfigValue = (key: keyof ClientConfig, newVal: any) => {
46+
setAppState((prevState) => ({
47+
...prevState,
48+
config: { ...prevState.config, [key]: newVal }
49+
}))
50+
localStorage.setItem('config' + key, JSON.stringify(newVal))
51+
setTimeout(() => {
52+
// After the setState is done, rerender
53+
GameRenderer.fullRender()
54+
}, 10)
55+
}
56+
4157
return (
42-
<appContext.Provider value={{ state: appState, setState: setAppState }}>{props.children}</appContext.Provider>
58+
<appContext.Provider value={{ state: appState, setState: setAppState, updateConfigValue }}>
59+
{props.children}
60+
</appContext.Provider>
4361
)
4462
}
4563

client/src/client-config.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
DEFAULT_GLOBAL_COLORS
1515
} from './colors'
1616
import { BrightButton, Button } from './components/button'
17+
import { useKeyboard } from './util/keyboard'
1718

1819
export type ClientConfig = typeof DEFAULT_CONFIG
1920

@@ -29,6 +30,7 @@ const DEFAULT_CONFIG = {
2930
showPaintBars: false,
3031
showPaintMarkers: true,
3132
showMapXY: true,
33+
focusRobotTurn: true,
3234
enableFancyPaint: true,
3335
streamRunnerGames: true,
3436
profileGames: false,
@@ -57,6 +59,7 @@ const configDescription: Record<keyof ClientConfig, string> = {
5759
showPaintBars: 'Show paint bars below all robots',
5860
showPaintMarkers: 'Show paint markers created using mark()',
5961
showMapXY: 'Show X,Y when hovering a tile',
62+
focusRobotTurn: 'Focus the robot when performing their turn during turn-stepping mode',
6063
enableFancyPaint: 'Enable fancy paint rendering',
6164
streamRunnerGames: 'Stream each round from the runner live as the game is being played',
6265
profileGames: 'Enable saving profiling data when running games',
@@ -86,6 +89,16 @@ export function getDefaultConfig(): ClientConfig {
8689
}
8790

8891
export const ConfigPage: React.FC<Props> = (props) => {
92+
const context = useAppContext()
93+
const keyboard = useKeyboard()
94+
95+
useEffect(() => {
96+
if (context.state.disableHotkeys) return
97+
98+
if (keyboard.keyCode === 'KeyF')
99+
context.updateConfigValue('focusRobotTurn', !context.state.config.focusRobotTurn)
100+
}, [keyboard.keyCode])
101+
89102
if (!props.open) return null
90103

91104
return (
@@ -236,15 +249,7 @@ const ConfigBooleanElement: React.FC<{ configKey: keyof ClientConfig }> = ({ con
236249
<input
237250
type={'checkbox'}
238251
checked={value as any}
239-
onChange={(e) => {
240-
context.setState((prevState) => ({
241-
...prevState,
242-
config: { ...prevState.config, [configKey]: e.target.checked }
243-
}))
244-
localStorage.setItem('config' + configKey, JSON.stringify(e.target.checked))
245-
// hopefully after the setState is done
246-
setTimeout(() => GameRenderer.fullRender(), 10)
247-
}}
252+
onChange={(e) => context.updateConfigValue(configKey, e.target.checked)}
248253
/>
249254
<div className={'ml-2 text-xs'}>{configDescription[configKey] ?? configKey}</div>
250255
</div>
@@ -265,16 +270,11 @@ const ConfigNumberElement: React.FC<{ configKey: keyof ClientConfig }> = ({ conf
265270
<NumInput
266271
value={value}
267272
changeValue={(newVal) => {
268-
context.setState((prevState) => ({
269-
...prevState,
270-
config: { ...context.state.config, [configKey]: newVal }
271-
}))
272-
localStorage.setItem('config' + configKey, JSON.stringify(newVal))
273-
// hopefully after the setState is done
274-
setTimeout(() => {
273+
context.updateConfigValue(configKey, newVal)
274+
if (configKey === 'resolutionScale') {
275275
// Trigger canvas dimension update to ensure resolution is updated
276276
GameRenderer.onMatchChange()
277-
}, 10)
277+
}
278278
}}
279279
min={10}
280280
max={200}

client/src/components/game/game-renderer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useRef } from 'react'
22
import { Vector } from '../../playback/Vector'
33
import { CurrentMap } from '../../playback/Map'
4-
import { useMatch, useRound } from '../../playback/GameRunner'
4+
import { useMatch, useRound, useTurnNumber } from '../../playback/GameRunner'
55
import { CanvasLayers, GameRenderer } from '../../playback/GameRenderer'
66
import { Space, VirtualSpaceRect } from 'react-zoomable-ui'
77
import { ResetZoomIcon } from '../../icons/resetzoom'
@@ -17,6 +17,9 @@ export const GameRendererPanel: React.FC = () => {
1717
const appContext = useAppContext()
1818
const round = useRound()
1919

20+
// Unused, but we want to rerender the tooltips when the turn changes as well
21+
const turn = useTurnNumber()
22+
2023
const { selectedBodyID } = GameRenderer.useCanvasClickEvents()
2124
const { hoveredTile } = GameRenderer.useCanvasHoverEvents()
2225
const selectedBody = selectedBodyID !== undefined ? round?.bodies.bodies.get(selectedBodyID) : undefined

client/src/components/game/tooltip.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,17 @@ export const FloatingTooltip: React.FC<{
1414
const [tooltipSize, setTooltipSize] = React.useState({ width: 0, height: 0 })
1515
React.useEffect(() => {
1616
const observer = new ResizeObserver((entries) => {
17-
if (entries[0]) {
18-
const borderBox = entries[0].borderBoxSize[0]
17+
const entry = entries[0]
18+
if (!entry) return
19+
20+
// Check if this property exists, it may not for older OSes
21+
if (entry.borderBoxSize) {
22+
const borderBox = Array.isArray(entry.borderBoxSize) ? entry.borderBoxSize[0] : entry.borderBoxSize
1923
setTooltipSize({ width: borderBox.inlineSize, height: borderBox.blockSize })
24+
} else {
25+
// Fallback to contentRect
26+
const rect = entry.contentRect
27+
setTooltipSize({ width: rect.width, height: rect.height })
2028
}
2129
})
2230
if (tooltipRef.current) observer.observe(tooltipRef.current)

client/src/components/sidebar/help/help.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export const HelpPage: React.FC<Props> = (props) => {
133133
{hotkeyElement(`R`, 'Resets the map camera if it has been panned/zoomed')}
134134
{hotkeyElement(`C`, 'Hides and unhides game control bar')}
135135
{hotkeyElement(`T`, 'Toggles per-turn playback for the current game')}
136+
{hotkeyElement(`F`, 'Toggles per-turn robot focus config')}
136137
{hotkeyElement(`.`, 'Skip to the very last round of the current game')}
137138
{hotkeyElement(`,`, 'Skip to the first round of the current game')}
138139
</div>

client/src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const CLIENT_VERSION = '1.3.2'
1+
export const CLIENT_VERSION = '1.3.3'
22
export const SPEC_VERSION = '1'
33
export const BATTLECODE_YEAR: number = 2025
44
export const MAP_SIZE_RANGE = {

client/src/playback/Actions.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,17 @@ export const ACTION_DEFINITIONS: Record<schema.Action, typeof Action<ActionUnion
9090
},
9191
[schema.Action.DamageAction]: class DamageAction extends Action<schema.DamageAction> {
9292
apply(round: Round): void {
93+
const src = round.bodies.getById(this.robotId)
9394
const target = round.bodies.getById(this.actionData.id())
9495

95-
// Apply damage to the target
96-
target.hp = Math.max(target.hp - this.actionData.damage(), 0)
96+
const damage = this.actionData.damage()
97+
if (src.robotType === schema.RobotType.MOPPER) {
98+
// Apply paint damage to the target
99+
target.paint = Math.max(target.paint - damage, 0)
100+
} else {
101+
// Apply HP damage to the target
102+
target.hp = Math.max(target.hp - damage, 0)
103+
}
97104
}
98105
},
99106
[schema.Action.SplashAction]: class SplashAction extends Action<schema.SplashAction> {
@@ -328,30 +335,42 @@ export const ACTION_DEFINITIONS: Record<schema.Action, typeof Action<ActionUnion
328335
},
329336
[schema.Action.TransferAction]: class TransferAction extends Action<schema.TransferAction> {
330337
apply(round: Round): void {
338+
const amount = this.actionData.amount()
339+
340+
if (amount === 0) {
341+
/* ! SCUFFED SPECIAL CASE: Resource pattern completed ! */
342+
return
343+
}
344+
331345
const src = round.bodies.getById(this.robotId)
332346
const dst = round.bodies.getById(this.actionData.id())
333347

334-
src.paint -= this.actionData.amount()
335-
dst.paint += this.actionData.amount()
348+
src.paint -= amount
349+
dst.paint += amount
336350
}
337351
draw(match: Match, ctx: CanvasRenderingContext2D): void {
338-
const srcBody = match.currentRound.bodies.getById(this.robotId)
339-
const dstBody = match.currentRound.bodies.getById(this.actionData.id())
352+
if (this.actionData.amount() === 0) {
353+
/* ! SCUFFED SPECIAL CASE: Resource pattern completed ! */
354+
const centerIdx = this.actionData.id()
355+
} else {
356+
const srcBody = match.currentRound.bodies.getById(this.robotId)
357+
const dstBody = match.currentRound.bodies.getById(this.actionData.id())
340358

341-
const from = srcBody.getInterpolatedCoords(match)
342-
const to = dstBody.getInterpolatedCoords(match)
359+
const from = srcBody.getInterpolatedCoords(match)
360+
const to = dstBody.getInterpolatedCoords(match)
343361

344-
renderUtils.renderLine(
345-
ctx,
346-
renderUtils.getRenderCoords(from.x, from.y, match.currentRound.map.staticMap.dimension),
347-
renderUtils.getRenderCoords(to.x, to.y, match.currentRound.map.staticMap.dimension),
348-
{
349-
color: '#11fc30',
350-
lineWidth: 0.06,
351-
opacity: 0.5,
352-
renderArrow: true
353-
}
354-
)
362+
renderUtils.renderLine(
363+
ctx,
364+
renderUtils.getRenderCoords(from.x, from.y, match.currentRound.map.staticMap.dimension),
365+
renderUtils.getRenderCoords(to.x, to.y, match.currentRound.map.staticMap.dimension),
366+
{
367+
color: '#11fc30',
368+
lineWidth: 0.06,
369+
opacity: 0.5,
370+
renderArrow: true
371+
}
372+
)
373+
}
355374
}
356375
},
357376
[schema.Action.MessageAction]: class MessageAction extends Action<schema.MessageAction> {

client/src/playback/GameRunner.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Game from './Game'
33
import Match from './Match'
44
import Round from './Round'
55
import { GameRenderer } from './GameRenderer'
6+
import { GameConfig } from '../app-context'
67

78
const SIMULATION_UPDATE_INTERVAL_MS = 17 // About 60 fps
89

@@ -148,6 +149,9 @@ class GameRunnerClass {
148149
if (!this.match) return
149150
// explicit rerender at the end so a render doesnt occur between these two steps
150151
this.match._stepTurn(delta)
152+
if (GameConfig.config.focusRobotTurn) {
153+
GameRenderer.setSelectedRobot(this.match.currentRound.lastSteppedRobotId)
154+
}
151155
GameRenderer.render()
152156
this._trigger(this._turnListeners)
153157
}
@@ -249,9 +253,7 @@ export function useRound(): Round | undefined {
249253

250254
export function useTurnNumber(): { current: number; max: number } | undefined {
251255
const round = useRound()
252-
const [turnIdentifierNumber, setTurnIdentifierNumber] = React.useState(
253-
round ? round.roundNumber * round.match.maxRound + round.turnNumber : undefined
254-
)
256+
const [turnIdentifierNumber, setTurnIdentifierNumber] = React.useState<number | undefined>(-1)
255257
React.useEffect(() => {
256258
const listener = () =>
257259
setTurnIdentifierNumber(round ? round.roundNumber * round.match.maxRound + round.turnNumber : undefined)
@@ -268,7 +270,7 @@ export function useTurnNumber(): { current: number; max: number } | undefined {
268270
max: round.turnsLength || 0
269271
}
270272
: undefined,
271-
[round, turnIdentifierNumber, round?.turnNumber]
273+
[round, round?.roundNumber, round?.turnNumber]
272274
)
273275
}
274276

client/src/playback/Match.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ export default class Match {
224224
this._jumpToTurn(targetTurn)
225225
}
226226

227+
/**
228+
* Jump to a turn within the current round's turns.R
229+
*/
227230
public _jumpToTurn(turn: number): void {
228231
if (!this.game.playable) return
229232

client/src/playback/Round.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import assert from 'assert'
99

1010
export default class Round {
1111
public turnNumber: number = 0
12+
public lastSteppedRobotId: number | undefined = undefined
1213
private initialRoundState: Round | null = null
1314

1415
constructor(
@@ -83,6 +84,9 @@ export default class Round {
8384
}
8485
}
8586

87+
/**
88+
* Step the current turn within the current delta.
89+
*/
8690
private stepTurn(): void {
8791
assert(this.turnNumber < this.turnsLength, 'Cannot step a round that is at the end')
8892

@@ -110,6 +114,8 @@ export default class Round {
110114
this.bodies.applyTurnDelta(this, turn)
111115

112116
this.turnNumber += 1
117+
118+
this.lastSteppedRobotId = turn.robotId()
113119
}
114120

115121
public copy(): Round {

0 commit comments

Comments
 (0)