romdevtools 0.28.0 → 0.30.0
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/AGENTS.md +53 -43
- package/CHANGELOG.md +91 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +84 -8
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +131 -24
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +53 -10
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
|
@@ -1,7 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/* ── platformer.c — Atari Lynx side-scrolling platformer (complete example) ───
|
|
2
|
+
*
|
|
3
|
+
* A COMPLETE, working game — title screen, lives + score, in-session
|
|
4
|
+
* hi-score, MIKEY music + SFX, gravity/jump physics, one-way platforms,
|
|
5
|
+
* pits, spikes, coins, a scrolling level, AND the Lynx's signature party
|
|
6
|
+
* trick: HARDWARE SPRITE SCALING. The hero is a Suzy-scaled sprite, and
|
|
7
|
+
* collectible GEMS breathe (pulse big↔small) every frame purely by
|
|
8
|
+
* rewriting two 8.8 fixed-point fields in a Sprite Control Block — no CPU
|
|
9
|
+
* pixel work at all. That pulse is the bait: the bigger the gem reads, the
|
|
10
|
+
* easier it is to grab, and the hardware does every frame of the animation.
|
|
11
|
+
*
|
|
12
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
13
|
+
* very different one. The markers tell you what's what:
|
|
14
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented Lynx footgun;
|
|
15
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
16
|
+
* GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
|
|
17
|
+
* freely.
|
|
18
|
+
*
|
|
19
|
+
* What depends on what:
|
|
20
|
+
* lynx_sfx.{h,c} — MIKEY 4-voice audio (voice 0 = jump/coin SFX, voice 1 =
|
|
21
|
+
* background melody, voice 2 = land/hurt SFX, voice 3 = noise/death).
|
|
22
|
+
* vendor/cc65/libsrc/lynx/ — the FULL cc65 Lynx driver source shipped into
|
|
23
|
+
* your project. The TGI driver (tgi/lynx-160-102-16.s) is REQUIRED
|
|
24
|
+
* reading when graphics misbehave: every TGI call is itself a Suzy
|
|
25
|
+
* sprite, and our scaled sprites ride the same engine via tgi_ioctl(0).
|
|
26
|
+
*
|
|
27
|
+
* SCROLLING ON THE LYNX (read this — it is the platform's biggest "where's
|
|
28
|
+
* the hardware feature?" surprise): the Lynx has NO hardware tilemap and NO
|
|
29
|
+
* background scroll register. Suzy is a SPRITE BLITTER, not a tile engine.
|
|
30
|
+
* So we scroll the level the honest way: keep a software camera (cam_x) and
|
|
31
|
+
* REDRAW the visible slice of the world every frame, painting each ground/
|
|
32
|
+
* platform column at its on-screen position (world_x - cam_x). The full-
|
|
33
|
+
* redraw TGI loop (below) makes that cheap enough — the whole 160-px window
|
|
34
|
+
* is a handful of tgi_bar fills. The camera is one-way (never scrolls back),
|
|
35
|
+
* the classic runner camera. See draw_level().
|
|
36
|
+
*
|
|
37
|
+
* PLAYERS: 1. This is a handheld — multiplayer on real hardware is ComLynx,
|
|
38
|
+
* a cable between TWO Lynx units. A single emulator instance has nobody on
|
|
39
|
+
* the other end of the cable, so this example is honestly single-player
|
|
40
|
+
* (no fake "P2" that could never work).
|
|
41
|
+
*
|
|
42
|
+
* SCREEN: 160x102. The system font is 8x8, so a full row of text is 20
|
|
43
|
+
* characters — keep the HUD line short and the layout compact.
|
|
44
|
+
*/
|
|
5
45
|
|
|
6
46
|
#include <tgi.h>
|
|
7
47
|
#include <joystick.h>
|
|
@@ -9,89 +49,584 @@
|
|
|
9
49
|
#include <stdint.h>
|
|
10
50
|
#include "lynx_sfx.h"
|
|
11
51
|
|
|
12
|
-
|
|
52
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
53
|
+
* name here automatically. Keep it <=16 chars of A-Z 0-9 space dash. */
|
|
54
|
+
#define GAME_TITLE "RIDGE ROMP"
|
|
13
55
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
56
|
+
/* ── GAME LOGIC (clay — reshape freely) — screen + world geometry ─────────── */
|
|
57
|
+
#define SCRW 160
|
|
58
|
+
#define SCRH 102
|
|
59
|
+
#define HUD_H 9 /* HUD bar height (keep it compact) */
|
|
60
|
+
#define GROUND_Y 90 /* default ground surface (screen Y) */
|
|
61
|
+
#define PLAYER_W 8 /* art is 8x8; SCALE keeps that 1:1 */
|
|
62
|
+
|
|
63
|
+
/* The level is a column map, 8 px per column. world_x of column c = c*8.
|
|
64
|
+
* ground_y[c] — screen-Y of the ground surface, 0xFF = pit (no floor).
|
|
65
|
+
* plat_y[c] — screen-Y of a one-way floating platform, 0 = none.
|
|
66
|
+
* COL_COUNT columns × 8 px = the level length; the run loops when the camera
|
|
67
|
+
* passes the end (we wrap cam_x back to 0 — the seam is a flat runway). */
|
|
68
|
+
#define NO_GROUND 0xFF
|
|
69
|
+
#define COL_COUNT 48 /* 48 * 8 = 384 px of level */
|
|
70
|
+
static const uint8_t ground_y[COL_COUNT] = {
|
|
71
|
+
90, 90, 90, 90, 90, 90, /* start runway */
|
|
72
|
+
90, 90, NO_GROUND, NO_GROUND, 90, 90, /* pit 1 (16 px) */
|
|
73
|
+
82, 82, 82, 90, 90, 90, /* a raised step */
|
|
74
|
+
90, NO_GROUND, NO_GROUND, NO_GROUND, 90, 90, /* pit 2 (24 px) */
|
|
75
|
+
90, 90, 74, 74, 74, 90, /* high mesa */
|
|
76
|
+
90, 90, 90, NO_GROUND, NO_GROUND, 90, /* pit 3 (16 px) */
|
|
77
|
+
90, 90, 90, 90, 90, 90,
|
|
78
|
+
90, 90, 90, 90, 90, 90, /* end runway (loop seam) */
|
|
79
|
+
};
|
|
80
|
+
static const uint8_t plat_y[COL_COUNT] = {
|
|
81
|
+
0, 0, 0, 0, 70, 70, /* slab over start */
|
|
82
|
+
0, 0, 0, 0, 0, 0,
|
|
83
|
+
0, 0, 0, 0, 58, 58, /* high slab */
|
|
84
|
+
58, 0, 0, 0, 0, 0,
|
|
85
|
+
0, 0, 0, 0, 0, 0,
|
|
86
|
+
0, 0, 64, 64, 64, 0, /* slab across pit 3 */
|
|
87
|
+
0, 0, 0, 0, 0, 0,
|
|
88
|
+
0, 0, 70, 70, 0, 0,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/* ── GAME LOGIC (clay) — physics tuning (all Q4.4: 16 = 1.0 px) ───────────── */
|
|
92
|
+
#define GRAVITY_Q44 6 /* +0.375 px/frame/frame */
|
|
93
|
+
#define JUMP_VEL_Q44 (-58) /* launch vy → ~7 px apex, ~6 tiles */
|
|
94
|
+
#define MAX_VY_Q44 56 /* terminal fall = 3.5 px/frame — *
|
|
95
|
+
* MUST stay under 4: the landing *
|
|
96
|
+
* window is 4 px (tunnelling else) */
|
|
97
|
+
#define MOVE_SPEED 2 /* px/frame walk + scroll speed */
|
|
98
|
+
#define SCROLL_WALL 72 /* past this the world scrolls (cam) */
|
|
99
|
+
#define START_LIVES 3
|
|
100
|
+
#define N_COINS 3
|
|
101
|
+
#define N_SPIKES 2
|
|
102
|
+
#define N_GEMS 2 /* the SCALING collectibles */
|
|
103
|
+
|
|
104
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
105
|
+
* SUZY HARDWARE SPRITE SCALING — the Lynx signature. Suzy renders every
|
|
106
|
+
* sprite through a Sprite Control Block (SCB) it walks in cart/work RAM.
|
|
107
|
+
* Two SCB fields, HSIZE and VSIZE, are 8.8 fixed-point scale factors
|
|
108
|
+
* ($0100 = 1.0): the SAME 8x8 source pixels render at any size, every
|
|
109
|
+
* frame, for free. We use it three ways here:
|
|
110
|
+
* - the HERO renders at a fixed 1.0x via the SAME SCB path (so forking in
|
|
111
|
+
* a depth/power-up scale is a one-line change);
|
|
112
|
+
* - the GEMS breathe — HSIZE/VSIZE sweep 0.75x↔1.75x every frame, a pure
|
|
113
|
+
* hardware animation that doubles as a difficulty tell (a fat gem is an
|
|
114
|
+
* easy grab, the collision box tracks the live hardware size);
|
|
115
|
+
* - it costs zero extra CPU vs. a fixed sprite — Suzy scales while it blits.
|
|
116
|
+
*
|
|
117
|
+
* The SCB, field by field (this is cc65's SCB_REHV_PAL from <_suzy.h>):
|
|
118
|
+
* sprctl0 bits 7-6 = bits per pixel (11 = 4bpp), bits 2-0 = sprite TYPE.
|
|
119
|
+
* TYPE_NORMAL (4) draws pens 1-15 and treats pen 0 as
|
|
120
|
+
* TRANSPARENT — that's how shaped sprites sit over the level.
|
|
121
|
+
* sprctl1 bit 7 LITERAL (raw nybbles, no RLE) + bits 5-4 reload depth:
|
|
122
|
+
* REHV means "this SCB carries HPOS, VPOS, HSIZE, VSIZE". The
|
|
123
|
+
* reload bits ARE the struct layout — mismatch them and Suzy
|
|
124
|
+
* reads palette bytes as size words.
|
|
125
|
+
* sprcoll $20 = NO_COLLIDE. Gameplay collision is done in C (in screen
|
|
126
|
+
* coordinates the collision buffer knows nothing about).
|
|
127
|
+
* next pointer to the next SCB, 0 = end of chain (one blit per call).
|
|
128
|
+
* data sprite pixel data (LITERAL 4bpp format below).
|
|
129
|
+
* hpos/vpos signed SCREEN position of the sprite's top-left corner.
|
|
130
|
+
* hsize/vsize 8.8 scale — THE party trick, rewritten per draw.
|
|
131
|
+
* penpal[8] 16 nybbles mapping pixel values 0-15 → palette pens.
|
|
132
|
+
*
|
|
133
|
+
* LITERAL 4bpp data format (hand-encodable): each sprite LINE is
|
|
134
|
+
* [offset byte][width/2 bytes of raw nybble pixels]
|
|
135
|
+
* where offset = 1 + bytes of pixel data; a final offset of 0 ends the
|
|
136
|
+
* sprite. 8 px @ 4bpp = 4 data bytes, so every line starts with 5.
|
|
137
|
+
*
|
|
138
|
+
* Drawing: tgi_sprite(&scb) → tgi_ioctl(0, &scb) — the TGI driver's
|
|
139
|
+
* documented escape hatch (see CONTROL in vendor/cc65/libsrc/lynx/tgi/
|
|
140
|
+
* lynx-160-102-16.s). It points Suzy's SCBNEXT at your SCB, aims VIDBAS at
|
|
141
|
+
* TGI's current DRAW page (so scaled sprites land in the same double-
|
|
142
|
+
* buffered frame as tgi_bar/tgi_outtextxy), fires SPRGO, and sleeps the CPU
|
|
143
|
+
* until SPRSYS reports the blit done.
|
|
144
|
+
*
|
|
145
|
+
* Requires: the cc65 crt0 Suzy init (already done before main()), and calls
|
|
146
|
+
* only between the tgi_busy() wait and tgi_updatedisplay() — i.e. while
|
|
147
|
+
* TGI's draw buffer is the blit target. Draw order = paint order: level
|
|
148
|
+
* fills first, scaled sprites after, HUD text last.
|
|
149
|
+
*/
|
|
150
|
+
static SCB_REHV_PAL scb = {
|
|
151
|
+
BPP_4 | TYPE_NORMAL, /* sprctl0: 4bpp, pen 0 transparent */
|
|
152
|
+
LITERAL | REHV, /* sprctl1: literal data, HV+size SCB */
|
|
153
|
+
0x20, /* sprcoll: NO_COLLIDE */
|
|
154
|
+
0, /* next: single-SCB chain */
|
|
155
|
+
0, /* data: set per draw */
|
|
156
|
+
0, 0, /* hpos, vpos */
|
|
157
|
+
0x0100, 0x0100, /* hsize, vsize (8.8) */
|
|
158
|
+
{ 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF } /* identity pens */
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/* Draw an 8x8 literal sprite at the given 8.8 scale, anchored at the
|
|
162
|
+
* top-left (x,y). The hero stays 1.0x; gems pulse. */
|
|
163
|
+
static void draw_scaled(unsigned char *data, int x, int y, unsigned scale) {
|
|
164
|
+
scb.data = data;
|
|
165
|
+
scb.hsize = scale;
|
|
166
|
+
scb.vsize = scale;
|
|
167
|
+
scb.hpos = x;
|
|
168
|
+
scb.vpos = y;
|
|
169
|
+
tgi_sprite(&scb);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* ── GAME LOGIC (clay) — 8x8 4bpp literal sprite art ────────────────────────
|
|
173
|
+
* Pens use the TGI default palette (cc65 lynx.h COLOR_* indices): 2 = red,
|
|
174
|
+
* 9 = yellow, $D = blue, $E = light-blue, $F = white, 0 = transparent. Each
|
|
175
|
+
* line: 5, then 4 nybble bytes; a final 0 byte ends the sprite. */
|
|
176
|
+
static unsigned char spr_hero[] = {
|
|
177
|
+
5, 0x00, 0x0E, 0xE0, 0x00, /* . . . E E . . . cyan runner, eyes */
|
|
178
|
+
5, 0x00, 0xEF, 0xFE, 0x00, /* . . E F F E . . */
|
|
179
|
+
5, 0x00, 0xE9, 0x9E, 0x00, /* . . E 9 9 E . . (eyes) */
|
|
180
|
+
5, 0x0E, 0xEE, 0xEE, 0xE0, /* . E E E E E E . body */
|
|
181
|
+
5, 0xEE, 0xEE, 0xEE, 0xEE, /* E E E E E E E E */
|
|
182
|
+
5, 0x0E, 0x0E, 0xE0, 0xE0, /* . E . E E . E . legs */
|
|
183
|
+
5, 0x0E, 0x00, 0x00, 0xE0, /* . E . . . . E . */
|
|
184
|
+
5, 0x0F, 0x00, 0x00, 0xF0, /* . F . . . . F . feet */
|
|
185
|
+
0
|
|
186
|
+
};
|
|
187
|
+
static unsigned char spr_gem[] = {
|
|
188
|
+
5, 0x00, 0x09, 0x90, 0x00, /* . . . 9 9 . . . yellow gem, white shine */
|
|
189
|
+
5, 0x00, 0x9F, 0xF9, 0x00, /* . . 9 F F 9 . . */
|
|
190
|
+
5, 0x09, 0xFD, 0x9F, 0x90, /* . 9 F D 9 F 9 . (blue facet) */
|
|
191
|
+
5, 0x9F, 0x99, 0x99, 0xF9, /* 9 F 9 9 9 9 F 9 */
|
|
192
|
+
5, 0x9F, 0x99, 0x99, 0xF9, /* 9 F 9 9 9 9 F 9 */
|
|
193
|
+
5, 0x09, 0x99, 0x99, 0x90, /* . 9 9 9 9 9 9 . */
|
|
194
|
+
5, 0x00, 0x99, 0x99, 0x00, /* . . 9 9 9 9 . . */
|
|
195
|
+
5, 0x00, 0x09, 0x90, 0x00, /* . . . 9 9 . . . */
|
|
196
|
+
0
|
|
21
197
|
};
|
|
22
|
-
#define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
|
|
23
198
|
|
|
24
|
-
|
|
199
|
+
/* ── GAME LOGIC (clay) — gem pulse (the SCALING signature) ──────────────────
|
|
200
|
+
* One shared phase drives every gem's HSIZE/VSIZE. The 8.8 scale sweeps
|
|
201
|
+
* SCALE_MIN..SCALE_MAX and back; gem_scale() returns the current value and
|
|
202
|
+
* gem_half() the matching on-screen half-width so the grab box tracks the
|
|
203
|
+
* hardware size exactly. */
|
|
204
|
+
#define SCALE_MIN 0x00C0u /* 0.75x → 6 px */
|
|
205
|
+
#define SCALE_MAX 0x01C0u /* 1.75x → 14 px */
|
|
206
|
+
static unsigned gem_phase; /* 0..255 triangle wave */
|
|
207
|
+
static unsigned gem_scale(void) {
|
|
208
|
+
unsigned t = gem_phase < 128 ? gem_phase : (255 - gem_phase); /* 0..127 */
|
|
209
|
+
return SCALE_MIN + (t * (SCALE_MAX - SCALE_MIN)) / 127u;
|
|
210
|
+
}
|
|
211
|
+
static uint8_t gem_half(void) {
|
|
212
|
+
return (uint8_t)((gem_scale() * 8u) >> 9); /* (8*scale>>8)/2 */
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
typedef struct { uint8_t alive; int16_t wx; uint8_t y; } Coin; /* world-x */
|
|
216
|
+
typedef struct { uint8_t alive; int16_t wx; uint8_t y; } Spike;
|
|
217
|
+
typedef struct { uint8_t alive; int16_t wx; uint8_t y; } Gem;
|
|
218
|
+
|
|
219
|
+
static Coin coins[N_COINS];
|
|
220
|
+
static Spike spikes[N_SPIKES];
|
|
221
|
+
static Gem gems[N_GEMS];
|
|
222
|
+
|
|
223
|
+
/* Player state. px is SCREEN x (camera holds it at SCROLL_WALL while
|
|
224
|
+
* scrolling); world x = px + cam_x. py is Q4.4 for sub-pixel gravity. */
|
|
225
|
+
static uint8_t px;
|
|
226
|
+
static int16_t py_q44;
|
|
227
|
+
static int8_t vy_q44;
|
|
228
|
+
static uint8_t on_ground;
|
|
229
|
+
static unsigned cam_x; /* software camera (one-way) */
|
|
230
|
+
static uint8_t lives;
|
|
231
|
+
static unsigned score, hiscore; /* hiscore: in-session only (see below) */
|
|
232
|
+
static uint8_t dist_sub; /* 64 px scrolled = +1 distance point */
|
|
233
|
+
static uint8_t hurt_timer;
|
|
234
|
+
static uint8_t prev_joy;
|
|
235
|
+
|
|
236
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
237
|
+
#define ST_TITLE 0
|
|
238
|
+
#define ST_PLAY 1
|
|
239
|
+
#define ST_OVER 2
|
|
240
|
+
static uint8_t state;
|
|
241
|
+
static uint8_t over_new_hi;
|
|
242
|
+
|
|
243
|
+
/* ── GAME LOGIC (clay) — Galois LFSR (taps $B8), period 255 ── */
|
|
244
|
+
static uint8_t rng_state = 0x5A;
|
|
245
|
+
static uint8_t rand8(void) {
|
|
246
|
+
uint8_t lsb = (uint8_t)(rng_state & 1);
|
|
247
|
+
rng_state >>= 1;
|
|
248
|
+
if (lsb) rng_state ^= 0xB8;
|
|
249
|
+
return rng_state;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ── GAME LOGIC (clay) — score text (no sprintf: it drags in ~6KB) ── */
|
|
253
|
+
static char numbuf[6];
|
|
254
|
+
static char *fmt5(unsigned v) {
|
|
25
255
|
uint8_t i;
|
|
26
|
-
for (i = 0; i <
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
256
|
+
for (i = 0; i < 5; i++) { numbuf[4 - i] = (char)('0' + v % 10); v /= 10; }
|
|
257
|
+
numbuf[5] = 0;
|
|
258
|
+
return numbuf;
|
|
259
|
+
}
|
|
260
|
+
static uint8_t udist(uint8_t a, uint8_t b) { return a > b ? a - b : b - a; }
|
|
261
|
+
|
|
262
|
+
/* ── GAME LOGIC (clay) — column-map lookups (world x → column) ──────────────
|
|
263
|
+
* The level loops: world x wraps at COL_COUNT*8 so the run is endless. */
|
|
264
|
+
#define LEVEL_LEN ((unsigned)COL_COUNT * 8u)
|
|
265
|
+
static uint8_t col_of(unsigned wx) { return (uint8_t)((wx % LEVEL_LEN) >> 3); }
|
|
266
|
+
|
|
267
|
+
/* ── GAME LOGIC (clay) — draw the scrolling level (SOFTWARE camera) ─────────
|
|
268
|
+
* No hardware scroll on the Lynx (see header). We paint the visible window
|
|
269
|
+
* column by column: for each on-screen column, look up the world column at
|
|
270
|
+
* (cam_x + screenX) and fill its ground body + grass cap + any platform
|
|
271
|
+
* slab. Per-column tgi_bar fills keep the code legible — the whole strip is
|
|
272
|
+
* well under the frame budget. */
|
|
273
|
+
static void draw_level(void) {
|
|
274
|
+
int sx;
|
|
275
|
+
uint8_t c, gy, pgy;
|
|
276
|
+
for (sx = 0; sx < SCRW; sx += 8) {
|
|
277
|
+
c = col_of(cam_x + (unsigned)sx);
|
|
278
|
+
gy = ground_y[c];
|
|
279
|
+
/* ground column: grass cap (green) over a dirt body (brown) */
|
|
280
|
+
if (gy != NO_GROUND) {
|
|
281
|
+
tgi_setcolor(COLOR_BROWN);
|
|
282
|
+
tgi_bar(sx, gy + 2, sx + 7, SCRH - 1);
|
|
283
|
+
tgi_setcolor(COLOR_LIGHTGREEN);
|
|
284
|
+
tgi_bar(sx, gy, sx + 7, gy + 1);
|
|
285
|
+
}
|
|
286
|
+
/* one-way platform slab (grey ledge) */
|
|
287
|
+
pgy = plat_y[c];
|
|
288
|
+
if (pgy) {
|
|
289
|
+
tgi_setcolor(COLOR_GREY);
|
|
290
|
+
tgi_bar(sx, pgy, sx + 7, pgy + 2);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* ── GAME LOGIC (clay) — shared scene painter (runs every frame) ────────────
|
|
296
|
+
* Full-redraw, painter's order: sky, far parallax hills, HUD bar, then the
|
|
297
|
+
* caller layers the level + sprites + text on top. Layered bands keep any
|
|
298
|
+
* one colour comfortably under the render-health blank threshold. */
|
|
299
|
+
static const unsigned char hill_x[6] = { 8, 44, 78, 112, 138, 156 };
|
|
300
|
+
static void draw_scene(void) {
|
|
301
|
+
uint8_t i, hx;
|
|
302
|
+
tgi_setcolor(COLOR_BLUE);
|
|
303
|
+
tgi_bar(0, 0, SCRW - 1, SCRH - 1); /* sky */
|
|
304
|
+
/* far parallax hills (drift slower than the camera → depth) */
|
|
305
|
+
tgi_setcolor(COLOR_PURPLE);
|
|
306
|
+
for (i = 0; i < 6; i++) {
|
|
307
|
+
hx = (uint8_t)((hill_x[i] + SCRW - (uint8_t)((cam_x >> 2) % SCRW)) % SCRW);
|
|
308
|
+
tgi_bar(hx, 62, (hx + 26 < SCRW ? hx + 26 : SCRW - 1), 89);
|
|
309
|
+
}
|
|
310
|
+
tgi_setcolor(COLOR_DARKGREY);
|
|
311
|
+
tgi_bar(0, 0, SCRW - 1, HUD_H - 1); /* HUD bar */
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* ── GAME LOGIC (clay) — place world objects across the level ── */
|
|
315
|
+
static void place_objects(void) {
|
|
316
|
+
uint8_t i, c;
|
|
317
|
+
for (i = 0; i < N_COINS; i++) {
|
|
318
|
+
coins[i].alive = 1;
|
|
319
|
+
coins[i].wx = (int16_t)(40 + i * 110);
|
|
320
|
+
c = col_of(coins[i].wx);
|
|
321
|
+
/* hover a little above the surface, with a touch of LFSR jitter so the
|
|
322
|
+
* pickup arc isn't a flat line */
|
|
323
|
+
coins[i].y = (uint8_t)((ground_y[c] == NO_GROUND ? 60 : ground_y[c] - 18)
|
|
324
|
+
- (rand8() & 7));
|
|
325
|
+
}
|
|
326
|
+
for (i = 0; i < N_SPIKES; i++) {
|
|
327
|
+
spikes[i].wx = (int16_t)(96 + i * 150);
|
|
328
|
+
c = col_of(spikes[i].wx);
|
|
329
|
+
spikes[i].alive = ground_y[c] != NO_GROUND;
|
|
330
|
+
spikes[i].y = (uint8_t)((ground_y[c] == NO_GROUND ? GROUND_Y : ground_y[c]) - 6);
|
|
331
|
+
}
|
|
332
|
+
/* gems sit higher (a reach jump) and pulse via the scaling idiom */
|
|
333
|
+
for (i = 0; i < N_GEMS; i++) {
|
|
334
|
+
gems[i].alive = 1;
|
|
335
|
+
gems[i].wx = (int16_t)(150 + i * 130);
|
|
336
|
+
gems[i].y = (uint8_t)(48 + (i & 1) * 8);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
341
|
+
static void start_game(void) {
|
|
342
|
+
px = 24;
|
|
343
|
+
py_q44 = (int16_t)((GROUND_Y - PLAYER_W) << 4);
|
|
344
|
+
vy_q44 = 0;
|
|
345
|
+
on_ground = 1;
|
|
346
|
+
cam_x = 0;
|
|
347
|
+
dist_sub = 0;
|
|
348
|
+
lives = START_LIVES;
|
|
349
|
+
score = 0;
|
|
350
|
+
hurt_timer = 0;
|
|
351
|
+
gem_phase = 0;
|
|
352
|
+
place_objects();
|
|
353
|
+
sfx_tone(0, 80, 8); /* start chirp */
|
|
354
|
+
state = ST_PLAY;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
static void game_over(void) {
|
|
358
|
+
over_new_hi = 0;
|
|
359
|
+
if (score > hiscore) {
|
|
360
|
+
/* ── In-session hi-score ONLY — and here's the honest why. Real Lynx
|
|
361
|
+
* carts persist via a 93Cxx serial EEPROM on the cart PCB (cc65 even
|
|
362
|
+
* ships lynx_eeprom_read/write for it; see vendor/cc65/libsrc/lynx/
|
|
363
|
+
* eeprom.s). PROBED: the bundled handy core emulates CEEPROM internally
|
|
364
|
+
* but its libretro build exposes NO save path — retro_get_memory(
|
|
365
|
+
* SAVE_RAM) returns NULL/size 0, so nothing survives host.hardReset()
|
|
366
|
+
* and a bit-banged round-trip reads back garbage under the WASM build.
|
|
367
|
+
* Wiring the EEPROM to SAVE_RAM is a future core round; until then a
|
|
368
|
+
* fake "save" would be lying. The hi-score DOES survive title↔play
|
|
369
|
+
* cycles within one power-on. ── */
|
|
370
|
+
hiscore = score;
|
|
371
|
+
over_new_hi = 1;
|
|
30
372
|
}
|
|
373
|
+
sfx_tone(2, 240, 24); /* voice 2: low game-over drone */
|
|
374
|
+
sfx_noise(16); /* voice 3: crunch */
|
|
375
|
+
state = ST_OVER;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* ── GAME LOGIC (clay) — death + respawn at the run start ── */
|
|
379
|
+
static void lose_life(void) {
|
|
380
|
+
sfx_noise(14); /* voice 3: splat */
|
|
381
|
+
if (lives) lives--;
|
|
382
|
+
if (lives == 0) { game_over(); return; }
|
|
383
|
+
/* respawn at a safe runway tile, keep the camera (one-way run) */
|
|
384
|
+
px = 24;
|
|
385
|
+
py_q44 = (int16_t)((GROUND_Y - PLAYER_W) << 4);
|
|
386
|
+
vy_q44 = 0;
|
|
387
|
+
on_ground = 1;
|
|
388
|
+
hurt_timer = 45;
|
|
389
|
+
prev_joy = 0xFF; /* swallow held jump across the respawn */
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* ── GAME LOGIC (clay) — landing probe against the column map ───────────────
|
|
393
|
+
* One-way platforms: only catch the player while FALLING through a narrow
|
|
394
|
+
* 4-px window at a surface's top. Probe both columns under the 8-px-wide
|
|
395
|
+
* feet so a foot half-off a ledge still lands. Returns the surface Y to snap
|
|
396
|
+
* to, or 0 for "no floor here". */
|
|
397
|
+
static uint8_t land_top(uint8_t feet) {
|
|
398
|
+
uint8_t c0, c1, gy, pgy;
|
|
399
|
+
unsigned wx = cam_x + px;
|
|
400
|
+
c0 = col_of(wx);
|
|
401
|
+
c1 = col_of(wx + 7);
|
|
402
|
+
/* platform slabs first (they sit above the ground) */
|
|
403
|
+
pgy = plat_y[c0]; if (!pgy) pgy = plat_y[c1];
|
|
404
|
+
if (pgy && (uint8_t)(feet + 1) >= pgy && feet <= (uint8_t)(pgy + 4)) return pgy;
|
|
405
|
+
/* then the ground surface */
|
|
406
|
+
gy = ground_y[c0];
|
|
407
|
+
if (gy == NO_GROUND) gy = ground_y[c1];
|
|
408
|
+
if (gy != NO_GROUND && (uint8_t)(feet + 1) >= gy && feet <= (uint8_t)(gy + 4))
|
|
409
|
+
return gy;
|
|
31
410
|
return 0;
|
|
32
411
|
}
|
|
33
412
|
|
|
413
|
+
/* ── GAME LOGIC (clay) — per-state frames. Each runs INSIDE the canonical
|
|
414
|
+
* loop below: scene already painted, tgi_updatedisplay not yet called. ── */
|
|
415
|
+
|
|
416
|
+
static unsigned attract_cam;
|
|
417
|
+
|
|
418
|
+
static void frame_title(uint8_t joy) {
|
|
419
|
+
cam_x = attract_cam; /* attract: the level drifts by */
|
|
420
|
+
draw_level();
|
|
421
|
+
/* a lone breathing gem sells the scaling idiom on the title screen —
|
|
422
|
+
* parked in a clear top-right zone (away from all the text) so the pulse
|
|
423
|
+
* reads cleanly. */
|
|
424
|
+
draw_scaled(spr_gem, 132, 14, gem_scale());
|
|
425
|
+
draw_scaled(spr_hero, 28, GROUND_Y - PLAYER_W, 0x0100);
|
|
426
|
+
attract_cam++;
|
|
427
|
+
if (attract_cam >= LEVEL_LEN) attract_cam -= LEVEL_LEN;
|
|
428
|
+
|
|
429
|
+
tgi_setcolor(COLOR_WHITE);
|
|
430
|
+
tgi_outtextxy(40, 1, GAME_TITLE); /* on the HUD bar */
|
|
431
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
432
|
+
tgi_outtextxy(52, 30, "PRESS A");
|
|
433
|
+
tgi_setcolor(COLOR_LIGHTGREY);
|
|
434
|
+
tgi_outtextxy(32, 44, "HI ");
|
|
435
|
+
tgi_outtextxy(56, 44, fmt5(hiscore));
|
|
436
|
+
tgi_outtextxy(24, 56, "1 PLAYER GAME"); /* handheld honesty */
|
|
437
|
+
|
|
438
|
+
if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) start_game();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
static void frame_over(uint8_t joy) {
|
|
442
|
+
tgi_setcolor(COLOR_DARKGREY);
|
|
443
|
+
tgi_bar(20, 28, 139, 66);
|
|
444
|
+
tgi_setcolor(COLOR_WHITE);
|
|
445
|
+
tgi_outtextxy(44, 32, "GAME OVER");
|
|
446
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
447
|
+
tgi_outtextxy(36, 42, "SCORE ");
|
|
448
|
+
tgi_outtextxy(84, 42, fmt5(score));
|
|
449
|
+
if (over_new_hi) { tgi_setcolor(COLOR_LIGHTGREEN); tgi_outtextxy(32, 54, "NEW HI SCORE"); }
|
|
450
|
+
else { tgi_setcolor(COLOR_LIGHTGREY); tgi_outtextxy(40, 54, "A = TITLE"); }
|
|
451
|
+
if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) state = ST_TITLE;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* clamp+test that a world object is on-screen; returns its screen-x or -1 */
|
|
455
|
+
static int obj_sx(int16_t wx) {
|
|
456
|
+
int16_t s = wx - (int16_t)cam_x;
|
|
457
|
+
if (s < 0 || s >= SCRW) return -1;
|
|
458
|
+
return (int)s;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
static void frame_play(uint8_t joy) {
|
|
462
|
+
uint8_t i, py8, feet, top, gh;
|
|
463
|
+
int s;
|
|
464
|
+
uint8_t moved = 0;
|
|
465
|
+
|
|
466
|
+
/* ── draw: level, world objects (camera-relative), hero, HUD ── */
|
|
467
|
+
draw_level();
|
|
468
|
+
|
|
469
|
+
for (i = 0; i < N_COINS; i++) {
|
|
470
|
+
if (!coins[i].alive) continue;
|
|
471
|
+
s = obj_sx(coins[i].wx); if (s < 0) continue;
|
|
472
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
473
|
+
tgi_bar(s, coins[i].y, s + 5, coins[i].y + 5);
|
|
474
|
+
tgi_setcolor(COLOR_BROWN);
|
|
475
|
+
tgi_bar(s + 2, coins[i].y + 2, s + 3, coins[i].y + 3);
|
|
476
|
+
}
|
|
477
|
+
for (i = 0; i < N_SPIKES; i++) {
|
|
478
|
+
if (!spikes[i].alive) continue;
|
|
479
|
+
s = obj_sx(spikes[i].wx); if (s < 0) continue;
|
|
480
|
+
tgi_setcolor(COLOR_RED);
|
|
481
|
+
tgi_bar(s, spikes[i].y, s + 1, spikes[i].y + 5);
|
|
482
|
+
tgi_bar(s + 2, spikes[i].y - 2, s + 3, spikes[i].y + 5);
|
|
483
|
+
tgi_bar(s + 4, spikes[i].y, s + 5, spikes[i].y + 5);
|
|
484
|
+
}
|
|
485
|
+
/* gems — drawn via the SCALING SCB (pulse this frame's hardware size) */
|
|
486
|
+
for (i = 0; i < N_GEMS; i++) {
|
|
487
|
+
if (!gems[i].alive) continue;
|
|
488
|
+
s = obj_sx(gems[i].wx); if (s < 0) continue;
|
|
489
|
+
draw_scaled(spr_gem, s, gems[i].y, gem_scale());
|
|
490
|
+
}
|
|
491
|
+
/* hero (blink while hurt) at a fixed 1.0x through the same SCB path */
|
|
492
|
+
py8 = (uint8_t)(py_q44 >> 4);
|
|
493
|
+
if (hurt_timer == 0 || (hurt_timer & 4))
|
|
494
|
+
draw_scaled(spr_hero, (int)px, py8, 0x0100);
|
|
495
|
+
|
|
496
|
+
tgi_setcolor(COLOR_WHITE);
|
|
497
|
+
tgi_outtextxy(2, 1, "SC");
|
|
498
|
+
tgi_outtextxy(20, 1, fmt5(score));
|
|
499
|
+
tgi_setcolor(COLOR_LIGHTGREY);
|
|
500
|
+
tgi_outtextxy(72, 1, "HI");
|
|
501
|
+
tgi_outtextxy(90, 1, fmt5(hiscore));
|
|
502
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
503
|
+
tgi_outtextxy(140, 1, "L");
|
|
504
|
+
numbuf[0] = (char)('0' + lives); numbuf[1] = 0;
|
|
505
|
+
tgi_outtextxy(148, 1, numbuf);
|
|
506
|
+
|
|
507
|
+
/* ── update: input ── */
|
|
508
|
+
if (joy & JOY_RIGHT_MASK) {
|
|
509
|
+
if (px < SCROLL_WALL) px += MOVE_SPEED;
|
|
510
|
+
else { cam_x += MOVE_SPEED; moved = MOVE_SPEED; }
|
|
511
|
+
}
|
|
512
|
+
if ((joy & JOY_LEFT_MASK) && px > 8) px -= MOVE_SPEED;
|
|
513
|
+
if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy) && on_ground) {
|
|
514
|
+
vy_q44 = JUMP_VEL_Q44;
|
|
515
|
+
on_ground = 0;
|
|
516
|
+
sfx_tone(0, 110, 6); /* voice 0: jump whoop */
|
|
517
|
+
}
|
|
518
|
+
if (hurt_timer) hurt_timer--;
|
|
519
|
+
if (cam_x >= LEVEL_LEN) cam_x -= LEVEL_LEN; /* loop the run */
|
|
520
|
+
|
|
521
|
+
/* distance scoring */
|
|
522
|
+
if (moved) {
|
|
523
|
+
dist_sub += moved;
|
|
524
|
+
if (dist_sub >= 64) { dist_sub -= 64; score++; }
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* ── physics: gravity + sub-pixel Y ── */
|
|
528
|
+
if (vy_q44 < MAX_VY_Q44) vy_q44 += GRAVITY_Q44;
|
|
529
|
+
py_q44 += vy_q44;
|
|
530
|
+
py8 = (uint8_t)(py_q44 >> 4);
|
|
531
|
+
|
|
532
|
+
/* fell below the screen (into a pit) → lose a life */
|
|
533
|
+
if (py_q44 < 0 || py8 >= SCRH - 2) { lose_life(); return; }
|
|
534
|
+
|
|
535
|
+
/* landing probe (only while falling) */
|
|
536
|
+
if (vy_q44 >= 0) {
|
|
537
|
+
feet = py8 + PLAYER_W;
|
|
538
|
+
top = land_top(feet);
|
|
539
|
+
if (top) {
|
|
540
|
+
py_q44 = (int16_t)((top - PLAYER_W) << 4);
|
|
541
|
+
vy_q44 = 0;
|
|
542
|
+
if (!on_ground) sfx_tone(2, 180, 3); /* voice 2: land thud */
|
|
543
|
+
on_ground = 1;
|
|
544
|
+
} else {
|
|
545
|
+
on_ground = 0;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/* ── collisions (screen space; gem box tracks the live hardware size) ── */
|
|
550
|
+
gh = gem_half();
|
|
551
|
+
for (i = 0; i < N_GEMS; i++) {
|
|
552
|
+
if (!gems[i].alive) continue;
|
|
553
|
+
s = obj_sx(gems[i].wx); if (s < 0) continue;
|
|
554
|
+
if (udist((uint8_t)(s + 4), (uint8_t)(px + 4)) < gh + 4
|
|
555
|
+
&& udist((uint8_t)(gems[i].y + 4), (uint8_t)(py8 + 4)) < gh + 4) {
|
|
556
|
+
gems[i].alive = 0;
|
|
557
|
+
score += 25; /* fat gem = fat points */
|
|
558
|
+
sfx_tone(0, 60, 6); /* voice 0: sparkle */
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
for (i = 0; i < N_COINS; i++) {
|
|
562
|
+
if (!coins[i].alive) continue;
|
|
563
|
+
s = obj_sx(coins[i].wx); if (s < 0) continue;
|
|
564
|
+
if (udist((uint8_t)(s + 3), (uint8_t)(px + 4)) < 8
|
|
565
|
+
&& udist((uint8_t)(coins[i].y + 3), (uint8_t)(py8 + 4)) < 8) {
|
|
566
|
+
coins[i].alive = 0;
|
|
567
|
+
score += 10;
|
|
568
|
+
sfx_tone(0, 70, 5); /* voice 0: coin ping */
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (hurt_timer == 0) {
|
|
572
|
+
for (i = 0; i < N_SPIKES; i++) {
|
|
573
|
+
if (!spikes[i].alive) continue;
|
|
574
|
+
s = obj_sx(spikes[i].wx); if (s < 0) continue;
|
|
575
|
+
if (udist((uint8_t)(s + 3), (uint8_t)(px + 4)) < 7
|
|
576
|
+
&& udist((uint8_t)(spikes[i].y + 3), (uint8_t)(py8 + 4)) < 7) {
|
|
577
|
+
lose_life();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
34
584
|
void main(void) {
|
|
35
|
-
|
|
36
|
-
int8_t vy = 0;
|
|
37
|
-
uint8_t joy, prev = 0, btn, grounded;
|
|
38
|
-
uint8_t i;
|
|
585
|
+
uint8_t joy;
|
|
39
586
|
|
|
40
587
|
tgi_install(&lynx_160_102_16_tgi);
|
|
41
588
|
tgi_init();
|
|
42
589
|
joy_install(&lynx_stdjoy_joy);
|
|
43
|
-
sfx_init();
|
|
590
|
+
sfx_init(); /* MIKEY up; background melody starts on voice 1 */
|
|
591
|
+
|
|
592
|
+
state = ST_TITLE;
|
|
593
|
+
prev_joy = 0;
|
|
594
|
+
attract_cam = 0;
|
|
595
|
+
hiscore = 0;
|
|
44
596
|
|
|
45
597
|
for (;;) {
|
|
46
|
-
/*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
598
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
599
|
+
* CANONICAL LYNX GAME LOOP — full-redraw every frame, in this order:
|
|
600
|
+
* 1. while (tgi_busy()) { } — WAIT for the previous frame's page
|
|
601
|
+
* flip. Skipping this is the #1 "Lynx screen stays blank" trap:
|
|
602
|
+
* drawing while the swap is pending loses the frame.
|
|
603
|
+
* 2. Repaint the WHOLE scene with tgi_bar fills — NOT tgi_clear()
|
|
604
|
+
* (which can leave the framebuffer stale on this toolchain+
|
|
605
|
+
* emulator path). TGI double-buffers; the back buffer holds the
|
|
606
|
+
* frame from two flips ago, so partial redraws ghost. The SOFTWARE
|
|
607
|
+
* camera (header) means scrolling = redrawing the visible slice.
|
|
608
|
+
* 3. Draw every object (every TGI call and every tgi_sprite() is a
|
|
609
|
+
* synchronous Suzy blit into the SAME draw page).
|
|
610
|
+
* 4. tgi_updatedisplay() — request the page flip at next VBL.
|
|
611
|
+
* 5. sfx_update() IMMEDIATELY after — MIKEY voice writes must land in
|
|
612
|
+
* vblank: handy reschedules its timer sweep on the spot when a
|
|
613
|
+
* voice CTL bit-3 write lands, and mid-frame that sweep can preempt
|
|
614
|
+
* an in-flight Suzy blit and eat sprites (the R57 bug — history in
|
|
615
|
+
* lynx_sfx.c). sfx_tone()/sfx_noise() only STAGE; sfx_update() is
|
|
616
|
+
* the hardware flush. */
|
|
50
617
|
while (tgi_busy()) { }
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
618
|
+
|
|
619
|
+
draw_scene();
|
|
620
|
+
joy = joy_read(JOY_1);
|
|
621
|
+
|
|
622
|
+
if (state == ST_TITLE) frame_title(joy);
|
|
623
|
+
else if (state == ST_PLAY) frame_play(joy);
|
|
624
|
+
else frame_over(joy);
|
|
625
|
+
|
|
59
626
|
tgi_updatedisplay();
|
|
60
627
|
sfx_update();
|
|
61
628
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
grounded = on_platform(px, py);
|
|
65
|
-
|
|
66
|
-
if (JOY_LEFT(joy) && px > 0) px--;
|
|
67
|
-
if (JOY_RIGHT(joy) && px < 154) px++;
|
|
68
|
-
if (btn && !prev && grounded) { vy = -6; sfx_tone(0, 100, 6); }
|
|
69
|
-
prev = btn;
|
|
70
|
-
|
|
71
|
-
{
|
|
72
|
-
/* Land-on-top via a CROSSING test. The old check demanded
|
|
73
|
-
* py+6 == platform.y EXACTLY after the move — falls step up to
|
|
74
|
-
* 4px/frame, so the exact value was usually skipped (fall-through),
|
|
75
|
-
* and the `py & 0xFC` snap then broke the equality for the next
|
|
76
|
-
* frame's grounded test (couldn't jump from floating platforms). */
|
|
77
|
-
int16_t old_py = py;
|
|
78
|
-
uint8_t i;
|
|
79
|
-
vy++;
|
|
80
|
-
if (vy > 4) vy = 4;
|
|
81
|
-
py += vy;
|
|
82
|
-
if (vy > 0) {
|
|
83
|
-
for (i = 0; i < N_PLATFORMS; i++) {
|
|
84
|
-
if (old_py + 6 <= platforms[i].y && py + 6 >= platforms[i].y
|
|
85
|
-
&& px + 6 > platforms[i].x
|
|
86
|
-
&& px < platforms[i].x + platforms[i].w) {
|
|
87
|
-
py = platforms[i].y - 6;
|
|
88
|
-
vy = 0;
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (py < 0) py = 0;
|
|
94
|
-
if (py > 96) py = 96;
|
|
95
|
-
}
|
|
629
|
+
gem_phase = (gem_phase + 4) & 255; /* advance the shared scaling pulse */
|
|
630
|
+
prev_joy = joy;
|
|
96
631
|
}
|
|
97
632
|
}
|