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,22 @@
1
+ {
2
+ "name": "@tmustier/pi-agent-guidance",
3
+ "version": "0.1.0",
4
+ "description": "Loads provider-specific context files (CLAUDE.md, CODEX.md, GEMINI.md) based on current model.",
5
+ "license": "MIT",
6
+ "author": "Thomas Mustier",
7
+ "keywords": [
8
+ "pi-package"
9
+ ],
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/tmustier/pi-extensions.git",
13
+ "directory": "agent-guidance"
14
+ },
15
+ "bugs": "https://github.com/tmustier/pi-extensions/issues",
16
+ "homepage": "https://github.com/tmustier/pi-extensions/tree/main/agent-guidance",
17
+ "pi": {
18
+ "extensions": [
19
+ "agent-guidance.ts"
20
+ ]
21
+ }
22
+ }
@@ -0,0 +1,75 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ PI_AGENT_DIR="$HOME/.pi/agent"
6
+
7
+ echo "Setting up agent-guidance..."
8
+ mkdir -p "$PI_AGENT_DIR/extensions"
9
+
10
+ AGENTS_FILE="$PI_AGENT_DIR/AGENTS.md"
11
+ CLAUDE_FILE="$PI_AGENT_DIR/CLAUDE.md"
12
+
13
+ create_placeholder() {
14
+ cat > "$AGENTS_FILE" << 'EOF'
15
+ # AGENTS.md
16
+
17
+ Universal guidelines for all AI models.
18
+
19
+ <!-- Add your cross-model guidance here -->
20
+ EOF
21
+ }
22
+
23
+ # Handle missing AGENTS.md
24
+ if [ ! -f "$AGENTS_FILE" ]; then
25
+ if [ -f "$CLAUDE_FILE" ] || [ -L "$CLAUDE_FILE" ]; then
26
+ echo ""
27
+ echo " ⚠️ No AGENTS.md found in $PI_AGENT_DIR/"
28
+ echo " You have an existing CLAUDE.md which was being used by Pi"
29
+ echo " across all models - if you want that guidance to continue"
30
+ echo " to apply across all models, it should be in AGENTS.md."
31
+ echo ""
32
+ read -p " Copy CLAUDE.md content to AGENTS.md? [y/N] " -n 1 -r
33
+ echo ""
34
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
35
+ cp "$CLAUDE_FILE" "$AGENTS_FILE"
36
+ echo " Copied CLAUDE.md → AGENTS.md"
37
+ echo " CLAUDE.md kept for Claude-specific guidance (edit as needed)."
38
+ else
39
+ create_placeholder
40
+ echo " Created placeholder AGENTS.md."
41
+ fi
42
+ echo ""
43
+ else
44
+ create_placeholder
45
+ echo ""
46
+ echo " ⚠️ No AGENTS.md found - created placeholder."
47
+ echo " Add your cross-model guidance there."
48
+ echo ""
49
+ fi
50
+ fi
51
+
52
+ # Symlink extension
53
+ target="$PI_AGENT_DIR/extensions/agent-guidance.ts"
54
+ if [ -L "$target" ]; then
55
+ echo " agent-guidance.ts already linked"
56
+ else
57
+ ln -sf "$SCRIPT_DIR/agent-guidance.ts" "$target"
58
+ echo " Linked agent-guidance.ts"
59
+ fi
60
+
61
+ echo ""
62
+ echo "Done!"
63
+ echo ""
64
+ echo "Template provider files in $SCRIPT_DIR/templates/:"
65
+ echo " CLAUDE.md (Anthropic)"
66
+ echo " CODEX.md (OpenAI)"
67
+ echo " GEMINI.md (Google)"
68
+ echo ""
69
+ echo "To install:"
70
+ echo " ln -s $SCRIPT_DIR/templates/CLAUDE.md $PI_AGENT_DIR/"
71
+ echo " ln -s $SCRIPT_DIR/templates/CODEX.md $PI_AGENT_DIR/"
72
+ echo " ln -s $SCRIPT_DIR/templates/GEMINI.md $PI_AGENT_DIR/"
73
+ echo ""
74
+ echo "Or all:"
75
+ echo " ln -s $SCRIPT_DIR/templates/*.md $PI_AGENT_DIR/"
@@ -0,0 +1,5 @@
1
+ # CLAUDE.md
2
+
3
+ Claude-specific guidelines. Loaded when using Anthropic models.
4
+
5
+ <!-- Add your Claude-specific preferences here -->
@@ -0,0 +1,92 @@
1
+ # CODEX.md
2
+
3
+ Adapted from [steipete/agent-scripts](https://github.com/steipete/agent-scripts/blob/main/AGENTS.MD).
4
+
5
+ Work style: telegraph; noun-phrases ok; drop grammar; min tokens.
6
+
7
+ ## Agent Protocol
8
+ - PRs: use `gh pr view/diff` (no URLs).
9
+ - Guardrails: use `trash` for deletes when available.
10
+ - Need upstream file: stage in `/tmp/`, then cherry-pick; never overwrite tracked.
11
+ - Bugs: add regression test when it fits.
12
+ - Keep files <~500 LOC; split/refactor as needed.
13
+ - Commits: Conventional Commits (`feat|fix|refactor|build|ci|chore|docs|style|perf|test`).
14
+ - CI: `gh run list/view` (rerun/fix til green).
15
+ - Prefer end-to-end verify; if blocked, say what's missing.
16
+ - New deps: quick health check (recent releases/commits, adoption).
17
+ - Web: search early; quote exact errors; prefer 2024–2025 sources.
18
+ - Style: telegraph. Drop filler/grammar. Min tokens.
19
+
20
+ ## Screenshots
21
+ - Size: `sips -g pixelWidth -g pixelHeight <file>` (prefer 2×).
22
+ - Optimize: `imageoptim <file>` (install: `brew install imageoptim-cli`).
23
+ - Replace asset; keep dimensions; commit; run gate; verify CI.
24
+
25
+ ## Docs
26
+ - Start: check for docs list script; open docs before coding.
27
+ - Follow links until domain makes sense.
28
+ - Keep notes short; update docs when behavior/API changes (no ship w/o docs).
29
+
30
+ ## PR Feedback
31
+ - Active PR: `gh pr view --json number,title,url --jq '"PR #\\(.number): \\(.title)\\n\\(.url)"'`.
32
+ - PR comments: `gh pr view …` + `gh api …/comments --paginate`.
33
+ - Replies: cite fix + file/line; resolve threads only after fix lands.
34
+ - When merging a PR: thank the contributor in `CHANGELOG.md`.
35
+
36
+ ## Flow & Runtime
37
+ - Use repo's package manager/runtime; no swaps w/o approval.
38
+ - Use background tasks for long jobs; tmux only for interactive/persistent (debugger/server).
39
+
40
+ ## Build / Test
41
+ - Before handoff: run full gate (lint/typecheck/tests/docs).
42
+ - CI red: `gh run list/view`, rerun, fix, push, repeat til green.
43
+ - Keep it observable (logs, panes, tails).
44
+
45
+ ## Git
46
+ - Safe by default: `git status/diff/log`. Push only when user asks.
47
+ - `git checkout` ok for PR review / explicit request.
48
+ - Branch changes require user consent.
49
+ - Destructive ops forbidden unless explicit (`reset --hard`, `clean`, `restore`, `rm`, …).
50
+ - Don't delete/rename unexpected stuff; stop + ask.
51
+ - No repo-wide S/R scripts; keep edits small/reviewable.
52
+ - Avoid manual `git stash`; if Git auto-stashes during pull/rebase, that's fine.
53
+ - If user types a command ("pull and push"), that's consent for that command.
54
+ - No amend unless asked.
55
+ - Big review: `git --no-pager diff --color=never`.
56
+ - Multi-agent: check `git status/diff` before edits; ship small commits.
57
+
58
+ ## Language/Stack Notes
59
+ - Swift: validate `swift build` + tests; keep concurrency attrs right.
60
+ - TypeScript: use repo PM; keep files small; follow existing patterns.
61
+
62
+ ## Critical Thinking
63
+ - Fix root cause (not band-aid).
64
+ - Unsure: read more code; if still stuck, ask w/ short options.
65
+ - Conflicts: call out; pick safer path.
66
+ - Unrecognized changes: assume other agent; keep going; focus your changes. If it causes issues, stop + ask user.
67
+ - Leave breadcrumb notes in thread.
68
+
69
+ ## Tools
70
+
71
+ ### trash
72
+ - Move files to Trash: `trash …` (safer than rm).
73
+
74
+ ### gh
75
+ - GitHub CLI for PRs/CI/releases. Given issue/PR URL: use `gh`, not web search.
76
+ - Examples: `gh issue view <url> --comments -R owner/repo`, `gh pr view <url> --comments --files -R owner/repo`.
77
+
78
+ ### tmux
79
+ - Use only when you need persistence/interaction (debugger/server).
80
+ - Quick refs: `tmux new -d -s shell`, `tmux attach -t shell`, `tmux list-sessions`, `tmux kill-session -t shell`.
81
+
82
+ <frontend_aesthetics>
83
+ Avoid "AI slop" UI. Be opinionated + distinctive.
84
+
85
+ Do:
86
+ - Typography: pick a real font; avoid Inter/Roboto/Arial/system defaults.
87
+ - Theme: commit to a palette; use CSS vars; bold accents > timid gradients.
88
+ - Motion: 1–2 high-impact moments (staggered reveal beats random micro-anim).
89
+ - Background: add depth (gradients/patterns), not flat default.
90
+
91
+ Avoid: purple-on-white clichés, generic component grids, predictable layouts.
92
+ </frontend_aesthetics>
@@ -0,0 +1,5 @@
1
+ # GEMINI.md
2
+
3
+ Gemini-specific guidelines. Loaded when using Google models.
4
+
5
+ <!-- Add your Gemini-specific preferences here -->
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-01-13
4
+ - Initial release.
@@ -0,0 +1,85 @@
1
+ # arcade
2
+
3
+ [Snake](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/examples/extensions/snake.ts) is cool, but have you tried:
4
+
5
+ - **sPIce-invaders** (`/spice-invaders`) - type `clawd` for a special challenge that gets harder every level
6
+ - **picman** (`/picman`)
7
+ - **ping** (`/ping`) - in a similar vein to [patriceckhart's](https://github.com/patriceckhart/pi-ng-pong)
8
+ - **tetris** (`/tetris`)
9
+ - **mario-not** (`/mario-not`) - Mario-style platformer (experimental)
10
+
11
+ <table>
12
+ <tr>
13
+ <td><img src="assets/spice-invaders.png" width="400"/></td>
14
+ <td><img src="assets/picman.png" width="400"/></td>
15
+ </tr>
16
+ <tr>
17
+ <td><img src="assets/ping.png" width="400"/></td>
18
+ <td><img src="assets/tetris.png" width="400"/></td>
19
+ </tr>
20
+ </table>
21
+
22
+ ## Install
23
+
24
+ ### Pi package manager
25
+
26
+ ```bash
27
+ pi install npm:@tmustier/pi-arcade
28
+ ```
29
+
30
+ ```bash
31
+ pi install git:github.com/tmustier/pi-extensions
32
+ ```
33
+
34
+ Then filter to just the games in `~/.pi/agent/settings.json`:
35
+
36
+ ```json
37
+ {
38
+ "packages": [
39
+ {
40
+ "source": "git:github.com/tmustier/pi-extensions",
41
+ "extensions": [
42
+ "arcade/spice-invaders.ts",
43
+ "arcade/picman.ts",
44
+ "arcade/ping.ts",
45
+ "arcade/tetris.ts",
46
+ "arcade/mario-not/mario-not.ts"
47
+ ]
48
+ }
49
+ ]
50
+ }
51
+ ```
52
+
53
+ ### Local clone
54
+
55
+ ```bash
56
+ # All games
57
+ ln -s ~/pi-extensions/arcade/*.ts ~/.pi/agent/extensions/
58
+ ln -s ~/pi-extensions/arcade/mario-not/mario-not.ts ~/.pi/agent/extensions/
59
+
60
+ # Or individual games
61
+ ln -s ~/pi-extensions/arcade/spice-invaders.ts ~/.pi/agent/extensions/
62
+ ln -s ~/pi-extensions/arcade/picman.ts ~/.pi/agent/extensions/
63
+ ln -s ~/pi-extensions/arcade/ping.ts ~/.pi/agent/extensions/
64
+ ln -s ~/pi-extensions/arcade/tetris.ts ~/.pi/agent/extensions/
65
+ ln -s ~/pi-extensions/arcade/mario-not/mario-not.ts ~/.pi/agent/extensions/
66
+ ```
67
+
68
+ Or add to `~/.pi/agent/settings.json`:
69
+
70
+ ```json
71
+ {
72
+ "extensions": [
73
+ "~/pi-extensions/arcade/spice-invaders.ts",
74
+ "~/pi-extensions/arcade/picman.ts",
75
+ "~/pi-extensions/arcade/ping.ts",
76
+ "~/pi-extensions/arcade/tetris.ts",
77
+ "~/pi-extensions/arcade/mario-not/mario-not.ts"
78
+ ]
79
+ }
80
+ ```
81
+
82
+ ## Changelog
83
+
84
+ See `CHANGELOG.md`.
85
+
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,30 @@
1
+ # mario-not (experimental)
2
+
3
+ Mario-style TUI platformer. This project is experimental and tuning is in flux.
4
+
5
+ ## Run
6
+
7
+ - Command: `/mario-not`
8
+ - Extension entrypoint: `arcade/mario-not/mario-not.ts`
9
+
10
+ ## Controls
11
+
12
+ - Move: Left/Right arrows, A/D, L
13
+ - Jump: Up, Space, H
14
+ - Walk toggle: X (run is default)
15
+ - Pause: P
16
+ - Stop: S
17
+ - Quit: Q or Esc
18
+
19
+ ## Dev
20
+
21
+ Tests:
22
+
23
+ ```bash
24
+ node --test arcade/mario-not/tests/*.test.js
25
+ npx tsc --noEmit -p arcade/mario-not/tsconfig.json
26
+ ```
27
+
28
+ Spec:
29
+
30
+ - `arcade/mario-not/spec.md`
@@ -0,0 +1,103 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ const { moveHorizontal, resolveVertical, overlaps } = require("./collision.js");
5
+ const { spawnParticles, setCue } = require("./effects.js");
6
+ const { BOSS_W, BOSS_H, BOSS_INVULN_TIME, BOSS_SCORE, GAME_MODES } = require("./constants.js");
7
+
8
+ /** @typedef {import("./types").GameState} GameState */
9
+ /** @typedef {import("./types").BossState} BossState */
10
+
11
+ /** @param {GameState} state */
12
+ function updateBoss(state) {
13
+ const boss = state.boss;
14
+ if (!boss || !boss.alive) return;
15
+
16
+ const cfg = state.config;
17
+ const dt = cfg.dt;
18
+
19
+ // Update invulnerability
20
+ if (boss.invuln > 0) {
21
+ boss.invuln = Math.max(0, boss.invuln - dt);
22
+ }
23
+
24
+ // Apply gravity
25
+ boss.vy = Math.min(boss.vy + cfg.gravity * dt, cfg.maxFall);
26
+
27
+ // Move horizontally
28
+ const blocked = moveHorizontal(state.level, boss, dt, BOSS_W, BOSS_H);
29
+ if (blocked) {
30
+ boss.vx = -boss.vx; // Bounce off walls
31
+ }
32
+
33
+ // Resolve vertical movement
34
+ resolveVertical(state.level, boss, dt, undefined, BOSS_H, BOSS_W, true);
35
+
36
+ // Check if boss fell into pit
37
+ if (boss.y >= state.level.height + 1) {
38
+ boss.alive = false;
39
+ defeatBoss(state);
40
+ }
41
+ }
42
+
43
+ /** @param {GameState} state @param {number} prevY @returns {boolean} - true if player took damage */
44
+ function resolveBossCollision(state, prevY) {
45
+ const boss = state.boss;
46
+ if (!boss || !boss.alive) return false;
47
+
48
+ const player = state.player;
49
+ const playerH = player.size === "big" ? 2 : 1;
50
+ const prevBottom = prevY + playerH;
51
+ const currBottom = player.y + playerH;
52
+ const falling = currBottom > prevBottom + 0.0001;
53
+
54
+ // Check collision with boss (2x2)
55
+ if (overlaps(player.x, player.y, 1, playerH, boss.x, boss.y - 1, BOSS_W, BOSS_H)) {
56
+ const stomp = falling && prevBottom <= boss.y - 1 + 0.1 && currBottom >= boss.y - 1;
57
+ if (stomp && boss.invuln <= 0) {
58
+ // Player stomped on boss
59
+ damageBoss(state);
60
+ player.vy = -state.config.jumpVel * 0.7;
61
+ return false;
62
+ } else if (boss.invuln <= 0) {
63
+ // Player touched boss from side - take damage
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+
70
+ /** @param {GameState} state */
71
+ function damageBoss(state) {
72
+ const boss = state.boss;
73
+ if (!boss || !boss.alive) return;
74
+
75
+ boss.health -= 1;
76
+ boss.invuln = BOSS_INVULN_TIME;
77
+ spawnParticles(state, boss.x + 1, boss.y - 1, 4);
78
+
79
+ if (boss.health <= 0) {
80
+ boss.alive = false;
81
+ defeatBoss(state);
82
+ } else {
83
+ // Boss gets faster when damaged
84
+ const speedMult = 1 + (boss.maxHealth - boss.health) * 0.2;
85
+ boss.vx = Math.sign(boss.vx) * 1.5 * speedMult;
86
+ setCue(state, `BOSS ${boss.health}/${boss.maxHealth}`, 0.5, false);
87
+ }
88
+ }
89
+
90
+ /** @param {GameState} state */
91
+ function defeatBoss(state) {
92
+ state.score += BOSS_SCORE;
93
+ spawnParticles(state, state.boss?.x || 0, state.boss?.y || 0, 8);
94
+ setCue(state, "BOSS DEFEATED!", 0, true);
95
+ state.mode = GAME_MODES.levelClear;
96
+ }
97
+
98
+ module.exports = {
99
+ updateBoss,
100
+ resolveBossCollision,
101
+ damageBoss,
102
+ defeatBoss,
103
+ };
@@ -0,0 +1,59 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ /**
5
+ * @typedef {Object} Level
6
+ * @property {number} width
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} PlayerState
11
+ * @property {number} x
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} Config
16
+ * @property {number} viewportWidth
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} GameState
21
+ * @property {Level} level
22
+ * @property {PlayerState} player
23
+ * @property {Config} config
24
+ * @property {number} cameraX
25
+ */
26
+
27
+ /** @param {number} value @param {number} min @param {number} max @returns {number} */
28
+ function clamp(value, min, max) {
29
+ return Math.max(min, Math.min(max, value));
30
+ }
31
+
32
+ /** @param {GameState} state @param {number} [viewportWidth] @returns {number} */
33
+ function getCameraX(state, viewportWidth) {
34
+ const width = typeof viewportWidth === "number" ? viewportWidth : state.config.viewportWidth;
35
+ const maxX = Math.max(0, state.level.width - width);
36
+ return clamp(state.cameraX || 0, 0, maxX);
37
+ }
38
+
39
+ /** @param {GameState} state @returns {number} */
40
+ function updateCamera(state) {
41
+ const width = state.config.viewportWidth;
42
+ const maxX = Math.max(0, state.level.width - width);
43
+ let cameraX = state.cameraX || 0;
44
+ const lead = Math.floor(width * 0.25);
45
+ const minX = cameraX + lead;
46
+ const maxXZone = cameraX + lead;
47
+ if (state.player.x < minX) {
48
+ cameraX = clamp(state.player.x - lead, 0, maxX);
49
+ } else if (state.player.x > maxXZone) {
50
+ cameraX = clamp(state.player.x - lead, 0, maxX);
51
+ }
52
+ state.cameraX = cameraX;
53
+ return cameraX;
54
+ }
55
+
56
+ module.exports = {
57
+ getCameraX,
58
+ updateCamera,
59
+ };
@@ -0,0 +1,91 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ const { isSolidAt } = require("./tiles.js");
5
+
6
+ /** @typedef {import("./types").Level} Level */
7
+
8
+ /** @param {Level} level @param {number} x @param {number} y @param {number} height @returns {boolean} */
9
+ function hitsSolid(level, x, y, height) {
10
+ if (isSolidAt(level, x, y)) return true;
11
+ if (height > 1 && isSolidAt(level, x, y - (height - 1))) return true;
12
+ return false;
13
+ }
14
+
15
+ /** @param {Level} level @param {number} x @param {number} y @param {boolean} allowBottomFall @returns {boolean} */
16
+ function isSolidBelow(level, x, y, allowBottomFall) {
17
+ if (allowBottomFall && Math.floor(y) >= level.height) return false;
18
+ return isSolidAt(level, x, y);
19
+ }
20
+
21
+ /**
22
+ * @param {Level} level
23
+ * @param {{ x: number, y: number, vx: number, vy: number, onGround: boolean }} entity
24
+ * @param {number} dt
25
+ * @param {(tileX: number, tileY: number) => void} [onHeadBump]
26
+ * @param {number} [height]
27
+ * @param {number} [width]
28
+ * @param {boolean} [allowBottomFall]
29
+ */
30
+ function resolveVertical(level, entity, dt, onHeadBump, height, width, allowBottomFall) {
31
+ const entityHeight = typeof height === "number" ? height : 1;
32
+ const entityWidth = typeof width === "number" ? width : 1;
33
+ const nextY = entity.y + entity.vy * dt;
34
+ const leftX = entity.x + 0.001;
35
+ const rightX = entity.x + entityWidth - 0.001;
36
+ if (entity.vy >= 0) {
37
+ const footY = nextY + 1;
38
+ if (isSolidBelow(level, leftX, footY, !!allowBottomFall) || isSolidBelow(level, rightX, footY, !!allowBottomFall)) {
39
+ entity.y = Math.floor(footY) - 1;
40
+ entity.vy = 0;
41
+ entity.onGround = true;
42
+ } else {
43
+ entity.y = nextY;
44
+ entity.onGround = false;
45
+ }
46
+ } else {
47
+ const headY = nextY - (entityHeight - 1);
48
+ if (isSolidAt(level, leftX, headY) || isSolidAt(level, rightX, headY)) {
49
+ const tileY = Math.floor(headY);
50
+ entity.y = tileY + entityHeight;
51
+ entity.vy = 0;
52
+ if (onHeadBump) {
53
+ const leftTileX = Math.floor(leftX);
54
+ const rightTileX = Math.floor(rightX);
55
+ onHeadBump(leftTileX, tileY);
56
+ if (rightTileX !== leftTileX) onHeadBump(rightTileX, tileY);
57
+ }
58
+ } else {
59
+ entity.y = nextY;
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * @param {Level} level
66
+ * @param {{ x: number, y: number, vx: number }} entity
67
+ * @param {number} dt
68
+ * @param {number} width
69
+ * @param {number} height
70
+ * @returns {boolean}
71
+ */
72
+ function moveHorizontal(level, entity, dt, width, height) {
73
+ if (!entity.vx) return false;
74
+ const nextX = entity.x + entity.vx * dt;
75
+ const probeX = entity.vx > 0 ? nextX + width - 0.001 : nextX + 0.001;
76
+ if (hitsSolid(level, probeX, entity.y, height)) return true;
77
+ entity.x = nextX;
78
+ return false;
79
+ }
80
+
81
+ /** @param {number} ax @param {number} ay @param {number} aw @param {number} ah @param {number} bx @param {number} by @param {number} bw @param {number} bh @returns {boolean} */
82
+ function overlaps(ax, ay, aw, ah, bx, by, bw, bh) {
83
+ return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
84
+ }
85
+
86
+ module.exports = {
87
+ hitsSolid,
88
+ resolveVertical,
89
+ moveHorizontal,
90
+ overlaps,
91
+ };
@@ -0,0 +1,36 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ // ANSI color codes for terminal output
5
+ const COLORS = {
6
+ reset: "\x1b[0m",
7
+ red: "\x1b[31m",
8
+ green: "\x1b[32m",
9
+ yellow: "\x1b[33m",
10
+ blue: "\x1b[34m",
11
+ magenta: "\x1b[35m",
12
+ cyan: "\x1b[36m",
13
+ white: "\x1b[37m",
14
+ gray: "\x1b[90m",
15
+ brightRed: "\x1b[91m",
16
+ brightGreen: "\x1b[92m",
17
+ brightYellow: "\x1b[93m",
18
+ brightCyan: "\x1b[96m",
19
+ orange: "\x1b[38;5;208m",
20
+ brown: "\x1b[38;5;94m",
21
+ };
22
+
23
+ /**
24
+ * Wrap a string with color codes
25
+ * @param {string} text
26
+ * @param {string} color
27
+ * @returns {string}
28
+ */
29
+ function colorize(text, color) {
30
+ return `${color}${text}${COLORS.reset}`;
31
+ }
32
+
33
+ module.exports = {
34
+ COLORS,
35
+ colorize,
36
+ };