mirror of
https://codeberg.org/demostf/frontend.git
synced 2026-06-03 18:24:12 +02:00
kill search
This commit is contained in:
parent
78d8d4eb9f
commit
72c4b6ee08
10 changed files with 413 additions and 117 deletions
BIN
images/kill_icons/dragons_fury.png
Normal file
BIN
images/kill_icons/dragons_fury.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1,008 B After Width: | Height: | Size: 1.1 KiB |
|
|
@ -15,6 +15,8 @@ import {getMapBoundaries} from "./MapBoundries";
|
||||||
import {createEffect, createSignal, untrack} from "solid-js";
|
import {createEffect, createSignal, untrack} from "solid-js";
|
||||||
import {Session, StateUpdate} from "./Session";
|
import {Session, StateUpdate} from "./Session";
|
||||||
import {DemoHead} from "../../header";
|
import {DemoHead} from "../../header";
|
||||||
|
import {EventSearch} from "./EventSearch";
|
||||||
|
import {Event} from "./Data/Parser";
|
||||||
|
|
||||||
export interface AnalyseProps {
|
export interface AnalyseProps {
|
||||||
header: DemoHead;
|
header: DemoHead;
|
||||||
|
|
@ -37,10 +39,13 @@ export const Analyser = (props: AnalyseProps) => {
|
||||||
const [clients, setClients] = createSignal<number>(0);
|
const [clients, setClients] = createSignal<number>(0);
|
||||||
const [helpOpen, setHelpOpen] = createSignal<boolean>(false);
|
const [helpOpen, setHelpOpen] = createSignal<boolean>(false);
|
||||||
const [gotoOpen, setGotoOpen] = createSignal<boolean>(false);
|
const [gotoOpen, setGotoOpen] = createSignal<boolean>(false);
|
||||||
|
const [searchOpen, setSearchOpen] = createSignal<boolean>(false);
|
||||||
|
const [search, setSearch] = createSignal<string>('');
|
||||||
const [gotoInput, setGotoInput] = createSignal<number>(0);
|
const [gotoInput, setGotoInput] = createSignal<number>(0);
|
||||||
const closeDialogs = () => {
|
const closeDialogs = () => {
|
||||||
setHelpOpen(false);
|
setHelpOpen(false);
|
||||||
setGotoOpen(false);
|
setGotoOpen(false);
|
||||||
|
setSearchOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|
@ -48,34 +53,45 @@ export const Analyser = (props: AnalyseProps) => {
|
||||||
|
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if (e) {
|
if (e) {
|
||||||
if (e.key === '.') {
|
const dialogOpen = searchOpen() | gotoOpen();
|
||||||
seek(1);
|
if (!dialogOpen) {
|
||||||
e.preventDefault();
|
if (e.key === '.') {
|
||||||
}
|
seek(1);
|
||||||
if (e.key === ',') {
|
e.preventDefault();
|
||||||
seek(-1);
|
}
|
||||||
e.preventDefault();
|
if (e.key === ',') {
|
||||||
}
|
seek(-1);
|
||||||
if (e.key === 'ArrowRight') {
|
e.preventDefault();
|
||||||
seek(15);
|
}
|
||||||
e.preventDefault();
|
if (e.key === 'ArrowRight') {
|
||||||
}
|
seek(15);
|
||||||
if (e.key === 'ArrowLeft') {
|
e.preventDefault();
|
||||||
seek(-15);
|
}
|
||||||
e.preventDefault();
|
if (e.key === 'ArrowLeft') {
|
||||||
}
|
seek(-15);
|
||||||
if (e.key === ' ') {
|
e.preventDefault();
|
||||||
togglePlay();
|
}
|
||||||
e.preventDefault();
|
if (e.key === ' ') {
|
||||||
}
|
togglePlay();
|
||||||
if (e.key === '?') {
|
e.preventDefault();
|
||||||
setHelpOpen(true);
|
}
|
||||||
setGotoOpen(false);
|
if (e.key === '?') {
|
||||||
e.preventDefault();
|
setHelpOpen(true);
|
||||||
|
setGotoOpen(false);
|
||||||
|
setSearchOpen(false);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!inShared && e.getModifierState("Control") && e.key === 'g') {
|
if (!inShared && e.getModifierState("Control") && e.key === 'g') {
|
||||||
setHelpOpen(false);
|
setHelpOpen(false);
|
||||||
setGotoOpen(true);
|
setGotoOpen(true);
|
||||||
|
setSearchOpen(false);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
if (!inShared && e.getModifierState("Control") && e.key === 'f') {
|
||||||
|
setHelpOpen(false);
|
||||||
|
setGotoOpen(false);
|
||||||
|
setSearchOpen(true);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
|
|
@ -213,6 +229,7 @@ export const Analyser = (props: AnalyseProps) => {
|
||||||
const buildings = () => parser.getBuildingsAtTick(tick());
|
const buildings = () => parser.getBuildingsAtTick(tick());
|
||||||
const projectiles = () => parser.getProjectilesAtTick(tick());
|
const projectiles = () => parser.getProjectilesAtTick(tick());
|
||||||
const kills = parser.getKills();
|
const kills = parser.getKills();
|
||||||
|
const events = parser.getEvents();
|
||||||
const playButtonText = () => (playing()) ? '⏸' : '▶️';
|
const playButtonText = () => (playing()) ? '⏸' : '▶️';
|
||||||
const inShared = session && !session.isOwner();
|
const inShared = session && !session.isOwner();
|
||||||
const isShared = () => sessionName() !== '';
|
const isShared = () => sessionName() !== '';
|
||||||
|
|
@ -263,7 +280,7 @@ export const Analyser = (props: AnalyseProps) => {
|
||||||
})}
|
})}
|
||||||
disabled={inShared}/>
|
disabled={inShared}/>
|
||||||
</div>
|
</div>
|
||||||
<Modal class="help" isOpen={helpOpen()} onCloseRequest={() => setHelpOpen(false)}
|
<Modal isOpen={helpOpen()} onCloseRequest={() => setHelpOpen(false)}
|
||||||
closeOnOutsideClick={true} overlayClass="modal-overlay" contentClass="modal-content">
|
closeOnOutsideClick={true} overlayClass="modal-overlay" contentClass="modal-content">
|
||||||
<h4>Keyboard Shortcuts</h4>
|
<h4>Keyboard Shortcuts</h4>
|
||||||
<table class="shortcuts">
|
<table class="shortcuts">
|
||||||
|
|
@ -310,7 +327,7 @@ export const Analyser = (props: AnalyseProps) => {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal class="goto" isOpen={gotoOpen()} onCloseRequest={() => setGotoOpen(false)}
|
<Modal isOpen={gotoOpen()} onCloseRequest={() => setGotoOpen(false)}
|
||||||
closeOnOutsideClick={true} overlayClass="modal-overlay" contentClass="modal-content">
|
closeOnOutsideClick={true} overlayClass="modal-overlay" contentClass="modal-content">
|
||||||
<h4>Goto Tick</h4>
|
<h4>Goto Tick</h4>
|
||||||
<form use:formSubmit={gotoTickSubmitted} class="goto">
|
<form use:formSubmit={gotoTickSubmitted} class="goto">
|
||||||
|
|
@ -319,6 +336,16 @@ export const Analyser = (props: AnalyseProps) => {
|
||||||
ref={autofocus} autofocus type="text" inputmode="numeric" min={0} max={lastTick - 1}/>
|
ref={autofocus} autofocus type="text" inputmode="numeric" min={0} max={lastTick - 1}/>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal isOpen={searchOpen()} onCloseRequest={() => setSearchOpen(false)}
|
||||||
|
closeOnOutsideClick={true} overlayClass="modal-overlay" contentClass="modal-content">
|
||||||
|
<EventSearch
|
||||||
|
players={players()}
|
||||||
|
search={search()}
|
||||||
|
onSearch={setSearch}
|
||||||
|
events={events}
|
||||||
|
onSelect={(event: Event) => setTickNow(event.tick)}
|
||||||
|
></EventSearch>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,13 @@
|
||||||
import {BuildingState, Kill, ParsedDemo, PlayerState, ProjectileState, ProjectileType, WorldBoundaries} from "./Parser";
|
import {
|
||||||
|
BuildingState,
|
||||||
|
Event,
|
||||||
|
Kill,
|
||||||
|
ParsedDemo,
|
||||||
|
PlayerState,
|
||||||
|
ProjectileState,
|
||||||
|
ProjectileType,
|
||||||
|
WorldBoundaries
|
||||||
|
} from "./Parser";
|
||||||
|
|
||||||
function getCacheBuster(): string {
|
function getCacheBuster(): string {
|
||||||
const url = document.querySelector('script[src*="viewer"]').attributes.src.value;
|
const url = document.querySelector('script[src*="viewer"]').attributes.src.value;
|
||||||
|
|
@ -33,7 +42,18 @@ export class AsyncParser {
|
||||||
const cachedData: ParsedDemo = event.data.demo;
|
const cachedData: ParsedDemo = event.data.demo;
|
||||||
console.log(`packed data: ${(cachedData.data.length / (1024 * 1024)).toFixed(1)}MB`);
|
console.log(`packed data: ${(cachedData.data.length / (1024 * 1024)).toFixed(1)}MB`);
|
||||||
this.world = cachedData.world;
|
this.world = cachedData.world;
|
||||||
this.demo = new ParsedDemo(cachedData.playerCount, cachedData.buildingCount, cachedData.projectileCount, 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.events,
|
||||||
|
cachedData.tickCount
|
||||||
|
);
|
||||||
resolve(this.demo);
|
resolve(this.demo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -76,4 +96,8 @@ export class AsyncParser {
|
||||||
getKills(): Kill[] {
|
getKills(): Kill[] {
|
||||||
return this.demo.kills
|
return this.demo.kills
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEvents(): Event[] {
|
||||||
|
return this.demo.events
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,14 @@ export async function parseDemo(bytes: Uint8Array, progressCallback: (progress:
|
||||||
let map = get_map(state);
|
let map = get_map(state);
|
||||||
let data = get_data(state);
|
let data = get_data(state);
|
||||||
|
|
||||||
|
let events = kills.map((kill: Kill) => {
|
||||||
|
return {
|
||||||
|
tick: kill.tick,
|
||||||
|
type: "kill",
|
||||||
|
kill
|
||||||
|
} as Event
|
||||||
|
});
|
||||||
|
|
||||||
return new ParsedDemo(
|
return new ParsedDemo(
|
||||||
playerCount,
|
playerCount,
|
||||||
buildingCount,
|
buildingCount,
|
||||||
|
|
@ -75,6 +83,7 @@ export async function parseDemo(bytes: Uint8Array, progressCallback: (progress:
|
||||||
data,
|
data,
|
||||||
kills,
|
kills,
|
||||||
playerInfo,
|
playerInfo,
|
||||||
|
events,
|
||||||
tickCount,
|
tickCount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -206,8 +215,20 @@ export class ParsedDemo {
|
||||||
public readonly tickCount: number;
|
public readonly tickCount: number;
|
||||||
public readonly kills: Kill[];
|
public readonly kills: Kill[];
|
||||||
public readonly playerInfo: PlayerInfo[];
|
public readonly playerInfo: PlayerInfo[];
|
||||||
|
public readonly events: Event[];
|
||||||
|
|
||||||
constructor(playerCount: number, buildingCount: number, projectileCount: 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[],
|
||||||
|
events: Event[],
|
||||||
|
tickCount: number
|
||||||
|
) {
|
||||||
this.playerCount = playerCount;
|
this.playerCount = playerCount;
|
||||||
this.buildingCount = buildingCount;
|
this.buildingCount = buildingCount;
|
||||||
this.projectileCount = projectileCount;
|
this.projectileCount = projectileCount;
|
||||||
|
|
@ -216,6 +237,7 @@ export class ParsedDemo {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.kills = kills;
|
this.kills = kills;
|
||||||
this.playerInfo = playerInfo;
|
this.playerInfo = playerInfo;
|
||||||
|
this.events = events;
|
||||||
this.tickCount = tickCount;
|
this.tickCount = tickCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -309,3 +331,11 @@ function unpackProjectile(bytes: Uint8Array, base: number, world: WorldBoundarie
|
||||||
projectileType,
|
projectileType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type KillEvent = {
|
||||||
|
type: "kill";
|
||||||
|
tick: number,
|
||||||
|
kill: Kill,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Event = KillEvent;
|
||||||
137
script/viewer/Analyse/EventSearch.tsx
Normal file
137
script/viewer/Analyse/EventSearch.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import {Event, Kill, PlayerState} from "./Data/Parser";
|
||||||
|
import {createEffect, createSignal, For, Show, untrack} from "solid-js";
|
||||||
|
import {getPlayer, KillIcon, PlayerName, PlayerNames} from "./Render/KillFeed";
|
||||||
|
import {autofocus} from "@solid-primitives/autofocus";
|
||||||
|
import {useKeyDownEvent} from "@solid-primitives/keyboard";
|
||||||
|
|
||||||
|
export interface EventSearchProps {
|
||||||
|
events: Event[];
|
||||||
|
players: PlayerState[];
|
||||||
|
onSearch: (string) => void;
|
||||||
|
search: string;
|
||||||
|
selectedEvent: number;
|
||||||
|
onSelect: (event: Event) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventSearch(props: EventSearchProps) {
|
||||||
|
const keyEvent = useKeyDownEvent();
|
||||||
|
const [selected, setSelected] = createSignal<number>(0);
|
||||||
|
|
||||||
|
const events = () => filterEvents(props.events, props.players, props.search);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const e = keyEvent();
|
||||||
|
|
||||||
|
untrack(() => {
|
||||||
|
if (e) {
|
||||||
|
const seekSelected = (offset) => {
|
||||||
|
const target = Math.max(0, Math.min(selected() + offset, events().length - 1));
|
||||||
|
setSelected(target);
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
seekSelected(-1);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
seekSelected(1);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
props.onSelect(events()[selected()]);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<div class="event-search">
|
||||||
|
<input type="text" ref={autofocus} autofocus value={props.search}
|
||||||
|
onInput={(e) => props.onSearch(e.target.value)}/>
|
||||||
|
<table class="event-list">
|
||||||
|
<For each={events()}>{(event, i) =>
|
||||||
|
<EventView event={event} highlighted={i() == selected()} players={props.players}/>
|
||||||
|
}</For>
|
||||||
|
</table>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventViewProps {
|
||||||
|
event: Event;
|
||||||
|
highlighted: boolean,
|
||||||
|
players: PlayerState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventView(props: EventViewProps) {
|
||||||
|
let row;
|
||||||
|
const highlightClass = () => ` ${props.highlighted ? 'highlighted' : ''}`;
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.highlighted) {
|
||||||
|
row.scrollIntoView(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<tr ref={row} class={props.event.type + highlightClass()}>
|
||||||
|
<Show when={props.event.type == "kill"}>
|
||||||
|
<KillView kill={props.event.kill} players={props.players}/>
|
||||||
|
</Show>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KillViewProps {
|
||||||
|
kill: Kill;
|
||||||
|
players: PlayerState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function KillView(props: KillViewProps) {
|
||||||
|
const attacker = getPlayer(props.players, props.kill.attacker);
|
||||||
|
const assister = getPlayer(props.players, props.kill.assister);
|
||||||
|
let victim = getPlayer(props.players, props.kill.victim);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<td class="kill-source">
|
||||||
|
<PlayerNames players={[attacker, assister]}/>
|
||||||
|
</td>
|
||||||
|
<td class="kill-icon">
|
||||||
|
<KillIcon kill={props.kill}/>
|
||||||
|
</td>
|
||||||
|
<td className="kill-target">
|
||||||
|
<PlayerName player={victim}/>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterEvents(events: Event[], players: PlayerState[], query: string): Event[] {
|
||||||
|
if (query === '') {
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
query = query.toLowerCase();
|
||||||
|
let filteredEvents = [].concat(events);
|
||||||
|
let queryParts = query.split(' ').filter(part => part.length > 0);
|
||||||
|
for (const queryPart of queryParts) {
|
||||||
|
const playersForPart = findPlayers(players, queryPart);
|
||||||
|
filteredEvents = filteredEvents.filter(event => eventMatches(event, playersForPart, queryPart));
|
||||||
|
}
|
||||||
|
return filteredEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPlayers(players: PlayerState[], queryPart: string): number[] {
|
||||||
|
return players.flatMap(player => {
|
||||||
|
if (player.info.name.toLowerCase().includes(queryPart)) {
|
||||||
|
return [player.info.userId]
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventMatches(event: Event, matchedPlayers: number[], queryPart: string): boolean {
|
||||||
|
if (event.type === "kill") {
|
||||||
|
const kill = event.kill;
|
||||||
|
return matchedPlayers.includes(kill.attacker) ||
|
||||||
|
matchedPlayers.includes(kill.assister) ||
|
||||||
|
matchedPlayers.includes(kill.victim);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,68 +1,89 @@
|
||||||
import {Kill, PlayerState} from "../Data/Parser";
|
import {Kill, PlayerState} from "../Data/Parser";
|
||||||
import {killAlias} from "./killAlias";
|
import {killAlias} from "./killAlias";
|
||||||
|
import {For, Show} from "solid-js";
|
||||||
|
|
||||||
export interface KillFeedProps {
|
export interface KillFeedProps {
|
||||||
kills: Kill[],
|
kills: Kill[],
|
||||||
tick: number;
|
tick: number;
|
||||||
players: PlayerState[];
|
players: PlayerState[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KillFeed(props: KillFeedProps) {
|
export function KillFeed(props: KillFeedProps) {
|
||||||
const {kills} = props;
|
const {kills} = props;
|
||||||
const relevantKills = () => kills.filter(kill => kill.tick <= props.tick && kill.tick >= (props.tick - 30 * 10));
|
const relevantKills = () => kills.filter(kill => kill.tick <= props.tick && kill.tick >= (props.tick - 30 * 10));
|
||||||
|
|
||||||
return <div class="killfeed">
|
return <ul class="killfeed">
|
||||||
<For each={relevantKills()}>{(kill) =>
|
<For each={relevantKills()}>{(kill) =>
|
||||||
<KillFeedItem kill={kill} players={props.players}/>
|
<KillFeedItem kill={kill} players={props.players}/>
|
||||||
}</For>
|
}</For>
|
||||||
</div>
|
</ul>
|
||||||
}
|
}
|
||||||
|
|
||||||
const teamMap = {
|
const teamMap = {
|
||||||
0: 'unknown',
|
0: 'unknown',
|
||||||
2: 'red',
|
2: 'red',
|
||||||
3: 'blue'
|
3: 'blue'
|
||||||
};
|
};
|
||||||
|
|
||||||
export function KillFeedItem({kill, players}: { kill: Kill, players: PlayerState[] }) {
|
interface KillFeedItemProps {
|
||||||
const alias = killAlias[kill.weapon] ? killAlias[kill.weapon] : kill.weapon;
|
kill: Kill;
|
||||||
const attacker = getPlayer(players, kill.attacker);
|
players: PlayerState[];
|
||||||
const assister = getPlayer(players, kill.assister);
|
|
||||||
let victim = getPlayer(players, kill.victim);
|
|
||||||
let killIcon;
|
|
||||||
try {
|
|
||||||
killIcon = `/images/kill_icons/${alias}.png`;
|
|
||||||
} catch (e) {
|
|
||||||
console.log(alias);
|
|
||||||
killIcon = `/images/kill_icons/skull.png`;
|
|
||||||
}
|
|
||||||
if (!victim) {
|
|
||||||
victim = {
|
|
||||||
team: 0,
|
|
||||||
info: {
|
|
||||||
name: 'Missing player'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div class="kill">
|
|
||||||
{(attacker && kill.attacker !== kill.victim) ?
|
|
||||||
<span class={"player " + teamMap[attacker.team]}>
|
|
||||||
{attacker.info.name}
|
|
||||||
</span> : ''}
|
|
||||||
{(assister && kill.assister !== kill.victim) ?
|
|
||||||
<span class={teamMap[assister.team]}>﹢</span> : ''}
|
|
||||||
{(assister && kill.assister !== kill.victim) ?
|
|
||||||
(<span class={"player " + teamMap[assister.team]}>
|
|
||||||
{assister.info.name}
|
|
||||||
</span>) : ''}
|
|
||||||
<img src={killIcon} class={`kill-icon ${kill.weapon}`}/>
|
|
||||||
<span class={"player " + teamMap[victim.team]}>
|
|
||||||
{victim.info.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayer(players: PlayerState[], entityId: number): PlayerState {
|
export function KillFeedItem(props: KillFeedItemProps) {
|
||||||
return players.find(player => player.info.userId == entityId);
|
const attacker = getPlayer(props.players, props.kill.attacker);
|
||||||
|
const assister = getPlayer(props.players, props.kill.assister);
|
||||||
|
let victim = getPlayer(props.players, props.kill.victim);
|
||||||
|
|
||||||
|
return <li class="kill">
|
||||||
|
<PlayerNames players={[attacker, assister]}/>
|
||||||
|
<KillIcon kill={props.kill}/>
|
||||||
|
<PlayerName player={victim}/>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KillIconProps {
|
||||||
|
kill: Kill;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KillIcon(props: KillIconProps) {
|
||||||
|
const alias = killAlias[props.kill.weapon] ? killAlias[props.kill.weapon] : props.kill.weapon;
|
||||||
|
let killIcon;
|
||||||
|
try {
|
||||||
|
killIcon = `/images/kill_icons/${alias}.png`;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(alias);
|
||||||
|
killIcon = `/images/kill_icons/skull.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <img src={killIcon} class={`kill-icon ${props.kill.weapon}`}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlayerNameProps {
|
||||||
|
player: PlayerState | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlayerName(props: PlayerNameProps) {
|
||||||
|
return <Show when={props.player}>
|
||||||
|
<span className={"player " + teamMap[props.player.team]}>
|
||||||
|
{props.player.info.name}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlayerNamesProps {
|
||||||
|
players: (PlayerState | null)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlayerNames(props: PlayerNamesProps) {
|
||||||
|
return <For each={props.players}>{(player, i) => <>
|
||||||
|
<Show when={i() > 0 && player}>
|
||||||
|
<span className={teamMap[player.team]}>+</span>
|
||||||
|
</Show>
|
||||||
|
<PlayerName player={player}/>
|
||||||
|
</>}</For>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlayer(players: PlayerState[], entityId: number): PlayerState | null {
|
||||||
|
return players.find(player => player.info.userId == entityId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,39 @@
|
||||||
export const killAlias = {
|
export const killAlias = {
|
||||||
'world': 'skull',
|
'world': 'skull',
|
||||||
'player': 'skull',
|
'player': 'skull',
|
||||||
'telefrag': 'skull',
|
'telefrag': 'skull',
|
||||||
'shotgun_pyro': 'shotgun',
|
'shotgun_pyro': 'shotgun',
|
||||||
'tf_projectile_pipe_remote': 'stickybomb_launcher',
|
'tf_projectile_pipe_remote': 'stickybomb_launcher',
|
||||||
'the_classic': 'classic',
|
'the_classic': 'classic',
|
||||||
'tf_projectile_arrow': 'huntsman',
|
'tf_projectile_arrow': 'huntsman',
|
||||||
'club': 'kukri',
|
'club': 'kukri',
|
||||||
'shotgun_primary': 'shotgun',
|
'shotgun_primary': 'shotgun',
|
||||||
'shotgun_soldier': 'shotgun',
|
'shotgun_soldier': 'shotgun',
|
||||||
'shotgun_hwg': 'shotgun',
|
'shotgun_hwg': 'shotgun',
|
||||||
'pickaxe': 'escape_plan',
|
'pickaxe': 'escape_plan',
|
||||||
'tf_projectile_pipe': 'grenade_launcher',
|
'tf_projectile_pipe': 'grenade_launcher',
|
||||||
'obj_sentrygun': 'sentrygun1',
|
'obj_sentrygun': 'sentrygun1',
|
||||||
'steel_fists': 'fists_of_steel',
|
'steel_fists': 'fists_of_steel',
|
||||||
'tf_projectile_rocket': 'rocket_launcher',
|
'tf_projectile_rocket': 'rocket_launcher',
|
||||||
'obj_sentrygun3': 'sentrygun3',
|
'obj_sentrygun3': 'sentrygun3',
|
||||||
'obj_sentrygun2': 'sentrygun2',
|
'obj_sentrygun2': 'sentrygun2',
|
||||||
'worldspawn': 'skull',
|
'worldspawn': 'skull',
|
||||||
'nonnonviolent_protest': 'conscientious_objector',
|
'nonnonviolent_protest': 'conscientious_objector',
|
||||||
'deflect_promode': 'deflect_rocket',
|
'deflect_promode': 'deflect_rocket',
|
||||||
'trigger_hurt': 'bleed',
|
'trigger_hurt': 'bleed',
|
||||||
'quake_rl': 'original',
|
'quake_rl': 'original',
|
||||||
'wrangler_kill': 'wrangler',
|
'wrangler_kill': 'wrangler',
|
||||||
'obj_minisentry': 'minisentry',
|
'obj_minisentry': 'minisentry',
|
||||||
'pistol_scout': 'pistol',
|
'pistol_scout': 'pistol',
|
||||||
'bleed_kill': 'bleed',
|
'bleed_kill': 'bleed',
|
||||||
'maxgun': 'lugermorph',
|
'maxgun': 'lugermorph',
|
||||||
'rocketlauncher_directhit': 'direct_hit',
|
'rocketlauncher_directhit': 'direct_hit',
|
||||||
'frontier_kill': 'frontier_justice',
|
'frontier_kill': 'frontier_justice',
|
||||||
'robot_arm_kill': 'gunslinger',
|
'robot_arm_kill': 'gunslinger',
|
||||||
'wrench_jag': 'jag',
|
'wrench_jag': 'jag',
|
||||||
'loose_cannon_explosion': 'loose_cannon',
|
'loose_cannon_explosion': 'loose_cannon',
|
||||||
'samrevolver': 'big_kill',
|
'samrevolver': 'big_kill',
|
||||||
'long_heatmaker': 'huo-long_heater',
|
'long_heatmaker': 'huo-long_heater',
|
||||||
'pep_pistol': 'pistol'
|
'pep_pistol': 'pistol',
|
||||||
|
'ai_flamethrower': 'flamethrower',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
@import 'viewer/Player.css';
|
@import 'viewer/Player.css';
|
||||||
@import 'viewer/PlayerSpec.css';
|
@import 'viewer/PlayerSpec.css';
|
||||||
@import 'viewer/Timeline.css';
|
@import 'viewer/Timeline.css';
|
||||||
|
@import 'viewer/EventSearch.css';
|
||||||
|
|
||||||
progress {
|
progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
55
style/pages/viewer/EventSearch.css
Normal file
55
style/pages/viewer/EventSearch.css
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
div.event-search {
|
||||||
|
width: 700px;
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.event-list {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
line-height: 34px;
|
||||||
|
table-layout: fixed;
|
||||||
|
|
||||||
|
tr.highlighted {
|
||||||
|
background: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill {
|
||||||
|
& .red {
|
||||||
|
color: #a75d50;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .blue {
|
||||||
|
color: #5b818f;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.kill-icon {
|
||||||
|
width: 60px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 60px;
|
||||||
|
max-height: 25px;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.kill-source {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue