pi-extensions 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.ralph/import-cc-codex.md +31 -0
  2. package/.ralph/import-cc-codex.state.json +14 -0
  3. package/.ralph/mario-not-impl.md +69 -0
  4. package/.ralph/mario-not-impl.state.json +14 -0
  5. package/.ralph/mario-not-spec.md +163 -0
  6. package/.ralph/mario-not-spec.state.json +14 -0
  7. package/LICENSE +21 -0
  8. package/README.md +65 -0
  9. package/RELEASING.md +34 -0
  10. package/agent-guidance/CHANGELOG.md +4 -0
  11. package/agent-guidance/README.md +102 -0
  12. package/agent-guidance/agent-guidance.ts +147 -0
  13. package/agent-guidance/package.json +22 -0
  14. package/agent-guidance/setup.sh +75 -0
  15. package/agent-guidance/templates/CLAUDE.md +5 -0
  16. package/agent-guidance/templates/CODEX.md +92 -0
  17. package/agent-guidance/templates/GEMINI.md +5 -0
  18. package/arcade/CHANGELOG.md +4 -0
  19. package/arcade/README.md +85 -0
  20. package/arcade/assets/picman.png +0 -0
  21. package/arcade/assets/ping.png +0 -0
  22. package/arcade/assets/spice-invaders.png +0 -0
  23. package/arcade/assets/tetris.png +0 -0
  24. package/arcade/mario-not/README.md +30 -0
  25. package/arcade/mario-not/boss.js +103 -0
  26. package/arcade/mario-not/camera.js +59 -0
  27. package/arcade/mario-not/collision.js +91 -0
  28. package/arcade/mario-not/colors.js +36 -0
  29. package/arcade/mario-not/constants.js +97 -0
  30. package/arcade/mario-not/core.js +39 -0
  31. package/arcade/mario-not/death.js +77 -0
  32. package/arcade/mario-not/effects.js +84 -0
  33. package/arcade/mario-not/enemies.js +31 -0
  34. package/arcade/mario-not/engine.js +171 -0
  35. package/arcade/mario-not/fireballs.js +98 -0
  36. package/arcade/mario-not/items.js +24 -0
  37. package/arcade/mario-not/levels.js +403 -0
  38. package/arcade/mario-not/logic.js +104 -0
  39. package/arcade/mario-not/mario-not.ts +297 -0
  40. package/arcade/mario-not/player.js +244 -0
  41. package/arcade/mario-not/render.js +257 -0
  42. package/arcade/mario-not/spec.md +548 -0
  43. package/arcade/mario-not/state.js +246 -0
  44. package/arcade/mario-not/tests/e2e.test.js +855 -0
  45. package/arcade/mario-not/tests/engine.test.js +888 -0
  46. package/arcade/mario-not/tests/fixtures/story0-frame.txt +4 -0
  47. package/arcade/mario-not/tests/fixtures/story1-camera.txt +4 -0
  48. package/arcade/mario-not/tests/fixtures/story1-glyphs.txt +4 -0
  49. package/arcade/mario-not/tests/fixtures/story10-item.txt +4 -0
  50. package/arcade/mario-not/tests/fixtures/story11-hazards.txt +4 -0
  51. package/arcade/mario-not/tests/fixtures/story12-used-block.txt +4 -0
  52. package/arcade/mario-not/tests/fixtures/story13-pipes.txt +4 -0
  53. package/arcade/mario-not/tests/fixtures/story14-goal.txt +4 -0
  54. package/arcade/mario-not/tests/fixtures/story15-hud-narrow.txt +2 -0
  55. package/arcade/mario-not/tests/fixtures/story16-unknown-tile.txt +4 -0
  56. package/arcade/mario-not/tests/fixtures/story17-mix.txt +4 -0
  57. package/arcade/mario-not/tests/fixtures/story18-hud-score.txt +2 -0
  58. package/arcade/mario-not/tests/fixtures/story19-cue.txt +4 -0
  59. package/arcade/mario-not/tests/fixtures/story2-enemy.txt +4 -0
  60. package/arcade/mario-not/tests/fixtures/story20-camera-offset.txt +4 -0
  61. package/arcade/mario-not/tests/fixtures/story21-hud-zero.txt +2 -0
  62. package/arcade/mario-not/tests/fixtures/story22-big-viewport.txt +4 -0
  63. package/arcade/mario-not/tests/fixtures/story23-camera-negative.txt +4 -0
  64. package/arcade/mario-not/tests/fixtures/story24-camera-width.txt +4 -0
  65. package/arcade/mario-not/tests/fixtures/story25-camera-positive.txt +4 -0
  66. package/arcade/mario-not/tests/fixtures/story26-hud-lives.txt +2 -0
  67. package/arcade/mario-not/tests/fixtures/story27-hud-coins.txt +2 -0
  68. package/arcade/mario-not/tests/fixtures/story28-item-viewport.txt +4 -0
  69. package/arcade/mario-not/tests/fixtures/story29-enemy-viewport.txt +4 -0
  70. package/arcade/mario-not/tests/fixtures/story3-hud.txt +2 -0
  71. package/arcade/mario-not/tests/fixtures/story30-hud-score.txt +2 -0
  72. package/arcade/mario-not/tests/fixtures/story31-particles-viewport.txt +4 -0
  73. package/arcade/mario-not/tests/fixtures/story32-paused-frame.txt +4 -0
  74. package/arcade/mario-not/tests/fixtures/story4-big.txt +4 -0
  75. package/arcade/mario-not/tests/fixtures/story5-resume-hud.txt +2 -0
  76. package/arcade/mario-not/tests/fixtures/story6-particles.txt +4 -0
  77. package/arcade/mario-not/tests/fixtures/story6-paused.txt +4 -0
  78. package/arcade/mario-not/tests/fixtures/story7-powerup.txt +4 -0
  79. package/arcade/mario-not/tests/fixtures/story8-hud-time.txt +2 -0
  80. package/arcade/mario-not/tests/fixtures/story9-hud-level.txt +2 -0
  81. package/arcade/mario-not/tiles.js +79 -0
  82. package/arcade/mario-not/tsconfig.json +14 -0
  83. package/arcade/mario-not/types.js +225 -0
  84. package/arcade/package.json +26 -0
  85. package/arcade/picman.ts +328 -0
  86. package/arcade/ping.ts +594 -0
  87. package/arcade/spice-invaders.ts +1104 -0
  88. package/arcade/tetris.ts +662 -0
  89. package/code-actions/CHANGELOG.md +4 -0
  90. package/code-actions/README.md +65 -0
  91. package/code-actions/actions.ts +107 -0
  92. package/code-actions/index.ts +148 -0
  93. package/code-actions/package.json +22 -0
  94. package/code-actions/search.ts +79 -0
  95. package/code-actions/snippets.ts +179 -0
  96. package/code-actions/ui.ts +120 -0
  97. package/files-widget/CHANGELOG.md +90 -0
  98. package/files-widget/DESIGN.md +452 -0
  99. package/files-widget/README.md +122 -0
  100. package/files-widget/TODO.md +141 -0
  101. package/files-widget/browser.ts +922 -0
  102. package/files-widget/comment.ts +5 -0
  103. package/files-widget/constants.ts +18 -0
  104. package/files-widget/demo.svg +1 -0
  105. package/files-widget/file-tree.ts +224 -0
  106. package/files-widget/file-viewer.ts +93 -0
  107. package/files-widget/git.ts +107 -0
  108. package/files-widget/index.ts +140 -0
  109. package/files-widget/input-utils.ts +3 -0
  110. package/files-widget/package.json +22 -0
  111. package/files-widget/types.ts +28 -0
  112. package/files-widget/utils.ts +26 -0
  113. package/files-widget/viewer.ts +424 -0
  114. package/import-cc-codex/research/import-chats-from-other-agents.md +135 -0
  115. package/import-cc-codex/spec.md +79 -0
  116. package/package.json +29 -0
  117. package/ralph-wiggum/CHANGELOG.md +7 -0
  118. package/ralph-wiggum/README.md +96 -0
  119. package/ralph-wiggum/SKILL.md +73 -0
  120. package/ralph-wiggum/index.ts +792 -0
  121. package/ralph-wiggum/package.json +25 -0
  122. package/raw-paste/CHANGELOG.md +7 -0
  123. package/raw-paste/README.md +52 -0
  124. package/raw-paste/index.ts +112 -0
  125. package/raw-paste/package.json +22 -0
  126. package/tab-status/CHANGELOG.md +4 -0
  127. package/tab-status/README.md +61 -0
  128. package/tab-status/assets/tab-status.png +0 -0
  129. package/tab-status/package.json +22 -0
  130. package/tab-status/tab-status.ts +179 -0
  131. package/usage-extension/CHANGELOG.md +17 -0
  132. package/usage-extension/README.md +120 -0
  133. package/usage-extension/index.ts +628 -0
  134. package/usage-extension/package.json +22 -0
  135. package/usage-extension/screenshot.png +0 -0
