These changes come from the owner's picks in the gameplay feel review (2026-10-24): all the fixes (F3, F5 to F23), the starred items X1 to X3, T1, T2, T9, T15, T16 and T21, and the owner's "yes" picks: T3, T4, T6, T10, T11, T13, T14, T17, T18, T22, T23, T27, X4 to X9 and X11. X10 (bomb and FFA calls) is not in this build. Not yet checked in real play: X5 at a real ping (only tested locally), and T18 (bots never got the cart to the last leg).
- No protocol change (still 43): 1.18.21 plays with 1.18.16 to 1.18.20 games and servers, and demos are unchanged. Everything new rides on fields older builds already ignore: movement events in the top six bits of
PlayerSnap::keys(older clients only read the low ten held-key bits), the attacker's position in the text of the victim's copy ofEV_HIT, the killer's HP and respawn time in the text of the kill event, and a newEV_GUNFIREevent type (older clients drop unknown events). - T10 is the built option, pending the owner's pick on a card. Crossbow body damage 50 -> 49 doubles the body-only time to kill, from 2.0 s to 4.0 s (see below).
Movement: input (src/client/game.cpp, src/game/pmove.cpp)
F17: taps shorter than a frame
- What: jump, dash and grapple presses are latched into the next command, the way fire already was.
- Why: a press and release inside one frame never reached a command, so the tap was lost.
T1: jump buffer for taps
- What: a jump tapped within the existing 0.1 s jump buffer jumps on landing exactly like holding jump (including the bhop bounce).
- Why: a tap up to 30 ms before landing did nothing, and each 10 ms late cost about 1.4 m/s, so tappers lost to auto-hop. Measured: taps 10, 30, 60 and 90 ms early give the same result as holding; 150 ms early does not.
T2: held dash
- What: holding dash fires a dash the moment one is allowed (pmove's fresh-press rule is dropped), and a press made up to 0.1 s too early is held client-side until then.
- Numbers: held on the floor, dashes come 720 ms apart (0.12 s dash + 0.6 s); in the air, two and then none until you land, as the 1.18.7 dash rule says. Bots are unaffected.
F18: strafe speed and frame rate
- What: a frame's turn is spread over its ticks; the last tick has the exact aim. A tick with fire, alt-fire or grapple, and every tick after it, keeps the exact aim, so shots go where you looked. No added latency. Not on the frame you spawn.
- Why: strafe-jump speed depended on fps. Speed loss over 1.5 s of A/D at 2.5 rad/s, against a 1000 fps run: 30 fps -10.8%, 60 fps -5.5%, 144 fps -2.2% before; now -0.1% or better at any fps (at 3.5 rad/s it was -16.6 / -7.5 / -2.5%).
T3: dash jump after a ground dash aimed up
- What: a dash that starts on the floor opens the 0.1 s dash-jump window (it reuses
jumpGrace), even if it lifts you off at once. - Why: a ground dash aimed slightly up left the floor on its first tick, so it could not be dash-jumped: at +0.5 deg you got a plain 11 m/s dash instead of 24. Now +0.5 deg and +2.9 deg ground dashes with jump 30 ms in give 24 m/s and 7.32 m/s up.
T4: dash-jump window for held hops
- What: every jump off the floor opens the same 0.1 s window, held bunny hops included.
- Why: hold-jump hoppers touched the floor for about 6 ms, so their window was 6 ms against 100 ms for a fresh jump press.
T6: ladders
- What: only W and S climb; S climbs down; W climbs down when you look more than 26 deg below level; A and D move sideways only. Keys still move you off the wall (seven map ladders need it).
- Why: any key climbed up, and down was a hard switch at 26 deg below level.
Movement: prediction (src/client/game.cpp, src/game/netclient.cpp)
F19: syringe refill predicted
- What: reconcile now also compares dash charges, recharge timers and the dash and hook cooldowns with the server's.
- Why: a syringe refill wasn't predicted: the dash pips stayed empty and the next dash or grapple came a round trip late as a ~1.2 m correction.
F20: grappling an enemy predicted
- What: the hook probe is built from the interpolated enemies you see, with the server's filters.
- Why: you were pulled toward the wall behind them, then corrected: about 1.4 m at 60 ms ping.
Combat: numbers (src/game/gamedefs.h)
T9: kills that fell just short
- Rifle body 4.95 -> 5.0: 20 body shots now kill (they left 1 HP). Perfect-aim body kill 2.88 s.
- Shotgun head pellet 2.55 -> 2.6: two point-blank head blasts do 100.4 (was 98.6).
- The sound editor's scenes show 5.0.
T10: crossbow body 50 -> 49 (pending the owner's pick)
- What: body 49, head unchanged at 75.
- Why: a headshot never saved a bolt at 100 HP: 50 + 50 and 75 + 50 both killed in two. Now two body bolts do 98 (three bolts needed) and head + body does 124 (two). This was the smallest change that makes a headshot matter.
- Trade-off, plainly: body-only time to kill doubles, from 2.0 s to 4.0 s (at the 2 s refire). With a headshot it stays 2.0 s.
T11: own rocket splash ramp
- What: your own splash damage is 10% at feet + 0.3 m or lower, rising linearly to 100% at feet + 1.2 m (
kSelfRampLow/kSelfRampHigh). - Why: self-damage jumped tenfold at feet + 0.3 m: a rocket at knee height cost about 30 HP, a hair lower 3. Now: at 0.5 m 10.2 (was 34), at 0.75 m 18.7, at 1.2 m and up 34. Rocket jumps (at the feet) are unchanged.
Combat: firing (src/game/server.cpp, src/client/game.cpp, src/client/weaponpredict.h)
T13: early clicks on slow weapons
- What: a fresh click on the sniper, crossbow or rocket up to 0.1 s early (
kTapBuffer) fires when the weapon is ready, on the command clock. A lob, a switch or the fire lock drops it. - Why: the old 0.15 s fire buffer was dead code, so early taps were dropped.
T14: weapon after a throw
- What: after a knife or syringe throw you switch back to the weapon held before (
ServerPlayer::prevWeapon, predicted byWeaponPredictor::prev). - Why: it always went to the first slot, the rocket.
F12: spawn fire lock mirrored
- What: the client mirrors the server's 0.25 s spawn fire lock (
kSpawnFireLock), shortened by your own ping. - Why: a click right after respawning played a shot the server never fired.
F22: per-player command clock
- What: refire is timed by a per-player command clock (the Quake 3 way), clamped to [server time - 0.1 s, server time]. Refire, the semi-auto buffer, the 0.25 s spawn lock and the syringe refill use it.
- Why: refire was timed by when packets arrived, so taps at the end of a cooldown fired with a later aim or were lost. Measured: a held rifle fires 7 shots in 0.96 s on exact cooldown ends; 200 commands of 8 ms in one tick give at most one shot. Before, the first command of a tick decided, up to 7 ms early aim at 125 Hz, or a tick late.
F21: lag compensation between ticks
- What: the rewind blends between two history records (never across a gap over 4 m, a teleport).
- Why: it landed up to a tick behind what you saw: about 8 cm at strafe speed at 125 Hz, more at lower tick rates.
F16: rocket splash and cover
- What: when the body centre is blocked, splash also tests the closest point and the head (at most 2 more rays, only then).
- Why: a half-peeking player took 0 damage instead of the expected amount. Test: 4.7 damage over a crate (was 0).
F14: shotgun headshots
- What: a shotgun blast counts as a headshot only when at least half its damage was to the head.
- Why: one head pellet made the whole blast a headshot. A 3 m neck-high blast with 4 of 20 pellets on the head (37.6 damage) was a HEADSHOT; a point-blank head blast (52) still is.
F15: bubble stream count
- What: the bubble stream counts only enemies you can see (at most 2 rays per enemy within 30 m, once per 1.8 s stream).
- Why: the sight table counts everyone within 20 m as seen, so 5 bubbles meant someone within 5 m, even through a wall. Test: 5 bubbles with an enemy in sight at 3 m, 15 with one behind a wall.
T21: ring-out credit
- What: a fall or ring-out within 3 s of being hit (rocket, crossbow knock, grapple pull; also the
killcommand) is the attacker's kill, checked against theirjoinId. Teammates, race, hide and seek and tag are excluded. Kill feed: "X [fell] Y". - Why: the victim lost a point and nobody got one. This matters on Temple, Gantry (ranked instagib), Dojo, Anvil and others.
Sound (src/client/game.cpp, src/client/clientfeel.h, src/game/pmove.cpp, src/game/server.cpp)
F3: own hurt sound
- What: at most once per 0.15 s; damage in between is summed, and volume is 0.45 + 0.011 per point (1.0 at 50; it was a fixed 0.7).
- Why: under the laser (and drain or vampire) it played about 40 times a second, around 9 voices at once, drowning footsteps. A 1 s laser burst hitting every 10 ms now plays 7 sounds instead of 100.
F8: movement sounds from movement events
- What: pmove reports its events per tick (
MoveState::events: jump, land, dash, super, hyper, hook, plus the landing speed); they're not sent or compared. The client collects them from each predicted tick and plays sounds from them. - Why: sounds came from comparing state once a frame, so a landing and a jump in the same step, or a dash and a dash jump in one frame, were missed: jump + dash together made a silent dash jump or hyper, and bunny-hop jump and land sounds mostly vanished (more at higher fps).
X2: enemy movement sounds
- What: the server collects each player's movement events between snapshots and sends them in the top bits of
PlayerSnap::keys(kKeyEvShift10, plusKEY_EV_LAND_HARDfor a landing over 12 m/s). The client plays others' jump, land (louder when hard), dash, super and hyper sounds in 3D. Demos carry skipped snapshots' events. - Why: enemies were silent in the air, and holding jump is optimal, so movers were almost silent, while bots hear dashes 36 m away. Test: 2 s of hops and a dash, 4 jumps, 4 landings and 1 dash heard.
X9: muffled distant gunfire
- What: living humans within 60 m (
kMuffledRange) who aren't sent the shooter getEV_GUNFIRE: the weapon and a spot rounded to 4 m, no shooter id. At most once per 0.3 s per shooter and listener; the scan runs at most 20 times a second per shooter. Not for the knife, syringe or locator. The client plays the weapon's sound at volume 0.45, pitch 0.8. - Why: gunfire more than 20 m away and out of sight was silent to players, while bots hear it out to 75 m. Older clients ignore the event.
F23 (sound part): remote footsteps on stairs
- Footsteps are probed on stairs when the climb is 45 degrees or less. Before, a step only counted under 3 m/s vertical speed, so stairs were silent.
- Dropped: the "TEN / FIVE SECONDS never play" part of F23. The owner removed those lines in 1.18.16.
X8: front and back
- What: a 3D sound behind you is up to 3 dB quieter and goes through a one-pole low-pass (about 4 kHz straight behind), both scaled by how far behind it is. Sounds in front and 2D voices cost the same as before.
- Why: sounds panned left and right only, so front and back sounded the same. Measured 5 m behind against in front: -3.1 dB, high frequencies x0.17.
HUD (src/client/game.cpp)
X7: smooth stairs
- What: on the floor, a height change under 0.6 m between frames is eased on the camera with an 8 ms time constant (95% in 25 ms). Camera only (shots leave from the real eye, so they can differ by up to 0.5 m for those 25 ms); reset on death; not in demos.
- Why: a stair step moved the camera up to 0.55 m in a single tick, so running up ten steps was ten jolts.
X4: instant hit sparks
- What: rifle, sniper, shotgun (not ricochets) and laser (at most 20 a second) shots are tested on the client against enemies as you see them, stopping at the first wall, and draw a red spark per hit at once. The hit sound and damage number still come from the server.
- Why: hit confirmation waited a round trip (about 80 ms online).
X5: your projectiles at the click
- What: rockets, lobs, crossbow bolts and knives get a local copy launched as the server launches them (0.3 m from the eye, same speed, spread, lob drop,
projectileExtrapolate), drawn from the muzzle and hidden for the first 0.8 m. The server's projectile takes over the drawn lead (at most 0.4 s ahead) and the offset blends out over 0.2 s; a copy nobody claims in 0.6 s is dropped. At most 8 copies, one ray a frame each. - Trade-off: a rocket can sit at a wall about a round trip before the server's explosion.
- Not checked at real ping: tested locally only.
X3: damage direction and enemy tracers
- What: a red arc round the crosshair toward the attacker, fading over 1 s, up to 6 at once (made a little bigger and stronger after the first try). Enemy rifle shots draw 0.2 s tracers. The attacker's position comes in the victim's copy of
EV_HIT(decimetres in the text). - Why: getting hit gave no direction, and enemy rifle shots drew no tracer.
X6: kill marker and death burst
- What: a red X kill marker for 0.45 s (fading over the last 0.2 s), shown while scoped too. A killed player in sight plays
SND_DEATHin 3D and a red burst (the explosion effect). No new assets. - Why: a kill looked the same as a hit, and a killed enemy just vanished.
F13: kill sound order
- What: the batched hit is sent before the kill.
- Why: hits are batched every 50 ms, so the killing hit's sound and number arrived after the kill sound (laser and sticky chains).
X1: death screen
- What: "KILLED BY X [WEAPON] - 37 HP" and "RESPAWN IN 1.4". The killer's HP is shown outside ranked only (and not when the killer is dead). Round modes show no countdown. Carried as "<killer hp> <respawn ms>" in the victim's kill event text.
- Why: the screen only said "DEAD - WATCHING X".
F23 (HUD part): weapon bar
- The HUD weapon bar fills over the shot's own refire, including alt-fire refire and the spawn lock.
Modes (src/game/gamemodes.cpp, src/game/server.cpp)
F11: spectating in team modes
- What: after dying in a team mode you watch your killer for 1.5 s (
kKillCamTime; the server gives you the killer's sight for that time), then cycling shows teammates only. - Why: dead players could watch an enemy's first-person view, which matters in bomb.
T16: team spawns
- What: team spawns are picked like FFA, but only enemies count, and spawns no enemy can see are preferred. A spawn a teammate stands on is taken.
- Why: spawns avoided teammates instead. Respawns in an enemy's sight: Summit KOTH 57% -> 22%, Midtown 33% -> 20%, Railyard 12% -> 9%.
F6: hide and seek seeker respawn
- What: a seeker who falls or dies is back after 2 s (
kSeekerRespawn, the owner's pick) and stays the seeker. - Why: the seeker was never respawned, so the round idled out its 300 s and every hider scored.
T23: hide and seek tiebreak
- What: each player's time hidden (alive, not seeker, in live rounds) is summed. A tie on score goes to the most time hidden ("tied for first - X stayed hidden longest (N s)"). Rock paper scissors only if that's level within half a tick.
- Why: rock paper scissors decided 4 of 6 bot hide matches, 2 of them 5-way ties after 15 to 20 minutes.
F9: payload and points round calls
- What: round 1 names no winner: "TEAM A pushed 57 m - TEAM B must beat it" with the ROUND OVER voice. Round 2 says "beats", "level with" or "short of TEAM A's 57 m".
- Why: defenders heard ROUND WON, then YOU LOSE 4 s later (9 of 12 Railyard matches).
T15: stopwatch round 2
- What: round 2 of payload and points ends as soon as the result is locked: the attackers are past the first attack's mark, or, against a finished attack, their clock is past its time. The stopwatch runs from when the round goes live. The built-in payload and points logic (
tools/modes/payload.qlogic,points.qlogic) does the same, so a custom mode based on them keeps it. - Why: 9 of 12 Railyard matches wasted about 3 minutes (up to 4.6).
T22: shorter round end in 1v1, duel and tourney
- What: the
roundEndTimemode parameter is 2.5 s for 1v1, duel and tourney (was the common 4 s;modeparams.hand theirtools/modes/*.qlogic). Other modes are unchanged, and a custom mode can still set its own. - Why: these rounds are one kill each way, so the 4 s break was a large share of the match.
T17: level attacks in payload and multi point
- What: each attack keeps when (since its round went live) it reached its best progress (
bestTime_,noteBest). Level on progress, the sooner one wins; dead level is a draw, and the tiebreaker pair of rounds is gone. The T15 stopwatch also locks round 2 as won once the attack is level and got there sooner. Calls: "TEAM A took 2 points by 3:40 - TEAM B must beat it"; "TEAM B took 2 points by 2:55 - level with TEAM A's 2 points, there sooner than its 3:40". The built-in logic reads it ascart.bestTime[team](payload.qlogic, points.qlogic), so custom modes on them keep the same result. - Why: Midtown went to tiebreaker rounds in 6 of 16 bot matches, each adding 8 to 11 minutes.
T18: payload's last leg
- What: on a payload map without staged spawns, an attacker past the last checkpoint respawns after 3 s (
kLastLegRespawn); everyone else after 5 s. It's in the mode's respawn delay (rulesRespawnDelayand payload.qlogic'srespawnDelay, with two new read-only logic fields,cart.checkpointCountandstagedSpawns), so the death screen's RESPAWN IN shows it. No map change. - Why: on Railyard's last leg attackers spawned 55 m from the cart and defenders 37 m (about 2.6 s more running each life), and only 3 of 40 bot rounds were delivered. Forward staged spawns would have made Railyard one-way and needed its spawns re-balanced.
- Not measured in play: in 25 minutes of advanced bots each way (3 s and 5 s), the bots never got the cart past about 16 of 136 m, so they never reached the last leg.
T27: FFA spawns
- What: FFA picks at random among the standing spawns nobody can see that are at least 75% as far as the best one (the six farthest only; at most 6 rays per living player).
- Why: it always took the single farthest spawn, so spawns were predictable. Harbor now uses 3 different spawns and Gantry 6 in 60 tries, none in sight.
F23 (modes part)
- 1v1 and duel calls use player names ("a & b" in 2v2) instead of "TEAM A wins".
- Killstreaks reset at match start, so they don't carry over from warmup or the last match.
- Multi point can't start on a map without points (and ends if it runs).
Bots (src/game/bots.cpp, src/game/botbrain.cpp, tools/bots, tools/maps)
F7: play vs bots sizes
- What: 1V1 is 1 enemy and 0 allies; DUEL uses the duel team size per side.
- Why: both used the team sliders, so they were really 2v3.
F10: bomb vs bots on Depot and Lodestone
- What: defender bots pick a site by their rank in the team (a new
teamRanksensor in the text brain: GRAMMAR.md, the bot editor's ast.js, default.qbrain). Depot's gatehouse front posts are moved inside the wall ends. Lodestone's rock ribs stop 2.5 m short of the cavern wall, and its portal posts are flush. - Why: sites were picked by slot-id parity, which sent every defender to the same site. Depot attackers stuck on the spawn wall: a 0.7 m gap for a 0.8 m player, 0 kills in 60 s in most rounds. Lodestone's ribs made dead-end corners.
- Map report: Lodestone points 7.6 -> 7.4; payload 9.0 -> 9.5; both-ways payload 8.6 -> 9.1. Every map still passes. The test harness now shares a route-search budget per tick.
X11: bots react to being hit
- What: being hit counts as hearing the attacker. After the bot's reaction time (450 / 300 / 200 / 150 ms by level) it gets a rough position (with the usual sound error), turns and hunts. Uses
lastHitBy. - Why: bots didn't react to being hit from beyond their hearing, by a sniper for example.
Servers (src/game/gamemodes.cpp)
F5: short-handed matches
- What: in an unranked match that loses a side, or everyone, the match holds (no rounds awarded, clocks wait) and says "not enough players - the match ends in 10 s unless someone joins". After 10 s (
kShortHanded), the side still there wins by forfeit in team modes ("X wins - the other side left"); otherwise it goes back to warmup. Someone joining in time resumes it. Tourney (walkovers), tag and hide and seek settle it themselves. A match started short-handed on purpose (forcestart, a lone vote) plays on. - Why: an empty or one-sided server kept playing: VS looped "round draw" every 7 s, KOTH stayed live, and an opponent leaving 1v1 or duel auto-awarded rounds until "TEAM A wins".
Tests
- New or extended headless tests for the fixes (
testFeelFixesin the weapons group, and the rules, movement and bot tests). Throw and syringe timing checks were widened by the command clock's 0.1 s window.