some basic viewer

This commit is contained in:
Robin Appelman 2023-04-29 14:54:53 +02:00
commit 5910b2f35a
45 changed files with 1089 additions and 1436 deletions

View file

@ -1,42 +0,0 @@
.analyse-menu {
position: absolute;
top: 0;
left: 0;
opacity: 0.5;
transition: all 0.5s;
&:hover {
opacity: 1;
}
.share-session {
background: transparent;
color: var(--primary-color);
font-size: 200%;
width: 32px;
height: 32px;
margin: 10px;
border: none;
cursor: pointer;
background-image: url("../images/link_white.svg");
background-size: contain;
&:active, &:focus {
outline: none;
}
}
.share-text {
color: var(--primary-color);
background-color: var(--text-secondary);
padding: 5px;
margin-top: 0;
border: 1px #888 solid;
border-radius: 5px;
&:active, &:focus {
outline: none;
}
}
}

View file

@ -1,75 +0,0 @@
.map-holder {
position: fixed;
top: 32px;
left: 0;
width: 100%;
height: calc(100% - 32px - 100px);
}
.time-control {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background-color: var(--primary-color-accent);
.timeline {
position: absolute;
bottom: 0;
left: 64px;
width: calc(100% - 64px);
}
.play-pause-button {
position: absolute;
bottom: 0;
left: 0;
height: 100px;
width: 64px;
background-color: transparent;
color: black;
font-size: 200%;
border: none;
&:focus, &:active {
outline: none;
}
}
}
.error-holder {
.error-image {
text-align: center;
padding: 20px;
font-size: 250%;
&::after {
display: block;
content: '';
background-size: contain;
background-repeat: no-repeat;
background-position: 50% 50%;
background-image: url('../images/teleporter.png');
width: 100%;
height: 500px;
margin: 50px 0;
}
}
.error {
background-color: #FF9494;
line-height: 32px;
margin: 0 -30px;
padding: 32px;
padding-left: 74px;
background-image: url('../images/error.png');
background-size: 32px;
background-repeat: no-repeat;
background-position: 32px 32px;
.error-hint {
margin-top: 32px;
}
}
}

View file

