picopilot 0.0.0 → 0.1.1

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 (334) hide show
  1. package/LICENSE +661 -0
  2. package/dist/bin.d.ts +3 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +4 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/cli.d.ts +15 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +59 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/audio.d.ts +17 -0
  11. package/dist/commands/audio.d.ts.map +1 -0
  12. package/dist/commands/audio.js +315 -0
  13. package/dist/commands/audio.js.map +1 -0
  14. package/dist/commands/export.d.ts +36 -0
  15. package/dist/commands/export.d.ts.map +1 -0
  16. package/dist/commands/export.js +257 -0
  17. package/dist/commands/export.js.map +1 -0
  18. package/dist/commands/gfx.d.ts +28 -0
  19. package/dist/commands/gfx.d.ts.map +1 -0
  20. package/dist/commands/gfx.js +521 -0
  21. package/dist/commands/gfx.js.map +1 -0
  22. package/dist/commands/init.d.ts +34 -0
  23. package/dist/commands/init.d.ts.map +1 -0
  24. package/dist/commands/init.js +151 -0
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/lint.d.ts +20 -0
  27. package/dist/commands/lint.d.ts.map +1 -0
  28. package/dist/commands/lint.js +90 -0
  29. package/dist/commands/lint.js.map +1 -0
  30. package/dist/commands/minify.d.ts +20 -0
  31. package/dist/commands/minify.d.ts.map +1 -0
  32. package/dist/commands/minify.js +129 -0
  33. package/dist/commands/minify.js.map +1 -0
  34. package/dist/commands/music.d.ts +13 -0
  35. package/dist/commands/music.d.ts.map +1 -0
  36. package/dist/commands/music.js +241 -0
  37. package/dist/commands/music.js.map +1 -0
  38. package/dist/commands/playtest.d.ts +29 -0
  39. package/dist/commands/playtest.d.ts.map +1 -0
  40. package/dist/commands/playtest.js +631 -0
  41. package/dist/commands/playtest.js.map +1 -0
  42. package/dist/commands/run.d.ts +30 -0
  43. package/dist/commands/run.d.ts.map +1 -0
  44. package/dist/commands/run.js +224 -0
  45. package/dist/commands/run.js.map +1 -0
  46. package/dist/commands/serve.d.ts +50 -0
  47. package/dist/commands/serve.d.ts.map +1 -0
  48. package/dist/commands/serve.js +213 -0
  49. package/dist/commands/serve.js.map +1 -0
  50. package/dist/commands/sfx.d.ts +13 -0
  51. package/dist/commands/sfx.d.ts.map +1 -0
  52. package/dist/commands/sfx.js +254 -0
  53. package/dist/commands/sfx.js.map +1 -0
  54. package/dist/commands/tokens.d.ts +36 -0
  55. package/dist/commands/tokens.d.ts.map +1 -0
  56. package/dist/commands/tokens.js +129 -0
  57. package/dist/commands/tokens.js.map +1 -0
  58. package/dist/commands/verify.d.ts +57 -0
  59. package/dist/commands/verify.d.ts.map +1 -0
  60. package/dist/commands/verify.js +213 -0
  61. package/dist/commands/verify.js.map +1 -0
  62. package/dist/commands/version.d.ts +12 -0
  63. package/dist/commands/version.d.ts.map +1 -0
  64. package/dist/commands/version.js +24 -0
  65. package/dist/commands/version.js.map +1 -0
  66. package/dist/engine/audio/index.d.ts +14 -0
  67. package/dist/engine/audio/index.d.ts.map +1 -0
  68. package/dist/engine/audio/index.js +14 -0
  69. package/dist/engine/audio/index.js.map +1 -0
  70. package/dist/engine/audio/music.d.ts +116 -0
  71. package/dist/engine/audio/music.d.ts.map +1 -0
  72. package/dist/engine/audio/music.js +138 -0
  73. package/dist/engine/audio/music.js.map +1 -0
  74. package/dist/engine/audio/section.d.ts +34 -0
  75. package/dist/engine/audio/section.d.ts.map +1 -0
  76. package/dist/engine/audio/section.js +67 -0
  77. package/dist/engine/audio/section.js.map +1 -0
  78. package/dist/engine/audio/sfx.d.ts +141 -0
  79. package/dist/engine/audio/sfx.d.ts.map +1 -0
  80. package/dist/engine/audio/sfx.js +506 -0
  81. package/dist/engine/audio/sfx.js.map +1 -0
  82. package/dist/engine/cart/cart.d.ts +75 -0
  83. package/dist/engine/cart/cart.d.ts.map +1 -0
  84. package/dist/engine/cart/cart.js +153 -0
  85. package/dist/engine/cart/cart.js.map +1 -0
  86. package/dist/engine/cart/errors.d.ts +29 -0
  87. package/dist/engine/cart/errors.d.ts.map +1 -0
  88. package/dist/engine/cart/errors.js +26 -0
  89. package/dist/engine/cart/errors.js.map +1 -0
  90. package/dist/engine/cart/gfx.d.ts +79 -0
  91. package/dist/engine/cart/gfx.d.ts.map +1 -0
  92. package/dist/engine/cart/gfx.js +171 -0
  93. package/dist/engine/cart/gfx.js.map +1 -0
  94. package/dist/engine/cart/index.d.ts +15 -0
  95. package/dist/engine/cart/index.d.ts.map +1 -0
  96. package/dist/engine/cart/index.js +15 -0
  97. package/dist/engine/cart/index.js.map +1 -0
  98. package/dist/engine/cart/map.d.ts +46 -0
  99. package/dist/engine/cart/map.d.ts.map +1 -0
  100. package/dist/engine/cart/map.js +118 -0
  101. package/dist/engine/cart/map.js.map +1 -0
  102. package/dist/engine/config/config.d.ts +74 -0
  103. package/dist/engine/config/config.d.ts.map +1 -0
  104. package/dist/engine/config/config.js +103 -0
  105. package/dist/engine/config/config.js.map +1 -0
  106. package/dist/engine/config/index.d.ts +7 -0
  107. package/dist/engine/config/index.d.ts.map +1 -0
  108. package/dist/engine/config/index.js +7 -0
  109. package/dist/engine/config/index.js.map +1 -0
  110. package/dist/engine/gfx/grid.d.ts +51 -0
  111. package/dist/engine/gfx/grid.d.ts.map +1 -0
  112. package/dist/engine/gfx/grid.js +116 -0
  113. package/dist/engine/gfx/grid.js.map +1 -0
  114. package/dist/engine/gfx/index.d.ts +17 -0
  115. package/dist/engine/gfx/index.d.ts.map +1 -0
  116. package/dist/engine/gfx/index.js +17 -0
  117. package/dist/engine/gfx/index.js.map +1 -0
  118. package/dist/engine/gfx/label.d.ts +34 -0
  119. package/dist/engine/gfx/label.d.ts.map +1 -0
  120. package/dist/engine/gfx/label.js +190 -0
  121. package/dist/engine/gfx/label.js.map +1 -0
  122. package/dist/engine/gfx/overlap.d.ts +42 -0
  123. package/dist/engine/gfx/overlap.d.ts.map +1 -0
  124. package/dist/engine/gfx/overlap.js +57 -0
  125. package/dist/engine/gfx/overlap.js.map +1 -0
  126. package/dist/engine/gfx/palette.d.ts +29 -0
  127. package/dist/engine/gfx/palette.d.ts.map +1 -0
  128. package/dist/engine/gfx/palette.js +46 -0
  129. package/dist/engine/gfx/palette.js.map +1 -0
  130. package/dist/engine/gfx/png.d.ts +31 -0
  131. package/dist/engine/gfx/png.d.ts.map +1 -0
  132. package/dist/engine/gfx/png.js +165 -0
  133. package/dist/engine/gfx/png.js.map +1 -0
  134. package/dist/engine/gfx/render.d.ts +42 -0
  135. package/dist/engine/gfx/render.d.ts.map +1 -0
  136. package/dist/engine/gfx/render.js +72 -0
  137. package/dist/engine/gfx/render.js.map +1 -0
  138. package/dist/engine/index.d.ts +22 -0
  139. package/dist/engine/index.d.ts.map +1 -0
  140. package/dist/engine/index.js +22 -0
  141. package/dist/engine/index.js.map +1 -0
  142. package/dist/engine/pico8/adapter.d.ts +267 -0
  143. package/dist/engine/pico8/adapter.d.ts.map +1 -0
  144. package/dist/engine/pico8/adapter.js +37 -0
  145. package/dist/engine/pico8/adapter.js.map +1 -0
  146. package/dist/engine/pico8/drive.d.ts +308 -0
  147. package/dist/engine/pico8/drive.d.ts.map +1 -0
  148. package/dist/engine/pico8/drive.js +581 -0
  149. package/dist/engine/pico8/drive.js.map +1 -0
  150. package/dist/engine/pico8/harness.d.ts +146 -0
  151. package/dist/engine/pico8/harness.d.ts.map +1 -0
  152. package/dist/engine/pico8/harness.js +251 -0
  153. package/dist/engine/pico8/harness.js.map +1 -0
  154. package/dist/engine/pico8/index.d.ts +21 -0
  155. package/dist/engine/pico8/index.d.ts.map +1 -0
  156. package/dist/engine/pico8/index.js +21 -0
  157. package/dist/engine/pico8/index.js.map +1 -0
  158. package/dist/engine/pico8/sentinel.d.ts +63 -0
  159. package/dist/engine/pico8/sentinel.d.ts.map +1 -0
  160. package/dist/engine/pico8/sentinel.js +86 -0
  161. package/dist/engine/pico8/sentinel.js.map +1 -0
  162. package/dist/engine/pico8/session-daemon-entry.d.ts +10 -0
  163. package/dist/engine/pico8/session-daemon-entry.d.ts.map +1 -0
  164. package/dist/engine/pico8/session-daemon-entry.js +11 -0
  165. package/dist/engine/pico8/session-daemon-entry.js.map +1 -0
  166. package/dist/engine/pico8/session-daemon-main.d.ts +44 -0
  167. package/dist/engine/pico8/session-daemon-main.d.ts.map +1 -0
  168. package/dist/engine/pico8/session-daemon-main.js +71 -0
  169. package/dist/engine/pico8/session-daemon-main.js.map +1 -0
  170. package/dist/engine/pico8/session-daemon.d.ts +68 -0
  171. package/dist/engine/pico8/session-daemon.d.ts.map +1 -0
  172. package/dist/engine/pico8/session-daemon.js +264 -0
  173. package/dist/engine/pico8/session-daemon.js.map +1 -0
  174. package/dist/engine/pico8/session.d.ts +140 -0
  175. package/dist/engine/pico8/session.d.ts.map +1 -0
  176. package/dist/engine/pico8/session.js +250 -0
  177. package/dist/engine/pico8/session.js.map +1 -0
  178. package/dist/engine/pico8/shell.d.ts +103 -0
  179. package/dist/engine/pico8/shell.d.ts.map +1 -0
  180. package/dist/engine/pico8/shell.js +324 -0
  181. package/dist/engine/pico8/shell.js.map +1 -0
  182. package/dist/engine/pico8/supervisor.d.ts +137 -0
  183. package/dist/engine/pico8/supervisor.d.ts.map +1 -0
  184. package/dist/engine/pico8/supervisor.js +140 -0
  185. package/dist/engine/pico8/supervisor.js.map +1 -0
  186. package/dist/engine/scaffold/index.d.ts +8 -0
  187. package/dist/engine/scaffold/index.d.ts.map +1 -0
  188. package/dist/engine/scaffold/index.js +8 -0
  189. package/dist/engine/scaffold/index.js.map +1 -0
  190. package/dist/engine/scaffold/scaffold.d.ts +49 -0
  191. package/dist/engine/scaffold/scaffold.d.ts.map +1 -0
  192. package/dist/engine/scaffold/scaffold.js +259 -0
  193. package/dist/engine/scaffold/scaffold.js.map +1 -0
  194. package/dist/engine/shrinko/adapter.d.ts +167 -0
  195. package/dist/engine/shrinko/adapter.d.ts.map +1 -0
  196. package/dist/engine/shrinko/adapter.js +107 -0
  197. package/dist/engine/shrinko/adapter.js.map +1 -0
  198. package/dist/engine/shrinko/index.d.ts +13 -0
  199. package/dist/engine/shrinko/index.d.ts.map +1 -0
  200. package/dist/engine/shrinko/index.js +13 -0
  201. package/dist/engine/shrinko/index.js.map +1 -0
  202. package/dist/engine/shrinko/shell.d.ts +69 -0
  203. package/dist/engine/shrinko/shell.d.ts.map +1 -0
  204. package/dist/engine/shrinko/shell.js +156 -0
  205. package/dist/engine/shrinko/shell.js.map +1 -0
  206. package/dist/engine/skills/index.d.ts +8 -0
  207. package/dist/engine/skills/index.d.ts.map +1 -0
  208. package/dist/engine/skills/index.js +8 -0
  209. package/dist/engine/skills/index.js.map +1 -0
  210. package/dist/engine/skills/skills.d.ts +104 -0
  211. package/dist/engine/skills/skills.d.ts.map +1 -0
  212. package/dist/engine/skills/skills.js +149 -0
  213. package/dist/engine/skills/skills.js.map +1 -0
  214. package/dist/index.d.ts +10 -0
  215. package/dist/index.d.ts.map +1 -0
  216. package/dist/index.js +10 -0
  217. package/dist/index.js.map +1 -0
  218. package/dist/version.d.ts +2 -0
  219. package/dist/version.d.ts.map +1 -0
  220. package/dist/version.js +19 -0
  221. package/dist/version.js.map +1 -0
  222. package/package.json +48 -2
  223. package/src/bin.ts +4 -0
  224. package/src/cli.ts +61 -0
  225. package/src/commands/audio.test.ts +280 -0
  226. package/src/commands/audio.ts +410 -0
  227. package/src/commands/export.test.ts +310 -0
  228. package/src/commands/export.ts +316 -0
  229. package/src/commands/gfx-io.test.ts +178 -0
  230. package/src/commands/gfx.test.ts +470 -0
  231. package/src/commands/gfx.ts +645 -0
  232. package/src/commands/init.test.ts +482 -0
  233. package/src/commands/init.ts +193 -0
  234. package/src/commands/lint.test.ts +137 -0
  235. package/src/commands/lint.ts +117 -0
  236. package/src/commands/minify.test.ts +166 -0
  237. package/src/commands/minify.ts +156 -0
  238. package/src/commands/music.test.ts +272 -0
  239. package/src/commands/music.ts +288 -0
  240. package/src/commands/playtest.test.ts +317 -0
  241. package/src/commands/playtest.ts +773 -0
  242. package/src/commands/run.test.ts +264 -0
  243. package/src/commands/run.ts +285 -0
  244. package/src/commands/serve.test.ts +227 -0
  245. package/src/commands/serve.ts +272 -0
  246. package/src/commands/sfx.test.ts +349 -0
  247. package/src/commands/sfx.ts +300 -0
  248. package/src/commands/tokens.test.ts +212 -0
  249. package/src/commands/tokens.ts +168 -0
  250. package/src/commands/verify.test.ts +238 -0
  251. package/src/commands/verify.ts +256 -0
  252. package/src/commands/version.test.ts +70 -0
  253. package/src/commands/version.ts +24 -0
  254. package/src/engine/audio/index.ts +47 -0
  255. package/src/engine/audio/music.test.ts +188 -0
  256. package/src/engine/audio/music.ts +218 -0
  257. package/src/engine/audio/section.test.ts +124 -0
  258. package/src/engine/audio/section.ts +79 -0
  259. package/src/engine/audio/sfx.test.ts +410 -0
  260. package/src/engine/audio/sfx.ts +694 -0
  261. package/src/engine/cart/cart.test.ts +304 -0
  262. package/src/engine/cart/cart.ts +211 -0
  263. package/src/engine/cart/errors.ts +39 -0
  264. package/src/engine/cart/fixtures/crlf.p8 +6 -0
  265. package/src/engine/cart/fixtures/full.p8 +174 -0
  266. package/src/engine/cart/fixtures/header-only.p8 +2 -0
  267. package/src/engine/cart/fixtures/malformed-no-header.p8 +2 -0
  268. package/src/engine/cart/fixtures/minimal.p8 +10 -0
  269. package/src/engine/cart/fixtures/no-trailing-newline.p8 +6 -0
  270. package/src/engine/cart/gfx.ts +195 -0
  271. package/src/engine/cart/index.ts +27 -0
  272. package/src/engine/cart/map.ts +134 -0
  273. package/src/engine/config/config.ts +115 -0
  274. package/src/engine/config/index.ts +12 -0
  275. package/src/engine/gfx/grid.test.ts +128 -0
  276. package/src/engine/gfx/grid.ts +136 -0
  277. package/src/engine/gfx/index.ts +34 -0
  278. package/src/engine/gfx/label.test.ts +105 -0
  279. package/src/engine/gfx/label.ts +229 -0
  280. package/src/engine/gfx/overlap.test.ts +71 -0
  281. package/src/engine/gfx/overlap.ts +66 -0
  282. package/src/engine/gfx/palette.test.ts +35 -0
  283. package/src/engine/gfx/palette.ts +54 -0
  284. package/src/engine/gfx/png.test.ts +111 -0
  285. package/src/engine/gfx/png.ts +196 -0
  286. package/src/engine/gfx/render.test.ts +180 -0
  287. package/src/engine/gfx/render.ts +106 -0
  288. package/src/engine/index.ts +21 -0
  289. package/src/engine/pico8/adapter.ts +303 -0
  290. package/src/engine/pico8/drive.test.ts +415 -0
  291. package/src/engine/pico8/drive.ts +729 -0
  292. package/src/engine/pico8/harness.test.ts +181 -0
  293. package/src/engine/pico8/harness.ts +354 -0
  294. package/src/engine/pico8/index.ts +20 -0
  295. package/src/engine/pico8/sentinel.test.ts +80 -0
  296. package/src/engine/pico8/sentinel.ts +93 -0
  297. package/src/engine/pico8/session-daemon-entry.ts +12 -0
  298. package/src/engine/pico8/session-daemon-main.ts +98 -0
  299. package/src/engine/pico8/session-daemon.test.ts +212 -0
  300. package/src/engine/pico8/session-daemon.ts +306 -0
  301. package/src/engine/pico8/session.test.ts +239 -0
  302. package/src/engine/pico8/session.ts +321 -0
  303. package/src/engine/pico8/shell.test.ts +583 -0
  304. package/src/engine/pico8/shell.ts +412 -0
  305. package/src/engine/pico8/supervisor.test.ts +105 -0
  306. package/src/engine/pico8/supervisor.ts +197 -0
  307. package/src/engine/scaffold/index.ts +15 -0
  308. package/src/engine/scaffold/scaffold.ts +278 -0
  309. package/src/engine/shrinko/adapter.ts +241 -0
  310. package/src/engine/shrinko/fixtures/count-no-compressed.txt +2 -0
  311. package/src/engine/shrinko/fixtures/count-over-budget.txt +3 -0
  312. package/src/engine/shrinko/fixtures/count-under-budget.txt +3 -0
  313. package/src/engine/shrinko/index.ts +33 -0
  314. package/src/engine/shrinko/shell.ts +224 -0
  315. package/src/engine/shrinko/shrinko.test.ts +182 -0
  316. package/src/engine/skills/index.ts +14 -0
  317. package/src/engine/skills/skills.test.ts +376 -0
  318. package/src/engine/skills/skills.ts +205 -0
  319. package/src/index.ts +13 -0
  320. package/src/skills/game-design-reference/SKILL.md +70 -0
  321. package/src/skills/game-jam/SKILL.md +45 -0
  322. package/src/skills/picopilot-art/SKILL.md +62 -0
  323. package/src/skills/picopilot-audio/SKILL.md +194 -0
  324. package/src/skills/picopilot-code/SKILL.md +97 -0
  325. package/src/skills/picopilot-code/reference/README.md +42 -0
  326. package/src/skills/picopilot-code/reference/mode7-racing.md +95 -0
  327. package/src/skills/picopilot-code/reference/platformer.md +119 -0
  328. package/src/skills/picopilot-code/reference/puzzle-grid.md +106 -0
  329. package/src/skills/picopilot-code/reference/rpg-menus-dialog.md +122 -0
  330. package/src/skills/picopilot-code/reference/top-down-adventure.md +117 -0
  331. package/src/skills/picopilot-code/reference/twin-stick-arcade.md +128 -0
  332. package/src/skills/picopilot-debug/SKILL.md +195 -0
  333. package/src/skills/picopilot-overview/SKILL.md +52 -0
  334. package/src/version.ts +22 -0
