diffs / #50

feat(#50): double jump for everyone

SHIPPED commit d0f7cdb +21 −2 request “double jump for everyone” demo replay

Requested-by: demo replay (synthetic wallet)
+ BUILD+ UNIT TESTS+ LINT+ DIFF GATE+ PAGE CLASSIFIER+ PLAYWRIGHT
src/features/r50-jump-puff.jsNEW+15 −0
1+// #50: double jump for everyone. A small puff under the cat on the second jump.
2+import { PAL } from "../palette.js";
3+export default {
4+ id: "jump-puff-50",
5+ order: 55,
6+ layer: "fg",
7+ draw(g, w) {
8+ const p = w.player; if (!p || !p.puffT || w.reduced) return;
9+ const r = Math.round((0.25 - p.puffT) * 24);
10+ g.fillStyle = PAL.playerShade;
11+ g.fillRect(Math.round(p.x) - 3 - r, Math.round(p.y) + 1, 2, 1);
12+ g.fillRect(Math.round(p.x) + 2 + r, Math.round(p.y) + 1, 2, 1);
13+ g.fillRect(Math.round(p.x) - 1, Math.round(p.y) + 2 + (r >> 1), 2, 1);
14+ },
15+};
1+// #50: double jump for everyone. A small puff under the cat on the second jump.
2+import { PAL } from "../palette.js";
3+export default {
4+ id: "jump-puff-50",
5+ order: 55,
6+ layer: "fg",
7+ draw(g, w) {
8+ const p = w.player; if (!p || !p.puffT || w.reduced) return;
9+ const r = Math.round((0.25 - p.puffT) * 24);
10+ g.fillStyle = PAL.playerShade;
11+ g.fillRect(Math.round(p.x) - 3 - r, Math.round(p.y) + 1, 2, 1);
12+ g.fillRect(Math.round(p.x) + 2 + r, Math.round(p.y) + 1, 2, 1);
13+ g.fillRect(Math.round(p.x) - 1, Math.round(p.y) + 2 + (r >> 1), 2, 1);
14+ },
15+};
src/player.js+6 −2
4 export const SPEED = 82, JUMP = 205, GRAV = 560;4 export const SPEED = 82, JUMP = 205, GRAV = 560;
5 export function makePlayer() {5 export function makePlayer() {
6 return {6 return {
7− x: 24, y: 40, vx: 0, vy: 0, f: 1, onGround: false, walk: 0, emote: null, emoteT: 0, jumps: 1,7+ x: 24, y: 40, vx: 0, vy: 0, f: 1, onGround: false, walk: 0, emote: null, emoteT: 0, jumps: 2, puffT: 0,
8 update(dt, w, input) {8 update(dt, w, input) {
9 const dir = (input.held("right") ? 1 : 0) - (input.held("left") ? 1 : 0);9 const dir = (input.held("right") ? 1 : 0) - (input.held("left") ? 1 : 0);
10 this.vx = dir * SPEED; if (dir) this.f = dir;10 this.vx = dir * SPEED; if (dir) this.f = dir;
11 this.walk = dir && this.onGround ? this.walk + dt : 0;11 this.walk = dir && this.onGround ? this.walk + dt : 0;
12− if (input.take("jump") && (this.onGround || this.jumpsLeft > 0)) { if (!this.onGround) this.jumpsLeft--; this.vy = -JUMP; this.onGround = false; }12+ if (input.take("jump") && (this.onGround || this.jumpsLeft > 0)) {
13+ if (!this.onGround) { this.jumpsLeft--; this.puffT = 0.25; }
14+ this.vy = -JUMP; this.onGround = false;
15+ }
16+ this.puffT = Math.max(0, this.puffT - dt);
17 for (let i = 0; i < 4; i++) if (input.take("e" + i)) { this.emote = i; this.emoteT = 2; }17 for (let i = 0; i < 4; i++) if (input.take("e" + i)) { this.emote = i; this.emoteT = 2; }
18 this.emoteT = Math.max(0, this.emoteT - dt); if (!this.emoteT) this.emote = null;18 this.emoteT = Math.max(0, this.emoteT - dt); if (!this.emoteT) this.emote = null;
19 this.vy = Math.min(320, this.vy + GRAV * dt);19 this.vy = Math.min(320, this.vy + GRAV * dt);
4 export const SPEED = 82, JUMP = 205, GRAV = 560;
5 export function makePlayer() {
6 return {
7− x: 24, y: 40, vx: 0, vy: 0, f: 1, onGround: false, walk: 0, emote: null, emoteT: 0, jumps: 1,
7+ x: 24, y: 40, vx: 0, vy: 0, f: 1, onGround: false, walk: 0, emote: null, emoteT: 0, jumps: 2, puffT: 0,
8 update(dt, w, input) {
9 const dir = (input.held("right") ? 1 : 0) - (input.held("left") ? 1 : 0);
10 this.vx = dir * SPEED; if (dir) this.f = dir;
11 this.walk = dir && this.onGround ? this.walk + dt : 0;
12− if (input.take("jump") && (this.onGround || this.jumpsLeft > 0)) { if (!this.onGround) this.jumpsLeft--; this.vy = -JUMP; this.onGround = false; }
12+ if (input.take("jump") && (this.onGround || this.jumpsLeft > 0)) {
13+ if (!this.onGround) { this.jumpsLeft--; this.puffT = 0.25; }
14+ this.vy = -JUMP; this.onGround = false;
15+ }
16+ this.puffT = Math.max(0, this.puffT - dt);
17 for (let i = 0; i < 4; i++) if (input.take("e" + i)) { this.emote = i; this.emoteT = 2; }
18 this.emoteT = Math.max(0, this.emoteT - dt); if (!this.emoteT) this.emote = null;
19 this.vy = Math.min(320, this.vy + GRAV * dt);
Before
BEFORE
After
AFTER
CI log tail
$ ci:build  (677 ms, exit 0)

$ ci:test  (160 ms, exit 0)
TAP version 13
# Subtest: palette: valid hex, at most 40 colours
ok 1 - palette: valid hex, at most 40 colours
  ---
  duration_ms: 1.08386
  type: 'test'
  ...
# Subtest: sprites: rectangular-ish rows of known keys
ok 2 - sprites: rectangular-ish rows of known keys
  ---
  duration_ms: 1.921427
  type: 'test'
  ...
# Subtest: levels: every room has a name, platforms and a draw function
ok 3 - levels: every room has a name, platforms and a draw function
  ---
  duration_ms: 4.821918
  type: 'test'
  ...
# Subtest: bosses: every file exports a class with update/draw
ok 4 - bosses: every file exports a class with update/draw
  ---
  duration_ms: 0.880719
  type: 'test'
  ...
# Subtest: features: default export with a unique id and only known hooks
ok 5 - features: default export with a unique id and only known hooks
  ---
  duration_ms: 6.453744
  type: 'test'
  ...
# Subtest: no network, storage or parent access anywhere in src
ok 6 - no network, storage or parent access anywhere in src
  ---
  duration_ms: 1.498105
  type: 'test'
  ...
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 105.519077

$ ci:lint  (652 ms, exit 0)

$ playwright 960x540  fps 60  long tasks 0  console errors 0