@ -4,7 +4,7 @@ import {throttle, debounce} from 'throttle-debounce';
import {Timeline} from './Render/Timeline';
import {SpecHUD} from './Render/SpecHUD';
import {AnalyseMenu} from './AnalyseMenu'
import {Header, WorldBoundaries} from "@demostf/parser-worker";
import {Header, WorldBoundaries} from "./Data/Parser";
import {AsyncParser} from "./Data/AsyncParser";
import {getMapBoundaries} from "./MapBoundries";
@ -21,10 +21,10 @@ export const Analyser = (props: AnalyseProps) => {
const parser = props.parser;
const intervalPerTick = props.header.interval_per_tick;
const [tick, setTick] = createSignal<number>();
const [scale, setScale] = createSignal<number>();
const [playing, setPlaying] = createSignal<boolean>();
const [sessionName, setSessionName] = createSignal<string>();
const [tick, setTick] = createSignal<number>(0);
const [scale, setScale] = createSignal<number>(1);
const [playing, setPlaying] = createSignal<boolean>(false);
const [sessionName, setSessionName] = createSignal<string>("");
let lastFrameTime = 0;
let playStartTick = 0;
@ -165,9 +165,9 @@ export const Analyser = (props: AnalyseProps) => {
<div class="time-control"
title={`${tickToTime(tick(), intervalPerTick)} (tick ${tick()})`}>
<input class="play-pause-button" type="button"
onClick={togglePlay}
value={playButtonText()}
disabled={disabled}
onClick={togglePlay}
/>
<Timeline parser={parser} tick={tick()}
onSetTick={throttle(50, (tick) => {

View file

@ -1,58 +0,0 @@
"use strict";
exports.__esModule = true;
exports.AsyncParser = void 0;
var parser_worker_1 = require("@demostf/parser-worker");
var AsyncParser = /** @class */ (function () {
function AsyncParser(buffer, progressCallback) {
this.buffer = buffer;
this.progressCallback = progressCallback;
}
AsyncParser.prototype.cache = function () {
var _this = this;
return new Promise(function (resolve, reject) {
var worker = new Worker(new URL('./ParseWorker.ts', import.meta.url));
worker.postMessage({
buffer: _this.buffer
}, [_this.buffer]);
worker.onmessage = function (event) {
if (event.data.error) {
reject(event.data.error);
return;
}
else if (event.data.progress) {
_this.progressCallback(event.data.progress);
return;
}
else if (event.data.demo) {
var cachedData = event.data.demo;
console.log("packed data: ".concat((cachedData.data.length / (1024 * 1024)).toFixed(1), "MB"));
_this.world = cachedData.world;
_this.demo = new parser_worker_1.ParsedDemo(cachedData.playerCount, cachedData.buildingCount, cachedData.world, cachedData.header, cachedData.data, cachedData.kills, cachedData.playerInfo, cachedData.tickCount);
resolve(_this.demo);
}
};
});
};
AsyncParser.prototype.getPlayersAtTick = function (tick) {
var players = [];
for (var i = 0; i < this.demo.playerCount; i++) {
players.push(this.demo.getPlayer(tick, i));
}
return players;
};
AsyncParser.prototype.getBuildingsAtTick = function (tick) {
var buildings = [];
for (var i = 0; i < this.demo.buildingCount; i++) {
var building = this.demo.getBuilding(tick, i);
if (building.health > 0) {
buildings.push(building);
}
}
return buildings;
};
AsyncParser.prototype.getKills = function () {
return this.demo.kills;
};
return AsyncParser;
}());
exports.AsyncParser = AsyncParser;

View file

@ -1,5 +1,9 @@
import {ParsedDemo, PlayerState, WorldBoundaries, Header, Kill, BuildingState} from "@demostf/parser-worker";
import {getMapBoundaries} from "../MapBoundries";
import {ParsedDemo, PlayerState, WorldBoundaries, Kill, BuildingState} from "./Parser";
function getCacheBuster(): string {
const url = document.querySelector('script[src*="viewer"]').attributes.src.value;
return url.substring("/viewer.js".length);
}
export class AsyncParser {
buffer: ArrayBuffer;
@ -14,7 +18,7 @@ export class AsyncParser {
cache(): Promise<ParsedDemo> {
return new Promise((resolve, reject) => {
const worker = new Worker(new URL('./ParseWorker.ts', import.meta.url));
const worker = new Worker(`/parse-worker.js${getCacheBuster()}`);
worker.postMessage({
buffer: this.buffer
}, [this.buffer]);

View file

@ -1,25 +0,0 @@
"use strict";
exports.__esModule = true;
var parser_worker_1 = require("@demostf/parser-worker");
/**
* @global postMessage
* @param event
*/
onmessage = function (event) {
var buffer = event.data.buffer;
var bytes = new Uint8Array(buffer);
(0, parser_worker_1.parseDemo)(bytes, function (progress) {
postMessage({
progress: progress
});
}).then(function (parsed) {
postMessage({
demo: parsed
}, [parsed.data.buffer]);
})["catch"](function (e) {
console.error(e);
postMessage({
error: e.message
});
});
};

View file

@ -1,4 +1,4 @@
import {parseDemo} from "@demostf/parser-worker";
import {parseDemo} from "./Parser";
declare function postMessage(message: any, transfer?: any[]): void;
@ -23,5 +23,4 @@ onmessage = (event: MessageEvent) => {
error: e.message
});
});
};

View file

@ -0,0 +1,259 @@
import {
get_assister_ids,
get_attacker_ids, get_data,
get_kill_ticks,
get_map, get_player_entity_id,
get_player_name, get_player_steam_id, get_player_user_id, get_victim_ids, get_weapon,
parse_demo,
XY
} from '@demostf/tf-demos-viewer';
import viewer from "@demostf/tf-demos-viewer";
function getCacheBuster(): string {
const url = self.location.href;
return url.substring(url.indexOf('?'));
}
export async function parseDemo(bytes: Uint8Array, progressCallback: (progress: number) => void): Promise<ParsedDemo> {
await viewer(`/tf-demo-viewer.wasm${getCacheBuster()}`);
const state = parse_demo(bytes, progressCallback);
let playerCount = state.player_count;
let buildingCount = state.building_count;
let boundaries = state.boundaries;
let interval_per_tick = state.interval_per_tick;
let tickCount = state.tick_count;
let kill_ticks = get_kill_ticks(state);
let attackers = get_attacker_ids(state);
let assisters = get_assister_ids(state);
let victims = get_victim_ids(state);
let playerInfo = [];
for (let i = 0; i < playerCount; i++) {
playerInfo.push({
name: get_player_name(state, i),
steamId: get_player_steam_id(state, i),
entityId: get_player_entity_id(state, i),
userId: get_player_user_id(state, i),
})
}
let kills = [];
for (let i = 0; i < kill_ticks.length; i++) {
kills.push({
tick: kill_ticks[i],
attacker: attackers[i],
assister: assisters[i],
victim: victims[i],
weapon: get_weapon(state, i),
})
}
let map = get_map(state);
let data = get_data(state);
return new ParsedDemo(
playerCount,
buildingCount,
{
boundary_min: {
x: boundaries.boundary_min.x,
y: boundaries.boundary_min.y,
},
boundary_max: {
x: boundaries.boundary_max.x,
y: boundaries.boundary_max.y,
}
},
{
map,
interval_per_tick
},
data,
kills,
playerInfo,
tickCount,
);
}
export interface PlayerInfo {
entityId: number,
name: string,
steamId: string,
userId: number,
}
export enum Team {
Other = 0,
Spectator = 1,
Red = 2,
Blue = 3,
}
export enum Class {
Other = 0,
Scout = 1,
Sniper = 2,
Solder = 3,
Demoman = 4,
Medic = 5,
Heavy = 6,
Pyro = 7,
Spy = 8,
Engineer = 9,
}
export enum BuildingType {
TeleporterEntrance = 0,
TeleporterExit = 1,
Dispenser = 2,
Level1Sentry = 3,
Level2Sentry = 4,
Level3Sentry = 5,
MiniSentry = 6,
Unknown = 7,
}
export interface WorldBoundaries {
boundary_min: {
x: number,
y: number
},
boundary_max: {
x: number,
y: number
}
}
export interface PlayerState {
position: {
x: number,
y: number
},
angle: number,
health: number,
team: Team,
playerClass: Class,
info: PlayerInfo,
charge: number,
}
export interface BuildingState {
position: {
x: number,
y: number
},
angle: number,
health: number,
level: number,
team: Team,
buildingType: BuildingType,
}
export interface Header {
interval_per_tick: number,
map: string
}
export interface Kill {
tick: number,
attacker: number,
assister: number,
victim: number,
weapon: string,
}
function unpack_f32(val: number, min: number, max: number): number {
const ratio = val / (Math.pow(2, 16) - 1);
return ratio * (max - min) + min;
}
function unpack_angle(val: number): number {
const ratio = val / (Math.pow(2, 8) - 1);
return ratio * 360;
}
export class ParsedDemo {
public readonly playerCount: number;
public readonly buildingCount: number;
public readonly world: WorldBoundaries;
public readonly data: Uint8Array;
public readonly header: Header;
public readonly tickCount: number;
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) {
this.playerCount = playerCount;
this.buildingCount = buildingCount;
this.world = world;
this.header = header;
this.data = data;
this.kills = kills;
this.playerInfo = playerInfo;
this.tickCount = tickCount;
}
getPlayer(tick: number, playerIndex: number): PlayerState {
if (playerIndex >= this.playerCount) {
throw new Error("Player out of bounds");
}
const base = ((playerIndex * this.tickCount) + tick) * PLAYER_PACK_SIZE;
return unpackPlayer(this.data, base, this.world, this.playerInfo[playerIndex]);
}
getBuilding(tick: number, buildingIndex: number): BuildingState {
if (buildingIndex >= this.buildingCount) {
throw new Error("Building out of bounds");
}
const base = (this.playerCount * this.tickCount * PLAYER_PACK_SIZE) + ((buildingIndex * this.tickCount) + tick) * BUILDING_PACK_SIZE;
return unpackBuilding(this.data, base, this.world);
}
}
const PLAYER_PACK_SIZE = 8;
const BUILDING_PACK_SIZE = 7;
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);
const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y);
const team_class_health = bytes[base + 4] + (bytes[base + 5] << 8);
const angle = unpack_angle(bytes[base + 6]);
const health = team_class_health & 1013;
const team = (team_class_health >> 14) as Team;
const playerClass = ((team_class_health >> 10) & 15) as Class;
const charge = bytes[base + 7];
return {
position: {x, y},
angle,
health,
team,
playerClass,
info,
charge
}
}
function unpackBuilding(bytes: Uint8Array, base: number, world: WorldBoundaries): BuildingState {
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_health = bytes[base + 4] + (bytes[base + 5] << 8);
const angle = unpack_angle(bytes[base + 6]);
const health = team_type_health & 1013;
const team = (((team_type_health >> 13) & 1) === 0) ? Team.Blue : Team.Red;
const level = (team_type_health >> 14);
const buildingType = ((team_type_health >> 10) & 7) as BuildingType;
return {
position: {x, y},
angle,
health,
team,
buildingType,
level,
}
}

View file

@ -1,5 +0,0 @@
.map-container {
width: 100%;
height: 100%;
background-color: black;
}

View file

@ -1,7 +1,8 @@
import {Panner} from "../Panner/Panner";
import {createEffect, createSignal, ParentProps} from "solid-js";
import {createEffect, createSignal, ParentProps, Show} from "solid-js";
import { createElementSize } from "@solid-primitives/resize-observer";
export class MapContainerProps {
export interface MapContainerProps {
contentSize: {
width: number;
height: number;
@ -9,25 +10,31 @@ export class MapContainerProps {
onScale?: (scale: number) => any;
}
export const MapContainer = ({children, contentSize, onScale}: ParentProps<MapContainerProps>) => {
export const MapContainer = (props: ParentProps<MapContainerProps>) => {
const [container, setContainer] = createSignal<Element>();
const scale = () => Math.min(
container().clientWidth / contentSize.width,
container().clientHeight / contentSize.height
);
const size = createElementSize(container);
const scale = () => {
if (size.width && size.height) {
return Math.min(size.width / props.contentSize.width, size.height / props.contentSize.height);
} else {
return 1;
}
}
createEffect(() => {
if (isFinite(scale())) {
onScale && onScale(scale());
props.onScale && props.onScale(scale());
}
});
return (
<div class="map-container" ref={setContainer}>
<Panner width={container().clientWidth} height={container().clientHeight}
scale={scale()} contentSize={contentSize}
onScale={onScale}>
{children}
</Panner>
<Show when={size.width}>
<Panner width={size.width} height={size.height}
scale={scale()} contentSize={props.contentSize}
onScale={props.onScale}>
{props.children}
</Panner>
</Show>
</div>
)
}
}

View file

@ -1,9 +0,0 @@
.map-background {
/*width: 1000px;*/
/*height: 1000px;*/
min-height: 500px;
min-width: 700px;
background: black no-repeat;
background-position: bottom left;
background-size: contain;
}

View file

@ -1,7 +1,8 @@
import {Player as PlayerDot} from './Render/Player';
import {Building as BuildingDot} from './Render/Building';
import {findMapAlias} from './MapBoundries';
import {PlayerState, Header, WorldBoundaries, BuildingState} from "@demostf/parser-worker";
import {PlayerState, Header, WorldBoundaries, BuildingState} from "./Data/Parser";
import {splitProps} from "solid-js";
export interface MapRenderProps {
header: Header;
@ -15,37 +16,31 @@ export interface MapRenderProps {
scale: number;
}
declare const require: {
<T>(path: string): T;
(paths: string[], callback: (...modules: any[]) => void): void;
ensure: (paths: string[], callback: (require: <T>(path: string) => T) => void) => void;
};
export function MapRender({header, players, size, world, scale, buildings}: MapRenderProps) {
const mapAlias = findMapAlias(header.map);
export function MapRender(props: MapRenderProps) {
const mapAlias = findMapAlias(props.header.map);
const image = `images/leveloverview/dist/${mapAlias}.webp`;
const background = `url(${image})`;
const playerDots = players
const playerDots = () => props.players
.filter((player: PlayerState) => player.health)
.map((player: PlayerState) => {
return <PlayerDot player={player} mapBoundary={world}
targetSize={size} scale={scale} />
return <PlayerDot player={player} mapBoundary={props.world}
targetSize={props.size} scale={props.scale} />
});
const buildingDots = buildings
const buildingDots = () => props.buildings
.filter((building: PlayerState) => building.position.x)
.map((building: PlayerState) => {
return <BuildingDot building={building}
mapBoundary={world}
targetSize={size} scale={scale}/>
mapBoundary={props.world}
targetSize={props.size} scale={props.scale}/>
});
return (
<svg class="map-background" width={size.width} height={size.height}
<svg class="map-background" width={props.size.width} height={props.size.height}
style={{"background-image": background}}>
{playerDots}
{buildingDots}
{playerDots()}
{buildingDots()}
</svg>
);
}

View file

@ -1,4 +1,5 @@
import {BuildingState, WorldBoundaries, BuildingType, Team} from "@demostf/parser-worker";
import {BuildingState, WorldBoundaries, BuildingType, Team} from "../Data/Parser";
import {Show} from "solid-js";
export interface BuildingProp {
building: BuildingState;
@ -54,17 +55,15 @@ export function Building({building, mapBoundary, targetSize, scale}: BuildingPro
const alpha = building.health / maxHealth;
try {
const image = getIcon(building);
const angle = (building.angle) ?
<polygon points="-6,14 0, 16 6,14 0,24" fill="white"
transform={`rotate(${270 - building.angle})`}/> : '';
return <g transform={`translate(${scaledX} ${scaledY}) scale(${1 / scale})`}
opacity={alpha}>
{angle}
<image href={image} class={"player-icon"} height={32}
width={32}
transform={`translate(-16 -16)`}/>
<image href={image} className={"player-icon"} height={32}
width={32}
transform={`translate(-16 -16)`}/>
<Show when={building.angle}>
<polygon points="-6,14 0, 16 6,14 0,24" fill="white"
transform={`rotate(${270 - building.angle})`}/>
</Show>
</g>
} catch (e) {
console.log(e);

View file

@ -1,45 +0,0 @@
.killfeed {
position: absolute;
top: 0;
right: 0;
padding: 15px;
user-select: none;
.kill {
.red {
color: #a75d50;
}
.blue {
color: #5b818f;
}
&.active-player {
background-color: #ddcfb2;
}
display: inline-block;
margin: 3px;
white-space: nowrap;
background-color: #2d2727cc;
border-radius: 5px;
padding: 5px 15px;
width: auto;
text-align: right;
color: var(--primary-color);
font-weight: bold;
float: right;
clear: both;
.player {
padding: 0 5px;
}
}
}
.kill-icon {
display: inline-block;
height: 22px;
width: auto;
vertical-align: middle;
filter: brightness(600%);
}

View file

@ -1,4 +1,4 @@
import {Kill, PlayerState} from "@demostf/parser-worker";
import {Kill, PlayerState} from "../Data/Parser";
import {killAlias} from "./killAlias";
export interface KillFeedProps {

View file

@ -1,4 +0,0 @@
.player-icon {
mask-size: cover;
mask-repeat: no-repeat;
}

View file

@ -1,4 +1,4 @@
import {PlayerState, WorldBoundaries, Team} from "@demostf/parser-worker";
import {PlayerState, WorldBoundaries, Team} from "../Data/Parser";
export interface PlayerProp {
player: PlayerState;

View file

@ -1,218 +0,0 @@
.blueSpecHolder {
position: absolute;
left: 0;
top: 50%;
transform: translate(0, -50%);
}
.redSpecHolder {
position: absolute;
right: 0;
top: 50%;
transform: translate(0, -50%);
}
.playerspec {
background-color: black;
color: white;
height: 42px;
width: 200px;
position: relative;
font-family: sans-serif;
margin-bottom: 2px;
user-select: none;
&.uber {
height: 28px;
}
.class-icon, .steam-avatar {
width: 42px;
height: 42px;
display: inline-block;
position: absolute;
top: 0;
left: 0;
background-position: top left;
background-size: 100% 100%;
&.uber {
height: 28px;
background-size: 28px 28px;
background-repeat: no-repeat;
background-position: 50% 50%;
}
}
.player-name {
display: inline-block;
position: relative;
padding: 0 5px;
white-space: nowrap;
width: 120px;
overflow: hidden;
text-overflow: ellipsis;
}
.health-container {
display: inline-block;
position: absolute;
left: 42px;
top: 0;
height: 28px;
width: calc(100% - 42px);
line-height: 28px;
font-weight: bold;
.health {
position: relative;
float: right;
padding: 0 5px;
}
.healthbar {
position: absolute;
top: 0;
left: 0;
height: 28px;
}
}
&.red {
.health-container {
background-color: #a75d50aa;
}
.healthbar {
background-color: #a75d50;
}
.class-icon.scout {
background-image: url('../../images/class_portraits/Icon_scout.jpg');
}
.class-icon.soldier {
background-image: url('../../images/class_portraits/Icon_soldier.jpg');
}
.class-icon.pyro {
background-image: url('../../images/class_portraits/Icon_pyro.jpg');
}
.class-icon.demoman {
background-image: url('../../images/class_portraits/Icon_demoman.jpg');
}
.class-icon.engineer {
background-image: url('../../images/class_portraits/Icon_engineer.jpg');
}
.class-icon.heavy {
background-image: url('../../images/class_portraits/Icon_heavy.jpg');
}
.class-icon.medic {
background-image: url('../../images/class_portraits/Icon_medic.jpg');
}
.class-icon.sniper {
background-image: url('../../images/class_portraits/Icon_sniper.jpg');
}
.class-icon.spy{
background-image: url('../../images/class_portraits/Icon_spy.jpg');
}
.class-icon.uber {
background-image: url('../../images/charge_red.svg');
}
.class-icon, .steam-avatar {
right: 0;
left: auto;
}
.health-container {
right: 42px;
left: auto;
}
.health {
float: left;
}
.player-name {
float: right;
direction: ltr;
text-align: right;
}
}
&.blue {
.health-container {
background-color: #5b818faa;
}
.healthbar {
background-color: #5b818f;
}
.class-icon.scout {
background-image: url('../../images/class_portraits/Icon_scout_blue.jpg');
}
.class-icon.soldier {
background-image: url('../../images/class_portraits/Icon_soldier_blue.jpg');
}
.class-icon.pyro {
background-image: url('../../images/class_portraits/Icon_pyro_blue.jpg');
}
.class-icon.demoman {
background-image: url('../../images/class_portraits/Icon_demoman_blue.jpg');
}
.class-icon.engineer {
background-image: url('../../images/class_portraits/Icon_engineer_blue.jpg');
}
.class-icon.heavy {
background-image: url('../../images/class_portraits/Icon_heavy_blue.jpg');
}
.class-icon.medic {
background-image: url('../../images/class_portraits/Icon_medic_blue.jpg');
}
.class-icon.sniper {
background-image: url('../../images/class_portraits/Icon_sniper_blue.jpg');
}
.class-icon.spy {
background-image: url('../../images/class_portraits/Icon_spy_blue.jpg');
}
.class-icon.uber {
background-image: url('../../images/charge_blue.svg');
}
}
&.overhealed {
.health {
color: #79d297;
}
.health:after {
position: absolute;
top: 21px;
right: 0;
padding: 0 5px;
font-size: 10px;
font-weight: bold;
content: 'OVERHEALED'
}
&.red .health:after {
position: absolute;
top: 21px;
left: 0;
right: auto;
}
}
&.dead {
.healthbar, .health {
display: none;
}
.health-container {
background-color: transparent;
}
.class-icon {
opacity: 0.5;
}
}
}

View file

@ -1,4 +1,4 @@
import {PlayerState} from "@demostf/parser-worker";
import {PlayerState} from "../Data/Parser";
export interface PlayerSpecProps {
player: PlayerState;

View file

@ -1,7 +1,7 @@
import {PlayersSpec} from './PlayerSpec';
import {KillFeed} from './KillFeed';
import {AsyncParser} from "../Data/AsyncParser";
import {PlayerState, Kill} from "@demostf/parser-worker";
import {PlayerState, Kill} from "../Data/Parser";
export interface SpecHUDProps {
tick: number;

View file

@ -1,23 +0,0 @@
.timeline {
width: 100%;
height: 100px;
/*bottom: 0;*/
/*position: relative;*/
}
.timeline-progress {
position: absolute;
bottom: -60px;
left: 0;
width: 100%;
height: 160px;
}
.timeline-background {
position: absolute;
bottom: 22px;
left: 0;
width: 100%;
height: 78px;
transform: scale(1, -1);
}

View file

@ -1,4 +1,4 @@
import {ParsedDemo, PlayerState, Header, WorldBoundaries, Team} from "@demostf/parser-worker";
import {ParsedDemo, PlayerState, Header, WorldBoundaries, Team} from "../Data/Parser";
export interface TimelineProps {
parser: AsyncParser;
@ -8,16 +8,13 @@ export interface TimelineProps {
}
export const Timeline = ({parser, tick, onSetTick, disabled}) => {
const background = <TimeLineBackground parser={parser}/>;
return (<div class="timeline">
{background}
<input class="timeline-progress" type="range" min={0}
max={parser.demo.tickCount} value={tick}
disabled={disabled}
return <div class="timeline">
<input max={parser.demo.tickCount} value={tick} class="timeline-progress" type="range" min={0}
onChange={(event) => {onSetTick(parseInt(event.target.value, 10))}}
disabled={disabled}
/>
</div>);
<TimeLineBackground parser={parser}/>
</div>;
}
import {AsyncParser} from "../Data/AsyncParser";