@@ -0,0 +1,97 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ /** @type {import("./types").Config} */
5
+ const DEFAULT_CONFIG = {
6
+ dt: 1 / 60,
7
+ gravity: 22,
8
+ maxFall: 9,
9
+ jumpVel: 12,
10
+ walkSpeed: 3,
11
+ runSpeed: 4.2,
12
+ groundAccel: 35,
13
+ groundDecel: 30,
14
+ airAccel: 22,
15
+ enemySpeed: 1,
16
+ mushroomScore: 1000,
17
+ viewportWidth: 40,
18
+ };
19
+
20
+ const SCORE_VALUES = {
21
+ coin: 100,
22
+ stomp: 50,
23
+ mushroom: 1000,
24
+ flagMin: 100,
25
+ flagMax: 5000,
26
+ timeBonus: 10, // per second remaining
27
+ };
28
+
29
+ const PLAYER_W = 1;
30
+ const PLAYER_H_SMALL = 1;
31
+ const PLAYER_H_BIG = 2;
32
+ const ENEMY_W = 1;
33
+ const ENEMY_H = 1;
34
+ const ITEM_W = 1;
35
+ const ITEM_H = 1;
36
+ const ITEM_SPEED = 1.2;
37
+ const INVULN_TIME = 1.2;
38
+ const START_LIVES = 3;
39
+ const START_TIME = 300;
40
+ const DEATH_WAIT = 1.2;
41
+ const DEATH_JUMP_VEL = 14;
42
+
43
+ /** @type {{ playing: "playing", paused: "paused", dead: "dead", levelClear: "level_clear", gameOver: "game_over", levelIntro: "level_intro" }} */
44
+ const GAME_MODES = {
45
+ playing: "playing",
46
+ paused: "paused",
47
+ dead: "dead",
48
+ levelClear: "level_clear",
49
+ gameOver: "game_over",
50
+ levelIntro: "level_intro",
51
+ };
52
+
53
+ const LEVEL_INTRO_TIME = 2.0;
54
+
55
+ // Fireball constants
56
+ const FIREBALL_SPEED = 4;
57
+ const FIREBALL_WAVE_AMP = 1.5;
58
+ const FIREBALL_WAVE_FREQ = 4;
59
+ const FIREBALL_SPAWN_INTERVAL = 3.0;
60
+
61
+ // Boss constants
62
+ const BOSS_W = 2;
63
+ const BOSS_H = 2;
64
+ const BOSS_SPEED = 1.5;
65
+ const BOSS_HEALTH = 5;
66
+ const BOSS_INVULN_TIME = 0.8;
67
+ const BOSS_SCORE = 5000;
68
+
69
+ module.exports = {
70
+ DEFAULT_CONFIG,
71
+ SCORE_VALUES,
72
+ PLAYER_W,
73
+ PLAYER_H_SMALL,
74
+ PLAYER_H_BIG,
75
+ ENEMY_W,
76
+ ENEMY_H,
77
+ ITEM_W,
78
+ ITEM_H,
79
+ ITEM_SPEED,
80
+ INVULN_TIME,
81
+ START_LIVES,
82
+ START_TIME,
83
+ DEATH_WAIT,
84
+ DEATH_JUMP_VEL,
85
+ GAME_MODES,
86
+ LEVEL_INTRO_TIME,
87
+ FIREBALL_SPEED,
88
+ FIREBALL_WAVE_AMP,
89
+ FIREBALL_WAVE_FREQ,
90
+ FIREBALL_SPAWN_INTERVAL,
91
+ BOSS_W,
92
+ BOSS_H,
93
+ BOSS_SPEED,
94
+ BOSS_HEALTH,
95
+ BOSS_INVULN_TIME,
96
+ BOSS_SCORE,
97
+ };
@@ -0,0 +1,39 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ /** @typedef {import("./types").Level} Level */
5
+
6
+ /** @param {number} seed @returns {() => number} */
7
+ function createRng(seed) {
8
+ let t = seed >>> 0;
9
+ return function next() {
10
+ t += 0x6D2B79F5;
11
+ let r = t;
12
+ r = Math.imul(r ^ (r >>> 15), r | 1);
13
+ r ^= r + Math.imul(r ^ (r >>> 7), r | 61);
14
+ return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
15
+ };
16
+ }
17
+
18
+ /** @param {string[]} lines @returns {Level} */
19
+ function makeLevel(lines) {
20
+ if (!Array.isArray(lines) || lines.length === 0) {
21
+ throw new Error("Level must be a non-empty array of strings.");
22
+ }
23
+ const width = lines[0].length;
24
+ for (const line of lines) {
25
+ if (line.length !== width) {
26
+ throw new Error("All level rows must be the same width.");
27
+ }
28
+ }
29
+ return {
30
+ width,
31
+ height: lines.length,
32
+ tiles: lines.map((line) => line.split("")),
33
+ };
34
+ }
35
+
36
+ module.exports = {
37
+ createRng,
38
+ makeLevel,
39
+ };
@@ -0,0 +1,77 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ const { isSolidAt } = require("./tiles.js");
5
+ const { setCue } = require("./effects.js");
6
+ const { INVULN_TIME, DEATH_WAIT, DEATH_JUMP_VEL, GAME_MODES } = require("./constants.js");
7
+
8
+ /** @typedef {import("./types").GameState} GameState */
9
+
10
+ /** @param {GameState} state */
11
+ function enterDeath(state) {
12
+ if (state.mode === GAME_MODES.dead || state.mode === GAME_MODES.gameOver) return;
13
+ state.mode = GAME_MODES.dead;
14
+ state.lives = Math.max(0, state.lives - 1);
15
+ state.deathTimer = 0;
16
+ state.deathJumped = false;
17
+ state.player.vx = 0;
18
+ state.player.vy = 0;
19
+ state.player.onGround = false;
20
+ }
21
+
22
+ /** @param {GameState} state */
23
+ function respawnPlayer(state) {
24
+ const player = state.player;
25
+ player.x = state.spawnX;
26
+ player.y = state.spawnY;
27
+ player.vx = 0;
28
+ player.vy = 0;
29
+ player.facing = 1;
30
+ player.onGround = isSolidAt(state.level, player.x, player.y + 1);
31
+ player.invuln = INVULN_TIME;
32
+ state.deathTimer = 0;
33
+ state.deathJumped = false;
34
+ state.mode = GAME_MODES.playing;
35
+ }
36
+
37
+ /** @param {GameState} state */
38
+ function setGameOver(state) {
39
+ state.mode = GAME_MODES.gameOver;
40
+ state.deathTimer = 0;
41
+ state.deathJumped = false;
42
+ state.player.vx = 0;
43
+ state.player.vy = 0;
44
+ setCue(state, "GAME OVER", 0, true);
45
+ }
46
+
47
+ /** @param {GameState} state @param {number} dt @returns {GameState} */
48
+ function stepDeath(state, dt) {
49
+ const player = state.player;
50
+ state.deathTimer += dt;
51
+ if (state.deathTimer < DEATH_WAIT) {
52
+ state.tick += 1;
53
+ return state;
54
+ }
55
+ if (!state.deathJumped) {
56
+ state.deathJumped = true;
57
+ player.vy = -DEATH_JUMP_VEL;
58
+ }
59
+ player.vy = Math.min(player.vy + state.config.gravity * dt, state.config.maxFall);
60
+ player.y += player.vy * dt;
61
+ if (player.y > state.level.height + 2) {
62
+ if (state.lives <= 0) {
63
+ setGameOver(state);
64
+ } else {
65
+ respawnPlayer(state);
66
+ }
67
+ }
68
+ state.tick += 1;
69
+ return state;
70
+ }
71
+
72
+ module.exports = {
73
+ enterDeath,
74
+ respawnPlayer,
75
+ setGameOver,
76
+ stepDeath,
77
+ };
@@ -0,0 +1,84 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ /**
5
+ * @typedef {Object} Particle
6
+ * @property {number} x
7
+ * @property {number} y
8
+ * @property {number} vx
9
+ * @property {number} vy
10
+ * @property {number} life
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} Cue
15
+ * @property {string} text
16
+ * @property {number} ttl
17
+ * @property {boolean} persist
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} Config
22
+ * @property {number} gravity
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} GameState
27
+ * @property {Config} config
28
+ * @property {Particle[]} particles
29
+ * @property {Cue | null} cue
30
+ */
31
+
32
+ const PARTICLE_VELOCITIES = [
33
+ { vx: -0.5, vy: -1.0 },
34
+ { vx: 0.5, vy: -1.0 },
35
+ { vx: -0.3, vy: -0.6 },
36
+ { vx: 0.3, vy: -0.6 },
37
+ ];
38
+ const PARTICLE_LIFE = 0.35;
39
+
40
+ /** @param {GameState} state @param {number} x @param {number} y @param {number} count */
41
+ function spawnParticles(state, x, y, count) {
42
+ for (let i = 0; i < count; i += 1) {
43
+ const vel = PARTICLE_VELOCITIES[i % PARTICLE_VELOCITIES.length];
44
+ state.particles.push({
45
+ x,
46
+ y,
47
+ vx: vel.vx,
48
+ vy: vel.vy,
49
+ life: PARTICLE_LIFE,
50
+ });
51
+ }
52
+ }
53
+
54
+ /** @param {GameState} state @param {number} dt */
55
+ function updateParticles(state, dt) {
56
+ const gravity = state.config.gravity * 0.2;
57
+ for (let i = state.particles.length - 1; i >= 0; i -= 1) {
58
+ const p = state.particles[i];
59
+ p.vy += gravity * dt;
60
+ p.x += p.vx * dt;
61
+ p.y += p.vy * dt;
62
+ p.life -= dt;
63
+ if (p.life <= 0) state.particles.splice(i, 1);
64
+ }
65
+ }
66
+
67
+ /** @param {GameState} state @param {string} text @param {number} ttl @param {boolean} persist */
68
+ function setCue(state, text, ttl, persist) {
69
+ state.cue = { text, ttl, persist };
70
+ }
71
+
72
+ /** @param {GameState} state @param {number} dt */
73
+ function updateCue(state, dt) {
74
+ if (!state.cue || state.cue.persist) return;
75
+ state.cue.ttl -= dt;
76
+ if (state.cue.ttl <= 0) state.cue = null;
77
+ }
78
+
79
+ module.exports = {
80
+ spawnParticles,
81
+ updateParticles,
82
+ setCue,
83
+ updateCue,
84
+ };
@@ -0,0 +1,31 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ const { moveHorizontal, resolveVertical } = require("./collision.js");
5
+ const { ENEMY_W, ENEMY_H } = require("./constants.js");
6
+
7
+ /** @typedef {import("./types").GameState} GameState */
8
+ /** @typedef {import("./types").Level} Level */
9
+ /** @typedef {import("./types").EnemyState} EnemyState */
10
+
11
+ /** @param {GameState} state */
12
+ function updateEnemies(state) {
13
+ const cfg = state.config;
14
+ for (const enemy of state.enemies) {
15
+ if (!enemy.alive) continue;
16
+ updateEnemy(state.level, enemy, cfg);
17
+ }
18
+ }
19
+
20
+ /** @param {Level} level @param {EnemyState} enemy @param {import("./types").Config} cfg */
21
+ function updateEnemy(level, enemy, cfg) {
22
+ enemy.vy = Math.min(enemy.vy + cfg.gravity * cfg.dt, cfg.maxFall);
23
+ const blocked = moveHorizontal(level, enemy, cfg.dt, ENEMY_W, ENEMY_H);
24
+ if (blocked) enemy.vx = -enemy.vx;
25
+ resolveVertical(level, enemy, cfg.dt, undefined, ENEMY_H, ENEMY_W, true);
26
+ if (enemy.y >= level.height + 1) enemy.alive = false;
27
+ }
28
+
29
+ module.exports = {
30
+ updateEnemies,
31
+ };
@@ -0,0 +1,171 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ const { createRng, makeLevel } = require("./core.js");
5
+ const { DEFAULT_CONFIG, START_LIVES, START_TIME, GAME_MODES, FIREBALL_SPAWN_INTERVAL } = require("./constants.js");
6
+ const { renderFrame, renderViewport, renderHud } = require("./render.js");
7
+ const { getCameraX, updateCamera } = require("./camera.js");
8
+ const { stepGame, setPaused } = require("./logic.js");
9
+ const { saveState, loadState, snapshotState } = require("./state.js");
10
+ const { isSolidAt } = require("./tiles.js");
11
+
12
+ /** @typedef {import("./types").GameState} GameState */
13
+ /** @typedef {import("./types").GameOptions} GameOptions */
14
+ /** @typedef {import("./types").Level} Level */
15
+
16
+ /**
17
+ * @param {Level} level
18
+ * @returns {{
19
+ * level: Level,
20
+ * enemies: import("./types").EnemyState[],
21
+ * fireballSpawners: import("./types").FireballSpawner[],
22
+ * boss: import("./types").BossState | null
23
+ * }}
24
+ */
25
+ function extractEntities(level) {
26
+ const tiles = level.tiles.map((row) => row.slice());
27
+ /** @type {import("./types").EnemyState[]} */
28
+ const enemies = [];
29
+ /** @type {import("./types").FireballSpawner[]} */
30
+ const fireballSpawners = [];
31
+ /** @type {import("./types").BossState | null} */
32
+ let boss = null;
33
+
34
+ for (let y = 0; y < level.height; y += 1) {
35
+ for (let x = 0; x < level.width; x += 1) {
36
+ const tile = tiles[y][x];
37
+ if (tile === "E") {
38
+ enemies.push({
39
+ x,
40
+ y,
41
+ vx: -1,
42
+ vy: 0,
43
+ alive: true,
44
+ onGround: false,
45
+ });
46
+ tiles[y][x] = " ";
47
+ } else if (tile === "<") {
48
+ // Fireball spawner shooting left
49
+ fireballSpawners.push({
50
+ x,
51
+ y,
52
+ timer: 0,
53
+ interval: FIREBALL_SPAWN_INTERVAL,
54
+ direction: -1,
55
+ });
56
+ tiles[y][x] = " ";
57
+ } else if (tile === ">") {
58
+ // Fireball spawner shooting right
59
+ fireballSpawners.push({
60
+ x,
61
+ y,
62
+ timer: 0,
63
+ interval: FIREBALL_SPAWN_INTERVAL,
64
+ direction: 1,
65
+ });
66
+ tiles[y][x] = " ";
67
+ } else if (tile === "W") {
68
+ // Boss (Bowser)
69
+ boss = {
70
+ x,
71
+ y,
72
+ vx: -1.5,
73
+ vy: 0,
74
+ alive: true,
75
+ health: 5,
76
+ maxHealth: 5,
77
+ invuln: 0,
78
+ onGround: false,
79
+ };
80
+ tiles[y][x] = " ";
81
+ }
82
+ }
83
+ }
84
+ return {
85
+ level: {
86
+ width: level.width,
87
+ height: level.height,
88
+ tiles,
89
+ },
90
+ enemies,
91
+ fireballSpawners,
92
+ boss,
93
+ };
94
+ }
95
+
96
+ /** @param {GameOptions} options @returns {GameState} */
97
+ function createGame(options) {
98
+ const opts = options || {};
99
+ const level = opts.level;
100
+ if (!level) throw new Error("createGame requires a level.");
101
+ const config = { ...DEFAULT_CONFIG, ...(opts.config || {}) };
102
+ const rng = createRng(opts.seed || 1);
103
+ const startX = typeof opts.startX === "number" ? opts.startX : 1;
104
+ const startY = typeof opts.startY === "number" ? opts.startY : 1;
105
+ const extracted = extractEntities(level);
106
+ const enemies = extracted.enemies.map((enemy) => {
107
+ const seeded = { ...enemy, vx: -config.enemySpeed };
108
+ seeded.onGround = isSolidAt(extracted.level, seeded.x, seeded.y + 1);
109
+ return seeded;
110
+ });
111
+ // Initialize boss if present
112
+ const boss = extracted.boss;
113
+ if (boss) {
114
+ boss.onGround = isSolidAt(extracted.level, boss.x, boss.y + 1);
115
+ }
116
+ /** @type {GameState} */
117
+ const state = {
118
+ level: extracted.level,
119
+ rng,
120
+ config,
121
+ tick: 0,
122
+ score: 0,
123
+ coins: 0,
124
+ lives: START_LIVES,
125
+ time: START_TIME,
126
+ levelIndex: typeof opts.levelIndex === "number" ? opts.levelIndex : 1,
127
+ mushroomSpawned: false,
128
+ mode: GAME_MODES.playing,
129
+ cameraX: 0,
130
+ particles: [],
131
+ cue: null,
132
+ spawnX: startX,
133
+ spawnY: startY,
134
+ deathTimer: 0,
135
+ deathJumped: false,
136
+ player: {
137
+ x: startX,
138
+ y: startY,
139
+ vx: 0,
140
+ vy: 0,
141
+ facing: 1,
142
+ onGround: false,
143
+ size: /** @type {"small" | "big"} */ ("small"),
144
+ invuln: 0,
145
+ },
146
+ enemies,
147
+ items: [],
148
+ fireballs: [],
149
+ fireballSpawners: extracted.fireballSpawners,
150
+ boss,
151
+ };
152
+ state.player.onGround = isSolidAt(state.level, startX, startY + 1);
153
+ return state;
154
+ }
155
+
156
+ module.exports = {
157
+ DEFAULT_CONFIG,
158
+ createRng,
159
+ makeLevel,
160
+ createGame,
161
+ stepGame,
162
+ renderFrame,
163
+ renderViewport,
164
+ renderHud,
165
+ getCameraX,
166
+ updateCamera,
167
+ setPaused,
168
+ saveState,
169
+ loadState,
170
+ snapshotState,
171
+ };
@@ -0,0 +1,98 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ const { FIREBALL_SPEED, FIREBALL_WAVE_AMP, FIREBALL_WAVE_FREQ } = require("./constants.js");
5
+
6
+ /** @typedef {import("./types").GameState} GameState */
7
+ /** @typedef {import("./types").FireballState} FireballState */
8
+
9
+ /** @param {GameState} state */
10
+ function updateFireballSpawners(state) {
11
+ const dt = state.config.dt;
12
+ const cameraX = state.cameraX || 0;
13
+ const viewportWidth = state.config.viewportWidth || 40;
14
+
15
+ for (const spawner of state.fireballSpawners) {
16
+ // Only spawn fireballs if spawner is near the viewport
17
+ const distanceFromCamera = spawner.x - cameraX;
18
+ const isNearViewport = distanceFromCamera > -10 && distanceFromCamera < viewportWidth + 10;
19
+
20
+ if (!isNearViewport) {
21
+ spawner.timer = 0; // Reset timer when off-screen
22
+ continue;
23
+ }
24
+
25
+ spawner.timer += dt;
26
+ if (spawner.timer >= spawner.interval) {
27
+ spawner.timer = 0;
28
+ // Spawn a fireball
29
+ state.fireballs.push({
30
+ x: spawner.x,
31
+ y: spawner.y,
32
+ vx: FIREBALL_SPEED * spawner.direction,
33
+ vy: 0,
34
+ alive: true,
35
+ pattern: "wave",
36
+ startY: spawner.y,
37
+ });
38
+ }
39
+ }
40
+ }
41
+
42
+ /** @param {GameState} state */
43
+ function updateFireballs(state) {
44
+ const dt = state.config.dt;
45
+ const level = state.level;
46
+
47
+ for (let i = state.fireballs.length - 1; i >= 0; i--) {
48
+ const fb = state.fireballs[i];
49
+ if (!fb.alive) {
50
+ state.fireballs.splice(i, 1);
51
+ continue;
52
+ }
53
+
54
+ // Move fireball
55
+ fb.x += fb.vx * dt;
56
+
57
+ // Wave pattern movement
58
+ if (fb.pattern === "wave") {
59
+ const elapsed = Math.abs(fb.x - (fb.startY + fb.vx * 0)); // Use x position for wave
60
+ fb.y = fb.startY + Math.sin(fb.x * FIREBALL_WAVE_FREQ) * FIREBALL_WAVE_AMP * 0.3;
61
+ }
62
+
63
+ // Remove if out of bounds
64
+ if (fb.x < -2 || fb.x > level.width + 2 || fb.y < -2 || fb.y > level.height + 2) {
65
+ state.fireballs.splice(i, 1);
66
+ }
67
+ }
68
+ }
69
+
70
+ /** @param {GameState} state @returns {boolean} */
71
+ function checkFireballCollision(state) {
72
+ const player = state.player;
73
+ const playerW = 1;
74
+ const playerH = player.size === "big" ? 2 : 1;
75
+
76
+ for (const fb of state.fireballs) {
77
+ if (!fb.alive) continue;
78
+
79
+ // Simple AABB collision
80
+ const fbW = 0.5;
81
+ const fbH = 0.5;
82
+ if (
83
+ player.x < fb.x + fbW &&
84
+ player.x + playerW > fb.x &&
85
+ player.y < fb.y + fbH &&
86
+ player.y + playerH > fb.y - (playerH - 1)
87
+ ) {
88
+ return true;
89
+ }
90
+ }
91
+ return false;
92
+ }
93
+
94
+ module.exports = {
95
+ updateFireballSpawners,
96
+ updateFireballs,
97
+ checkFireballCollision,
98
+ };
@@ -0,0 +1,24 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ const { moveHorizontal, resolveVertical } = require("./collision.js");
5
+ const { ITEM_W, ITEM_H } = require("./constants.js");
6
+
7
+ /** @typedef {import("./types").GameState} GameState */
8
+
9
+ /** @param {GameState} state */
10
+ function updateItems(state) {
11
+ const cfg = state.config;
12
+ for (const item of state.items) {
13
+ if (!item.alive) continue;
14
+ item.vy = Math.min(item.vy + cfg.gravity * cfg.dt, cfg.maxFall);
15
+ const blocked = moveHorizontal(state.level, item, cfg.dt, ITEM_W, ITEM_H);
16
+ if (blocked) item.vx = -item.vx;
17
+ resolveVertical(state.level, item, cfg.dt, undefined, ITEM_H, ITEM_W, true);
18
+ if (item.y >= state.level.height + 1) item.alive = false;
19
+ }
20
+ }
21
+
22
+ module.exports = {
23
+ updateItems,
24
+ };