mirror of
https://codeberg.org/demostf/frontend.git
synced 2026-06-03 18:24:12 +02:00
242 lines
7.9 KiB
TypeScript
242 lines
7.9 KiB
TypeScript
import {BuildingDestroyedEvent, Class, Event, Kill, PlayerState, UberEvent} from "./Data/Parser";
|
|
import {createEffect, createSignal, For, Show, untrack} from "solid-js";
|
|
import {getPlayer, KillIcon, PlayerName, PlayerNames, teamMap} 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}
|
|
onClick={() => props.onSelect(event)}/>
|
|
}</For>
|
|
</table>
|
|
</div>)
|
|
}
|
|
|
|
interface EventViewProps {
|
|
event: Event;
|
|
highlighted: boolean,
|
|
players: PlayerState[];
|
|
onClick: () => void;
|
|
}
|
|
|
|
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()} onClick={() => props.onClick()}>
|
|
<Show when={props.event.type == "kill"}>
|
|
<KillView kill={props.event.kill} players={props.players}/>
|
|
</Show>
|
|
<Show when={props.event.type == "building_destroyed"}>
|
|
<BuildingDestroyedView event={props.event} players={props.players}/>
|
|
</Show>
|
|
<Show when={props.event.type == "uber"}>
|
|
<UberView event={props.event} 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>
|
|
<td className="tick">
|
|
#{props.kill.tick}
|
|
</td>
|
|
</>
|
|
}
|
|
|
|
interface BuildingDestroyedViewProps {
|
|
event: BuildingDestroyedEvent;
|
|
f
|
|
players: PlayerState[];
|
|
}
|
|
|
|
function BuildingDestroyedView(props: BuildingDestroyedViewProps) {
|
|
const attacker = getPlayer(props.players, props.event.attacker_id);
|
|
const assister = getPlayer(props.players, props.event.assister_id);
|
|
let victim = getPlayer(props.players, props.event.victim_id);
|
|
|
|
return <>
|
|
<td class="kill-source">
|
|
<PlayerNames players={[attacker, assister]}/>
|
|
</td>
|
|
<td class="kill-icon">
|
|
<KillIcon kill={props.event}/>
|
|
</td>
|
|
<td className="kill-target">
|
|
<PlayerName player={victim}/><span class={teamMap[victim.team]}>({props.event.building_type})</span>
|
|
</td>
|
|
<td className="tick">
|
|
#{props.event.tick}
|
|
</td>
|
|
</>
|
|
}
|
|
|
|
interface UberViewProps {
|
|
event: UberEvent;
|
|
players: PlayerState[];
|
|
}
|
|
|
|
function UberView(props: UberViewProps) {
|
|
const medic = getPlayer(props.players, props.event.user_id);
|
|
const target = getPlayer(props.players, props.event.target_id);
|
|
|
|
return <>
|
|
<td class="kill-source">
|
|
<PlayerName player={medic}/>
|
|
</td>
|
|
<td class="kill-icon">
|
|
ubered
|
|
</td>
|
|
<td className="kill-target">
|
|
<PlayerName player={target}/>
|
|
</td>
|
|
<td className="tick">
|
|
#{props.event.tick}
|
|
</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);
|
|
|
|
let remainingPlayers = players;
|
|
for (const queryPart of queryParts) {
|
|
// we only search by class for players we haven't already matched
|
|
// this allows "<name of scout1> scout" to find all cases of <scout1> and another scout
|
|
// instead of matching all other <scout1> events because they are also a scout
|
|
const playersForPart = findPlayersByName(players, queryPart) + findPlayersByClass(remainingPlayers, queryPart);
|
|
remainingPlayers = remainingPlayers.filter(player => !playersForPart.includes(player.info.userId));
|
|
filteredEvents = filteredEvents.filter(event => eventMatches(event, playersForPart, queryPart));
|
|
}
|
|
return filteredEvents;
|
|
}
|
|
|
|
function findPlayersByName(players: PlayerState[], queryPart: string): number[] {
|
|
return players.flatMap(player => {
|
|
if (player.info.name.toLowerCase().includes(queryPart)) {
|
|
return [player.info.userId]
|
|
} else {
|
|
return [];
|
|
}
|
|
})
|
|
}
|
|
|
|
function findPlayersByClass(players: PlayerState[], queryPart: string): number[] {
|
|
return players.flatMap(player => {
|
|
if (reverseClassMap.hasOwnProperty(queryPart) && reverseClassMap[queryPart] == player.playerClass) {
|
|
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 if (event.type === "building_destroyed") {
|
|
return queryPart === "destroyed" ||
|
|
matchedPlayers.includes(event.attacker_id) ||
|
|
matchedPlayers.includes(event.assister_id) ||
|
|
matchedPlayers.includes(event.victim_id) ||
|
|
event.weapon.includes(queryPart) ||
|
|
event.building_type.includes(queryPart);
|
|
} else if (event.type === "uber") {
|
|
return queryPart === "uber" ||
|
|
matchedPlayers.includes(event.user_id) ||
|
|
matchedPlayers.includes(event.target_id);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const reverseClassMap = {
|
|
'scout': Class.Scout,
|
|
'sniper': Class.Sniper,
|
|
'soldier': Class.Solder,
|
|
'demo': Class.Demoman,
|
|
'demoman': Class.Demoman,
|
|
'medic': Class.Medic,
|
|
'heavy': Class.Heavy,
|
|
'heavyweapons': Class.Heavy,
|
|
'pyro': Class.Pyro,
|
|
'spy': Class.Spy,
|
|
'engineer': Class.Engineer,
|
|
'engi': Class.Engineer,
|
|
};
|