Two things from actually playing it. Neither is what it looks like from the outside, so here is what the code is doing.
1. The bots are not cheating on the shot — they are cheating on the aim
shoot() is one code path for everyone, and the spread is applied to bots exactly as it is to people: weapon spread, the move penalty, spray growth, blind wobble. That part is honest and should stay.
What is not honest is how they point. In bots.ts:
const aim =Math.atan2(target.y- bot.y, target.x- bot.x)+(random(state)-0.5)*0.06;turn(bot, aim,7* dt);
bot.input.fire= off <0.07&& range < weapon.range&& bot.ammo>0;
That is a perfect angle to the target recomputed every tick, blurred by ±1.7°, with the trigger pulled whenever the barrel is within 4°. A Vandal's own spread is 0.9°. So the bot is effectively always on target, and — the part that makes it feel like an aimbot — it has no reaction time and no tracking error: it acquires the instant you are visible and follows you perfectly while you move, which no person can do.
What it needs, in order of how much it will change the feel:
Reaction time. When a target first becomes visible, a bot should not fire for 200–350ms. Track when acquisition happened and gate the trigger on it. This alone stops the "I died before I saw anything" feeling.
Aim error that settles. Start the error wide when a target is acquired (several degrees) and let it shrink the longer the bot holds that same target, rather than a flat ±1.7° forever. A bot that has been tracking you for two seconds should be deadly; one that just swung onto you should not be.
Error that scales with your movement. A strafing target should cost them accuracy; right now atan2 to your exact position removes the hardest part of aiming.
Keep the spread shared — the fix belongs in bots.ts, not weapons.ts. And please keep it deterministic in the state, the way the rest of the simulation is.
2. Walking near a wall makes the view skip
This is a client/server disagreement, and the wall is where it is worst.
moveCircle resolves each axis separately and, when blocked, snaps to the tile edge (col * TILE - r - 0.01), with a fallback that reverts the axis entirely if the snap is still blocked. So a player pressed against a wall produces positions that alternate between a snapped edge value and a free one, or that do not move at all for a tick.
The client, meanwhile, has no prediction (a known gap): it draws the camera from look(snap.me), an exponential smooth at 0.35 toward the last snapshot at 15Hz. Away from walls the server agrees with your input and the smoothing is invisible; at a wall the server keeps correcting you and the camera is repeatedly dragged toward a snapped coordinate. That reads as skipping, and it will be worst exactly where you noticed it.
The proper fix is the one already on the list: predict your own movement on the client and reconcile against the snapshot, rather than smoothing your own body like a remote one. For that to work, the client must run the same collision resolution as the server — so the sensible shape is to move the step-and-slide maths into a module both can use, rather than writing it twice.
While you are in there: the axis-by-axis snap oscillates against a corner. Resolving movement as a slide along the wall (project the remaining motion onto the wall's axis) is stable, and it is what makes prediction agree with the server instead of fighting it.
The bar: the simulation stays deterministic and server-authoritative — prediction is a drawing optimisation, never a source of truth, and a mismatch resolves to the server's answer. Test the new collision maths directly (a circle walking into a corner should end up in one place, whichever axis is resolved first) and test the bot's reaction gate as a pure function of when acquisition happened.
Two things from actually playing it. Neither is what it looks like from the outside, so here is what the code is doing.
1. The bots are not cheating on the shot — they are cheating on the aim
shoot()is one code path for everyone, and the spread is applied to bots exactly as it is to people: weapon spread, the move penalty, spray growth, blind wobble. That part is honest and should stay.What is not honest is how they point. In
bots.ts:That is a perfect angle to the target recomputed every tick, blurred by ±1.7°, with the trigger pulled whenever the barrel is within 4°. A Vandal's own spread is 0.9°. So the bot is effectively always on target, and — the part that makes it feel like an aimbot — it has no reaction time and no tracking error: it acquires the instant you are visible and follows you perfectly while you move, which no person can do.
What it needs, in order of how much it will change the feel:
atan2to your exact position removes the hardest part of aiming.Keep the spread shared — the fix belongs in
bots.ts, notweapons.ts. And please keep it deterministic in the state, the way the rest of the simulation is.2. Walking near a wall makes the view skip
This is a client/server disagreement, and the wall is where it is worst.
moveCircleresolves each axis separately and, when blocked, snaps to the tile edge (col * TILE - r - 0.01), with a fallback that reverts the axis entirely if the snap is still blocked. So a player pressed against a wall produces positions that alternate between a snapped edge value and a free one, or that do not move at all for a tick.The client, meanwhile, has no prediction (a known gap): it draws the camera from
look(snap.me), an exponential smooth at0.35toward the last snapshot at 15Hz. Away from walls the server agrees with your input and the smoothing is invisible; at a wall the server keeps correcting you and the camera is repeatedly dragged toward a snapped coordinate. That reads as skipping, and it will be worst exactly where you noticed it.The proper fix is the one already on the list: predict your own movement on the client and reconcile against the snapshot, rather than smoothing your own body like a remote one. For that to work, the client must run the same collision resolution as the server — so the sensible shape is to move the step-and-slide maths into a module both can use, rather than writing it twice.
While you are in there: the axis-by-axis snap oscillates against a corner. Resolving movement as a slide along the wall (project the remaining motion onto the wall's axis) is stable, and it is what makes prediction agree with the server instead of fighting it.
The bar: the simulation stays deterministic and server-authoritative — prediction is a drawing optimisation, never a source of truth, and a mismatch resolves to the server's answer. Test the new collision maths directly (a circle walking into a corner should end up in one place, whichever axis is resolved first) and test the bot's reaction gate as a pure function of when acquisition happened.