Skip to content

Commit

Permalink
kill search
Browse files Browse the repository at this point in the history
  • Loading branch information
icewind1991 committed Dec 6, 2024
1 parent 78d8d4e commit 72c4b6e
Show file tree
Hide file tree
Showing 10 changed files with 413 additions and 117 deletions.
Binary file added images/kill_icons/dragons_fury.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/kill_icons/tomislav.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 53 additions & 26 deletions script/viewer/Analyse/Analyser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {getMapBoundaries} from "./MapBoundries";
import {createEffect, createSignal, untrack} from "solid-js";
import {Session, StateUpdate} from "./Session";
import {DemoHead} from "../../header";
import {EventSearch} from "./EventSearch";
import {Event} from "./Data/Parser";

export interface AnalyseProps {
header: DemoHead;
Expand All @@ -37,45 +39,59 @@ export const Analyser = (props: AnalyseProps) => {
const [clients, setClients] = createSignal<number>(0);
const [helpOpen, setHelpOpen] = 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 closeDialogs = () => {
setHelpOpen(false);
setGotoOpen(false);
setSearchOpen(false);
};

createEffect(() => {
const e = event();

untrack(() => {
if (e) {
if (e.key === '.') {
seek(1);
e.preventDefault();
}
if (e.key === ',') {
seek(-1);
e.preventDefault();
}
if (e.key === 'ArrowRight') {
seek(15);
e.preventDefault();
}
if (e.key === 'ArrowLeft') {
seek(-15);
e.preventDefault();
}
if (e.key === ' ') {
togglePlay();
e.preventDefault();
}
if (e.key === '?') {
setHelpOpen(true);
setGotoOpen(false);
e.preventDefault();
const dialogOpen = searchOpen() | gotoOpen();
if (!dialogOpen) {
if (e.key === '.') {
seek(1);
e.preventDefault();
}
if (e.key === ',') {
seek(-1);
e.preventDefault();
}
if (e.key === 'ArrowRight') {
seek(15);
e.preventDefault();
}
if (e.key === 'ArrowLeft') {
seek(-15);
e.preventDefault();
}
if (e.key === ' ') {
togglePlay();
e.preventDefault();
}
if (e.key === '?') {
setHelpOpen(true);
setGotoOpen(false);
setSearchOpen(false);
e.preventDefault();
}
}
if (!inShared && e.getModifierState("Control") && e.key === 'g') {
setHelpOpen(false);
setGotoOpen(true);
setSearchOpen(false);
e.preventDefault();
}
if (!inShared && e.getModifierState("Control") && e.key === 'f') {
setHelpOpen(false);
setGotoOpen(false);
setSearchOpen(true);
e.preventDefault();
}
if (e.key === 'Escape') {
Expand Down Expand Up @@ -213,6 +229,7 @@ export const Analyser = (props: AnalyseProps) => {
const buildings = () => parser.getBuildingsAtTick(tick());
const projectiles = () => parser.getProjectilesAtTick(tick());
const kills = parser.getKills();
const events = parser.getEvents();
const playButtonText = () => (playing()) ? '⏸' : '▶️';
const inShared = session && !session.isOwner();
const isShared = () => sessionName() !== '';
Expand Down Expand Up @@ -263,7 +280,7 @@ export const Analyser = (props: AnalyseProps) => {
})}
disabled={inShared}/>
</div>
<Modal class="help" isOpen={helpOpen()} onCloseRequest={() => setHelpOpen(false)}
<Modal isOpen={helpOpen()} onCloseRequest={() => setHelpOpen(false)}
closeOnOutsideClick={true} overlayClass="modal-overlay" contentClass="modal-content">
<h4>Keyboard Shortcuts</h4>
<table class="shortcuts">
Expand Down Expand Up @@ -310,7 +327,7 @@ export const Analyser = (props: AnalyseProps) => {
</tbody>
</table>
</Modal>
<Modal class="goto" isOpen={gotoOpen()} onCloseRequest={() => setGotoOpen(false)}
<Modal isOpen={gotoOpen()} onCloseRequest={() => setGotoOpen(false)}
closeOnOutsideClick={true} overlayClass="modal-overlay" contentClass="modal-content">
<h4>Goto Tick</h4>
<form use:formSubmit={gotoTickSubmitted} class="goto">
Expand All @@ -319,6 +336,16 @@ export const Analyser = (props: AnalyseProps) => {
ref={autofocus} autofocus type="text" inputmode="numeric" min={0} max={lastTick - 1}/>
</form>
</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>
);
}
Expand Down
28 changes: 26 additions & 2 deletions script/viewer/Analyse/Data/AsyncParser.ts
Original file line number Diff line number Diff line change
@@ -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 {
const url = document.querySelector('script[src*="viewer"]').attributes.src.value;
Expand Down Expand Up @@ -33,7 +42,18 @@ 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.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);
}
}
Expand Down Expand Up @@ -76,4 +96,8 @@ export class AsyncParser {
getKills(): Kill[] {
return this.demo.kills
}

getEvents(): Event[] {
return this.demo.events
}
}
34 changes: 32 additions & 2 deletions script/viewer/Analyse/Data/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ export async function parseDemo(bytes: Uint8Array, progressCallback: (progress:
let map = get_map(state);
let data = get_data(state);

let events = kills.map((kill: Kill) => {
return {
tick: kill.tick,
type: "kill",
kill
} as Event
});

return new ParsedDemo(
playerCount,
buildingCount,
Expand All @@ -75,6 +83,7 @@ export async function parseDemo(bytes: Uint8Array, progressCallback: (progress:
data,
kills,
playerInfo,
events,
tickCount,
);
}
Expand Down Expand Up @@ -206,8 +215,20 @@ export class ParsedDemo {
public readonly tickCount: number;
public readonly kills: Kill[];
public readonly playerInfo: PlayerInfo[];

constructor(playerCount: number, buildingCount: number, projectileCount: number, world: WorldBoundaries, header: Header, data: Uint8Array, kills: Kill[], playerInfo: PlayerInfo[], tickCount: number) {
public readonly events: Event[];

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.buildingCount = buildingCount;
this.projectileCount = projectileCount;
Expand All @@ -216,6 +237,7 @@ export class ParsedDemo {
this.data = data;
this.kills = kills;
this.playerInfo = playerInfo;
this.events = events;
this.tickCount = tickCount;
}

Expand Down Expand Up @@ -309,3 +331,11 @@ function unpackProjectile(bytes: Uint8Array, base: number, world: WorldBoundarie
projectileType,
}
}

export type KillEvent = {
type: "kill";
tick: number,
kill: Kill,
}

export type Event = KillEvent;
137 changes: 137 additions & 0 deletions script/viewer/Analyse/EventSearch.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 72c4b6e

Please sign in to comment.