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

forked from benjivers/val

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

Keyboard shortcuts