@@ -0,0 +1,119 @@
1
+ # Platformer reference (side-view, gravity + tile collision)
2
+
3
+ Structure for a side-scrolling platformer: the player has velocity, gravity
4
+ pulls it down, and collision is resolved AXIS BY AXIS against solid map tiles.
5
+ This is the shape that gets platformers wrong most often, so follow the axis
6
+ order and the corner checks exactly.
7
+
8
+ ## The load-bearing rules (get these wrong and it feels broken)
9
+
10
+ 1. Gravity and input change VELOCITY (`dx`,`dy`), never position directly. A
11
+ fall must ACCELERATE, so `dy += gravity` each frame, not `y += gravity`.
12
+ 2. Move and resolve X FIRST, then Y SEPARATELY. Moving both then testing once
13
+ sticks the player in corners and forbids wall-sliding.
14
+ 3. An 8px sprite occupies pixels `x .. x+7` (NOT `x+8`). Check tile collision at
15
+ all four corners; `x+8` reads the NEXT tile and gives phantom collisions.
16
+ 4. Reset `grounded` to false every frame BEFORE the Y resolve, then set it true
17
+ only when a downward move hits a floor. Otherwise jump never re-arms (or
18
+ arms forever = infinite mid-air jumps).
19
+ 5. `_update` is 30fps. Tune `gravity`/jump for 30 steps/sec, or switch the whole
20
+ cart to `_update60()` and tune for 60. Do not mix.
21
+
22
+ ## Solid-tile test (map flag 0 = solid, by convention)
23
+
24
+ ```lua
25
+ -- pixel (x,y) -> is its map tile solid? flag 0 set = solid.
26
+ function solid(x,y) return fget(mget(x\8,y\8),0) end
27
+
28
+ -- 8x8 actor a: any of its four corners on a solid tile?
29
+ function hits(a)
30
+ return solid(a.x,a.y) or solid(a.x+7,a.y)
31
+ or solid(a.x,a.y+7) or solid(a.x+7,a.y+7)
32
+ end
33
+ ```
34
+
35
+ ## Player update (input -> velocity -> per-axis move+resolve)
36
+
37
+ ```lua
38
+ gravity=0.3
39
+ accel=0.5
40
+ friction=0.85
41
+ jump=-4
42
+
43
+ function make_player(x,y)
44
+ return {x=x,y=y,dx=0,dy=0,grounded=false}
45
+ end
46
+
47
+ function update_player(p)
48
+ -- input into horizontal velocity
49
+ if btn(0) then p.dx-=accel end
50
+ if btn(1) then p.dx+=accel end
51
+ -- jump only when standing
52
+ if btnp(4) and p.grounded then p.dy=jump end
53
+
54
+ -- physics on velocity
55
+ p.dy+=gravity
56
+ p.dx*=friction
57
+
58
+ -- resolve X
59
+ p.x+=p.dx
60
+ if hits(p) then
61
+ -- back out and stop horizontal motion (simple, non-swept)
62
+ p.x-=p.dx p.dx=0
63
+ end
64
+
65
+ -- resolve Y (reset grounded first)
66
+ p.y+=p.dy
67
+ p.grounded=false
68
+ if hits(p) then
69
+ p.y-=p.dy
70
+ if p.dy>0 then p.grounded=true end -- landed on a floor
71
+ p.dy=0
72
+ end
73
+ end
74
+ ```
75
+
76
+ ## Camera follow with level-edge clamp
77
+
78
+ ```lua
79
+ -- level_w = level width in PIXELS. mid() clamps so you never scroll past the map.
80
+ function update_camera(p)
81
+ local cx=mid(0, p.x-64+4, level_w-128) -- +4 centers the 8px sprite
82
+ camera(cx,0)
83
+ end
84
+
85
+ function _draw()
86
+ cls()
87
+ update_camera(player)
88
+ map() -- draw the tilemap in world space
89
+ draw_player(player)
90
+ camera() -- RESET before HUD or the score scrolls off with the world
91
+ print("score "..score,2,2,7)
92
+ end
93
+ ```
94
+
95
+ ## Animation (walk cycle from a timer, flip when facing left)
96
+
97
+ ```lua
98
+ function draw_player(p)
99
+ -- 4-frame walk (sprites 1..4), advancing ~every 8 update frames; flip when moving left
100
+ local s=1+flr(t()*4%4)
101
+ spr(s, flr(p.x), flr(p.y), 1,1, p.dx<0) -- 6th arg = flip_x (boolean)
102
+ end
103
+ ```
104
+
105
+ ## Genre pitfalls checklist
106
+
107
+ - [ ] Gravity accumulates into `dy`, not into `y`.
108
+ - [ ] X resolved, THEN Y resolved (two separate move+test blocks).
109
+ - [ ] Corner checks use `x+7`/`y+7`, not `x+8`/`y+8`.
110
+ - [ ] `grounded` reset to false each frame before the Y resolve.
111
+ - [ ] Constants tuned for the actual framerate (`_update` = 30, `_update60` = 60).
112
+ - [ ] `camera()` reset before drawing the HUD.
113
+
114
+ ## Level fast (high speed) tunneling
115
+
116
+ Backing out the whole move (`p.x-=p.dx`) is fine at low speeds. If an actor can
117
+ move more than ~4-6 px/frame it can tunnel THROUGH a 1-tile wall. When you need
118
+ that, step the move in 1px increments (or sub-steps) and test each step, instead
119
+ of one big move + one test.
@@ -0,0 +1,106 @@
1
+ # Puzzle / grid reference (discrete cells, tile-step movement, rules)
2
+
3
+ Structure for a grid puzzle (block-pushing, rule-based, or match games). The
4
+ world is a 2D array of CELLS, the player moves ONE cell per input (not by
5
+ velocity), and a
6
+ turn is: read intent, test the move against rules, commit if legal. There is no
7
+ gravity and no sub-pixel motion here; do NOT reach for the platformer shape.
8
+
9
+ ## The load-bearing rules
10
+
11
+ 1. State is a GRID, not a pixel position. Store `col,row` (integers), render by
12
+ multiplying by 8 (`x=col*8`). Movement changes `col,row` by exactly +/-1.
13
+ 2. Input is DISCRETE: use `btnp` (fires once per press), never `btn` (fires every
14
+ frame = the player rockets across the board).
15
+ 3. A move is a TRANSACTION: compute the target cell, check every rule (in bounds?
16
+ wall? pushable block that itself can move?), and only then mutate the grid.
17
+ Never move first and undo, it corrupts multi-object pushes.
18
+ 4. Grids are 1-based in Lua. Guard bounds explicitly (`c>=1 and c<=cols`).
19
+
20
+ ## Grid setup (rows as split strings = cheap + readable levels)
21
+
22
+ ```lua
23
+ cols,rows=16,16
24
+ -- a level as one string per row: .=floor #=wall @=player $=box
25
+ level=split("################, #@.....#......#, #..$..........#, ...(more rows)...", ",")
26
+
27
+ grid={} -- grid[r][c] = tile char
28
+ player={c=2,r=2}
29
+
30
+ function load_level()
31
+ grid={}
32
+ for r=1,rows do
33
+ grid[r]={}
34
+ local row=level[r]
35
+ for c=1,cols do
36
+ local ch=sub(row,c,c)
37
+ grid[r][c]=ch
38
+ if ch=="@" then player.c,player.r=c,r grid[r][c]="." end
39
+ end
40
+ end
41
+ end
42
+ ```
43
+
44
+ ## Discrete move as a transaction (with box-push)
45
+
46
+ ```lua
47
+ -- dir -> delta. split pairs are the idiomatic lookup.
48
+ dc=split"0,0,-1,1" -- up,down,left,right dx (matches btnp 2,3,0,1 order below)
49
+ dr=split"-1,1,0,0"
50
+
51
+ function cell(c,r) -- safe read; out of bounds reads as wall
52
+ if c<1 or c>cols or r<1 or r>rows then return "#" end
53
+ return grid[r][c]
54
+ end
55
+
56
+ function try_move(dx,dy)
57
+ local nc,nr=player.c+dx,player.r+dy
58
+ local t=cell(nc,nr)
59
+ if t=="#" then return end -- wall: no move
60
+ if t=="$" then -- box: can it be pushed one further?
61
+ local bc,br=nc+dx,nr+dy
62
+ if cell(bc,br)~="." then return end -- blocked behind: whole move illegal
63
+ grid[br][bc]="$" grid[nr][nc]="." -- commit the push
64
+ end
65
+ player.c,player.r=nc,nr -- commit the player move
66
+ end
67
+
68
+ function _update()
69
+ -- btnp order: 0 left,1 right,2 up,3 down
70
+ if btnp(0) then try_move(-1,0) end
71
+ if btnp(1) then try_move(1,0) end
72
+ if btnp(2) then try_move(0,-1) end
73
+ if btnp(3) then try_move(0,1) end
74
+ end
75
+ ```
76
+
77
+ ## Render (grid -> screen, cell*8)
78
+
79
+ ```lua
80
+ function _draw()
81
+ cls(1)
82
+ for r=1,rows do for c=1,cols do
83
+ local t=grid[r][c]
84
+ if t=="#" then rectfill((c-1)*8,(r-1)*8,(c-1)*8+7,(r-1)*8+7,5)
85
+ elseif t=="$" then spr(2,(c-1)*8,(r-1)*8) end
86
+ end end
87
+ spr(1,(player.c-1)*8,(player.r-1)*8) -- player
88
+ end
89
+ ```
90
+
91
+ ## Win / rule evaluation
92
+
93
+ Evaluate win AFTER a committed move, by scanning the grid (e.g. every box on a
94
+ target). For a game where the RULES are themselves objects on the grid, the rule
95
+ set is DATA you re-parse each turn (scan the grid for rule triples, rebuild a
96
+ properties table), then movement/legality queries that table. Keep the rule pass
97
+ separate from the move pass; do not interleave them.
98
+
99
+ ## Genre pitfalls checklist
100
+
101
+ - [ ] Position is integer `col,row`, rendered as `*8`, not a pixel velocity.
102
+ - [ ] Input uses `btnp`, not `btn`.
103
+ - [ ] Moves are validated fully BEFORE any grid mutation (transaction).
104
+ - [ ] Multi-object pushes check the space behind the pushed object.
105
+ - [ ] Bounds guarded (1-based); out-of-bounds reads treated as wall.
106
+ - [ ] Win/rule evaluation is a separate pass after the move commits.
@@ -0,0 +1,122 @@
1
+ # RPG menus + dialog reference (UI state machines, text reveal)
2
+
3
+ Structure for the menu/dialog side of an RPG or any UI-driven game (title menus,
4
+ inventory, shop, dialog boxes, turn-based battle menus). The defining problem is
5
+ NOT movement, it is a STACK of UI STATES, a cursor over a list of options, and
6
+ text that reveals over time. Get the state stack and the cursor-wrap right; the
7
+ "game feel" here is menu responsiveness and readable text pacing.
8
+
9
+ ## The load-bearing rules
10
+
11
+ 1. UI is a STACK of modes (`{"map"}` -> push `"menu"` -> push `"item"`). Input and
12
+ draw dispatch on the TOP of the stack. Opening a submenu PUSHES; canceling
13
+ (btnp 5 / X) POPS. This is cleaner than a flat `mode` string once menus nest.
14
+ 2. Menu input is DISCRETE: `btnp` only. A cursor is an index into an options list;
15
+ moving wraps with `%`. Confirm = btnp(4), cancel = btnp(5).
16
+ 3. Dialog text REVEALS over time (one char every few frames), not all at once.
17
+ Track a reveal counter; a confirm press either fast-forwards to full text or,
18
+ if already full, advances to the next line.
19
+ 4. Draw menus in SCREEN space. If the field behind is camera-offset, `camera()`
20
+ (reset) before drawing the box, or it slides with the world.
21
+
22
+ ## Mode stack + dispatch
23
+
24
+ ```lua
25
+ stack={"field"}
26
+ function top() return stack[#stack] end
27
+ function push(m) add(stack,m) end
28
+ function pop() if #stack>1 then deli(stack) end end -- never pop the base mode
29
+
30
+ function _update()
31
+ local m=top()
32
+ if m=="field" then
33
+ update_field()
34
+ if btnp(4) then push("menu") menu_cur=1 end
35
+ elseif m=="menu" then
36
+ update_menu()
37
+ elseif m=="dialog" then
38
+ update_dialog()
39
+ end
40
+ end
41
+ ```
42
+
43
+ ## Cursor over a list, with wrap
44
+
45
+ ```lua
46
+ menu_items=split"items,magic,status,save" -- data as a split string
47
+ menu_cur=1
48
+
49
+ function update_menu()
50
+ -- up/down move the cursor, wrapping (1-based, so the %+1 dance)
51
+ if btnp(2) then menu_cur=(menu_cur-2)%#menu_items+1 end
52
+ if btnp(3) then menu_cur=menu_cur%#menu_items+1 end
53
+ if btnp(4) then choose(menu_items[menu_cur]) end -- confirm
54
+ if btnp(5) then pop() end -- cancel closes the menu
55
+ end
56
+
57
+ function draw_menu()
58
+ camera() -- SCREEN space
59
+ rectfill(80,8,120,40,0) rect(80,8,120,40,7)
60
+ for i,it in ipairs(menu_items) do
61
+ local y=12+(i-1)*7
62
+ print(it,88,y, i==menu_cur and 7 or 5)
63
+ if i==menu_cur then print(">",83,y,7) end
64
+ end
65
+ end
66
+ ```
67
+
68
+ ## Dialog with timed text reveal
69
+
70
+ ```lua
71
+ dlg={text="",shown=0}
72
+ function say(s) dlg.text=s dlg.shown=0 push("dialog") end
73
+
74
+ function update_dialog()
75
+ if dlg.shown < #dlg.text then
76
+ dlg.shown+=0.5 -- ~2 chars/frame; tune for pacing
77
+ if btnp(4) then dlg.shown=#dlg.text end -- confirm fast-forwards to full
78
+ else
79
+ if btnp(4) then pop() end -- fully shown: confirm closes/advances
80
+ end
81
+ end
82
+
83
+ function draw_dialog()
84
+ camera()
85
+ rectfill(4,100,123,123,1) rect(4,100,123,123,7)
86
+ print(sub(dlg.text,1,flr(dlg.shown)), 8,104, 7) -- sub reveals up to `shown`
87
+ end
88
+ ```
89
+
90
+ (For multi-line/word-wrapped dialog, pre-split the text into lines that fit the box
91
+ width and reveal line by line. `print` glyphs are ~4px wide, so ~28 chars per line
92
+ in a full-width box.)
93
+
94
+ ## Turn-based battle menu (same primitives)
95
+
96
+ A battle is the SAME machinery: a mode on the stack (`"battle"`), a cursor over
97
+ actions (`Fight/Item/Run`), pushing a target-select submenu, then running the
98
+ action and popping back. Keep the battle STATE (hp, turn order) separate from the
99
+ MENU state (which option is highlighted); do not entangle them.
100
+
101
+ ## Coroutine for scripted sequences (optional)
102
+
103
+ For a scripted intro / cutscene (text, then move a sprite, then more text), a
104
+ coroutine reads far cleaner than a hand-rolled step counter:
105
+
106
+ ```lua
107
+ function intro()
108
+ say("long ago...") yield_until_closed()
109
+ -- move the king sprite, wait, etc.
110
+ say("...a hero was needed.") yield_until_closed()
111
+ end
112
+ -- drive it: seq=cocreate(intro); each frame if costatus(seq)~="dead" then coresume(seq) end
113
+ ```
114
+
115
+ ## Genre pitfalls checklist
116
+
117
+ - [ ] UI is a STACK (push submenu / pop on cancel), dispatched on the top mode.
118
+ - [ ] Menu input is `btnp` only; cursor index WRAPS with `%` (mind 1-based).
119
+ - [ ] Dialog text reveals over time; confirm fast-forwards, then advances.
120
+ - [ ] Menus/boxes drawn in SCREEN space (`camera()` reset first).
121
+ - [ ] Battle/menu STATE kept separate from which option is highlighted.
122
+ - [ ] Word-wrap pre-computed to the box width (~28 chars full-width).
@@ -0,0 +1,117 @@
1
+ # Top-down adventure reference (tiled world, 4-dir movement, interaction)
2
+
3
+ Structure for a top-down explorer (action-adventure, RPG overworld). The player walks a
4
+ tiled world in 4 (or 8) directions, collides with solid tiles, the camera follows
5
+ across a map larger than the screen, and the player interacts with specific tiles
6
+ (doors, chests, NPCs). Unlike the platformer there is no gravity; unlike the
7
+ puzzle grid, movement is smooth pixel motion, not cell-stepping.
8
+
9
+ ## The load-bearing rules
10
+
11
+ 1. The player has a pixel position and a FACING direction. Movement is smooth
12
+ (pixels/frame), but collision is still tested against map tiles via
13
+ `mget`/`fget`.
14
+ 2. Resolve X and Y separately (same axis-by-axis idea as the platformer) so the
15
+ player slides along walls instead of sticking on corners.
16
+ 3. Facing is state you keep for animation AND for "what am I interacting with"
17
+ (the tile in front of the player).
18
+ 4. The map is bigger than 128x128: `camera` follows the player and CLAMPs to the
19
+ map's pixel bounds. `map()` draws the whole tilemap in world space.
20
+
21
+ ## Movement + per-axis wall collision
22
+
23
+ ```lua
24
+ speed=1.5
25
+ player={x=64,y=64,face=3} -- face: 0 left,1 right,2 up,3 down (btn order)
26
+
27
+ function solid(x,y) return fget(mget(x\8,y\8),0) end -- flag 0 = solid
28
+ function blocked(x,y) -- 8x8 body, four corners
29
+ return solid(x,y) or solid(x+7,y) or solid(x,y+7) or solid(x+7,y+7)
30
+ end
31
+
32
+ function update_player(p)
33
+ local dx,dy=0,0
34
+ if btn(0) then dx=-speed p.face=0 end
35
+ if btn(1) then dx=speed p.face=1 end
36
+ if btn(2) then dy=-speed p.face=2 end
37
+ if btn(3) then dy=speed p.face=3 end
38
+
39
+ -- resolve X then Y separately (slide along walls)
40
+ if not blocked(p.x+dx,p.y) then p.x+=dx end
41
+ if not blocked(p.x,p.y+dy) then p.y+=dy end
42
+ end
43
+ ```
44
+
45
+ ## The tile in front (interaction target)
46
+
47
+ ```lua
48
+ fdx=split"-1,1,0,0" -- indexed by face+1: left,right,up,down
49
+ fdy=split"0,0,-1,1"
50
+
51
+ function front_tile(p)
52
+ -- the map cell one step ahead in the facing direction
53
+ local fx=p.x+4 + fdx[p.face+1]*8 -- +4 = center of the 8px body
54
+ local fy=p.y+4 + fdy[p.face+1]*8
55
+ return fx\8, fy\8
56
+ end
57
+
58
+ function _update()
59
+ update_player(player)
60
+ if btnp(4) then -- action button: interact with the tile ahead
61
+ local tx,ty=front_tile(player)
62
+ local s=mget(tx,ty)
63
+ if s==CHEST then mset(tx,ty,CHEST_OPEN) got_item() end
64
+ -- doors, signs, NPCs: branch on the sprite id at (tx,ty)
65
+ end
66
+ end
67
+ ```
68
+
69
+ ## Camera follow with map-edge clamp
70
+
71
+ ```lua
72
+ -- map_w,map_h = map size in PIXELS (tiles*8).
73
+ function update_camera(p)
74
+ local cx=mid(0, p.x-64, map_w-128)
75
+ local cy=mid(0, p.y-64, map_h-128)
76
+ camera(cx,cy)
77
+ end
78
+
79
+ function _draw()
80
+ cls()
81
+ update_camera(player)
82
+ map() -- whole tilemap, world space
83
+ spr(walk_frame(player), flr(player.x), flr(player.y), 1,1, player.face==0)
84
+ camera() -- RESET before the HUD
85
+ draw_hud()
86
+ end
87
+ ```
88
+
89
+ ## Rooms / streaming (optional, for large worlds)
90
+
91
+ Two common approaches:
92
+ - One big shared map, camera just moves over it (simplest; limited by the 128x32
93
+ map size, extendable with the shared bank).
94
+ - Room-based: each screen is a fixed 16x16 tile region; when the player crosses
95
+ an edge, snap the camera to the next room and reposition the player. Store room
96
+ coords, draw `map(room_c*16, room_r*16, 0,0, 16,16)`.
97
+
98
+ ## Animation (frame per facing + walk cycle)
99
+
100
+ ```lua
101
+ -- 2-frame walk per direction; base sprite per facing in a split lookup.
102
+ face_base=split"16,16,32,48" -- left/right share sprites, flipped
103
+ function walk_frame(p)
104
+ local moving = btn(0) or btn(1) or btn(2) or btn(3)
105
+ local step = moving and flr(t()*4%2) or 0
106
+ return face_base[p.face+1]+step
107
+ end
108
+ ```
109
+
110
+ ## Genre pitfalls checklist
111
+
112
+ - [ ] Movement is smooth pixel motion, but collision still tests map tiles.
113
+ - [ ] X and Y resolved separately (wall-slide, no corner-stick).
114
+ - [ ] Facing is kept as state and drives BOTH animation and interaction target.
115
+ - [ ] Interaction reads the tile ONE step ahead in the facing direction.
116
+ - [ ] Camera follows and clamps to the map's PIXEL bounds; reset before HUD.
117
+ - [ ] `btnp` for the discrete action button; `btn` for continuous walking.
@@ -0,0 +1,128 @@
1
+ # Twin-stick / arcade reference (many entities, spawn/despawn, juice)
2
+
3
+ Structure for an action arcade cart (shooter, bullet-hell, wave survival). The
4
+ defining problem is MANY short-lived entities at once (bullets, enemies,
5
+ particles) that spawn and despawn every frame. Get the entity-list lifecycle and
6
+ the despawn-while-iterating right, then layer on the "juice" that makes arcade
7
+ carts feel good. Shipped arcade carts are `rnd`-heavy for exactly that juice.
8
+
9
+ ## The load-bearing rules
10
+
11
+ 1. One flat list per KIND (`bullets`, `enemies`, `particles`), each entity a
12
+ table. `add` to spawn, `del`/`deli` to despawn.
13
+ 2. Despawn safely. Deleting the CURRENT item inside `foreach` is safe. Deleting
14
+ inside an ASCENDING `for i=1,#list` is NOT (it errors once the list shrinks).
15
+ Use `foreach`, or iterate DESCENDING when you delete by index.
16
+ 3. Off-screen or dead entities must be removed, or the list grows unbounded and
17
+ the cart slows/crashes. Cull every frame.
18
+ 4. Movement is velocity-based and free (no grid, no gravity unless you want it).
19
+ `sin`/`cos` take TURNS (0..1) for aiming/spread.
20
+
21
+ ## Entity lists + safe cull
22
+
23
+ ```lua
24
+ bullets={}
25
+ enemies={}
26
+ parts={}
27
+
28
+ function fire(x,y,ang,spd)
29
+ -- ang in TURNS (0..1). sin is screen-inverted, so this aims correctly on screen.
30
+ add(bullets,{x=x,y=y,dx=cos(ang)*spd,dy=sin(ang)*spd,life=90})
31
+ end
32
+
33
+ function update_bullets()
34
+ foreach(bullets,function(b)
35
+ b.x+=b.dx b.y+=b.dy b.life-=1
36
+ -- cull off-screen OR expired; del of the CURRENT item under foreach is safe
37
+ if b.life<=0 or b.x<-4 or b.x>131 or b.y<-4 or b.y>131 then
38
+ del(bullets,b)
39
+ end
40
+ end)
41
+ end
42
+ ```
43
+
44
+ ## Spawning waves + aiming at the player
45
+
46
+ ```lua
47
+ function spawn_enemy()
48
+ -- rnd picks a random screen edge; flr for an integer choice
49
+ add(enemies,{x=flr(rnd(128)),y=-8,hp=3})
50
+ end
51
+
52
+ function enemy_shoot(e)
53
+ -- atan2 gives the turn-angle from e to the player; feed it straight to fire()
54
+ fire(e.x,e.y, atan2(player.x-e.x, player.y-e.y), 1.5)
55
+ end
56
+ ```
57
+
58
+ ## Particles / juice (why arcade carts call rnd a lot)
59
+
60
+ ```lua
61
+ function burst(x,y,col,n)
62
+ for i=1,n do
63
+ local a=rnd() -- random turn 0..1
64
+ local s=0.5+rnd(1.5) -- random speed
65
+ add(parts,{x=x,y=y,dx=cos(a)*s,dy=sin(a)*s,life=10+rnd(10),col=col})
66
+ end
67
+ end
68
+
69
+ function update_parts()
70
+ foreach(parts,function(p)
71
+ p.x+=p.dx p.y+=p.dy p.dx*=0.9 p.dy*=0.9 p.life-=1
72
+ if p.life<=0 then del(parts,p) end
73
+ end)
74
+ end
75
+
76
+ function draw_parts()
77
+ foreach(parts,function(p) pset(p.x,p.y,p.col) end)
78
+ end
79
+ ```
80
+
81
+ Screen shake is a cheap, high-impact juice: offset the camera by a decaying
82
+ random amount, then RESET it.
83
+
84
+ ```lua
85
+ shake=0
86
+ function add_shake(n) shake=max(shake,n) end
87
+ function apply_shake()
88
+ if shake>0 then
89
+ camera(rnd(shake)-shake/2, rnd(shake)-shake/2) -- random offset
90
+ shake*=0.8
91
+ if shake<0.5 then shake=0 end
92
+ else
93
+ camera() -- no shake: ensure camera is reset (draw state persists!)
94
+ end
95
+ end
96
+ ```
97
+
98
+ ## Collision (cheap circle test for round-ish sprites)
99
+
100
+ ```lua
101
+ -- squared-distance test avoids sqrt. r = sum of radii.
102
+ function hit(a,b,r)
103
+ local dx,dy=a.x-b.x,a.y-b.y
104
+ return dx*dx+dy*dy < r*r
105
+ end
106
+ ```
107
+
108
+ ## Draw + reset discipline
109
+
110
+ ```lua
111
+ function _draw()
112
+ cls()
113
+ apply_shake()
114
+ map() ; foreach(enemies,draw_enemy) ; foreach(bullets,draw_bullet) ; draw_parts()
115
+ camera() -- RESET before HUD (shake/camera persist otherwise)
116
+ print("score "..score,2,2,7)
117
+ end
118
+ ```
119
+
120
+ ## Genre pitfalls checklist
121
+
122
+ - [ ] Each entity KIND is its own list; entities are tables.
123
+ - [ ] Despawn uses `foreach`+`del` (safe) or a descending index loop, never an
124
+ ascending `for i=1,#list` with in-loop `del`.
125
+ - [ ] Off-screen/dead entities are culled EVERY frame (no unbounded growth).
126
+ - [ ] Angles are TURNS (0..1); `atan2`/`sin`/`cos` used consistently.
127
+ - [ ] `rnd(x)` wrapped with `flr` when an integer is needed.
128
+ - [ ] `camera()` reset after any shake, before the HUD.