An update from the owner's gameplay feel review (the review's items F1 and F2, the owner's dash bar report, and the owner's new dash timing).
- Network protocol 43 (was 42). The own movement state now carries each spent dash charge's recharge timer and the speed a running dash started from, so 1.18.7 doesn't play with 1.18.6 or older games and servers: a 1.18.6 client joining a 1.18.7 server (or the other way round) is turned away with the usual "version mismatch (server 43)" reject. Update the game and the dedicated servers together (servers auto-update when 1.18.7 reaches production; until then the TESTING build only plays with TESTING servers). Demos recorded with protocol 42 or older are converted at startup as always (the original kept in
demos/old/<version>/): their snapshots had no recharge timers, so they read as nothing recharging. - Gameplay changed only in the dash (below: its timing, and the wall bounce / dash jump / super dash speeds). Everything else is in what the client shows and predicts.
Movement: dash timing (src/game/pmove.h, src/game/pmove.cpp)
- Owner: "2nd dash you can use after 0.6 seconds", then "dashes take 1.2 seconds to recharge, but you can use them 0.6 seconds apart, so when you jump in the air with 2 dashes, you can use both of them within 1 second, without recharging any charges", with refills picked on a card as "Landing refills, 1.2 s wait".
- Before (1.17 to 1.18.6): two charges, and every dash waited 1.2 s (
kDashCooldown) from the end of the previous one. From full in the air, the second dash came 1.32 s after the first (start to start), so both didn't fit in a second. - Now, two timers:
- Cooldown between dashes:
kDashCooldown1.2 -> 0.6 s, still counted from the END of the previous dash (dashCooldownMs, whole ms), still counting only while no dash runs. - Recharge per spent charge: new
kDashRecharge = 1.2 s. A spent charge gets its own timer (MoveState::dashRechargeMs[], whole ms, oldest first,dashRechargingof them), started when the dash that spent it ends: while that dash runs, its timer waits at the full 1.2 s, and older timers keep counting. Once a timer is over, the charge can come back, but only the way charges always have: ending a step on the floor refills all of them, a bhop landing (jump held) or a hard-landing bounce up to one, a dash jump all, a super dash or a wall bounce exactly one. No refill during a dash's own steps, on a ladder or off a grapple, as before. A charge whose 1.2 s is over while you're in the air stays spent until you land. A teammate's syringe still gives every charge back at once and now also clears the recharges (dashRefillAll). - Why per-charge timers rather than one: the owner's rule is per dash ("dashes take 1.2 seconds to recharge"), and with two charges the second can be spent while the first is still recharging. One timer couldn't say which charge comes back when.
- Cooldown between dashes:
- The dash techs' refills and the recharge. A dash jump still gives back the charge its own dash just spent, at once (owner: "exempt the dash jump"; that charge's recharge timer, the newest, is dropped), so from two charges you keep both, as before. Any other charge still recharging stays out until its 1.2 s: a ground dash, then 0.6 s later a dash jump, leaves one. (A first 1.18.7 build applied the recharge to the dash jump too, so a dash jump from two left one; the owner exempted it.) A super dash and a wall bounce follow the recharge: they still leave exactly one, down to one if you hold more (custom modes with 3+ charges), else up to one only if a charge has recharged. The client's super / hyper sound used to tell the two apart by the charges left (one = hyper). That no longer works, so it now uses pmove's own aimed-down test (
dashAimedDown). - Measured with the game's movement code (1 ms steps, a fresh dash press every other ms,
build/quark_tests dashtech):
| Pressing dash repeatedly | 1.18.6 | 1.18.7 |
|---|---|---|
| in the air from full: the two dashes, start to start | 1,320 ms | 720 ms (0.12 s dash + 0.6 s), then none until you land |
| on the floor, start to start | 1,320 ms (4 in 4 s) | 720 ms (6 in 4 s): the 1.2 s recharge is over before the 0.6 s spacing lets the third go |
| bunny-hopping (jump held), start to start at least | about 1,200 ms (the cooldown) | 600 ms (6 in 4 s; a dash on a landing tick is a dash jump, which ends at once) |
| chained super dashes over 4 s | 4 | 5, at least 668 ms start to start |
| quick dash: air, two dashes | 520 ms | 320 ms |
| quick dash: floor | 520 ms (8 in 4 s) | 320 ms (13 in 4 s) |
| quick dash: bunny-hopping, start to start at least | about 400 ms (the cooldown) | 200 ms (14 in 4 s) |
| quick dash: chained super dashes | 6 in 4 s, at least 386 ms start to start |
On the floor a spent charge comes back exactly 1,200 ms after its dash ended (tested).
- Quick dash (
MOVE_QUICK_DASH: duel QUICK DASH / DASHING BLADES, the BLADES arena) keeps a third of both (kQuickDashCooldown1/3, unchanged): 0.2 s apart (was a 0.4 s cooldown) and a 0.4 s recharge. The BLADES description now reads "KNIVES ONLY - QUICK DASH, 0.2 S APART" (was "DASH COOLDOWN 0.4 S"; kept about as short as the other arena lines). - Custom modes: the DASH COOLDOWN multiplier (
move_dash_cooldown) scales both timers, so there's no new setting and old.qmodefiles load unchanged. The MOVEMENT page's standard value is now 0.6 s. An old file's multiplier keeps its number: 2x used to mean 2.4 s between dashes and now means 1.2 s apart with a 2.4 s recharge. Charges stay at most 10 (2 x the 5x limit, now an explicit capkMaxDashCharges, the timers' room). - Tests:
dashtech: the exact cooldown and recharge at every step of two air dashes (0.599 s after the end: nothing; 0.6 s: the second; both recharged in the air but nothing back until landing), the ground, bhop, super dash chain and quick dash spacings above, the ground's 1,200 ms recharge, the dash jump keeping both from full and leaving one when the other charge is still recharging, and an own-state round trip.syringe: a throw clears a 30 s cooldown and two 30 s recharges.demos: a protocol 42 record converts to 43 (charges and cooldown kept, nothing recharging), next to the 41 and 40 cases. - One shared rule:
dashReady(const MoveState&)(pmove.h) says whether a fresh press in the next command starts a dash: a charge left, no dash running, the cooldown over by that command's first 1 ms step. The bots (fight dashes, run dashes, breaking free of a grapple pull), the HUD pips and the map report all use it, so they match pmove exactly. Bots needed nothing else: their dash rates are per-second chances gated by that check, not tuned around the old 1.2 s.
Movement: dash tech speeds (src/game/pmove.cpp)
- Owner: "the wall bounce dash, super, and hyper should all have their horizontal speed changed from 24/24/32 into 16/24/+5 of current".
- The names in the code (the same names the SUPER / HYPER sounds use, game.cpp: SND_SUPER for a dash cut short by a jump, SND_HYPER when that dash was aimed down):
- wall bounce = the dash wall bounce: a dash into a wall under 45 deg from its normal reflects off it (
kDashBounceAngle). - super = the dash jump (Celeste's super): jump while a dash runs along the floor.
- hyper = the super dash (Celeste's hyper): the same off a dash aimed at least 14.5 deg down (
kSuperDashDown).
- wall bounce = the dash wall bounce: a dash into a wall under 45 deg from its normal reflects off it (
- Wall bounce: 24 -> 16 m/s. It reflected at the dash's burst (24, or the dash's exit speed when that was higher, up to 32), ran the rest of the dash at it and left with it. Now it's the new
kDashBounceSpeed = 16(x a custom mode's DASH SPEED, which scaled the burst before): the reflected rest of the dash runs at 16 and leaves at 16 (dashBurst()gives a bounced dash's speed). A dash jump after a bounce keeps the 16 (as it kept the burst before); a super dash after one adds to 16. Measured: at 30 deg into a wall it leaves at (-8.0, 13.86) = 16 m/s (was (-12.0, 20.78) = 24). - Super (dash jump): unchanged, it leaves with the dash's burst (24 from a standstill or a run; the dash's exit speed, up to 32, when faster).
- Hyper (super dash): the speed you had + 5. It was the burst x 1.25 (
kSuperDashBoost), at least the burst, at most 36: 30 m/s whatever you came in with, up to 36 at speed. Now it's the horizontal speed you had when that dash started (the newMoveState::dashFromSpeed, or the bounce's 16 after a wall bounce) + 5 m/s (kSuperDashBoostis now 5, an addition).- What "current" means, and why: the speed you carried into the dash, not the dash's own burst. During the dash you always move at the burst (24), so "+5 of the burst" would be a flat 29, barely different from today's flat 30. The owner's request is a change from a fixed number to one that follows your momentum ("of current"). This way a hyper always adds 5 to what you had.
- Floor: never less than the same dash leaves with on its own (its exit speed: your speed + 1, at least 11). Without it, a hyper from a standstill would leave at 5 m/s, slower than a plain dash. With it, a hyper from a standstill is 11 m/s (was 30).
- Cap:
kSuperDashMax36 is kept. It's there because the dash's speed limit is intended (owner decision 5.4). Without it, hypers chained off bhop landings would add 5 each without end. The cap was already 36. - Custom modes: SUPER DASH BOOST (
move_super_dash_boost) now scales the +5 (the MOVEMENT page shows standard 5 m/s; it was x1.25). Old files load unchanged; an old 2x now means +10 m/s instead of x2.5. SUPER DASH SPEED (MAX) still scales the 36.
| super dash (hyper) from | 1.18.6 | 1.18.7 |
|---|---|---|
| a standstill | 30 m/s | 11 m/s (the dash's own floor) |
| a run (9.5 m/s), or out of a bhop at 9.5 | 30 | 14.5 |
| 20 m/s | 30 | 25 |
| 30 m/s | 36 (cap) | 35 |
| 33 m/s and up | 36 (cap) | 36 (cap) |
Chained hypers off bhop landings (jump held, forward, looking down, a dash press whenever it's allowed) over 4 s: 5 hypers, fastest 31.0 m/s; with quick dash, 6, reaching the 36 cap. The hop (half a jump at the threshold up to a full jump straight down) is unchanged.
- Map report: the speed build-up's super-dash variant used to reach 30 m/s off the first hyper. Now it chains +5s, so the run-up to 28 / 32 / 36 m/s goes from 0 / 17 / 20 m to 24 / 38 / 44 m (the take-off envelope at each speed is the same). No map's score changed (viaduct's tech path: 9.4 -> 9.2 jumps per 100 m, challenge still 8.0).
- Tests:
dashtech: the hyper from a run and out of a bhop is the speed before + 5 (14.5), from a standstill 11, 30 -> 35 and 33 -> 36; the wall bounce leaves at 16, a dash jump after it keeps 16; the chained hypers never pass 36.
On the wire (src/game/protocol.cpp)
- Protocol 43 also adds the f32
dashFromSpeedto the dash block (opt bit 2, sent only while a dash runs: 4 bytes), so a predicted hyper is exact. Older demos read it as the dash's exit speed - 1. - Own movement state (protocol 43): the opt bit 4 block is now a byte of
charges | recharging << 4, the u16 cooldown, then a u16 per recharging charge (it was the u8 charges + the u16 cooldown). It's sent only when charges aren't 2 or a timer runs, as before: 3 bytes plus 2 per recharging charge, for at most about 1.3 s after a dash, and only in your own snapshot. Measured: 54 bytes for an idle own state, 61 with two charges recharging. Exact (whole ms), so prediction replays from any snapshot to the same state: the replay test now also compares the recharges, 0 of 357 snapshot points differ (345 of them with a charge recharging). A hostile count above 10 is dropped on read.
Map report (tests/mapreport.cpp, docs/MAP_REPORT.md, docs/MAP_METRICS.md)
- The movement envelope the map checks grade with is simulated with pmove, so it follows the new timing. Two changes to the simulation itself. The speed build-up dashes when
dashReadysays a press would go (it checked the old cooldown field by hand). The take-offs get a new option: a second dash pressed the moment pmove allows it. Before, the second dash only came from fixed presses 0.125 s and 0.3 s after the first, which the 1.2 s cooldown always refused (and the 0.6 s one would too), so the envelope had quietly dropped to one dash since that cooldown came back (protocol 42). Grading is unchanged; no metric was loosened. - Envelope (from a take-off at each speed: rise, then across at the take-off's level, 3.5 m up and 8 m up):
| speed | 1.18.6 | 1.18.7 |
|---|---|---|
| 9.5 m/s (a run) | 4.2 m up; 21.5 / 11.5 / 0 m across | 8.1 m up; 47.5 / 43.5 / 33.0 m across |
| 20 m/s | 7.1 m up; 33.0 / 29.5 / 0 m | 11.7 m up; 48.5 / 44.0 / 37.0 m |
| 28 m/s | 11.2 m up; 55.5 / 51.5 / 45.0 m | 17.4 m up; 70.5 / 66.5 / 61.0 m |
| 36 m/s | 13.1 m up; 67.5 / 63.5 / 57.0 m (42 m of run-up to get there) | 20.0 m up; 83.0 / 79.5 / 73.5 m (44 m of run-up, with the new super dash speeds) |
At a run that's close to the 1.17 envelope (42 m across, 8.9 m up: a dash jump and two more dashes, no cooldown).
- Scores: every map check in
quark_testsstill passes, as before (dojo's low layout, movement and creativity grades are unchanged by this). What moved: harbor's movement score 8.4 -> 8.3; viaduct's movement 8.6 -> 8.7 and its race challenge 7.6 -> 8.0 (more jumps per 100 m on the tech path); the tech payoff sub-score of anvil 7.5 -> 7.4 and depot 6.9 -> 6.8 (a sub-score; the movement score is the mean); lodestone's race challenge 3.6 -> 3.8. The roof and wall-top areas a good player reaches beyond the run are unchanged on every map (the playable footprint already bounds them), and none of it is out of bounds.
Weapon switch predicted (src/client/game.cpp, src/client/weaponpredict.h)
- Before: after a weapon key, the view model, weapon name, slot highlight, cooldown bar, scope and every part of local fire prediction (shot sound, tracer, refire, laser beam, the muzzle point projectiles start from) kept the old weapon until a snapshot confirmed the switch: one round trip, about 80 ms online. Firing in that window played the old weapon's sound, tracer and cooldown (rocket to laser showed a rocket sound and a 1.2 s bar) while the server fired the new one.
- The held-fire bug: when the snapshot confirmed, the client cleared its wanted weapon, so the next command carried "no change" (255). The local fire lock compared that against the last command's weapon, saw a change with fire held and locked local fire: your own laser beam and its sound stopped until you let go, while the server kept firing and dealing damage.
- Now: the client keeps a predicted held weapon. From the first command after the key it runs the server's switch rules on what it knows: the mode's weapon pool (
weaponMask, the same function the server uses), a weapon that's away (a thrown knife or syringe) only when nothing else is left, no switch while dead, frozen players can still switch (as on the server). It also predicts the server's switch to the next weapon after a knife or syringe throw. - Rejections: the choice goes out in every command until a snapshot's acknowledged command number covers the first command that carried it; from then on the snapshot's weapon is the answer. If the server accepted, nothing changes on screen; if it refused, its weapon comes back once. No timing margin is guessed. A reconnect clears the choice.
- The fire lock now follows the server's own rule exactly: it locks only when a command switches weapon with fire or alt-fire held.
- Also fixed: pressing A, then B, then A again within one round trip left you on B (a stale snapshot still said A, so the client dropped the choice).
- Demos and spectating still show the snapshot's weapon. Cost: a few integer compares per tick, nothing allocated.
- Test
rules weaponpredictruns the predictor against a real server and checks the server's weapon and fire lock after every command: plain switch, fire right after the key held through the confirmation, a switch with fire held, an unforeseen refusal, a weapon outside the pool, a knife throw and asking for the knife while it's away, death and respawn, and a 40 ms round trip. About 4,500 commands checked, 0 wrong.
Round clock on the HUD (src/client/game.cpp, src/client/hudclock.h)
- Before: since 1.17.0 the line under the mode title showed the time left only in overtime (and in drain, KOTH and time trial, which have their own lines). The 1.17 overtime change replaced the plain clock with two overtime-only branches, so FFA, tag, hide and seek, bomb, payload, both-ways payload, multi point and race showed no clock unless you turned on the verbose HUD. Tag's 60 s fuse is the whole mode and wasn't on screen.
- Now (owner: "the round clock should appear in all modes with a timer"): FFA, tag, hide, payload, npayload, points and race show
m:ss; bomb shows the round clock before the plant andBOMB m:ssafter it (the big red fuse under the objective stays); any overtimeOVERTIME m:ss; FFA overtime, VS / duel / 1v1 / tourney (drain), KOTH (FIRST TO N, untimed) and time trial are unchanged. A live round with no timer shows nothing rather than a frozen 0:00. - The line is built into a stack buffer with no per-frame allocation (the old string concatenations are gone).
- Test
rules hudclockstarts a real server in 12 modes and checks the line each client receives, plus the planted bomb, overtime, FFA overtime, time trial, an untimed round and the stalemate by hand. Checked with headless screenshots in each mode.
Dash bar (src/client/game.cpp)
- Before (1.18.6): while the 1.2 s cooldown ran, each charged pip was drawn growing from empty. One dash left one charge, but both pips looked empty, so it read as if one dash had spent both (owner's report). An earlier 1.18.7 build drew a full dim pip with the progress lit over it.
- Now (owner: "it should still be light grey that turns to blue (the whole bar not the slider) aka the off color to the on color"): a pip per charge. A charge you have but can't dash with yet (the dash still running, or the 0.6 s cooldown since it ended) is a whole light grey pip with no progress fill. It turns wholly to the HUD color (blue by default) the moment a press would dash (
dashReady, pmove's own rule). A spent charge is just the dark backing, while it recharges and after, until a refill gives it back (so in the air it stays empty until you land). The DASH label is white when you can dash, grey otherwise. The pip is also grey during the 0.12 s burst, because a press then is dropped; without that it would flash blue between the dash and its cooldown. Cost: one rectangle per pip, fewer than before. - The CONTROLS page tip now reads: "You hold 2 dashes, usable 0.6 s apart (from the end of the last one). A spent dash recharges for 1.2 s from the end of the dash that spent it; after that, landing gives it back (a bunny hop gives one)."
Corrections
- The 1.18.2 full changelog said holding dash for 4 s gives 4 dashes 1201 ms apart. A dash needs a fresh press, so holding gives one dash; pressing repeatedly gives dashes about 1320 ms apart, start to start (the 120 ms dash plus the 1.2 s cooldown), and with quick dash about 520 ms.