show projectiles in viewer

This commit is contained in:
Robin Appelman 2024-12-05 21:30:49 +01:00
commit 22ad43a4b7
7 changed files with 215 additions and 89 deletions

8
package-lock.json generated
View file

@ -6,7 +6,7 @@
"": {
"dependencies": {
"@demostf/edit": "0.2.0",
"@demostf/tf-demos-viewer": "^0.1.2",
"@demostf/tf-demos-viewer": "^0.2.0",
"@lutaok/solid-modal": "^0.1.1",
"@solid-primitives/autofocus": "^0.0.111",
"@solid-primitives/keyboard": "^1.2.8",
@ -23,9 +23,9 @@
"integrity": "sha512-s9wk3QVm+aTpMhIyfdGIHRm5qHp7FQ1dq/Jn0fms+lXsB1xY3wgjfWH+5gwRjjo/Dd3UMNM0o3atjO2uh+CxOQ=="
},
"node_modules/@demostf/tf-demos-viewer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@demostf/tf-demos-viewer/-/tf-demos-viewer-0.1.2.tgz",
"integrity": "sha512-BeeUWsFmICuma2FWJK4gjrILwwdgZbeCm2TELZfcJDV4QYfmukI8weNdQb3RFurxcebFZ0IER9fA0oXAdiGYpw=="
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@demostf/tf-demos-viewer/-/tf-demos-viewer-0.2.0.tgz",
"integrity": "sha512-Ef/hlTGWPknog1mXnJJ2Uz158Ud6vyLaUBlQ7j1rI/772zKmVeQEfnKY5ut5ZlfoELwNH5Aj3+QBSqSU/DV19A=="
},
"node_modules/@lutaok/solid-modal": {
"version": "0.1.1",

View file

@ -1,7 +1,7 @@
{
"dependencies": {
"@demostf/edit": "0.2.0",
"@demostf/tf-demos-viewer": "^0.1.2",
"@demostf/tf-demos-viewer": "^0.2.0",
"@lutaok/solid-modal": "^0.1.1",
"@solid-primitives/autofocus": "^0.0.111",
"@solid-primitives/keyboard": "^1.2.8",

View file

@ -211,6 +211,7 @@ export const Analyser = (props: AnalyseProps) => {
const players = () => parser.getPlayersAtTick(tick());
const buildings = () => parser.getBuildingsAtTick(tick());
const projectiles = () => parser.getProjectilesAtTick(tick());
const kills = parser.getKills();
const playButtonText = () => (playing()) ? '⏸' : '▶️';
const inShared = session && !session.isOwner();
@ -226,6 +227,7 @@ export const Analyser = (props: AnalyseProps) => {
<MapRender size={worldSize}
players={players()}
buildings={buildings()}
projectiles={projectiles()}
header={props.header}
world={backgroundBoundaries}
scale={scale()}/>

View file

@ -1,4 +1,4 @@
import {ParsedDemo, PlayerState, WorldBoundaries, Kill, BuildingState} from "./Parser";
import {BuildingState, Kill, ParsedDemo, PlayerState, ProjectileState, ProjectileType, WorldBoundaries} from "./Parser";
function getCacheBuster(): string {
const url = document.querySelector('script[src*="viewer"]').attributes.src.value;
@ -33,7 +33,7 @@ export class AsyncParser {
const cachedData: ParsedDemo = event.data.demo;
console.log(`packed data: ${(cachedData.data.length / (1024 * 1024)).toFixed(1)}MB`);
this.world = cachedData.world;
this.demo = new ParsedDemo(cachedData.playerCount, cachedData.buildingCount, cachedData.world, cachedData.header, cachedData.data, cachedData.kills, cachedData.playerInfo, cachedData.tickCount);
this.demo = new ParsedDemo(cachedData.playerCount, cachedData.buildingCount, cachedData.projectileCount, cachedData.world, cachedData.header, cachedData.data, cachedData.kills, cachedData.playerInfo, cachedData.tickCount);
resolve(this.demo);
}
}
@ -61,6 +61,18 @@ export class AsyncParser {
return buildings;
}
getProjectilesAtTick(tick: number): ProjectileState[] {
const projectiles: ProjectileState[] = [];
for (let i = 0; i < this.demo.projectileCount; i++) {
const projectile = this.demo.getProjectile(tick, i);
if (projectile.projectileType !== ProjectileType.Unknown && projectile.position.x > this.world.boundary_min.x && projectile.position.y > this.world.boundary_min.y) {
projectiles.push(projectile);
}
}
return projectiles;
}
getKills(): Kill[] {
return this.demo.kills
}

View file

@ -20,6 +20,7 @@ export async function parseDemo(bytes: Uint8Array, progressCallback: (progress:
let playerCount = state.player_count;
let buildingCount = state.building_count;
let projectileCount = state.projectile_count;
let boundaries = state.boundaries;
let interval_per_tick = state.interval_per_tick;
let tickCount = state.tick_count;
@ -56,6 +57,7 @@ export async function parseDemo(bytes: Uint8Array, progressCallback: (progress:
return new ParsedDemo(
playerCount,
buildingCount,
projectileCount,
{
boundary_min: {
x: boundaries.boundary_min.x,
@ -115,6 +117,16 @@ export enum BuildingType {
Unknown = 7,
}
export enum ProjectileType {
Rocket = 0,
HealingArrow = 1,
Sticky = 2,
Pipe = 3,
Flare = 4,
LooseCannon = 5,
Unknown = 7,
}
export interface WorldBoundaries {
boundary_min: {
x: number,
@ -151,6 +163,16 @@ export interface BuildingState {
buildingType: BuildingType,
}
export interface ProjectileState {
position: {
x: number,
y: number
},
angle: number,
team: Team,
projectileType: ProjectileType,
}
export interface Header {
interval_per_tick: number,
map: string
@ -177,6 +199,7 @@ function unpack_angle(val: number): number {
export class ParsedDemo {
public readonly playerCount: number;
public readonly buildingCount: number;
public readonly projectileCount: number;
public readonly world: WorldBoundaries;
public readonly data: Uint8Array;
public readonly header: Header;
@ -184,9 +207,10 @@ export class ParsedDemo {
public readonly kills: Kill[];
public readonly playerInfo: PlayerInfo[];
constructor(playerCount: number, buildingCount: number, world: WorldBoundaries, header: Header, data: Uint8Array, kills: Kill[], playerInfo: PlayerInfo[], tickCount: number) {
constructor(playerCount: number, buildingCount: number, projectileCount: number, world: WorldBoundaries, header: Header, data: Uint8Array, kills: Kill[], playerInfo: PlayerInfo[], tickCount: number) {
this.playerCount = playerCount;
this.buildingCount = buildingCount;
this.projectileCount = projectileCount;
this.world = world;
this.header = header;
this.data = data;
@ -212,10 +236,22 @@ export class ParsedDemo {
const base = (this.playerCount * this.tickCount * PLAYER_PACK_SIZE) + ((buildingIndex * this.tickCount) + tick) * BUILDING_PACK_SIZE;
return unpackBuilding(this.data, base, this.world);
}
getProjectile(tick: number, projectileIndex: number): ProjectileState {
if (projectileIndex >= this.projectileCount) {
throw new Error("Projectile out of bounds");
}
const base = (this.playerCount * this.tickCount * PLAYER_PACK_SIZE) +
(this.buildingCount * this.tickCount * BUILDING_PACK_SIZE) +
((projectileIndex * this.tickCount) + tick) * PROJECTILE_PACK_SIZE;
return unpackProjectile(this.data, base, this.world);
}
}
const PLAYER_PACK_SIZE = 8;
const BUILDING_PACK_SIZE = 7;
const PROJECTILE_PACK_SIZE = 6;
function unpackPlayer(bytes: Uint8Array, base: number, world: WorldBoundaries, info: PlayerInfo): PlayerState {
const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x);
@ -257,3 +293,19 @@ function unpackBuilding(bytes: Uint8Array, base: number, world: WorldBoundaries)
level,
}
}
function unpackProjectile(bytes: Uint8Array, base: number, world: WorldBoundaries): ProjectileState {
const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x);
const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y);
const team_type = bytes[base + 4];
const team = (((team_type >> 4) & 1) === 0) ? Team.Blue : Team.Red;
const projectileType = ((team_type >> 5) & 7) as ProjectileType;
const angle = unpack_angle(bytes[base + 5]);
return {
position: {x, y},
angle,
team,
projectileType,
}
}

View file

@ -1,13 +1,15 @@
import {Player as PlayerDot} from './Render/Player';
import {Building as BuildingDot} from './Render/Building';
import {Projectile as ProjectileDot} from './Render/Projectile';
import {findMapAlias} from './MapBoundries';
import {PlayerState, Header, WorldBoundaries, BuildingState} from "./Data/Parser";
import {createEffect, Show} from "solid-js";
import {PlayerState, Header, WorldBoundaries, BuildingState, ProjectileState} from "./Data/Parser";
import {Show} from "solid-js";
export interface MapRenderProps {
header: Header;
players: PlayerState[];
buildings: BuildingState[];
projectiles: ProjectileState[];
size: {
width: number;
height: number;
@ -33,7 +35,14 @@ export function MapRender(props: MapRenderProps) {
}</For>
<For each={props.buildings}>{(building) =>
<Show when={building.position.x}>
<BuildingDot building={building} mapBoundary={props.world} targetSize={props.size} scale={props.scale}/>
<BuildingDot building={building} mapBoundary={props.world} targetSize={props.size}
scale={props.scale}/>
</Show>
}</For>
<For each={props.projectiles}>{(projectile) =>
<Show when={projectile.position.x}>
<ProjectileDot projectile={projectile} mapBoundary={props.world} targetSize={props.size}
scale={props.scale}/>
</Show>
}</For>
</svg>

View file

@ -0,0 +1,51 @@
import {ProjectileState, ProjectileType, Team, WorldBoundaries} from "../Data/Parser";
import {Show} from "solid-js";
export interface ProjectileProp {
projectile: ProjectileState;
mapBoundary: WorldBoundaries;
targetSize: {
width: number;
height: number;
};
scale: number;
}
export function Projectile(props: ProjectileProp) {
const worldWidth = props.mapBoundary.boundary_max.x - props.mapBoundary.boundary_min.x;
const worldHeight = props.mapBoundary.boundary_max.y - props.mapBoundary.boundary_min.y;
const x = () => props.projectile.position.x - props.mapBoundary.boundary_min.x;
const y = () => props.projectile.position.y - props.mapBoundary.boundary_min.y;
const scaledX = () => x() / worldWidth * props.targetSize.width;
const scaledY = () => (worldHeight - y()) / worldHeight * props.targetSize.height;
const teamColor = () => (props.projectile.team === Team.Red) ? '#a75d50' : '#5b818f';
const transform = () => `translate(${scaledX()} ${scaledY()}) scale(${1 / props.scale})`;
const rotate = () => `rotate(${270 - props.projectile.angle})`;
try {
return <g transform={transform()}>
<Show when={projectileIsAngled(props.projectile.projectileType)}>
<polygon points="-3,-4 0,0 3,-4 0,8" stroke="white" fill={teamColor()}
transform={rotate()}/>
</Show>
<Show when={!projectileIsAngled(props.projectile.projectileType)}>
<circle r={3} stroke-width={1} stroke="white" fill={teamColor()}/>
</Show>
</g>
} catch (e) {
console.log(e);
return null;
}
}
function projectileIsAngled(type: ProjectileType): boolean {
switch (type) {
case ProjectileType.Flare:
case ProjectileType.HealingArrow:
case ProjectileType.Rocket:
return true;
default:
return false;
}
}