romdevtools 0.28.0 → 0.29.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 +51 -41
- package/CHANGELOG.md +46 -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 +1 -1
- package/src/host/LibretroHost.js +59 -1
- 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/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/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- 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 +4 -3
- 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/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,8 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/* ── shmup.c — Atari Lynx depth-dive shooter (complete example game) ─────────
|
|
2
|
+
*
|
|
3
|
+
* A COMPLETE, working game — title screen, score + lives, in-session
|
|
4
|
+
* hi-score, MIKEY music + SFX, and the Lynx's signature party trick:
|
|
5
|
+
* HARDWARE SPRITE SCALING. Enemies dive at you out of the horizon and
|
|
6
|
+
* Suzy (the blitter) scales them up in HARDWARE as they approach —
|
|
7
|
+
* far = tiny speck, near = looming hull — by changing two 8.8 fixed-point
|
|
8
|
+
* fields in the sprite's control block. No CPU pixel work at all.
|
|
9
|
+
*
|
|
10
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
11
|
+
* very different one. The markers tell you what's what:
|
|
12
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented Lynx footgun;
|
|
13
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
14
|
+
* GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
|
|
15
|
+
*
|
|
16
|
+
* What depends on what:
|
|
17
|
+
* lynx_sfx.{h,c} — MIKEY 4-voice audio (voice 0 = player SFX, voice 1 =
|
|
18
|
+
* background melody, voice 2 = impact SFX, voice 3 = noise/explosions).
|
|
19
|
+
* vendor/cc65/libsrc/lynx/ — the FULL cc65 Lynx driver source shipped into
|
|
20
|
+
* your project. The TGI driver (tgi/lynx-160-102-16.s) is REQUIRED
|
|
21
|
+
* reading when graphics misbehave: every TGI call is itself a Suzy
|
|
22
|
+
* sprite, and our scaled sprites ride the same engine via tgi_ioctl(0).
|
|
23
|
+
*
|
|
24
|
+
* PLAYERS: 1. This is a handheld — multiplayer on real hardware is ComLynx,
|
|
25
|
+
* a cable between TWO Lynx units. A single emulator instance has nobody on
|
|
26
|
+
* the other end of the cable, so this example is honestly single-player
|
|
27
|
+
* (no fake "P2" that could never work).
|
|
28
|
+
*
|
|
29
|
+
* SCREEN: 160x102. The system font is 8x8, so a full row of text is 20
|
|
30
|
+
* characters — keep HUD lines short and the layout compact.
|
|
31
|
+
*/
|
|
6
32
|
|
|
7
33
|
#include <tgi.h>
|
|
8
34
|
#include <joystick.h>
|
|
@@ -10,38 +36,50 @@
|
|
|
10
36
|
#include <stdint.h>
|
|
11
37
|
#include "lynx_sfx.h"
|
|
12
38
|
|
|
13
|
-
|
|
14
|
-
|
|
39
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
40
|
+
* name here automatically. Keep it <=16 chars of A-Z 0-9 space dash. */
|
|
41
|
+
#define GAME_TITLE "VOID PLUNGE"
|
|
15
42
|
|
|
16
|
-
|
|
43
|
+
/* ── GAME LOGIC (clay — reshape freely) — object pools & tuning ───────────── */
|
|
44
|
+
#define MAX_BULLETS 4
|
|
45
|
+
#define MAX_ENEMIES 4
|
|
46
|
+
#define START_LIVES 3
|
|
17
47
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
48
|
+
/* The depth corridor enemies dive through (screen-Y is our depth axis):
|
|
49
|
+
* Y_FAR is the horizon (vanishing band), Y_NEAR is "in your face". */
|
|
50
|
+
#define Y_FAR 22
|
|
51
|
+
#define Y_NEAR 97
|
|
52
|
+
#define DEPTH_SPAN (Y_NEAR - Y_FAR) /* 75 px of travel */
|
|
23
53
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
/* Suzy scale (8.8 fixed point: $0100 = 1.0 = one screen pixel per texel).
|
|
55
|
+
* The 8x8 art renders 2 px wide at the horizon and 20 px wide up close —
|
|
56
|
+
* a 10x growth you can't miss, and the hardware does ALL of it. */
|
|
57
|
+
#define SCALE_FAR 0x0040u /* 0.25x → 2 px */
|
|
58
|
+
#define SCALE_NEAR 0x0280u /* 2.50x → 20 px */
|
|
59
|
+
#define SHIP_SCALE 0x0200u /* your ship: fixed 2x */
|
|
28
60
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
61
|
+
typedef struct { uint8_t alive; uint8_t lane; unsigned y_fp; } Enemy;
|
|
62
|
+
typedef struct { uint8_t alive; uint8_t x, y; } Bullet;
|
|
63
|
+
|
|
64
|
+
static Enemy enemies[MAX_ENEMIES];
|
|
65
|
+
static Bullet bullets[MAX_BULLETS];
|
|
66
|
+
static uint8_t ship_x, ship_y; /* ship CENTER (sprites draw centered) */
|
|
67
|
+
static uint8_t lives, level, kills;
|
|
68
|
+
static unsigned score;
|
|
69
|
+
static unsigned hiscore; /* in-session only — see EEPROM note */
|
|
70
|
+
static unsigned enemy_speed; /* 8.8 px/frame down the corridor */
|
|
71
|
+
static uint8_t spawn_interval, spawn_timer;
|
|
72
|
+
static uint8_t fire_cd, hurt_timer;
|
|
73
|
+
static uint8_t prev_joy;
|
|
40
74
|
|
|
41
|
-
/*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
76
|
+
#define ST_TITLE 0
|
|
77
|
+
#define ST_PLAY 1
|
|
78
|
+
#define ST_OVER 2
|
|
79
|
+
static uint8_t state;
|
|
80
|
+
static uint8_t over_new_hi;
|
|
81
|
+
|
|
82
|
+
/* ── GAME LOGIC (clay) — Galois LFSR (taps $B8), period 255 ── */
|
|
45
83
|
static uint8_t rng_state = 0xA5;
|
|
46
84
|
static uint8_t rand8(void) {
|
|
47
85
|
uint8_t lsb = (uint8_t)(rng_state & 1);
|
|
@@ -50,134 +88,423 @@ static uint8_t rand8(void) {
|
|
|
50
88
|
return rng_state;
|
|
51
89
|
}
|
|
52
90
|
|
|
53
|
-
|
|
91
|
+
/* Scrolling starfield so the dark space field is never one flat colour
|
|
92
|
+
* (a >=92% single-colour frame trips the render-health audit as "blank"). */
|
|
93
|
+
#define N_STARS 24
|
|
94
|
+
static uint8_t star_x[N_STARS];
|
|
95
|
+
static uint8_t star_y[N_STARS];
|
|
96
|
+
|
|
97
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
98
|
+
* SUZY HARDWARE SPRITE SCALING — the Lynx signature. Suzy renders every
|
|
99
|
+
* sprite through a Sprite Control Block (SCB) it walks in cart/work RAM.
|
|
100
|
+
* Two SCB fields, HSIZE and VSIZE, are 8.8 fixed-point scale factors
|
|
101
|
+
* ($0100 = 1.0): the SAME 8x8 source pixels render at any size, every
|
|
102
|
+
* frame, for free. That is this whole game's depth illusion.
|
|
103
|
+
*
|
|
104
|
+
* The SCB, field by field (this is cc65's SCB_REHV_PAL from <_suzy.h>):
|
|
105
|
+
* sprctl0 %BBxx
|
|
106
|
+
* bits 7-6 = bits per pixel (11 = 4bpp), bits 2-0 = sprite TYPE.
|
|
107
|
+
* TYPE_NORMAL (4) draws pens 1-15 and treats pen 0 as
|
|
108
|
+
* TRANSPARENT — that's how shaped sprites sit over the field.
|
|
109
|
+
* sprctl1 bit 7 LITERAL (data below is raw nybbles, no RLE packets) +
|
|
110
|
+
* bits 5-4 reload depth: REHV means "this SCB carries HPOS,
|
|
111
|
+
* VPOS, HSIZE, VSIZE". The reload bits ARE the struct layout —
|
|
112
|
+
* mismatch them and Suzy reads palette bytes as size words.
|
|
113
|
+
* sprcoll $20 = NO_COLLIDE. We do gameplay collision in C (in DEPTH
|
|
114
|
+
* coordinates, which the collision buffer knows nothing about).
|
|
115
|
+
* next pointer to the next SCB, 0 = end of chain. One blit per call
|
|
116
|
+
* here; chain SCBs and one SPRGO draws them all.
|
|
117
|
+
* data sprite pixel data (format below).
|
|
118
|
+
* hpos/vpos signed SCREEN position of the sprite's top-left corner.
|
|
119
|
+
* hsize/vsize 8.8 scale — THE party trick. We recompute these every
|
|
120
|
+
* frame from each enemy's depth.
|
|
121
|
+
* penpal[8] 16 nybbles mapping pixel values 0-15 → palette pens
|
|
122
|
+
* (identity here; sprite art can be recoloured per-SCB for free
|
|
123
|
+
* — e.g. one art block, four enemy colours).
|
|
124
|
+
*
|
|
125
|
+
* LITERAL 4bpp data format (hand-encodable): each sprite LINE is
|
|
126
|
+
* [offset byte][width/2 bytes of raw nybble pixels]
|
|
127
|
+
* where offset = 1 + bytes of pixel data (Suzy adds it to find the next
|
|
128
|
+
* line), and a final offset of 0 ends the sprite. 8 px @ 4bpp = 4 data
|
|
129
|
+
* bytes, so every line starts with 5. (The packed/RLE format is what
|
|
130
|
+
* sprpck emits; literal is friendlier to author by hand.)
|
|
131
|
+
*
|
|
132
|
+
* Drawing: tgi_sprite(&scb) → tgi_ioctl(0, &scb) — the TGI driver's
|
|
133
|
+
* documented escape hatch (see CONTROL in vendor/cc65/libsrc/lynx/tgi/
|
|
134
|
+
* lynx-160-102-16.s). It points Suzy's SCBNEXT at your SCB, aims VIDBAS
|
|
135
|
+
* at TGI's current DRAW page (so scaled sprites land in the same
|
|
136
|
+
* double-buffered frame as tgi_bar/tgi_outtextxy), fires SPRGO, and
|
|
137
|
+
* sleeps the CPU until SPRSYS reports the blit done.
|
|
138
|
+
*
|
|
139
|
+
* Requires: the cc65 crt0 Suzy init (SUZYBUSEN=1, SPRSYS, HOFF/VOFF=0 —
|
|
140
|
+
* already done before main()), and calls only between the tgi_busy()
|
|
141
|
+
* wait and tgi_updatedisplay() — i.e. while TGI's draw buffer is the
|
|
142
|
+
* blit target. Draw order = paint order: background bars first, scaled
|
|
143
|
+
* sprites after, HUD text last.
|
|
144
|
+
*/
|
|
145
|
+
static SCB_REHV_PAL scb = {
|
|
146
|
+
BPP_4 | TYPE_NORMAL, /* sprctl0: 4bpp, pen 0 transparent */
|
|
147
|
+
LITERAL | REHV, /* sprctl1: literal data, HV+size SCB */
|
|
148
|
+
0x20, /* sprcoll: NO_COLLIDE */
|
|
149
|
+
0, /* next: single-SCB chain */
|
|
150
|
+
0, /* data: set per draw */
|
|
151
|
+
0, 0, /* hpos, vpos */
|
|
152
|
+
0x0100, 0x0100, /* hsize, vsize (8.8) */
|
|
153
|
+
{ 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF } /* identity pens */
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/* Draw an 8x8 literal sprite CENTERED on (cx,cy) at the given 8.8 scale.
|
|
157
|
+
* Centering matters: hpos/vpos are the TOP-LEFT, so a sprite scaled around
|
|
158
|
+
* its corner would slide right/down as it grows. Anchoring the centre keeps
|
|
159
|
+
* the dive reading as "coming straight at you". */
|
|
160
|
+
static void draw_scaled(unsigned char *data, int cx, int cy, unsigned scale) {
|
|
161
|
+
unsigned w = scale >> 5; /* on-screen size: (8 * scale) >> 8 */
|
|
162
|
+
if (w == 0) w = 1;
|
|
163
|
+
scb.data = data;
|
|
164
|
+
scb.hsize = scale;
|
|
165
|
+
scb.vsize = scale;
|
|
166
|
+
scb.hpos = cx - (int)(w >> 1);
|
|
167
|
+
scb.vpos = cy - (int)(w >> 1);
|
|
168
|
+
tgi_sprite(&scb);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* ── GAME LOGIC (clay) — 8x8 4bpp literal sprite art ────────────────────────
|
|
172
|
+
* Pens use the TGI default palette: 2 = red, 3 = pink, 9 = yellow,
|
|
173
|
+
* $D = blue, $F = white, 0 = transparent. Each line: 5, then 4 nybble
|
|
174
|
+
* bytes; final 0 byte ends the sprite (format in the idiom block above). */
|
|
175
|
+
static unsigned char spr_enemy[] = {
|
|
176
|
+
5, 0x00, 0x02, 0x20, 0x00, /* . . . 2 2 . . . red diver, pink core */
|
|
177
|
+
5, 0x00, 0x23, 0x32, 0x00, /* . . 2 3 3 2 . . */
|
|
178
|
+
5, 0x02, 0x3F, 0xF3, 0x20, /* . 2 3 F F 3 2 . */
|
|
179
|
+
5, 0x22, 0x3F, 0xF3, 0x22, /* 2 2 3 F F 3 2 2 */
|
|
180
|
+
5, 0x23, 0x33, 0x33, 0x32, /* 2 3 3 3 3 3 3 2 */
|
|
181
|
+
5, 0x02, 0x23, 0x32, 0x20, /* . 2 2 3 3 2 2 . */
|
|
182
|
+
5, 0x00, 0x22, 0x22, 0x00, /* . . 2 2 2 2 . . */
|
|
183
|
+
5, 0x00, 0x02, 0x20, 0x00, /* . . . 2 2 . . . */
|
|
184
|
+
0
|
|
185
|
+
};
|
|
186
|
+
static unsigned char spr_ship[] = {
|
|
187
|
+
5, 0x00, 0x0F, 0xF0, 0x00, /* . . . F F . . . yellow interceptor */
|
|
188
|
+
5, 0x00, 0x09, 0x90, 0x00, /* . . . 9 9 . . . */
|
|
189
|
+
5, 0x00, 0x99, 0x99, 0x00, /* . . 9 9 9 9 . . */
|
|
190
|
+
5, 0x00, 0x9D, 0xD9, 0x00, /* . . 9 D D 9 . . */
|
|
191
|
+
5, 0x09, 0x9D, 0xD9, 0x90, /* . 9 9 D D 9 9 . */
|
|
192
|
+
5, 0x99, 0x99, 0x99, 0x99, /* 9 9 9 9 9 9 9 9 */
|
|
193
|
+
5, 0x90, 0x9D, 0xD9, 0x09, /* 9 . 9 D D 9 . 9 */
|
|
194
|
+
5, 0x00, 0xD0, 0x0D, 0x00, /* . . D . . D . . */
|
|
195
|
+
0
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/* ── GAME LOGIC (clay) — depth → screen mapping ─────────────────────────────
|
|
199
|
+
* Screen-Y doubles as the depth axis: an enemy at the horizon (Y_FAR) is
|
|
200
|
+
* far away; at Y_NEAR it has reached you. Scale and X both interpolate on
|
|
201
|
+
* the same depth fraction, so divers fan OUT of the vanishing point toward
|
|
202
|
+
* their lane while they grow — a poor man's perspective projection. */
|
|
203
|
+
static unsigned scale_for_y(uint8_t y) {
|
|
204
|
+
unsigned span = (unsigned)(y - Y_FAR);
|
|
205
|
+
return SCALE_FAR + (span * (SCALE_NEAR - SCALE_FAR)) / DEPTH_SPAN;
|
|
206
|
+
}
|
|
207
|
+
static uint8_t enemy_screen_x(const Enemy *e) {
|
|
208
|
+
uint8_t y = (uint8_t)(e->y_fp >> 8);
|
|
209
|
+
int span = (int)(y - Y_FAR);
|
|
210
|
+
return (uint8_t)(80 + ((int)(e->lane - 80) * span) / DEPTH_SPAN);
|
|
211
|
+
}
|
|
212
|
+
/* Current half-width in pixels (collision box tracks the HARDWARE scale —
|
|
213
|
+
* a far speck is genuinely harder to hit than a looming hull). */
|
|
214
|
+
static uint8_t enemy_half(const Enemy *e) {
|
|
215
|
+
return (uint8_t)(scale_for_y((uint8_t)(e->y_fp >> 8)) >> 6); /* (w/2) */
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
static void spawn_enemy(void) {
|
|
54
219
|
uint8_t i;
|
|
55
220
|
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
56
221
|
if (!enemies[i].alive) {
|
|
57
|
-
enemies[i].x = (uint8_t)(8 + (rand8() % (160 - 16)));
|
|
58
|
-
enemies[i].y = 0;
|
|
59
222
|
enemies[i].alive = 1;
|
|
223
|
+
enemies[i].lane = (uint8_t)(14 + (rand8() % 132)); /* target column */
|
|
224
|
+
enemies[i].y_fp = (unsigned)Y_FAR << 8;
|
|
60
225
|
return;
|
|
61
226
|
}
|
|
62
227
|
}
|
|
63
228
|
}
|
|
64
229
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
230
|
+
static void fire_bullet(void) {
|
|
231
|
+
uint8_t i;
|
|
232
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
233
|
+
if (!bullets[i].alive) {
|
|
234
|
+
bullets[i].alive = 1;
|
|
235
|
+
bullets[i].x = ship_x;
|
|
236
|
+
bullets[i].y = ship_y - 8;
|
|
237
|
+
sfx_tone(0, 70, 4); /* voice 0: pew */
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* ── GAME LOGIC (clay) — score text (no sprintf: it drags in ~6KB) ── */
|
|
244
|
+
static char numbuf[6];
|
|
245
|
+
static char *fmt5(unsigned v) {
|
|
246
|
+
uint8_t i;
|
|
247
|
+
for (i = 0; i < 5; i++) { numbuf[4 - i] = (char)('0' + v % 10); v /= 10; }
|
|
248
|
+
numbuf[5] = 0;
|
|
249
|
+
return numbuf;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ── GAME LOGIC (clay) — shared scene painter (runs every frame) ────────────
|
|
253
|
+
* Full-redraw, painter's order: space field, horizon bands, stars, then the
|
|
254
|
+
* caller layers sprites + text on top. Layered bands keep any one colour
|
|
255
|
+
* comfortably under the render-health blank threshold. */
|
|
256
|
+
static void draw_scene(void) {
|
|
257
|
+
uint8_t i;
|
|
258
|
+
tgi_setcolor(COLOR_BLACK);
|
|
259
|
+
tgi_bar(0, 0, 159, 101); /* deep space */
|
|
260
|
+
tgi_setcolor(COLOR_DARKGREY);
|
|
261
|
+
tgi_bar(0, 0, 159, 8); /* HUD bar */
|
|
262
|
+
tgi_bar(0, Y_FAR - 4, 159, Y_FAR - 3); /* horizon glow, outer */
|
|
263
|
+
tgi_setcolor(COLOR_PURPLE);
|
|
264
|
+
tgi_bar(0, Y_FAR - 2, 159, Y_FAR - 1); /* horizon glow, inner */
|
|
265
|
+
tgi_setcolor(COLOR_WHITE);
|
|
266
|
+
for (i = 0; i < N_STARS; i++) {
|
|
267
|
+
tgi_setpixel(star_x[i], star_y[i]);
|
|
268
|
+
tgi_setpixel(star_x[i], (uint8_t)((star_y[i] + 1) % 102));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
static void drift_stars(void) {
|
|
272
|
+
uint8_t i;
|
|
273
|
+
for (i = 0; i < N_STARS; i++) {
|
|
274
|
+
if (star_y[i] >= 101) { star_y[i] = Y_FAR; star_x[i] = (uint8_t)(rand8() % 160); }
|
|
275
|
+
else star_y[i]++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
280
|
+
static void start_game(void) {
|
|
281
|
+
uint8_t i;
|
|
282
|
+
for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
|
|
283
|
+
for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
|
|
284
|
+
ship_x = 80; ship_y = 92;
|
|
285
|
+
lives = START_LIVES; level = 1; kills = 0;
|
|
286
|
+
score = 0;
|
|
287
|
+
enemy_speed = 0x00B0; /* 0.69 px/frame — ~109-frame dives */
|
|
288
|
+
spawn_interval = 120; /* level 1: one diver at a time */
|
|
289
|
+
spawn_timer = 30;
|
|
290
|
+
fire_cd = 0; hurt_timer = 0;
|
|
291
|
+
state = ST_PLAY;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
static void game_over(void) {
|
|
295
|
+
over_new_hi = 0;
|
|
296
|
+
if (score > hiscore) {
|
|
297
|
+
/* ── In-session hi-score ONLY — and here's the honest why. Real Lynx
|
|
298
|
+
* carts persist via a 93Cxx serial EEPROM on the cart PCB (cc65 even
|
|
299
|
+
* ships lynx_eeprom_read/write for it — bit-banged over A7/A1/AUDIN;
|
|
300
|
+
* see vendor/cc65/libsrc/lynx/eeprom.s). PROBED 2026-06: the bundled
|
|
301
|
+
* handy core emulates CEEPROM internally but its libretro build
|
|
302
|
+
* exposes NO save path — retro_get_memory(SAVE_RAM) returns
|
|
303
|
+
* NULL/size 0, so nothing can survive host.hardReset(), and the
|
|
304
|
+
* bit-banged round-trip reads back garbage under the WASM build.
|
|
305
|
+
* Wiring the EEPROM to SAVE_RAM is a future core round; until then a
|
|
306
|
+
* fake "save" would be lying. The hi-score DOES survive title↔play
|
|
307
|
+
* cycles within one power-on. ── */
|
|
308
|
+
hiscore = score;
|
|
309
|
+
over_new_hi = 1;
|
|
310
|
+
}
|
|
311
|
+
sfx_tone(2, 240, 24); /* voice 2: low game-over drone */
|
|
312
|
+
state = ST_OVER;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* ── GAME LOGIC (clay) — per-state frames. Each runs INSIDE the canonical
|
|
316
|
+
* loop below: scene already painted, tgi_updatedisplay not yet called. ── */
|
|
317
|
+
|
|
318
|
+
static unsigned attract_y_fp = (unsigned)Y_FAR << 8;
|
|
319
|
+
static uint8_t attract_lane = 120;
|
|
320
|
+
|
|
321
|
+
static void frame_title(uint8_t joy) {
|
|
322
|
+
uint8_t ty;
|
|
323
|
+
/* Attract demo: one enemy dives on a loop — the scaling idiom IS the
|
|
324
|
+
* title screen's pitch. */
|
|
325
|
+
attract_y_fp += 0x00C0;
|
|
326
|
+
ty = (uint8_t)(attract_y_fp >> 8);
|
|
327
|
+
if (ty >= Y_NEAR) { attract_y_fp = (unsigned)Y_FAR << 8; attract_lane = (uint8_t)(30 + (rand8() % 100)); }
|
|
328
|
+
else {
|
|
329
|
+
Enemy demo;
|
|
330
|
+
demo.lane = attract_lane; demo.y_fp = attract_y_fp;
|
|
331
|
+
draw_scaled(spr_enemy, enemy_screen_x(&demo), ty, scale_for_y(ty));
|
|
332
|
+
}
|
|
333
|
+
draw_scaled(spr_ship, 80, 92, SHIP_SCALE);
|
|
334
|
+
|
|
335
|
+
tgi_setcolor(COLOR_WHITE);
|
|
336
|
+
tgi_outtextxy(36, 1, GAME_TITLE); /* on the HUD bar */
|
|
337
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
338
|
+
tgi_outtextxy(48, 38, "PRESS A");
|
|
339
|
+
tgi_setcolor(COLOR_LIGHTGREY);
|
|
340
|
+
tgi_outtextxy(28, 50, "HI ");
|
|
341
|
+
tgi_outtextxy(52, 50, fmt5(hiscore));
|
|
342
|
+
tgi_outtextxy(24, 62, "1 PLAYER GAME"); /* handheld honesty */
|
|
343
|
+
|
|
344
|
+
if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) start_game();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
static void frame_over(uint8_t joy) {
|
|
348
|
+
tgi_setcolor(COLOR_DARKGREY);
|
|
349
|
+
tgi_bar(20, 34, 139, 70);
|
|
350
|
+
tgi_setcolor(COLOR_WHITE);
|
|
351
|
+
tgi_outtextxy(44, 38, "GAME OVER");
|
|
352
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
353
|
+
tgi_outtextxy(36, 48, "SCORE ");
|
|
354
|
+
tgi_outtextxy(84, 48, fmt5(score));
|
|
355
|
+
if (over_new_hi) { tgi_setcolor(COLOR_LIGHTGREEN); tgi_outtextxy(32, 58, "NEW HI SCORE"); }
|
|
356
|
+
else { tgi_setcolor(COLOR_LIGHTGREY); tgi_outtextxy(36, 58, "A = TITLE"); }
|
|
357
|
+
if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) state = ST_TITLE;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
static void frame_play(uint8_t joy) {
|
|
361
|
+
uint8_t i, j, ex, ey, hw;
|
|
362
|
+
|
|
363
|
+
/* ── draw: enemies (each rescaled from its depth EVERY frame), ship,
|
|
364
|
+
* bullets, HUD ── */
|
|
365
|
+
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
366
|
+
if (!enemies[i].alive) continue;
|
|
367
|
+
ey = (uint8_t)(enemies[i].y_fp >> 8);
|
|
368
|
+
draw_scaled(spr_enemy, enemy_screen_x(&enemies[i]), ey, scale_for_y(ey));
|
|
369
|
+
}
|
|
370
|
+
if (hurt_timer == 0 || (hurt_timer & 4)) /* blink while hurt */
|
|
371
|
+
draw_scaled(spr_ship, ship_x, ship_y, SHIP_SCALE);
|
|
372
|
+
tgi_setcolor(COLOR_WHITE);
|
|
373
|
+
for (i = 0; i < MAX_BULLETS; i++)
|
|
374
|
+
if (bullets[i].alive)
|
|
375
|
+
tgi_bar(bullets[i].x - 1, bullets[i].y - 2, bullets[i].x, bullets[i].y + 1);
|
|
376
|
+
|
|
377
|
+
tgi_setcolor(COLOR_WHITE);
|
|
378
|
+
tgi_outtextxy(2, 1, "SC");
|
|
379
|
+
tgi_outtextxy(20, 1, fmt5(score));
|
|
380
|
+
tgi_setcolor(COLOR_LIGHTGREY);
|
|
381
|
+
tgi_outtextxy(66, 1, "HI");
|
|
382
|
+
tgi_outtextxy(84, 1, fmt5(hiscore));
|
|
383
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
384
|
+
tgi_outtextxy(132, 1, "L");
|
|
385
|
+
numbuf[0] = (char)('0' + lives); numbuf[1] = 0;
|
|
386
|
+
tgi_outtextxy(140, 1, numbuf);
|
|
387
|
+
|
|
388
|
+
/* ── update: ship ── */
|
|
389
|
+
if ((joy & JOY_LEFT_MASK) && ship_x > 9) ship_x -= 2;
|
|
390
|
+
if ((joy & JOY_RIGHT_MASK) && ship_x < 150) ship_x += 2;
|
|
391
|
+
if ((joy & JOY_UP_MASK) && ship_y > 70) ship_y--;
|
|
392
|
+
if ((joy & JOY_DOWN_MASK) && ship_y < 96) ship_y++;
|
|
393
|
+
if (JOY_BTN_1(joy) && fire_cd == 0) { fire_bullet(); fire_cd = 8; }
|
|
394
|
+
if (fire_cd) fire_cd--;
|
|
395
|
+
if (hurt_timer) hurt_timer--;
|
|
396
|
+
|
|
397
|
+
/* bullets fly "away" up the corridor */
|
|
398
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
399
|
+
if (!bullets[i].alive) continue;
|
|
400
|
+
if (bullets[i].y < Y_FAR + 3) { bullets[i].alive = 0; continue; }
|
|
401
|
+
bullets[i].y -= 3;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* enemies dive (subpixel 8.8 speed) */
|
|
405
|
+
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
406
|
+
if (!enemies[i].alive) continue;
|
|
407
|
+
enemies[i].y_fp += enemy_speed;
|
|
408
|
+
ey = (uint8_t)(enemies[i].y_fp >> 8);
|
|
409
|
+
if (ey >= Y_NEAR) {
|
|
410
|
+
/* Reached your depth plane: ram you, or whoosh past. */
|
|
411
|
+
ex = enemy_screen_x(&enemies[i]);
|
|
412
|
+
hw = enemy_half(&enemies[i]);
|
|
413
|
+
enemies[i].alive = 0;
|
|
414
|
+
if (hurt_timer == 0
|
|
415
|
+
&& (uint8_t)(ex > ship_x ? ex - ship_x : ship_x - ex) < hw + 7
|
|
416
|
+
&& (uint8_t)(ey > ship_y ? ey - ship_y : ship_y - ey) < hw + 6) {
|
|
417
|
+
sfx_tone(2, 220, 10); /* voice 2: thump */
|
|
418
|
+
sfx_noise(12); /* voice 3: crunch */
|
|
419
|
+
hurt_timer = 45;
|
|
420
|
+
if (lives) lives--;
|
|
421
|
+
if (lives == 0) { game_over(); return; }
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* bullets vs enemies — in the SCALED box: the hitbox grows with the
|
|
427
|
+
* hardware sprite, so range determines difficulty (far 3pt speck, mid
|
|
428
|
+
* 2pt, near 1pt barn door). */
|
|
429
|
+
for (i = 0; i < MAX_BULLETS; i++) {
|
|
430
|
+
if (!bullets[i].alive) continue;
|
|
431
|
+
for (j = 0; j < MAX_ENEMIES; j++) {
|
|
432
|
+
if (!enemies[j].alive) continue;
|
|
433
|
+
ex = enemy_screen_x(&enemies[j]);
|
|
434
|
+
ey = (uint8_t)(enemies[j].y_fp >> 8);
|
|
435
|
+
hw = enemy_half(&enemies[j]);
|
|
436
|
+
if ((uint8_t)(bullets[i].x > ex ? bullets[i].x - ex : ex - bullets[i].x) < hw + 2
|
|
437
|
+
&& (uint8_t)(bullets[i].y > ey ? bullets[i].y - ey : ey - bullets[i].y) < hw + 3) {
|
|
438
|
+
bullets[i].alive = 0;
|
|
439
|
+
enemies[j].alive = 0;
|
|
440
|
+
sfx_noise(8); /* voice 3: boom */
|
|
441
|
+
score += (ey < Y_FAR + 25) ? 3 : (ey < Y_FAR + 50) ? 2 : 1;
|
|
442
|
+
kills++;
|
|
443
|
+
if (kills >= 10) { /* level ramp */
|
|
444
|
+
kills = 0;
|
|
445
|
+
level++;
|
|
446
|
+
if (enemy_speed < 0x0200) enemy_speed += 0x18;
|
|
447
|
+
if (spawn_interval > 40) spawn_interval -= 10;
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (spawn_timer == 0) { spawn_timer = spawn_interval; spawn_enemy(); }
|
|
455
|
+
else spawn_timer--;
|
|
456
|
+
}
|
|
70
457
|
|
|
71
458
|
void main(void) {
|
|
72
|
-
uint8_t joy,
|
|
459
|
+
uint8_t joy, i;
|
|
73
460
|
uint32_t srng = 0x1234;
|
|
74
461
|
|
|
75
462
|
tgi_install(&lynx_160_102_16_tgi);
|
|
76
463
|
tgi_init();
|
|
77
464
|
joy_install(&lynx_stdjoy_joy);
|
|
78
|
-
sfx_init();
|
|
465
|
+
sfx_init(); /* MIKEY up; background melody starts on voice 1 */
|
|
79
466
|
|
|
80
|
-
player.x = 76; player.y = 90; player.alive = 1;
|
|
81
|
-
for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
|
|
82
|
-
for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
|
|
83
467
|
for (i = 0; i < N_STARS; i++) {
|
|
84
468
|
srng = srng * 1103515245u + 12345u;
|
|
85
469
|
star_x[i] = (uint8_t)((srng >> 16) % 160);
|
|
86
470
|
srng = srng * 1103515245u + 12345u;
|
|
87
|
-
star_y[i] = (uint8_t)((srng >> 16) % 102);
|
|
471
|
+
star_y[i] = (uint8_t)(Y_FAR + ((srng >> 16) % (102 - Y_FAR)));
|
|
88
472
|
}
|
|
89
|
-
|
|
90
|
-
|
|
473
|
+
state = ST_TITLE;
|
|
474
|
+
prev_joy = 0;
|
|
91
475
|
|
|
92
476
|
for (;;) {
|
|
93
|
-
/*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* Skipping this is the #1 "Lynx screen stays blank" trap
|
|
97
|
-
* while the
|
|
98
|
-
* 2.
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
477
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
478
|
+
* CANONICAL LYNX GAME LOOP — full-redraw every frame, in this order:
|
|
479
|
+
* 1. while (tgi_busy()) { } — WAIT for the previous frame's page
|
|
480
|
+
* flip. Skipping this is the #1 "Lynx screen stays blank" trap:
|
|
481
|
+
* drawing while the swap is pending loses the frame.
|
|
482
|
+
* 2. Repaint the WHOLE scene with tgi_bar fills — NOT tgi_clear()
|
|
483
|
+
* (which can leave the framebuffer stale on this toolchain+
|
|
484
|
+
* emulator path). TGI double-buffers; the back buffer holds the
|
|
485
|
+
* frame from two flips ago, so partial redraws ghost.
|
|
486
|
+
* 3. Draw every object (every TGI call and every tgi_sprite() is a
|
|
487
|
+
* synchronous Suzy blit into the SAME draw page).
|
|
488
|
+
* 4. tgi_updatedisplay() — request the page flip at next VBL.
|
|
489
|
+
* 5. sfx_update() IMMEDIATELY after — MIKEY voice writes must land
|
|
490
|
+
* in vblank: handy reschedules its timer sweep on the spot when
|
|
491
|
+
* a voice CTL bit-3 write lands, and mid-frame that sweep can
|
|
492
|
+
* preempt an in-flight Suzy blit and eat sprites (the R57 bug —
|
|
493
|
+
* history in lynx_sfx.c). sfx_tone()/sfx_noise() only STAGE;
|
|
494
|
+
* sfx_update() is the hardware flush. */
|
|
103
495
|
while (tgi_busy()) { }
|
|
104
496
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
* - green planet surface along the bottom
|
|
112
|
-
* - a drifting white/yellow starfield over the space. */
|
|
113
|
-
tgi_setcolor(COLOR_BLUE);
|
|
114
|
-
tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy()); /* base space field */
|
|
115
|
-
tgi_setcolor(COLOR_GREY);
|
|
116
|
-
tgi_bar(0, 34, 159, 60); /* nebula band */
|
|
117
|
-
tgi_setcolor(COLOR_GREEN);
|
|
118
|
-
tgi_bar(0, 84, 159, 101); /* planet surface */
|
|
119
|
-
tgi_setcolor(COLOR_LIGHTGREEN);
|
|
120
|
-
tgi_bar(0, 78, 159, 83); /* surface horizon */
|
|
121
|
-
/* starfield (bright specks; also drifts downward each frame) */
|
|
122
|
-
tgi_setcolor(COLOR_WHITE);
|
|
123
|
-
for (i = 0; i < N_STARS; i++) {
|
|
124
|
-
tgi_setpixel(star_x[i], star_y[i]);
|
|
125
|
-
tgi_setpixel(star_x[i], (star_y[i] + 1) % 102);
|
|
126
|
-
}
|
|
497
|
+
draw_scene();
|
|
498
|
+
joy = joy_read(JOY_1);
|
|
499
|
+
|
|
500
|
+
if (state == ST_TITLE) frame_title(joy);
|
|
501
|
+
else if (state == ST_PLAY) frame_play(joy);
|
|
502
|
+
else frame_over(joy);
|
|
127
503
|
|
|
128
|
-
/* Render game objects on top */
|
|
129
|
-
tgi_setcolor(COLOR_YELLOW);
|
|
130
|
-
tgi_bar(player.x, player.y, player.x + 6, player.y + 6);
|
|
131
|
-
tgi_setcolor(COLOR_WHITE);
|
|
132
|
-
for (i = 0; i < MAX_BULLETS; i++) {
|
|
133
|
-
if (bullets[i].alive) tgi_bar(bullets[i].x, bullets[i].y, bullets[i].x + 2, bullets[i].y + 4);
|
|
134
|
-
}
|
|
135
|
-
tgi_setcolor(COLOR_RED);
|
|
136
|
-
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
137
|
-
if (enemies[i].alive) tgi_bar(enemies[i].x, enemies[i].y, enemies[i].x + 6, enemies[i].y + 6);
|
|
138
|
-
}
|
|
139
504
|
tgi_updatedisplay();
|
|
140
505
|
sfx_update();
|
|
141
506
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (star_y[i] >= 101) star_y[i] = 0; else star_y[i]++;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/* Input + state */
|
|
148
|
-
joy = joy_read(JOY_1);
|
|
149
|
-
fire_now = JOY_BTN_1(joy) ? 1 : 0;
|
|
150
|
-
if (JOY_LEFT(joy) && player.x > 0) player.x--;
|
|
151
|
-
if (JOY_RIGHT(joy) && player.x < 154) player.x++;
|
|
152
|
-
if (JOY_UP(joy) && player.y > 8) player.y--;
|
|
153
|
-
if (JOY_DOWN(joy) && player.y < 96) player.y++;
|
|
154
|
-
if (fire_now && !prev_btn) { fire(); sfx_tone(0, 80, 4); }
|
|
155
|
-
prev_btn = fire_now;
|
|
156
|
-
|
|
157
|
-
for (i = 0; i < MAX_BULLETS; i++) {
|
|
158
|
-
if (!bullets[i].alive) continue;
|
|
159
|
-
if (bullets[i].y < 2) { bullets[i].alive = 0; continue; }
|
|
160
|
-
bullets[i].y -= 3;
|
|
161
|
-
}
|
|
162
|
-
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
163
|
-
if (!enemies[i].alive) continue;
|
|
164
|
-
enemies[i].y++;
|
|
165
|
-
if (enemies[i].y >= 102) enemies[i].alive = 0;
|
|
166
|
-
}
|
|
167
|
-
spawn_timer++;
|
|
168
|
-
if (spawn_timer >= 28) { spawn_timer = 0; spawn(); }
|
|
169
|
-
|
|
170
|
-
for (i = 0; i < MAX_BULLETS; i++) {
|
|
171
|
-
if (!bullets[i].alive) continue;
|
|
172
|
-
for (j = 0; j < MAX_ENEMIES; j++) {
|
|
173
|
-
if (!enemies[j].alive) continue;
|
|
174
|
-
if (aabb(&bullets[i], &enemies[j])) {
|
|
175
|
-
bullets[i].alive = 0;
|
|
176
|
-
enemies[j].alive = 0;
|
|
177
|
-
sfx_noise(8);
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
507
|
+
drift_stars();
|
|
508
|
+
prev_joy = joy;
|
|
182
509
|
}
|
|
183
510
|
}
|