Skip to content

Commit

Permalink
Merge branch 'master' into public-release
Browse files Browse the repository at this point in the history
  • Loading branch information
TheApplePieGod committed Jan 10, 2025
2 parents 384d5e2 + d9b549f commit e0d244b
Show file tree
Hide file tree
Showing 14 changed files with 130 additions and 47 deletions.
20 changes: 19 additions & 1 deletion client/src/app-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'
import Game from './playback/Game'
import Tournament, { DEFAULT_TOURNAMENT_STATE, TournamentState } from './playback/Tournament'
import { ClientConfig, getDefaultConfig } from './client-config'
import { GameRenderer } from './playback/GameRenderer'

export interface TimelineMarker {
round: number
Expand All @@ -28,6 +29,7 @@ const DEFAULT_APP_STATE: AppState = {
export interface AppContext {
state: AppState
setState: (value: React.SetStateAction<AppState>) => void
updateConfigValue: (key: keyof ClientConfig, newVal: any) => void
}

interface Props {
Expand All @@ -37,9 +39,25 @@ interface Props {
const appContext = React.createContext({} as AppContext)
export const AppContextProvider: React.FC<Props> = (props) => {
const [appState, setAppState] = React.useState(DEFAULT_APP_STATE)

GameConfig.config = appState.config

const updateConfigValue = (key: keyof ClientConfig, newVal: any) => {
setAppState((prevState) => ({
...prevState,
config: { ...prevState.config, [key]: newVal }
}))
localStorage.setItem('config' + key, JSON.stringify(newVal))
setTimeout(() => {
// After the setState is done, rerender
GameRenderer.fullRender()
}, 10)
}

return (
<appContext.Provider value={{ state: appState, setState: setAppState }}>{props.children}</appContext.Provider>
<appContext.Provider value={{ state: appState, setState: setAppState, updateConfigValue }}>
{props.children}
</appContext.Provider>
)
}

Expand Down
34 changes: 17 additions & 17 deletions client/src/client-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DEFAULT_GLOBAL_COLORS
} from './colors'
import { BrightButton, Button } from './components/button'
import { useKeyboard } from './util/keyboard'

export type ClientConfig = typeof DEFAULT_CONFIG

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

export const ConfigPage: React.FC<Props> = (props) => {
const context = useAppContext()
const keyboard = useKeyboard()

useEffect(() => {
if (context.state.disableHotkeys) return

if (keyboard.keyCode === 'KeyF')
context.updateConfigValue('focusRobotTurn', !context.state.config.focusRobotTurn)
}, [keyboard.keyCode])

if (!props.open) return null

return (
Expand Down Expand Up @@ -236,15 +249,7 @@ const ConfigBooleanElement: React.FC<{ configKey: keyof ClientConfig }> = ({ con
<input
type={'checkbox'}
checked={value as any}
onChange={(e) => {
context.setState((prevState) => ({
...prevState,
config: { ...prevState.config, [configKey]: e.target.checked }
}))
localStorage.setItem('config' + configKey, JSON.stringify(e.target.checked))
// hopefully after the setState is done
setTimeout(() => GameRenderer.fullRender(), 10)
}}
onChange={(e) => context.updateConfigValue(configKey, e.target.checked)}
/>
<div className={'ml-2 text-xs'}>{configDescription[configKey] ?? configKey}</div>
</div>
Expand All @@ -265,16 +270,11 @@ const ConfigNumberElement: React.FC<{ configKey: keyof ClientConfig }> = ({ conf
<NumInput
value={value}
changeValue={(newVal) => {
context.setState((prevState) => ({
...prevState,
config: { ...context.state.config, [configKey]: newVal }
}))
localStorage.setItem('config' + configKey, JSON.stringify(newVal))
// hopefully after the setState is done
setTimeout(() => {
context.updateConfigValue(configKey, newVal)
if (configKey === 'resolutionScale') {
// Trigger canvas dimension update to ensure resolution is updated
GameRenderer.onMatchChange()
}, 10)
}
}}
min={10}
max={200}
Expand Down
5 changes: 4 additions & 1 deletion client/src/components/game/game-renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useRef } from 'react'
import { Vector } from '../../playback/Vector'
import { CurrentMap } from '../../playback/Map'
import { useMatch, useRound } from '../../playback/GameRunner'
import { useMatch, useRound, useTurnNumber } from '../../playback/GameRunner'
import { CanvasLayers, GameRenderer } from '../../playback/GameRenderer'
import { Space, VirtualSpaceRect } from 'react-zoomable-ui'
import { ResetZoomIcon } from '../../icons/resetzoom'
Expand All @@ -17,6 +17,9 @@ export const GameRendererPanel: React.FC = () => {
const appContext = useAppContext()
const round = useRound()

// Unused, but we want to rerender the tooltips when the turn changes as well
const turn = useTurnNumber()

const { selectedBodyID } = GameRenderer.useCanvasClickEvents()
const { hoveredTile } = GameRenderer.useCanvasHoverEvents()
const selectedBody = selectedBodyID !== undefined ? round?.bodies.bodies.get(selectedBodyID) : undefined
Expand Down
12 changes: 10 additions & 2 deletions client/src/components/game/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ export const FloatingTooltip: React.FC<{
const [tooltipSize, setTooltipSize] = React.useState({ width: 0, height: 0 })
React.useEffect(() => {
const observer = new ResizeObserver((entries) => {
if (entries[0]) {
const borderBox = entries[0].borderBoxSize[0]
const entry = entries[0]
if (!entry) return

// Check if this property exists, it may not for older OSes
if (entry.borderBoxSize) {
const borderBox = Array.isArray(entry.borderBoxSize) ? entry.borderBoxSize[0] : entry.borderBoxSize
setTooltipSize({ width: borderBox.inlineSize, height: borderBox.blockSize })
} else {
// Fallback to contentRect
const rect = entry.contentRect
setTooltipSize({ width: rect.width, height: rect.height })
}
})
if (tooltipRef.current) observer.observe(tooltipRef.current)
Expand Down
1 change: 1 addition & 0 deletions client/src/components/sidebar/help/help.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export const HelpPage: React.FC<Props> = (props) => {
{hotkeyElement(`R`, 'Resets the map camera if it has been panned/zoomed')}
{hotkeyElement(`C`, 'Hides and unhides game control bar')}
{hotkeyElement(`T`, 'Toggles per-turn playback for the current game')}
{hotkeyElement(`F`, 'Toggles per-turn robot focus config')}
{hotkeyElement(`.`, 'Skip to the very last round of the current game')}
{hotkeyElement(`,`, 'Skip to the first round of the current game')}
</div>
Expand Down
2 changes: 1 addition & 1 deletion client/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const CLIENT_VERSION = '1.3.2'
export const CLIENT_VERSION = '1.3.3'
export const SPEC_VERSION = '1'
export const BATTLECODE_YEAR: number = 2025
export const MAP_SIZE_RANGE = {
Expand Down
57 changes: 38 additions & 19 deletions client/src/playback/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,17 @@ export const ACTION_DEFINITIONS: Record<schema.Action, typeof Action<ActionUnion
},
[schema.Action.DamageAction]: class DamageAction extends Action<schema.DamageAction> {
apply(round: Round): void {
const src = round.bodies.getById(this.robotId)
const target = round.bodies.getById(this.actionData.id())

// Apply damage to the target
target.hp = Math.max(target.hp - this.actionData.damage(), 0)
const damage = this.actionData.damage()
if (src.robotType === schema.RobotType.MOPPER) {
// Apply paint damage to the target
target.paint = Math.max(target.paint - damage, 0)
} else {
// Apply HP damage to the target
target.hp = Math.max(target.hp - damage, 0)
}
}
},
[schema.Action.SplashAction]: class SplashAction extends Action<schema.SplashAction> {
Expand Down Expand Up @@ -328,30 +335,42 @@ export const ACTION_DEFINITIONS: Record<schema.Action, typeof Action<ActionUnion
},
[schema.Action.TransferAction]: class TransferAction extends Action<schema.TransferAction> {
apply(round: Round): void {
const amount = this.actionData.amount()

if (amount === 0) {
/* ! SCUFFED SPECIAL CASE: Resource pattern completed ! */
return
}

const src = round.bodies.getById(this.robotId)
const dst = round.bodies.getById(this.actionData.id())

src.paint -= this.actionData.amount()
dst.paint += this.actionData.amount()
src.paint -= amount
dst.paint += amount
}
draw(match: Match, ctx: CanvasRenderingContext2D): void {
const srcBody = match.currentRound.bodies.getById(this.robotId)
const dstBody = match.currentRound.bodies.getById(this.actionData.id())
if (this.actionData.amount() === 0) {
/* ! SCUFFED SPECIAL CASE: Resource pattern completed ! */
const centerIdx = this.actionData.id()
} else {
const srcBody = match.currentRound.bodies.getById(this.robotId)
const dstBody = match.currentRound.bodies.getById(this.actionData.id())

const from = srcBody.getInterpolatedCoords(match)
const to = dstBody.getInterpolatedCoords(match)
const from = srcBody.getInterpolatedCoords(match)
const to = dstBody.getInterpolatedCoords(match)

renderUtils.renderLine(
ctx,
renderUtils.getRenderCoords(from.x, from.y, match.currentRound.map.staticMap.dimension),
renderUtils.getRenderCoords(to.x, to.y, match.currentRound.map.staticMap.dimension),
{
color: '#11fc30',
lineWidth: 0.06,
opacity: 0.5,
renderArrow: true
}
)
renderUtils.renderLine(
ctx,
renderUtils.getRenderCoords(from.x, from.y, match.currentRound.map.staticMap.dimension),
renderUtils.getRenderCoords(to.x, to.y, match.currentRound.map.staticMap.dimension),
{
color: '#11fc30',
lineWidth: 0.06,
opacity: 0.5,
renderArrow: true
}
)
}
}
},
[schema.Action.MessageAction]: class MessageAction extends Action<schema.MessageAction> {
Expand Down
10 changes: 6 additions & 4 deletions client/src/playback/GameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Game from './Game'
import Match from './Match'
import Round from './Round'
import { GameRenderer } from './GameRenderer'
import { GameConfig } from '../app-context'

const SIMULATION_UPDATE_INTERVAL_MS = 17 // About 60 fps

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

export function useTurnNumber(): { current: number; max: number } | undefined {
const round = useRound()
const [turnIdentifierNumber, setTurnIdentifierNumber] = React.useState(
round ? round.roundNumber * round.match.maxRound + round.turnNumber : undefined
)
const [turnIdentifierNumber, setTurnIdentifierNumber] = React.useState<number | undefined>(-1)
React.useEffect(() => {
const listener = () =>
setTurnIdentifierNumber(round ? round.roundNumber * round.match.maxRound + round.turnNumber : undefined)
Expand All @@ -268,7 +270,7 @@ export function useTurnNumber(): { current: number; max: number } | undefined {
max: round.turnsLength || 0
}
: undefined,
[round, turnIdentifierNumber, round?.turnNumber]
[round, round?.roundNumber, round?.turnNumber]
)
}

Expand Down
3 changes: 3 additions & 0 deletions client/src/playback/Match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ export default class Match {
this._jumpToTurn(targetTurn)
}

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

Expand Down
6 changes: 6 additions & 0 deletions client/src/playback/Round.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import assert from 'assert'

export default class Round {
public turnNumber: number = 0
public lastSteppedRobotId: number | undefined = undefined
private initialRoundState: Round | null = null

constructor(
Expand Down Expand Up @@ -83,6 +84,9 @@ export default class Round {
}
}

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

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

this.turnNumber += 1

this.lastSteppedRobotId = turn.robotId()
}

public copy(): Round {
Expand Down
17 changes: 17 additions & 0 deletions engine/src/main/battlecode/server/GameMaker.java
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,14 @@ public void addDamageAction(int damagedRobotID, int damage){
});
}

// Moppers send damage actions when removing paint for per turn visualization
public void addRemovePaintAction(int affectedRobotID, int amountRemoved){
applyToBuilders((builder) -> {
int action = DamageAction.createDamageAction(builder, affectedRobotID, amountRemoved);
builder.addAction(action, Action.DamageAction);
});
}

/// Visually indicate a tile has been painted
public void addPaintAction(MapLocation loc, boolean isSecondary){
applyToBuilders((builder) -> {
Expand Down Expand Up @@ -585,6 +593,15 @@ public void addTransferAction(int otherRobotID, int amount){
});
}

//IMPORTANT: We are overloading the transferAction for this and must
// maintain invariant that 0 resource transfers are not allowed by engine.
public void addCompleteResourcePatternAction(MapLocation loc){
applyToBuilders((builder) -> {
int action = TransferAction.createTransferAction(builder, locationToInt(loc), 0);
builder.addAction(action, Action.TransferAction);
});
}

/// Visually indicate messaging from one robot to another
public void addMessageAction(int receiverID, int data){
applyToBuilders((builder) -> {
Expand Down
7 changes: 5 additions & 2 deletions engine/src/main/battlecode/world/InternalRobot.java
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ public void mopperAttack(MapLocation loc, boolean useSecondaryColor) {
robot.addPaint(-GameConstants.MOPPER_ATTACK_PAINT_DEPLETION);
addPaint(GameConstants.MOPPER_ATTACK_PAINT_ADDITION);
this.gameWorld.getMatchMaker().addAttackAction(robot.getID());
this.gameWorld.getMatchMaker().addRemovePaintAction(robot.getID(), GameConstants.MOPPER_ATTACK_PAINT_DEPLETION);
}
}

Expand Down Expand Up @@ -486,6 +487,7 @@ public void mopSwing(Direction dir) { // NOTE: only works for moppers!
if(this.team != robot.getTeam()){
robot.addPaint(-GameConstants.MOPPER_SWING_PAINT_DEPLETION);
affectedIDs.add(robot.ID);
this.gameWorld.getMatchMaker().addRemovePaintAction(robot.getID(), GameConstants.MOPPER_SWING_PAINT_DEPLETION);
}
}
}
Expand Down Expand Up @@ -597,10 +599,11 @@ public void processEndOfTurn() {

if (this.getType().isRobotType()){
Team owningTeam = this.gameWorld.teamFromPaint(this.gameWorld.getPaint(this.location));
int multiplier = this.getType() == UnitType.MOPPER ? GameConstants.MOPPER_PAINT_PENALTY_MULTIPLIER : 1;
if (owningTeam == Team.NEUTRAL) {
this.addPaint(-GameConstants.PENALTY_NEUTRAL_TERRITORY);
this.addPaint(-GameConstants.PENALTY_NEUTRAL_TERRITORY*multiplier);
} else if (owningTeam == this.getTeam().opponent()) {
this.addPaint(-GameConstants.PENALTY_ENEMY_TERRITORY);
this.addPaint(-GameConstants.PENALTY_ENEMY_TERRITORY*multiplier);
int allyRobotCount = 0;
for (InternalRobot robot : this.gameWorld.getAllRobotsWithinRadiusSquared(this.location, 2, this.team)){
if (robot.ID != this.ID)
Expand Down
Loading

0 comments on commit e0d244b

Please sign in to comment.