diffs / #47

feat(#47): boss throws pizza

SHIPPED commit 55596ff +38 −4 request “add a boss that throws pizza” demo replay

Requested-by: demo replay (synthetic wallet)
+ BUILD+ UNIT TESTS+ LINT+ DIFF GATE+ PAGE CLASSIFIER+ PLAYWRIGHT
src/boss-registry.js+1 −1
3 // Boss classes are discovered from src/bosses/*.js by export name, so a new boss needs no import here.3 // Boss classes are discovered from src/bosses/*.js by export name, so a new boss needs no import here.
4 const B = Object.assign({}, ...Object.values(import.meta.glob("./bosses/*.js", { eager: true })));4 const B = Object.assign({}, ...Object.values(import.meta.glob("./bosses/*.js", { eager: true })));
5 const registry = () => ({5 const registry = () => ({
6− rooftop: null,6+ rooftop: B.PizzaBoss,
7 lobby: null,7 lobby: null,
8 alley: null,8 alley: null,
9 });9 });
3 // Boss classes are discovered from src/bosses/*.js by export name, so a new boss needs no import here.
4 const B = Object.assign({}, ...Object.values(import.meta.glob("./bosses/*.js", { eager: true })));
5 const registry = () => ({
6− rooftop: null,
6+ rooftop: B.PizzaBoss,
7 lobby: null,
8 alley: null,
9 });
src/bosses/pizzaBoss.jsNEW+31 −0
1+export class PizzaBoss extends Boss {
2+ hp = 12;
3+ throwEvery = 1.4;
4+ projectile = "pizza";
5+ art = "ogre";
6+ volley = 0;
7+
8+ // every third throw is a fan of three slices
9+ throw(w) {
10+ this.volley = (this.volley + 1) % 3;
11+ if (this.volley !== 0) return super.throw(w);
12+ for (const spread of [-24, 0, 24]) {
13+ // aim left of, at, and right of the cat
14+ const tx = (w.player ? w.player.x : 40) + spread;
15+ w.projectiles.push({
16+ kind: "pizza",
17+ x: this.x + 4,
18+ y: this.y - 4,
19+ vx: (tx - this.x) / 1.5,
20+ vy: -80 - w.rand() * 20,
21+ r: 0,
22+ spin: 8,
23+ life: 4,
24+ });
25+ }
26+ if (w.level.shake) w.shake = Math.max(w.shake, w.level.shake);
27+ }
28+}
29+
30+// imports are hoisted, so the class reads first
31+import { Boss } from "./boss.js";
1+export class PizzaBoss extends Boss {
2+ hp = 12;
3+ throwEvery = 1.4;
4+ projectile = "pizza";
5+ art = "ogre";
6+ volley = 0;
7+
8+ // every third throw is a fan of three slices
9+ throw(w) {
10+ this.volley = (this.volley + 1) % 3;
11+ if (this.volley !== 0) return super.throw(w);
12+ for (const spread of [-24, 0, 24]) {
13+ // aim left of, at, and right of the cat
14+ const tx = (w.player ? w.player.x : 40) + spread;
15+ w.projectiles.push({
16+ kind: "pizza",
17+ x: this.x + 4,
18+ y: this.y - 4,
19+ vx: (tx - this.x) / 1.5,
20+ vy: -80 - w.rand() * 20,
21+ r: 0,
22+ spin: 8,
23+ life: 4,
24+ });
25+ }
26+ if (w.level.shake) w.shake = Math.max(w.shake, w.level.shake);
27+ }
28+}
29+
30+// imports are hoisted, so the class reads first
31+import { Boss } from "./boss.js";
src/levels/rooftop.js+6 −3
2 // Roof tops are one-way platforms. The door on the left roof leads down to the lobby; walk off the left edge for the alley.2 // Roof tops are one-way platforms. The door on the left roof leads down to the lobby; walk off the left edge for the alley.
3 import { PAL } from "../palette.js";3 import { PAL } from "../palette.js";
4 import { blit } from "../sprites.js";4 import { blit } from "../sprites.js";
5+import { PizzaBoss } from "../bosses/pizzaBoss.js";
6 6
7 const BLOCKS = [7 const BLOCKS = [
8 { x: 0, y: 96, w: 80, h: 84, rows: 5 },8 { x: 0, y: 96, w: 80, h: 84, rows: 5 },
export default {export default {
19 bossAt: { x: 262, y: 92 },19 bossAt: { x: 262, y: 92 },
20 exits: { left: { to: "alley", at: { x: 300, y: 150 } } },20 exits: { left: { to: "alley", at: { x: 300, y: 150 } } },
21 doors: [{ x: 52, y: 84, to: "lobby", at: { x: 40, y: 150 } }],21 doors: [{ x: 52, y: 84, to: "lobby", at: { x: 40, y: 150 } }],
21− boss: null,22+ boss: new PizzaBoss({ x: 262, y: 92 }),
22− spawn: [],23+ spawn: ["pizza"],
23− music: "rooftop-a",24+ music: "rooftop-boss",
25+ bossIntro: true,
26+ shake: 0.2,
27 draw(g) {27 draw(g) {
28 for (const b of BLOCKS) {28 for (const b of BLOCKS) {
29 g.fillStyle = PAL.wall; g.fillRect(b.x, b.y, b.w, b.h);29 g.fillStyle = PAL.wall; g.fillRect(b.x, b.y, b.w, b.h);
2 // Roof tops are one-way platforms. The door on the left roof leads down to the lobby; walk off the left edge for the alley.
3 import { PAL } from "../palette.js";
4 import { blit } from "../sprites.js";
5+import { PizzaBoss } from "../bosses/pizzaBoss.js";
6
7 const BLOCKS = [
8 { x: 0, y: 96, w: 80, h: 84, rows: 5 },
export default {
19 bossAt: { x: 262, y: 92 },
20 exits: { left: { to: "alley", at: { x: 300, y: 150 } } },
21 doors: [{ x: 52, y: 84, to: "lobby", at: { x: 40, y: 150 } }],
21− boss: null,
22− spawn: [],
23− music: "rooftop-a",
22+ boss: new PizzaBoss({ x: 262, y: 92 }),
23+ spawn: ["pizza"],
24+ music: "rooftop-boss",
25+ bossIntro: true,
26+ shake: 0.2,
27 draw(g) {
28 for (const b of BLOCKS) {
29 g.fillStyle = PAL.wall; g.fillRect(b.x, b.y, b.w, b.h);
Before
BEFORE
After
AFTER
CI log tail
$ ci:build  (760 ms, exit 0)

$ ci:test  (199 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.547037
  type: 'test'
  ...
# Subtest: sprites: rectangular-ish rows of known keys
ok 2 - sprites: rectangular-ish rows of known keys
  ---
  duration_ms: 4.60926
  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: 6.938871
  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: 1.359145
  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: 7.652769
  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.830637
  type: 'test'
  ...
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 129.118903

$ ci:lint  (717 ms, exit 0)

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