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.
- package/LICENSE +661 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/audio.d.ts +17 -0
- package/dist/commands/audio.d.ts.map +1 -0
- package/dist/commands/audio.js +315 -0
- package/dist/commands/audio.js.map +1 -0
- package/dist/commands/export.d.ts +36 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +257 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/gfx.d.ts +28 -0
- package/dist/commands/gfx.d.ts.map +1 -0
- package/dist/commands/gfx.js +521 -0
- package/dist/commands/gfx.js.map +1 -0
- package/dist/commands/init.d.ts +34 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +151 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +20 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +90 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/minify.d.ts +20 -0
- package/dist/commands/minify.d.ts.map +1 -0
- package/dist/commands/minify.js +129 -0
- package/dist/commands/minify.js.map +1 -0
- package/dist/commands/music.d.ts +13 -0
- package/dist/commands/music.d.ts.map +1 -0
- package/dist/commands/music.js +241 -0
- package/dist/commands/music.js.map +1 -0
- package/dist/commands/playtest.d.ts +29 -0
- package/dist/commands/playtest.d.ts.map +1 -0
- package/dist/commands/playtest.js +631 -0
- package/dist/commands/playtest.js.map +1 -0
- package/dist/commands/run.d.ts +30 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +224 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/serve.d.ts +50 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +213 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/sfx.d.ts +13 -0
- package/dist/commands/sfx.d.ts.map +1 -0
- package/dist/commands/sfx.js +254 -0
- package/dist/commands/sfx.js.map +1 -0
- package/dist/commands/tokens.d.ts +36 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +129 -0
- package/dist/commands/tokens.js.map +1 -0
- package/dist/commands/verify.d.ts +57 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +213 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/version.d.ts +12 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +24 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/engine/audio/index.d.ts +14 -0
- package/dist/engine/audio/index.d.ts.map +1 -0
- package/dist/engine/audio/index.js +14 -0
- package/dist/engine/audio/index.js.map +1 -0
- package/dist/engine/audio/music.d.ts +116 -0
- package/dist/engine/audio/music.d.ts.map +1 -0
- package/dist/engine/audio/music.js +138 -0
- package/dist/engine/audio/music.js.map +1 -0
- package/dist/engine/audio/section.d.ts +34 -0
- package/dist/engine/audio/section.d.ts.map +1 -0
- package/dist/engine/audio/section.js +67 -0
- package/dist/engine/audio/section.js.map +1 -0
- package/dist/engine/audio/sfx.d.ts +141 -0
- package/dist/engine/audio/sfx.d.ts.map +1 -0
- package/dist/engine/audio/sfx.js +506 -0
- package/dist/engine/audio/sfx.js.map +1 -0
- package/dist/engine/cart/cart.d.ts +75 -0
- package/dist/engine/cart/cart.d.ts.map +1 -0
- package/dist/engine/cart/cart.js +153 -0
- package/dist/engine/cart/cart.js.map +1 -0
- package/dist/engine/cart/errors.d.ts +29 -0
- package/dist/engine/cart/errors.d.ts.map +1 -0
- package/dist/engine/cart/errors.js +26 -0
- package/dist/engine/cart/errors.js.map +1 -0
- package/dist/engine/cart/gfx.d.ts +79 -0
- package/dist/engine/cart/gfx.d.ts.map +1 -0
- package/dist/engine/cart/gfx.js +171 -0
- package/dist/engine/cart/gfx.js.map +1 -0
- package/dist/engine/cart/index.d.ts +15 -0
- package/dist/engine/cart/index.d.ts.map +1 -0
- package/dist/engine/cart/index.js +15 -0
- package/dist/engine/cart/index.js.map +1 -0
- package/dist/engine/cart/map.d.ts +46 -0
- package/dist/engine/cart/map.d.ts.map +1 -0
- package/dist/engine/cart/map.js +118 -0
- package/dist/engine/cart/map.js.map +1 -0
- package/dist/engine/config/config.d.ts +74 -0
- package/dist/engine/config/config.d.ts.map +1 -0
- package/dist/engine/config/config.js +103 -0
- package/dist/engine/config/config.js.map +1 -0
- package/dist/engine/config/index.d.ts +7 -0
- package/dist/engine/config/index.d.ts.map +1 -0
- package/dist/engine/config/index.js +7 -0
- package/dist/engine/config/index.js.map +1 -0
- package/dist/engine/gfx/grid.d.ts +51 -0
- package/dist/engine/gfx/grid.d.ts.map +1 -0
- package/dist/engine/gfx/grid.js +116 -0
- package/dist/engine/gfx/grid.js.map +1 -0
- package/dist/engine/gfx/index.d.ts +17 -0
- package/dist/engine/gfx/index.d.ts.map +1 -0
- package/dist/engine/gfx/index.js +17 -0
- package/dist/engine/gfx/index.js.map +1 -0
- package/dist/engine/gfx/label.d.ts +34 -0
- package/dist/engine/gfx/label.d.ts.map +1 -0
- package/dist/engine/gfx/label.js +190 -0
- package/dist/engine/gfx/label.js.map +1 -0
- package/dist/engine/gfx/overlap.d.ts +42 -0
- package/dist/engine/gfx/overlap.d.ts.map +1 -0
- package/dist/engine/gfx/overlap.js +57 -0
- package/dist/engine/gfx/overlap.js.map +1 -0
- package/dist/engine/gfx/palette.d.ts +29 -0
- package/dist/engine/gfx/palette.d.ts.map +1 -0
- package/dist/engine/gfx/palette.js +46 -0
- package/dist/engine/gfx/palette.js.map +1 -0
- package/dist/engine/gfx/png.d.ts +31 -0
- package/dist/engine/gfx/png.d.ts.map +1 -0
- package/dist/engine/gfx/png.js +165 -0
- package/dist/engine/gfx/png.js.map +1 -0
- package/dist/engine/gfx/render.d.ts +42 -0
- package/dist/engine/gfx/render.d.ts.map +1 -0
- package/dist/engine/gfx/render.js +72 -0
- package/dist/engine/gfx/render.js.map +1 -0
- package/dist/engine/index.d.ts +22 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +22 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/pico8/adapter.d.ts +267 -0
- package/dist/engine/pico8/adapter.d.ts.map +1 -0
- package/dist/engine/pico8/adapter.js +37 -0
- package/dist/engine/pico8/adapter.js.map +1 -0
- package/dist/engine/pico8/drive.d.ts +308 -0
- package/dist/engine/pico8/drive.d.ts.map +1 -0
- package/dist/engine/pico8/drive.js +581 -0
- package/dist/engine/pico8/drive.js.map +1 -0
- package/dist/engine/pico8/harness.d.ts +146 -0
- package/dist/engine/pico8/harness.d.ts.map +1 -0
- package/dist/engine/pico8/harness.js +251 -0
- package/dist/engine/pico8/harness.js.map +1 -0
- package/dist/engine/pico8/index.d.ts +21 -0
- package/dist/engine/pico8/index.d.ts.map +1 -0
- package/dist/engine/pico8/index.js +21 -0
- package/dist/engine/pico8/index.js.map +1 -0
- package/dist/engine/pico8/sentinel.d.ts +63 -0
- package/dist/engine/pico8/sentinel.d.ts.map +1 -0
- package/dist/engine/pico8/sentinel.js +86 -0
- package/dist/engine/pico8/sentinel.js.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts +10 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.js +11 -0
- package/dist/engine/pico8/session-daemon-entry.js.map +1 -0
- package/dist/engine/pico8/session-daemon-main.d.ts +44 -0
- package/dist/engine/pico8/session-daemon-main.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-main.js +71 -0
- package/dist/engine/pico8/session-daemon-main.js.map +1 -0
- package/dist/engine/pico8/session-daemon.d.ts +68 -0
- package/dist/engine/pico8/session-daemon.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon.js +264 -0
- package/dist/engine/pico8/session-daemon.js.map +1 -0
- package/dist/engine/pico8/session.d.ts +140 -0
- package/dist/engine/pico8/session.d.ts.map +1 -0
- package/dist/engine/pico8/session.js +250 -0
- package/dist/engine/pico8/session.js.map +1 -0
- package/dist/engine/pico8/shell.d.ts +103 -0
- package/dist/engine/pico8/shell.d.ts.map +1 -0
- package/dist/engine/pico8/shell.js +324 -0
- package/dist/engine/pico8/shell.js.map +1 -0
- package/dist/engine/pico8/supervisor.d.ts +137 -0
- package/dist/engine/pico8/supervisor.d.ts.map +1 -0
- package/dist/engine/pico8/supervisor.js +140 -0
- package/dist/engine/pico8/supervisor.js.map +1 -0
- package/dist/engine/scaffold/index.d.ts +8 -0
- package/dist/engine/scaffold/index.d.ts.map +1 -0
- package/dist/engine/scaffold/index.js +8 -0
- package/dist/engine/scaffold/index.js.map +1 -0
- package/dist/engine/scaffold/scaffold.d.ts +49 -0
- package/dist/engine/scaffold/scaffold.d.ts.map +1 -0
- package/dist/engine/scaffold/scaffold.js +259 -0
- package/dist/engine/scaffold/scaffold.js.map +1 -0
- package/dist/engine/shrinko/adapter.d.ts +167 -0
- package/dist/engine/shrinko/adapter.d.ts.map +1 -0
- package/dist/engine/shrinko/adapter.js +107 -0
- package/dist/engine/shrinko/adapter.js.map +1 -0
- package/dist/engine/shrinko/index.d.ts +13 -0
- package/dist/engine/shrinko/index.d.ts.map +1 -0
- package/dist/engine/shrinko/index.js +13 -0
- package/dist/engine/shrinko/index.js.map +1 -0
- package/dist/engine/shrinko/shell.d.ts +69 -0
- package/dist/engine/shrinko/shell.d.ts.map +1 -0
- package/dist/engine/shrinko/shell.js +156 -0
- package/dist/engine/shrinko/shell.js.map +1 -0
- package/dist/engine/skills/index.d.ts +8 -0
- package/dist/engine/skills/index.d.ts.map +1 -0
- package/dist/engine/skills/index.js +8 -0
- package/dist/engine/skills/index.js.map +1 -0
- package/dist/engine/skills/skills.d.ts +104 -0
- package/dist/engine/skills/skills.d.ts.map +1 -0
- package/dist/engine/skills/skills.js +149 -0
- package/dist/engine/skills/skills.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +48 -2
- package/src/bin.ts +4 -0
- package/src/cli.ts +61 -0
- package/src/commands/audio.test.ts +280 -0
- package/src/commands/audio.ts +410 -0
- package/src/commands/export.test.ts +310 -0
- package/src/commands/export.ts +316 -0
- package/src/commands/gfx-io.test.ts +178 -0
- package/src/commands/gfx.test.ts +470 -0
- package/src/commands/gfx.ts +645 -0
- package/src/commands/init.test.ts +482 -0
- package/src/commands/init.ts +193 -0
- package/src/commands/lint.test.ts +137 -0
- package/src/commands/lint.ts +117 -0
- package/src/commands/minify.test.ts +166 -0
- package/src/commands/minify.ts +156 -0
- package/src/commands/music.test.ts +272 -0
- package/src/commands/music.ts +288 -0
- package/src/commands/playtest.test.ts +317 -0
- package/src/commands/playtest.ts +773 -0
- package/src/commands/run.test.ts +264 -0
- package/src/commands/run.ts +285 -0
- package/src/commands/serve.test.ts +227 -0
- package/src/commands/serve.ts +272 -0
- package/src/commands/sfx.test.ts +349 -0
- package/src/commands/sfx.ts +300 -0
- package/src/commands/tokens.test.ts +212 -0
- package/src/commands/tokens.ts +168 -0
- package/src/commands/verify.test.ts +238 -0
- package/src/commands/verify.ts +256 -0
- package/src/commands/version.test.ts +70 -0
- package/src/commands/version.ts +24 -0
- package/src/engine/audio/index.ts +47 -0
- package/src/engine/audio/music.test.ts +188 -0
- package/src/engine/audio/music.ts +218 -0
- package/src/engine/audio/section.test.ts +124 -0
- package/src/engine/audio/section.ts +79 -0
- package/src/engine/audio/sfx.test.ts +410 -0
- package/src/engine/audio/sfx.ts +694 -0
- package/src/engine/cart/cart.test.ts +304 -0
- package/src/engine/cart/cart.ts +211 -0
- package/src/engine/cart/errors.ts +39 -0
- package/src/engine/cart/fixtures/crlf.p8 +6 -0
- package/src/engine/cart/fixtures/full.p8 +174 -0
- package/src/engine/cart/fixtures/header-only.p8 +2 -0
- package/src/engine/cart/fixtures/malformed-no-header.p8 +2 -0
- package/src/engine/cart/fixtures/minimal.p8 +10 -0
- package/src/engine/cart/fixtures/no-trailing-newline.p8 +6 -0
- package/src/engine/cart/gfx.ts +195 -0
- package/src/engine/cart/index.ts +27 -0
- package/src/engine/cart/map.ts +134 -0
- package/src/engine/config/config.ts +115 -0
- package/src/engine/config/index.ts +12 -0
- package/src/engine/gfx/grid.test.ts +128 -0
- package/src/engine/gfx/grid.ts +136 -0
- package/src/engine/gfx/index.ts +34 -0
- package/src/engine/gfx/label.test.ts +105 -0
- package/src/engine/gfx/label.ts +229 -0
- package/src/engine/gfx/overlap.test.ts +71 -0
- package/src/engine/gfx/overlap.ts +66 -0
- package/src/engine/gfx/palette.test.ts +35 -0
- package/src/engine/gfx/palette.ts +54 -0
- package/src/engine/gfx/png.test.ts +111 -0
- package/src/engine/gfx/png.ts +196 -0
- package/src/engine/gfx/render.test.ts +180 -0
- package/src/engine/gfx/render.ts +106 -0
- package/src/engine/index.ts +21 -0
- package/src/engine/pico8/adapter.ts +303 -0
- package/src/engine/pico8/drive.test.ts +415 -0
- package/src/engine/pico8/drive.ts +729 -0
- package/src/engine/pico8/harness.test.ts +181 -0
- package/src/engine/pico8/harness.ts +354 -0
- package/src/engine/pico8/index.ts +20 -0
- package/src/engine/pico8/sentinel.test.ts +80 -0
- package/src/engine/pico8/sentinel.ts +93 -0
- package/src/engine/pico8/session-daemon-entry.ts +12 -0
- package/src/engine/pico8/session-daemon-main.ts +98 -0
- package/src/engine/pico8/session-daemon.test.ts +212 -0
- package/src/engine/pico8/session-daemon.ts +306 -0
- package/src/engine/pico8/session.test.ts +239 -0
- package/src/engine/pico8/session.ts +321 -0
- package/src/engine/pico8/shell.test.ts +583 -0
- package/src/engine/pico8/shell.ts +412 -0
- package/src/engine/pico8/supervisor.test.ts +105 -0
- package/src/engine/pico8/supervisor.ts +197 -0
- package/src/engine/scaffold/index.ts +15 -0
- package/src/engine/scaffold/scaffold.ts +278 -0
- package/src/engine/shrinko/adapter.ts +241 -0
- package/src/engine/shrinko/fixtures/count-no-compressed.txt +2 -0
- package/src/engine/shrinko/fixtures/count-over-budget.txt +3 -0
- package/src/engine/shrinko/fixtures/count-under-budget.txt +3 -0
- package/src/engine/shrinko/index.ts +33 -0
- package/src/engine/shrinko/shell.ts +224 -0
- package/src/engine/shrinko/shrinko.test.ts +182 -0
- package/src/engine/skills/index.ts +14 -0
- package/src/engine/skills/skills.test.ts +376 -0
- package/src/engine/skills/skills.ts +205 -0
- package/src/index.ts +13 -0
- package/src/skills/game-design-reference/SKILL.md +70 -0
- package/src/skills/game-jam/SKILL.md +45 -0
- package/src/skills/picopilot-art/SKILL.md +62 -0
- package/src/skills/picopilot-audio/SKILL.md +194 -0
- package/src/skills/picopilot-code/SKILL.md +97 -0
- package/src/skills/picopilot-code/reference/README.md +42 -0
- package/src/skills/picopilot-code/reference/mode7-racing.md +95 -0
- package/src/skills/picopilot-code/reference/platformer.md +119 -0
- package/src/skills/picopilot-code/reference/puzzle-grid.md +106 -0
- package/src/skills/picopilot-code/reference/rpg-menus-dialog.md +122 -0
- package/src/skills/picopilot-code/reference/top-down-adventure.md +117 -0
- package/src/skills/picopilot-code/reference/twin-stick-arcade.md +128 -0
- package/src/skills/picopilot-debug/SKILL.md +195 -0
- package/src/skills/picopilot-overview/SKILL.md +52 -0
- 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.
|