fable/val Public Public: anyone can read this; nobody can change it.

forked from benjivers/val

workbenchthis machine
Files 3c63a82833bc2b178340f47c85d7acc2a4952706
1
/**
2
 * How a bot points a gun.
3
 *
4
 * The shot itself is shared with people — weapon spread, the move penalty,
5
 * the spray, the blind wobble all live in `weapons.ts` and apply to everyone
6
 * the same way. What lives here is the part a person is bad at and a bot was
7
 * not: seeing somebody, and getting the barrel onto them.
8
 *
9
 * A hold on a target has three honest costs. It takes a moment to notice
10
 * (`reaction`), the barrel starts somewhere wrong and only settles the longer
11
 * the same target is held (`aimCone`), and a target crossing the view is
12
 * harder to stay on than one walking down the barrel (`crossRate`).
13
 *
14
 * Everything here is a pure function of a track and a roll, so the caller
15
 * supplies the randomness — the match's seeded `random`, so a replay of a
16
 * seed still plays the same round.
17
 */
18
19
export type Point = { x: number; y: number };
20
21
/** One bot's hold on one target. Scratch space, carried on the player. */
22
export type Track = {
23
  /** The target being held, or null when nothing is in sight. */
24
  id: string | null;
25
  /** Match time the hold started: every cost here is measured from it. */
26
  since: number;
27
  /** Match time the target was last actually seen. */
28
  seen: number;
29
  /** Seconds between seeing this target and being allowed to pull. */
30
  reaction: number;
31
  /** How wrong the barrel is, in radians, signed. */
32
  error: number;
33
  /** Where the target was last seen, to measure how it is moving. */
34
  x: number;
35
  y: number;
36
};
37
38
export const emptyTrack = (): Track => ({ id: null, since: 0, seen: 0, reaction: 0, error: 0, x: 0, y: 0 });
39
40
/** A bot notices somebody somewhere in here, never sooner. */
41
export const REACTION_MIN = 0.2;
42
export const REACTION_MAX = 0.35;
43
/** A hold survives a blink — a smoke edge, a corner — without re-reacting. */
44
export const FORGET = 0.4;
45
/** Seconds for the cone to fall by 1/e while the same target is held. */
46
export const SETTLE = 0.8;
47
/** The cone a bot settles to: under a Vandal's own spread, never zero. */
48
export const CONE_FLOOR = 0.008;
49
/** What the cone opens to the instant a target is acquired (~6°). */
50
export const CONE_SWING = 0.105;
51
/** What a target crossing the view at a full run adds to it (~3.4°). */
52
export const CONE_MOVE = 0.06;
53
/** How fast the error wanders inside the cone, in cone-widths a second. */
54
export const DRIFT = 2.4;
55
56
const clamp01 = (n: number): number => (n < 0 ? 0 : n > 1 ? 1 : n);
57
58
/** The reaction a bot draws on acquiring a target: 200–350ms. */
59
export const reactionTime = (roll: number): number =>
60
  REACTION_MIN + (REACTION_MAX - REACTION_MIN) * clamp01(roll);
61
62
/** Seconds this target has been held. */
63
export const heldFor = (track: Track, now: number): number => Math.max(0, now - track.since);
64
65
/**
66
 * The trigger gate, as a pure function of when acquisition happened: no
67
 * target, or a target seen less than a reaction ago, is no shot.
68
 */
69
export const canFire = (track: Track, now: number): boolean =>
70
  track.id !== null && now - track.since >= track.reaction;
71
72
/**
73
 * The half-angle the barrel may be wrong by: wide on the swing, settling
74
 * toward the floor the longer one target is held, and wider again for a
75
 * target crossing the view (`crossing` is 0 for still, 1 for a full run).
76
 */
77
export function aimCone(held: number, crossing: number): number {
78
  return CONE_FLOOR + CONE_SWING * Math.exp(-Math.max(0, held) / SETTLE) + CONE_MOVE * clamp01(crossing);
79
}
80
81
/**
82
 * Point the track at a target. A different target — or the same one after
83
 * the hold was forgotten — starts a fresh hold: a new reaction to serve and
84
 * an error drawn anywhere in the swing cone.
85
 */
86
export function acquire(track: Track, id: string, now: number, at: Point, roll: () => number): void {
87
  if (track.id !== id || now - track.seen > FORGET || now < track.since) {
88
    track.id = id;
89
    track.since = now;
90
    track.reaction = reactionTime(roll());
91
    track.error = (roll() - 0.5) * 2 * aimCone(0, 0);
92
    track.x = at.x;
93
    track.y = at.y;
94
  }
95
  track.seen = now;
96
}
97
98
/** Nothing in sight. The hold survives a blink, then it is gone. */
99
export function lose(track: Track, now: number): void {
100
  if (track.id !== null && now - track.seen > FORGET) track.id = null;
101
}
102
103
/**
104
 * How fast the target is crossing the bot's line of sight, in units a
105
 * second. Walking down the barrel is easy to stay on; strafing is not, and
106
 * it is the part `atan2` to an exact position used to give away for free.
107
 */
108
export function crossRate(track: Track, bot: Point, at: Point, dt: number): number {
109
  const moved = { x: at.x - track.x, y: at.y - track.y };
110
  track.x = at.x;
111
  track.y = at.y;
112
  if (dt <= 0) return 0;
113
  const ax = at.x - bot.x;
114
  const ay = at.y - bot.y;
115
  const len = Math.hypot(ax, ay) || 1;
116
  return Math.abs(moved.x * (-ay / len) + moved.y * (ax / len)) / dt;
117
}
118
119
/**
120
 * Wander the error inside the cone and hand it back. It is a held mistake,
121
 * not fresh noise every tick: noise resampled at 30Hz averages to a perfect
122
 * barrel over a burst, which is the thing that felt like an aimbot.
123
 */
124
export function drift(track: Track, cone: number, dt: number, roll: () => number): number {
125
  track.error += (roll() - 0.5) * 2 * cone * DRIFT * dt;
126
  track.error = Math.max(-cone, Math.min(cone, track.error));
127
  return track.error;
128
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts