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(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 () } 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 ( props.onClick()}> ); } 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 <> #{props.kill.tick} } 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 <> ({props.event.building_type}) #{props.event.tick} } 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 <> ubered #{props.event.tick} } 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 " scout" to find all cases of and another scout // instead of matching all other 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, };