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);
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts