diff --git a/client/package-lock.json b/client/package-lock.json index 5d18e299..562d66f4 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -19,6 +19,7 @@ "pako": "^2.1.0", "path": "^0.12.7", "process": "^0.11.10", + "re-resizable": "^6.10.3", "react": "^18.2.0", "react-color": "^2.19.3", "react-custom-scrollbars-2": "^4.5.0", @@ -6756,6 +6757,15 @@ "node": ">=0.10.0" } }, + "node_modules/re-resizable": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.10.3.tgz", + "integrity": "sha512-zvWb7X3RJMA4cuSrqoxgs3KR+D+pEXnGrD2FAD6BMYAULnZsSF4b7AOVyG6pC3VVNVOtlagGDCDmZSwWLjjBBw==", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "18.2.0", "license": "MIT", diff --git a/client/package.json b/client/package.json index 1e44d6c8..1c44731f 100644 --- a/client/package.json +++ b/client/package.json @@ -138,6 +138,7 @@ "pako": "^2.1.0", "path": "^0.12.7", "process": "^0.11.10", + "re-resizable": "^6.10.3", "react": "^18.2.0", "react-color": "^2.19.3", "react-custom-scrollbars-2": "^4.5.0", diff --git a/client/src/app.tsx b/client/src/app.tsx index ece2c9b4..52a5d5cd 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -8,4 +8,10 @@ import '../style.css' const elem = document.getElementById('root') const root = ReactDom.createRoot(elem!) -root.render() +root.render( + + + + + +) diff --git a/client/src/components/sidebar/game/game.tsx b/client/src/components/sidebar/game/game.tsx index fce0fe32..20892242 100644 --- a/client/src/components/sidebar/game/game.tsx +++ b/client/src/components/sidebar/game/game.tsx @@ -122,6 +122,8 @@ export const GamePage: React.FC = React.memo((props) => {
+
+ ) : (
Select a game to see stats
diff --git a/client/src/components/sidebar/game/quick-line-chart.tsx b/client/src/components/sidebar/game/quick-line-chart.tsx index 6948c058..58d6aa7b 100644 --- a/client/src/components/sidebar/game/quick-line-chart.tsx +++ b/client/src/components/sidebar/game/quick-line-chart.tsx @@ -40,8 +40,14 @@ export const QuickLineChart: React.FC = ({ setCanvasResolution(canvas, width, height, resolution) - const max = Math.max(...data.map((d) => Math.max(d.team0, d.team1))) - const { xScale, yScale, innerWidth, innerHeight } = getAxes(width, height, margin, { x: data.length, y: max }) + let maxX = -9999999 + let maxY = -9999999 + for (const d of data) { + maxX = Math.max(maxX, d.round) + maxY = Math.max(maxY, Math.max(d.team0, d.team1)) + } + + const { xScale, yScale, innerWidth, innerHeight } = getAxes(width, height, margin, { x: maxX, y: maxY }) context.clearRect(0, 0, width, height) @@ -65,11 +71,11 @@ export const QuickLineChart: React.FC = ({ height, margin, { - range: { min: 0, max: data.length }, + range: { min: 0, max: maxX }, options: { textColor: 'white', lineColor: 'white' } }, { - range: { min: 0, max: max }, + range: { min: 0, max: maxY }, options: { textColor: 'white', lineColor: 'white' } } ) diff --git a/client/src/components/sidebar/game/resource-graph.tsx b/client/src/components/sidebar/game/resource-graph.tsx index 3dece35c..35f44399 100644 --- a/client/src/components/sidebar/game/resource-graph.tsx +++ b/client/src/components/sidebar/game/resource-graph.tsx @@ -1,34 +1,37 @@ import React from 'react' -import assert from 'assert' import { useRound } from '../../../playback/GameRunner' import Round from '../../../playback/Round' import { LineChartDataPoint, QuickLineChart } from './quick-line-chart' +import { TeamRoundStat } from '../../../playback/RoundStat' interface Props { active: boolean - property: string + property: keyof TeamRoundStat propertyDisplayName: string } -function hasKey(obj: O, key: PropertyKey): key is keyof O { - return key in obj -} -function getChartData(round: Round, property: string): LineChartDataPoint[] { - const values = [0, 1].map((index) => - round.match.stats.map((roundStat) => { - const teamStat = roundStat.getTeamStat(round.match.game.teams[index]) - assert(hasKey(teamStat, property), `TeamStat missing property '${property}' when rendering chart`) - return teamStat[property] +function getChartData(round: Round, property: keyof TeamRoundStat): LineChartDataPoint[] { + const teams = round.match.game.teams + + const result: LineChartDataPoint[] = [] + + // Sparser graph as datapoints increase + const interval = Math.ceil(round.roundNumber / 500) + + for (let i = 0; i < round.roundNumber; i += interval) { + const roundStat = round.match.stats[i] + + const team0Stat = roundStat.getTeamStat(teams[0]) + const team1Stat = roundStat.getTeamStat(teams[1]) + + result.push({ + round: i + 1, + team0: team0Stat[property] as number, + team1: team1Stat[property] as number }) - ) + } - return values[0].slice(0, round.roundNumber).map((value, index) => { - return { - round: index + 1, - team0: value as number, - team1: values[1][index] as number - } - }) + return result } export const ResourceGraph: React.FC = (props: Props) => { @@ -38,7 +41,7 @@ export const ResourceGraph: React.FC = (props: Props) => { return (

{props.propertyDisplayName}

- +
) } diff --git a/client/src/components/sidebar/game/team-table.tsx b/client/src/components/sidebar/game/team-table.tsx index 4f5b5dfd..bc7aca68 100644 --- a/client/src/components/sidebar/game/team-table.tsx +++ b/client/src/components/sidebar/game/team-table.tsx @@ -110,7 +110,10 @@ export const UnitsTable: React.FC = ({ teamStat, teamIdx }) => ['Mopper', ] ] - let data: [string, number[]][] = [['Count', [0, 0, 0, 0, 0, 0]]] + let data: [string, number[]][] = [ + ['Count', [0, 0, 0, 0, 0, 0]], + ['Paint', [0, 0, 0, 0, 0, 0]] + ] if (teamStat) { data = [ [ @@ -118,6 +121,12 @@ export const UnitsTable: React.FC = ({ teamStat, teamIdx }) => Object.values(schema.RobotType) .filter((k) => typeof k === 'number' && k !== schema.RobotType.NONE) .map((k) => teamStat.robotCounts[k as schema.RobotType]) + ], + [ + 'Paint', + Object.values(schema.RobotType) + .filter((k) => typeof k === 'number' && k !== schema.RobotType.NONE) + .map((k) => teamStat.robotPaints[k as schema.RobotType]) ] ] } 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 c2c8b540..b6febd8d 100644 --- a/client/src/components/sidebar/map-editor/map-editor-field.tsx +++ b/client/src/components/sidebar/map-editor/map-editor-field.tsx @@ -46,6 +46,7 @@ export const MapEditorBrushRowField: React.FC = (props: Props) => { case MapEditorBrushFieldType.TEAM: field = ( = ({ open, scaffold }) => { const MemoConsole = React.useMemo(() => , [consoleLines.effectiveLength()]) - if (!open) return null + if (open && !nativeAPI) return <>Run the client locally to use the runner - if (!nativeAPI) return <>Run the client locally to use the runner + /* Keep the component mounted but hidden when !open so we retain any locally resized/edited elements */ const lastLogLine = consoleLines.get(consoleLines.length() - 1) const runDisabled = !teamA || !teamB || maps.size === 0 || !langVersion return ( -
+
{!setup ? ( <> {error &&
{`Setup Error: ${error}`}
} @@ -286,7 +293,7 @@ const TeamSelector: React.FC = ({ teamA, teamB, options, onCh onChangeB(e)}> {teamB === undefined && } - {[...options].map((t) => ( + {[...options].sort().map((t) => ( @@ -331,21 +338,36 @@ const MapSelector: React.FC = ({ maps, availableMaps, onSelect return (
-
- {[...availableMaps].map((m) => { - const selected = maps.has(m) - return ( -
(maps.has(m) ? onDeselect(m) : onSelect(m))} - > - {m} - -
- ) - })} -
+ +
+ {[...availableMaps].sort().map((m) => { + const selected = maps.has(m) + return ( +
(maps.has(m) ? onDeselect(m) : onSelect(m))} + > + {m} + +
+ ) + })} +
+
) } @@ -377,6 +399,11 @@ export const Console: React.FC = ({ lines }) => { setPopout(false) GameRunner.jumpToRound(round) GameRenderer.setSelectedRobot(id) + + // If turn playback is enabled, focus the robot's exact turn as well + if (GameRunner.match?.playbackPerTurn) { + GameRunner.jumpToRobotTurn(id) + } } const ConsoleRow = (props: { index: number; style: any }) => { @@ -390,7 +417,7 @@ export const Console: React.FC = ({ lines }) => { const team = found[1] const id = Number(found[2]) const round = Number(found[3]) - const ogText = found[4] + const ogText = found[4].replace(/\n/g, ' ') return (
@@ -400,9 +427,7 @@ export const Console: React.FC = ({ lines }) => { > {`[Team ${team}, ID #${id}, Round ${round}]`} - - {ogText} - + {ogText}
) } diff --git a/client/src/components/sidebar/sidebar.tsx b/client/src/components/sidebar/sidebar.tsx index 077794df..4f261a05 100644 --- a/client/src/components/sidebar/sidebar.tsx +++ b/client/src/components/sidebar/sidebar.tsx @@ -213,7 +213,7 @@ export const Sidebar: React.FC = () => {
)} -
+
diff --git a/client/src/components/toggle.tsx b/client/src/components/toggle.tsx index d5d4da21..384ae6a0 100644 --- a/client/src/components/toggle.tsx +++ b/client/src/components/toggle.tsx @@ -8,13 +8,14 @@ interface OptionProp { } interface Props { + initialValue?: string options: Record onChange: (value: any) => void flipOnRightClickCanvas?: boolean } export const Toggle: React.FC = (props: Props) => { - const [value, setValue] = React.useState(Object.values(props.options)[0].value) + const [value, setValue] = React.useState(props.initialValue ?? Object.values(props.options)[0].value) const { canvasRightClick } = GameRenderer.useCanvasClickEvents() const onClick = (val: any) => { diff --git a/client/src/constants.ts b/client/src/constants.ts index 49f12997..06cba4f0 100644 --- a/client/src/constants.ts +++ b/client/src/constants.ts @@ -1,4 +1,4 @@ -export const CLIENT_VERSION = '1.4.0' +export const CLIENT_VERSION = '1.4.1' export const SPEC_VERSION = '1' export const BATTLECODE_YEAR: number = 2025 export const MAP_SIZE_RANGE = { diff --git a/client/src/playback/Actions.ts b/client/src/playback/Actions.ts index 18e562d3..d264265e 100644 --- a/client/src/playback/Actions.ts +++ b/client/src/playback/Actions.ts @@ -40,7 +40,13 @@ export default class Actions { for (let i = 0; i < this.actions.length; i++) { this.actions[i].duration-- if (this.actions[i].duration == 0) { - this.actions.splice(i, 1) + // If action render order matters, use this (slower) + //this.actions.splice(i, 1) + + // Otherwise, this is faster + this.actions[i] = this.actions[this.actions.length - 1] + this.actions.pop() + i-- } } diff --git a/client/src/playback/Bodies.ts b/client/src/playback/Bodies.ts index 8dcf63e0..4d6937cf 100644 --- a/client/src/playback/Bodies.ts +++ b/client/src/playback/Bodies.ts @@ -5,7 +5,7 @@ import Round from './Round' import * as renderUtils from '../util/RenderUtil' import { MapEditorBrush } from '../components/sidebar/map-editor/MapEditorBrush' import { StaticMap } from './Map' -import { Vector } from './Vector' +import { Vector, vectorDistSquared, vectorEq } from './Vector' import { Colors, currentColors } from '../colors' import { INDICATOR_DOT_SIZE, @@ -145,21 +145,22 @@ export default class Bodies { draw( match: Match, - ctx: CanvasRenderingContext2D, - overlayCtx: CanvasRenderingContext2D, + bodyCtx: CanvasRenderingContext2D | null, + overlayCtx: CanvasRenderingContext2D | null, config: ClientConfig, selectedBodyID?: number, hoveredTile?: Vector ): void { for (const body of this.bodies.values()) { - body.draw( - match, - ctx, - overlayCtx, - config, - body.id === selectedBodyID, - body.pos.x === hoveredTile?.x && body.pos.y === hoveredTile?.y - ) + if (bodyCtx) { + body.draw(match, bodyCtx) + } + + const selected = selectedBodyID === body.id + const hovered = !!hoveredTile && vectorEq(body.pos, hoveredTile) + if (overlayCtx) { + body.drawOverlay(match, overlayCtx, config, selected, hovered) + } } } @@ -245,40 +246,41 @@ export class Body { return this.game.robotTypeMetadata.get(this.robotType) ?? assert.fail('Robot missing metadata!') } - public draw( + public drawOverlay( match: Match, ctx: CanvasRenderingContext2D, - overlayCtx: CanvasRenderingContext2D, config: ClientConfig, selected: boolean, hovered: boolean ): void { + if (!this.game.playable) return + + // Draw various statuses + const focused = selected || hovered + if (focused) { + this.drawPath(match, ctx) + } + if (focused || config.showAllRobotRadii) { + this.drawRadii(match, ctx, !selected) + } + if (focused || config.showAllIndicators) { + this.drawIndicators(match, ctx, !selected && !config.showAllIndicators) + } + if (focused || config.showHealthBars) { + this.drawHealthBar(match, ctx) + } + if (focused || config.showPaintBars) { + this.drawPaintBar(match, ctx, focused || config.showHealthBars) + } + } + + public draw(match: Match, ctx: CanvasRenderingContext2D): void { const pos = this.getInterpolatedCoords(match) const renderCoords = renderUtils.getRenderCoords(pos.x, pos.y, match.currentRound.map.staticMap.dimension) if (this.dead) ctx.globalAlpha = 0.5 renderUtils.renderCenteredImageOrLoadingIndicator(ctx, getImageIfLoaded(this.imgPath), renderCoords, this.size) ctx.globalAlpha = 1 - - // Draw various statuses - const focused = selected || hovered - if (this.game.playable) { - if (focused) { - this.drawPath(match, overlayCtx) - } - if (focused || config.showAllRobotRadii) { - this.drawRadii(match, overlayCtx, !selected) - } - if (focused || config.showAllIndicators) { - this.drawIndicators(match, overlayCtx, !selected && !config.showAllIndicators) - } - if (focused || config.showHealthBars) { - this.drawHealthBar(match, overlayCtx) - } - if (focused || config.showPaintBars) { - this.drawPaintBar(match, overlayCtx, focused || config.showHealthBars) - } - } } private drawPath(match: Match, ctx: CanvasRenderingContext2D) { @@ -570,16 +572,8 @@ export const BODY_DEFINITIONS: Record = { this.size = 1.5 } - public draw( - match: Match, - ctx: CanvasRenderingContext2D, - overlayCtx: CanvasRenderingContext2D, - config: ClientConfig, - selected: boolean, - hovered: boolean - ): void { - super.draw(match, ctx, overlayCtx, config, selected, hovered) - super.drawLevel(match, ctx) + public draw(match: Match, ctx: CanvasRenderingContext2D): void { + super.draw(match, ctx) } }, @@ -594,16 +588,8 @@ export const BODY_DEFINITIONS: Record = { this.size = 1.5 } - public draw( - match: Match, - ctx: CanvasRenderingContext2D, - overlayCtx: CanvasRenderingContext2D, - config: ClientConfig, - selected: boolean, - hovered: boolean - ): void { - super.draw(match, ctx, overlayCtx, config, selected, hovered) - super.drawLevel(match, ctx) + public draw(match: Match, ctx: CanvasRenderingContext2D): void { + super.draw(match, ctx) } }, @@ -618,16 +604,8 @@ export const BODY_DEFINITIONS: Record = { this.size = 1.5 } - public draw( - match: Match, - ctx: CanvasRenderingContext2D, - overlayCtx: CanvasRenderingContext2D, - config: ClientConfig, - selected: boolean, - hovered: boolean - ): void { - super.draw(match, ctx, overlayCtx, config, selected, hovered) - super.drawLevel(match, ctx) + public draw(match: Match, ctx: CanvasRenderingContext2D): void { + super.draw(match, ctx) } }, @@ -641,15 +619,8 @@ export const BODY_DEFINITIONS: Record = { this.imgPath = `robots/${this.team.colorName.toLowerCase()}/mopper_64x64.png` } - public draw( - match: Match, - ctx: CanvasRenderingContext2D, - overlayCtx: CanvasRenderingContext2D, - config: ClientConfig, - selected: boolean, - hovered: boolean - ): void { - super.draw(match, ctx, overlayCtx, config, selected, hovered) + public draw(match: Match, ctx: CanvasRenderingContext2D): void { + super.draw(match, ctx) } }, @@ -663,15 +634,8 @@ export const BODY_DEFINITIONS: Record = { this.imgPath = `robots/${this.team.colorName.toLowerCase()}/soldier_64x64.png` } - public draw( - match: Match, - ctx: CanvasRenderingContext2D, - overlayCtx: CanvasRenderingContext2D, - config: ClientConfig, - selected: boolean, - hovered: boolean - ): void { - super.draw(match, ctx, overlayCtx, config, selected, hovered) + public draw(match: Match, ctx: CanvasRenderingContext2D): void { + super.draw(match, ctx) } }, @@ -685,15 +649,8 @@ export const BODY_DEFINITIONS: Record = { this.imgPath = `robots/${this.team.colorName.toLowerCase()}/splasher_64x64.png` } - public draw( - match: Match, - ctx: CanvasRenderingContext2D, - overlayCtx: CanvasRenderingContext2D, - config: ClientConfig, - selected: boolean, - hovered: boolean - ): void { - super.draw(match, ctx, overlayCtx, config, selected, hovered) + public draw(match: Match, ctx: CanvasRenderingContext2D): void { + super.draw(match, ctx) } } } diff --git a/client/src/playback/GameRenderer.ts b/client/src/playback/GameRenderer.ts index 355f30e9..847cefb7 100644 --- a/client/src/playback/GameRenderer.ts +++ b/client/src/playback/GameRenderer.ts @@ -94,6 +94,17 @@ class GameRendererClass { return this.canvas(layer).getContext('2d') } + renderOverlay() { + const ctx = this.ctx(CanvasLayers.Overlay) + const match = GameRunner.match + if (!match || !ctx) return + + const currentRound = match.currentRound + + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) + currentRound.bodies.draw(match, null, ctx, GameConfig.config, this.selectedBodyID, this.mouseTile) + } + render() { const ctx = this.ctx(CanvasLayers.Dynamic) const overlayCtx = this.ctx(CanvasLayers.Overlay) @@ -158,7 +169,7 @@ class GameRendererClass { if (!newTile) return if (newTile.x !== this.mouseTile?.x || newTile.y !== this.mouseTile?.y) { this.mouseTile = newTile - this.render() + this.renderOverlay() this._trigger(this._canvasHoverListeners) } } diff --git a/client/src/playback/GameRunner.ts b/client/src/playback/GameRunner.ts index 513a00ee..44710f9a 100644 --- a/client/src/playback/GameRunner.ts +++ b/client/src/playback/GameRunner.ts @@ -172,6 +172,14 @@ class GameRunnerClass { this._trigger(this._turnListeners) } + jumpToRobotTurn(robotId: number) { + if (!this.match) return + // explicit rerender at the end so a render doesnt occur between these two steps + this.match._jumpToRobotTurn(robotId) + GameRenderer.render() + this._trigger(this._turnListeners) + } + jumpToStart() { if (!this.match) return // explicit rerender at the end so a render doesnt occur between these two steps diff --git a/client/src/playback/Match.ts b/client/src/playback/Match.ts index 4f3bbd35..57ea2c3d 100644 --- a/client/src/playback/Match.ts +++ b/client/src/playback/Match.ts @@ -10,7 +10,7 @@ import * as Profiler from './Profiler' import * as Timeline from './Timeline' // Amount of rounds before a snapshot of the game state is saved for the next recalculation -const SNAPSHOT_EVERY = 50 +const SNAPSHOT_EVERY = 40 // Amount of simulation steps before the round counter is progressed const MAX_SIMULATION_STEPS = 50000 @@ -225,7 +225,7 @@ export default class Match { } /** - * Jump to a turn within the current round's turns.R + * Jump to a turn within the current round's turns. */ public _jumpToTurn(turn: number): void { if (!this.game.playable) return @@ -235,6 +235,17 @@ export default class Match { this.currentRound.jumpToTurn(turn) } + /** + * Jump to the turn of a specific robot within the current round's turns, if it exists. + */ + public _jumpToRobotTurn(robotId: number): void { + if (!this.game.playable) return + + this._roundSimulation() + + this.currentRound.jumpToRobotTurn(robotId) + } + private _updateSimulationRoundsByTime(deltaTime: number): void { // This works because of the way floor works const deltaRounds = Math.floor(this._currentSimulationStep / MAX_SIMULATION_STEPS) @@ -315,6 +326,12 @@ export default class Match { ? this.currentRound : closestSnapshot.copy() + // While we are jumping (forward) to a round, mark the intermediate rounds + // as transient. This way, we do not store initial state for these rounds that + // we are simply passing through, as they will never have any sort of backward + // turn stepping. + updatingRound.isTransient = true + while (updatingRound.roundNumber < roundNumber) { // Fully apply the previous round by applying each turn sequentially updatingRound.jumpToTurn(updatingRound.turnsLength) @@ -330,6 +347,8 @@ export default class Match { } } + updatingRound.isTransient = false + this.currentRound = updatingRound } diff --git a/client/src/playback/Round.ts b/client/src/playback/Round.ts index b0769141..44232237 100644 --- a/client/src/playback/Round.ts +++ b/client/src/playback/Round.ts @@ -10,6 +10,7 @@ import assert from 'assert' export default class Round { public turnNumber: number = 0 public lastSteppedRobotId: number | undefined = undefined + public isTransient: boolean = false private initialRoundState: Round | null = null constructor( @@ -84,6 +85,21 @@ export default class Round { } } + /** + * Jumps to a specific robot's turn in the round, if it exists. + */ + public jumpToRobotTurn(robotId: number): void { + if (!this.currentDelta) return // Final round does not have a delta, so there is nothing to jump to + + for (let i = 0; i < this.currentDelta.turnsLength(); i++) { + const turn = this.currentDelta.turns(i)! + if (turn.robotId() === robotId) { + this.jumpToTurn(i + 1) + return + } + } + } + /** * Step the current turn within the current delta. */ @@ -93,7 +109,7 @@ export default class Round { const turn = this.currentDelta!.turns(this.turnNumber) assert(turn, 'Turn not found to step to') - if (!this.initialRoundState) { + if (!this.initialRoundState && !this.isTransient) { // Store the initial round state for resetting only after stepping, that way snapshots dont need to store it, halving memory usage assert(this.turnNumber === 0, 'Initial round state should only be set at turn 0') this.initialRoundState = this.copy() diff --git a/client/src/playback/RoundStat.ts b/client/src/playback/RoundStat.ts index 6a18981d..14b0d56f 100644 --- a/client/src/playback/RoundStat.ts +++ b/client/src/playback/RoundStat.ts @@ -15,7 +15,9 @@ const EMPTY_ROBOT_COUNTS: Record = { export class TeamRoundStat { robotCounts: Record = { ...EMPTY_ROBOT_COUNTS } + robotPaints: Record = { ...EMPTY_ROBOT_COUNTS } moneyAmount: number = 0 + totalPaint: number = 0 paintPercent: number = 0 resourcePatterns: number = 0 @@ -24,6 +26,7 @@ export class TeamRoundStat { // Copy any internal objects here newStat.robotCounts = { ...this.robotCounts } + newStat.robotPaints = { ...this.robotPaints } return newStat } @@ -98,9 +101,11 @@ export default class RoundStat { } } - // Clear robot counts for recomputing + // Clear values for recomputing for (const stat of this.teams.values()) { + stat.totalPaint = 0 stat.robotCounts = { ...EMPTY_ROBOT_COUNTS } + stat.robotPaints = { ...EMPTY_ROBOT_COUNTS } } // Compute total robot counts @@ -111,6 +116,8 @@ export default class RoundStat { if (body.dead) continue teamStat.robotCounts[body.robotType]++ + teamStat.robotPaints[body.robotType] += body.paint + teamStat.totalPaint += body.paint } const timems = Date.now() - time diff --git a/client/src/playback/Vector.ts b/client/src/playback/Vector.ts index a15de51d..c898c578 100644 --- a/client/src/playback/Vector.ts +++ b/client/src/playback/Vector.ts @@ -6,6 +6,7 @@ export interface Vector { export const getEmptyVector = () => ({ x: 0, y: 0 }) as Vector export const vectorLength = (a: Vector) => Math.sqrt(a.x * a.x + a.y * a.y) export const vectorLengthSquared = (a: Vector) => a.x * a.x + a.y * a.y +export const vectorEq = (a: Vector, b: Vector) => a.x === b.x && a.y === b.y export const vectorDot = (a: Vector, b: Vector) => a.x * b.x + a.y * b.y export const vectorAdd = (a: Vector, b: Vector) => ({ x: a.x + b.x, y: a.y + b.y }) export const vectorSub = (a: Vector, b: Vector) => ({ x: a.x - b.x, y: a.y - b.y }) diff --git a/specs/specs.pdf b/specs/specs.pdf index dcd204d0..f8d634e3 100644 Binary files a/specs/specs.pdf and b/specs/specs.pdf differ