Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ npm run build
- `CTM_SAS | CS2 Agent Model` by Alex (Creative Commons Attribution)
- `PHOENIX | CS2 Agent Model` by Alex (Creative Commons Attribution)
- `AWP with Anims` by Addison Ye β€” https://sketchfab.com/3d-models/awp-with-anims-d45669ad333d4885a854fcf899628a39 (Creative Commons Attribution)
- `Desert Eagle | First Person Animations` by 1Matzh β€” https://sketchfab.com/3d-models/desert-eagle-first-person-animations-09a213d8510a42d1b747135e85712eff (Creative Commons Attribution)
- Knife swing sound effects by Joseph SARDIN from BigSoundBank (`Sword through the air 2`, `Sword that cuts 3`, plus two additional swipe variants)
- Valve/CS2 proprietary audio is not bundled in this repo; drop-in replacement files can be placed in `public/audio/`.

Expand Down
Binary file removed public/viewmodels/deagle/deagle.glb
Binary file not shown.
51 changes: 47 additions & 4 deletions src/combat/BotController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,39 @@ export interface BotParams {
* very first frame it sees you.
*/
reactionDelaySec: number;
/**
* Angular aim inaccuracy (radians). The bot aims at a point that wanders
* around the true target within roughly this cone, so it is not a
* pixel-perfect aimbot: it drifts, over/under-shoots, and misses like a human.
* The wander is smoothed over time and scales with distance.
*/
aimWanderRad: number;
}

export const DEFAULT_BOT_PARAMS: BotParams = {
turnRateRadPerSec: 3.2,
turnRateRadPerSec: 2.4,
stopDistance: 6,
stuckSpeed: 0.6,
engageRange: 1500,
fireAngleTol: 0.07, // ~4 degrees
reactionDelaySec: 0.75,
fireAngleTol: 0.05, // ~3 degrees
reactionDelaySec: 0.9,
aimWanderRad: 0.06, // ~3.5 degrees of wander
};

/** Normalizes an angle to (-Ο€, Ο€]. */
function wrapAngle(angle: number): number {
return Math.atan2(Math.sin(angle), Math.cos(angle));
}

/** Approximate standard-normal sample (Box–Muller) for bot aim wander. */
function gaussian(): number {
let u = 0;
let v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}

/**
* Pure bot AI: given the bot's movement state and what it perceives, decide the
* movement input, how far to turn (yaw + pitch) this tick, and whether to fire.
Expand Down Expand Up @@ -134,6 +151,9 @@ export class BotController {
private hasAimTarget = false;
/** How long the current target has been continuously within engage range. */
private targetSeenSec = 0;
/** Smoothly-drifting world-space aim offset that makes the bot miss like a human. */
private readonly aimWander = new Vector3();
private readonly perturbedTarget = new Vector3();

constructor(spawn: Vector3, yawDeg = 0, params: BotParams = DEFAULT_BOT_PARAMS) {
this.params = params;
Expand All @@ -145,6 +165,7 @@ export class BotController {
this.wantsFire = false;
this.hasAimTarget = false;
this.targetSeenSec = 0;
this.aimWander.set(0, 0, 0);
}

getFeet(): Vector3 {
Expand Down Expand Up @@ -185,6 +206,28 @@ export class BotController {
tick(dt: number, world: CollisionAdapter, perception: BotPerception): void {
const debug = this.movement.getDebugState();
const horizontalSpeed = Math.hypot(debug.velocity.x, debug.velocity.z);

// Perturb the aim with a smoothly-drifting offset so the bot is not a
// pixel-perfect aimbot. The wander is an Ornstein-Uhlenbeck-style random
// walk (pulled back toward zero) whose amplitude grows with distance, so
// far targets are genuinely hard for the bot to hit.
let aimPerception = perception;
if (perception.targetFeet) {
const feet = this.movement.getFeetPosition();
const dist = Math.hypot(
perception.targetFeet.x - feet.x,
perception.targetFeet.y - feet.y,
perception.targetFeet.z - feet.z,
);
const amp = Math.tan(this.params.aimWanderRad) * Math.max(dist, 1);
const pull = Math.exp(-dt * 1.6); // relax toward zero
this.aimWander.x = this.aimWander.x * pull + gaussian() * amp * (1 - pull);
this.aimWander.y = this.aimWander.y * pull + gaussian() * amp * 0.5 * (1 - pull);
this.aimWander.z = this.aimWander.z * pull + gaussian() * amp * (1 - pull);
this.perturbedTarget.copy(perception.targetFeet).add(this.aimWander);
aimPerception = { targetFeet: this.perturbedTarget };
}

const decision = decideBotInput(
{
feet: this.movement.getFeetPosition(),
Expand All @@ -193,7 +236,7 @@ export class BotController {
horizontalSpeed,
grounded: debug.grounded,
},
perception,
aimPerception,
this.params,
dt,
);
Expand Down
Loading
Loading