wren/val Public Public: anyone can read this; nobody can change it.
forked from benjivers/val
workbenchthis machine
Files 67215f710113bc2875d484e3beaed9d381f49946
| 1 | /** |
| 2 | * Who killed whom, read off the match rather than written into it. |
| 3 | * |
| 4 | * The round loop counts kills and deaths on the players and says a line |
| 5 | * about it; it keeps no list. Rather than change the rules to carry one, |
| 6 | * the arena compares the players before and after a tick: somebody who was |
| 7 | * alive and is not is a victim, somebody whose kill count went up is a |
| 8 | * killer, and the line the loop said settles which of them did it when two |
| 9 | * fall in the same thirtieth of a second. |
| 10 | * |
| 11 | * Pure functions over plain marks. Nothing here knows a position, and a fall |
| 12 | * never carries one — the feed must not be a way to learn where anybody is. |
| 13 | */ |
| 14 | |
| 15 | import type { Player, Side } from "./state.ts"; |
| 16 | import type { WeaponId } from "./weapons.ts"; |
| 17 | |
| 18 | /** Everything about a player this module needs, taken at one instant. */ |
| 19 | export type Mark = { |
| 20 | id: string; |
| 21 | name: string; |
| 22 | side: Side; |
| 23 | alive: boolean; |
| 24 | kills: number; |
| 25 | weapon: WeaponId; |
| 26 | }; |
| 27 | |
| 28 | export type Fall = { |
| 29 | killer: string | null; |
| 30 | victim: string; |
| 31 | /** The killer's gun when the kill came from a shot; null when it did not. */ |
| 32 | weapon: WeaponId | null; |
| 33 | headshot: boolean; |
| 34 | /** The match clock when it happened. */ |
| 35 | at: number; |
| 36 | }; |
| 37 | |
| 38 | export const marksOf = (players: Player[]): Mark[] => |
| 39 | players.map((p) => ({ id: p.id, name: p.name, side: p.side, alive: p.alive, kills: p.kills, weapon: p.weapon })); |
| 40 | |
| 41 | export type Context = { |
| 42 | at: number; |
| 43 | /** The lines the round loop said this tick, or the most recent few. */ |
| 44 | events: string[]; |
| 45 | /** Players whose shot landed on this tick, so a molly is not called a gun. */ |
| 46 | shooters: Set<string>; |
| 47 | }; |
| 48 | |
| 49 | /** |
| 50 | * The falls between two instants. Killers are matched to victims by the line |
| 51 | * the loop said, and by side and a spare kill when there is no line. |
| 52 | */ |
| 53 | export function fallsBetween(before: Mark[], after: Mark[], context: Context): Fall[] { |
| 54 | const was = new Map(before.map((m) => [m.id, m])); |
| 55 | const falls: Fall[] = []; |
| 56 | const credit = new Map<string, number>(); |
| 57 | for (const mark of after) { |
| 58 | const old = was.get(mark.id); |
| 59 | if (old && mark.kills > old.kills) credit.set(mark.id, mark.kills - old.kills); |
| 60 | } |
| 61 | const named = (killer: Mark, victim: Mark): boolean => |
| 62 | context.events.includes(`${killer.name} killed ${victim.name}`) || |
| 63 | context.events.includes(`${killer.name} headshot ${victim.name}`); |
| 64 | |
| 65 | for (const mark of after) { |
| 66 | const old = was.get(mark.id); |
| 67 | if (!old || !old.alive || mark.alive) continue; |
| 68 | const able = after.filter((m) => (credit.get(m.id) ?? 0) > 0 && m.side !== mark.side); |
| 69 | const killer = able.find((m) => named(m, mark)) ?? able[0] ?? null; |
| 70 | if (killer) credit.set(killer.id, (credit.get(killer.id) ?? 1) - 1); |
| 71 | falls.push({ |
| 72 | killer: killer ? killer.name : null, |
| 73 | victim: mark.name, |
| 74 | weapon: killer && context.shooters.has(killer.id) ? killer.weapon : null, |
| 75 | headshot: killer ? context.events.includes(`${killer.name} headshot ${mark.name}`) : false, |
| 76 | at: context.at, |
| 77 | }); |
| 78 | } |
| 79 | return falls; |
| 80 | } |
| 81 | |
| 82 | /** The feed as it is shown: the last few falls, and only the recent ones. */ |
| 83 | export const trimFeed = (feed: Fall[], now: number, keep = 5, life = 8): Fall[] => |
| 84 | feed.filter((f) => now - f.at <= life).slice(-keep); |