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.
- package/.ralph/import-cc-codex.md +31 -0
- package/.ralph/import-cc-codex.state.json +14 -0
- package/.ralph/mario-not-impl.md +69 -0
- package/.ralph/mario-not-impl.state.json +14 -0
- package/.ralph/mario-not-spec.md +163 -0
- package/.ralph/mario-not-spec.state.json +14 -0
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/RELEASING.md +34 -0
- package/agent-guidance/CHANGELOG.md +4 -0
- package/agent-guidance/README.md +102 -0
- package/agent-guidance/agent-guidance.ts +147 -0
- package/agent-guidance/package.json +22 -0
- package/agent-guidance/setup.sh +75 -0
- package/agent-guidance/templates/CLAUDE.md +5 -0
- package/agent-guidance/templates/CODEX.md +92 -0
- package/agent-guidance/templates/GEMINI.md +5 -0
- package/arcade/CHANGELOG.md +4 -0
- package/arcade/README.md +85 -0
- package/arcade/assets/picman.png +0 -0
- package/arcade/assets/ping.png +0 -0
- package/arcade/assets/spice-invaders.png +0 -0
- package/arcade/assets/tetris.png +0 -0
- package/arcade/mario-not/README.md +30 -0
- package/arcade/mario-not/boss.js +103 -0
- package/arcade/mario-not/camera.js +59 -0
- package/arcade/mario-not/collision.js +91 -0
- package/arcade/mario-not/colors.js +36 -0
- package/arcade/mario-not/constants.js +97 -0
- package/arcade/mario-not/core.js +39 -0
- package/arcade/mario-not/death.js +77 -0
- package/arcade/mario-not/effects.js +84 -0
- package/arcade/mario-not/enemies.js +31 -0
- package/arcade/mario-not/engine.js +171 -0
- package/arcade/mario-not/fireballs.js +98 -0
- package/arcade/mario-not/items.js +24 -0
- package/arcade/mario-not/levels.js +403 -0
- package/arcade/mario-not/logic.js +104 -0
- package/arcade/mario-not/mario-not.ts +297 -0
- package/arcade/mario-not/player.js +244 -0
- package/arcade/mario-not/render.js +257 -0
- package/arcade/mario-not/spec.md +548 -0
- package/arcade/mario-not/state.js +246 -0
- package/arcade/mario-not/tests/e2e.test.js +855 -0
- package/arcade/mario-not/tests/engine.test.js +888 -0
- package/arcade/mario-not/tests/fixtures/story0-frame.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story1-camera.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story1-glyphs.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story10-item.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story11-hazards.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story12-used-block.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story13-pipes.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story14-goal.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story15-hud-narrow.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story16-unknown-tile.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story17-mix.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story18-hud-score.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story19-cue.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story2-enemy.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story20-camera-offset.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story21-hud-zero.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story22-big-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story23-camera-negative.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story24-camera-width.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story25-camera-positive.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story26-hud-lives.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story27-hud-coins.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story28-item-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story29-enemy-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story3-hud.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story30-hud-score.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story31-particles-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story32-paused-frame.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story4-big.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story5-resume-hud.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story6-particles.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story6-paused.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story7-powerup.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story8-hud-time.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story9-hud-level.txt +2 -0
- package/arcade/mario-not/tiles.js +79 -0
- package/arcade/mario-not/tsconfig.json +14 -0
- package/arcade/mario-not/types.js +225 -0
- package/arcade/package.json +26 -0
- package/arcade/picman.ts +328 -0
- package/arcade/ping.ts +594 -0
- package/arcade/spice-invaders.ts +1104 -0
- package/arcade/tetris.ts +662 -0
- package/code-actions/CHANGELOG.md +4 -0
- package/code-actions/README.md +65 -0
- package/code-actions/actions.ts +107 -0
- package/code-actions/index.ts +148 -0
- package/code-actions/package.json +22 -0
- package/code-actions/search.ts +79 -0
- package/code-actions/snippets.ts +179 -0
- package/code-actions/ui.ts +120 -0
- package/files-widget/CHANGELOG.md +90 -0
- package/files-widget/DESIGN.md +452 -0
- package/files-widget/README.md +122 -0
- package/files-widget/TODO.md +141 -0
- package/files-widget/browser.ts +922 -0
- package/files-widget/comment.ts +5 -0
- package/files-widget/constants.ts +18 -0
- package/files-widget/demo.svg +1 -0
- package/files-widget/file-tree.ts +224 -0
- package/files-widget/file-viewer.ts +93 -0
- package/files-widget/git.ts +107 -0
- package/files-widget/index.ts +140 -0
- package/files-widget/input-utils.ts +3 -0
- package/files-widget/package.json +22 -0
- package/files-widget/types.ts +28 -0
- package/files-widget/utils.ts +26 -0
- package/files-widget/viewer.ts +424 -0
- package/import-cc-codex/research/import-chats-from-other-agents.md +135 -0
- package/import-cc-codex/spec.md +79 -0
- package/package.json +29 -0
- package/ralph-wiggum/CHANGELOG.md +7 -0
- package/ralph-wiggum/README.md +96 -0
- package/ralph-wiggum/SKILL.md +73 -0
- package/ralph-wiggum/index.ts +792 -0
- package/ralph-wiggum/package.json +25 -0
- package/raw-paste/CHANGELOG.md +7 -0
- package/raw-paste/README.md +52 -0
- package/raw-paste/index.ts +112 -0
- package/raw-paste/package.json +22 -0
- package/tab-status/CHANGELOG.md +4 -0
- package/tab-status/README.md +61 -0
- package/tab-status/assets/tab-status.png +0 -0
- package/tab-status/package.json +22 -0
- package/tab-status/tab-status.ts +179 -0
- package/usage-extension/CHANGELOG.md +17 -0
- package/usage-extension/README.md +120 -0
- package/usage-extension/index.ts +628 -0
- package/usage-extension/package.json +22 -0
- 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
|
+
};
|