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,169 +1,335 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* PC Engine "shmup" — a vertical shoot-'em-up scaffold.
|
|
1
|
+
/* ── main.c — PC Engine vertical shooter (complete example game) ─────────────
|
|
3
2
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* A COMPLETE, working game — title screen, lives, score + persistent hi-score
|
|
4
|
+
* (in-session — a bare HuCard can't save), music + SFX, enemy waves, and the PCE's signature
|
|
5
|
+
* hardware feature: LARGE MULTI-SPRITE OBJECTS. The boss is a 64x32 war
|
|
6
|
+
* machine built from two 32x32 HuC6270 sprites that move as one unit — the
|
|
7
|
+
* kind of object that needs 8+ hardware sprites on the NES and exactly TWO
|
|
8
|
+
* SATB entries here.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - 64-sprite shadow SATB + satb_dma()
|
|
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 PCE footgun; reshape
|
|
13
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
14
|
+
* GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* opaque starfield BG.
|
|
16
|
+
* What depends on what:
|
|
17
|
+
* pce_hw.h / pce_video.c / pce_input.c / pce_sound.c — the helper lib
|
|
18
|
+
* (VDC/VCE/PSG register dances + joypad). The HARDWARE IDIOM markers in
|
|
19
|
+
* pce_video.c say which parts are load-bearing.
|
|
20
|
+
* cc65's pce crt0 + pce.lib are auto-linked; the 'rom32k' linker preset
|
|
21
|
+
* (applied automatically to example projects) gives a 32KB HuCard.
|
|
24
22
|
*
|
|
25
|
-
*
|
|
23
|
+
* SINGLE PLAYER, honestly: the stock PC Engine has ONE controller port;
|
|
24
|
+
* 2P needs a TurboTap. The geargrafx core implements the TurboTap but ships
|
|
25
|
+
* with it disabled (a core option, no headless override today), so a second
|
|
26
|
+
* pad's input never reaches the game — verified by scanning all 5 multitap
|
|
27
|
+
* slots while driving port-1 input (force-enabling geargrafx_turbotap DOES
|
|
28
|
+
* deliver pad 2, so a future host core-option round can unlock PCE 2P).
|
|
29
|
+
* This game is therefore 1P by design.
|
|
30
|
+
*
|
|
31
|
+
* Frame budget (NTSC, 60fps, 7.16MHz 65C02-class CPU): the whole update
|
|
32
|
+
* (6 bullets × 6 enemies + 6 × boss AABB ≈ 42 checks worst case, plus a
|
|
33
|
+
* 256-word SATB copy in vblank) fits comfortably inside one frame.
|
|
26
34
|
*/
|
|
27
35
|
#include <pce.h>
|
|
28
36
|
#include "pce_hw.h"
|
|
29
37
|
|
|
30
|
-
/*
|
|
31
|
-
|
|
32
|
-
#define
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
39
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
40
|
+
#define GAME_TITLE "ZENITH BARRAGE"
|
|
41
|
+
|
|
42
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
43
|
+
* VRAM map (WORD addresses — the VDC is a 16-bit-word machine; a tile is 16
|
|
44
|
+
* words, a 16x16 sprite cell is 64). Sprites and BG tiles share one 64KB
|
|
45
|
+
* VRAM, so lay it out ONCE and keep the SATB out of pattern space:
|
|
46
|
+
* $0000 BAT (32x32 background map — matches vdc_init's VDC_MWR setting)
|
|
47
|
+
* $1000 font glyphs (38 tiles: blank, 0-9, A-Z, dash)
|
|
48
|
+
* $1400 starfield BG tiles
|
|
49
|
+
* $1800 16x16 sprite cells: ship, bullet, enemy
|
|
50
|
+
* $1900 BOSS pattern cells — 4-ALIGNED cell index (see the boss idiom)
|
|
51
|
+
* $7F00 shadow SATB destination (satb_dma copies it here, VDC reads it) */
|
|
52
|
+
#define BAT_VRAM 0x0000
|
|
53
|
+
#define FONT_VRAM 0x1000
|
|
54
|
+
#define STAR0_VRAM 0x1400 /* deep-space band tile (solid colour 1) */
|
|
55
|
+
#define STAR1_VRAM 0x1410 /* lighter band tile (solid colour 2) */
|
|
56
|
+
#define STAR2_VRAM 0x1420 /* band tile + a twinkling star pixel */
|
|
57
|
+
#define SHIP_VRAM 0x1800
|
|
58
|
+
#define BULLET_VRAM 0x1840
|
|
59
|
+
#define ENEMY_VRAM 0x1880
|
|
60
|
+
#define BOSS_VRAM 0x1900 /* 8 cells: left half TL,TR,BL,BR + right half */
|
|
39
61
|
|
|
40
62
|
#define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
|
|
41
63
|
|
|
64
|
+
/* Sprite pattern codes = VRAM >> 6 (the 16x16 cell index). */
|
|
65
|
+
#define SHIP_PAT (SHIP_VRAM >> 6)
|
|
66
|
+
#define BULLET_PAT (BULLET_VRAM >> 6)
|
|
67
|
+
#define ENEMY_PAT (ENEMY_VRAM >> 6)
|
|
68
|
+
#define BOSSL_PAT (BOSS_VRAM >> 6) /* 0x64 — multiple of 4 */
|
|
69
|
+
#define BOSSR_PAT ((BOSS_VRAM >> 6) + 4) /* 0x68 — multiple of 4 */
|
|
70
|
+
|
|
71
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
72
|
+
* Object pools — fixed slots, no allocation. SATB slot plan (slot order is
|
|
73
|
+
* also priority: LOWER slot wins overlaps on the HuC6270):
|
|
74
|
+
* 0 player ship
|
|
75
|
+
* 1-6 bullets
|
|
76
|
+
* 7-12 enemies (waves + boss drones share the pool)
|
|
77
|
+
* 14,15 the boss's two 32x32 halves
|
|
78
|
+
* Everything else stays parked off-screen. */
|
|
42
79
|
#define MAX_BULLETS 6
|
|
43
80
|
#define MAX_ENEMIES 6
|
|
81
|
+
#define SLOT_SHIP 0
|
|
82
|
+
#define SLOT_BULLET 1
|
|
83
|
+
#define SLOT_ENEMY 7
|
|
84
|
+
#define SLOT_BOSS_L 14
|
|
85
|
+
#define SLOT_BOSS_R 15
|
|
44
86
|
|
|
45
|
-
|
|
46
|
-
#define
|
|
47
|
-
#define
|
|
48
|
-
#define
|
|
49
|
-
#define G_C 12
|
|
50
|
-
#define G_O 13
|
|
51
|
-
#define G_R 14
|
|
52
|
-
#define G_E 15
|
|
53
|
-
#define NUM_GLYPHS 16
|
|
87
|
+
#define PAL_SHIP 0
|
|
88
|
+
#define PAL_BULLET 1
|
|
89
|
+
#define PAL_ENEMY 2
|
|
90
|
+
#define PAL_BOSS 3
|
|
54
91
|
|
|
55
|
-
|
|
56
|
-
/*
|
|
57
|
-
|
|
58
|
-
/* 1 */ {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
|
|
59
|
-
/* 2 */ {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F},
|
|
60
|
-
/* 3 */ {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
|
|
61
|
-
/* 4 */ {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02},
|
|
62
|
-
/* 5 */ {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
|
|
63
|
-
/* 6 */ {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E},
|
|
64
|
-
/* 7 */ {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
|
|
65
|
-
/* 8 */ {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E},
|
|
66
|
-
/* 9 */ {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C},
|
|
67
|
-
/* S */ {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E},
|
|
68
|
-
/* C */ {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E},
|
|
69
|
-
/* O */ {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E},
|
|
70
|
-
/* R */ {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11},
|
|
71
|
-
/* E */ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F},
|
|
72
|
-
};
|
|
92
|
+
#define START_LIVES 3
|
|
93
|
+
#define SHIP_MIN_Y 80 /* keeps the ship out of the boss's altitude */
|
|
94
|
+
#define OFFSCREEN_Y 0x1F0 /* park unused sprites below the display */
|
|
73
95
|
|
|
74
|
-
/* ---- game state --------------------------------------------------------- */
|
|
75
96
|
typedef struct { u16 x, y; u8 alive; } Obj;
|
|
76
97
|
|
|
77
98
|
static Obj player;
|
|
78
99
|
static Obj bullets[MAX_BULLETS];
|
|
79
100
|
static Obj enemies[MAX_ENEMIES];
|
|
80
|
-
static u16 score;
|
|
101
|
+
static u16 score, hiscore;
|
|
102
|
+
static u8 lives;
|
|
103
|
+
static u8 level; /* +1 per boss defeated — feeds speed/HP */
|
|
104
|
+
static u8 kills; /* kills since the last boss — triggers the next */
|
|
105
|
+
static u8 invuln; /* post-hit mercy frames (ship flickers) */
|
|
106
|
+
static u8 fire_cd;
|
|
81
107
|
static u8 spawn_timer;
|
|
108
|
+
static u8 twinkle_timer;
|
|
82
109
|
static u16 rng;
|
|
83
110
|
static u8 pad, prev_pad;
|
|
84
111
|
static u8 sfx_timer;
|
|
112
|
+
static u8 hud_dirty;
|
|
113
|
+
|
|
114
|
+
/* Boss state: ONE logical object that happens to be two hardware sprites. */
|
|
115
|
+
static u8 boss_active;
|
|
116
|
+
static u16 boss_x, boss_y;
|
|
117
|
+
static u8 boss_dir;
|
|
118
|
+
static u8 boss_hp;
|
|
119
|
+
static u8 boss_flash; /* hit feedback: swap palette for a few frames */
|
|
120
|
+
static u8 boss_shot_timer;
|
|
121
|
+
|
|
122
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
123
|
+
#define ST_TITLE 0
|
|
124
|
+
#define ST_PLAY 1
|
|
125
|
+
#define ST_OVER 2
|
|
126
|
+
static u8 state;
|
|
127
|
+
|
|
128
|
+
static u16 tile_buf[16]; /* scratch for one 8x8 tile */
|
|
129
|
+
static u16 spr_buf[64]; /* scratch for one 16x16 sprite cell */
|
|
130
|
+
|
|
131
|
+
/* ── GAME LOGIC (clay) — 5x7 glyph font: blank, 0-9, A-Z, dash ──────────────
|
|
132
|
+
* Each glyph is 7 rows of 5 bits (bit4 = leftmost). upload_font() expands
|
|
133
|
+
* them into 8x8 1-plane tiles; drawn with BG sub-palette 1 (white). */
|
|
134
|
+
#define G_BLANK 0
|
|
135
|
+
#define G_DIGIT 1 /* '0'..'9' -> glyphs 1..10 */
|
|
136
|
+
#define G_ALPHA 11 /* 'A'..'Z' -> glyphs 11..36 */
|
|
137
|
+
#define G_DASH 37
|
|
138
|
+
#define NUM_GLYPHS 38
|
|
139
|
+
|
|
140
|
+
static const u8 FONT5x7[NUM_GLYPHS][7] = {
|
|
141
|
+
{0,0,0,0,0,0,0},
|
|
142
|
+
{0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
|
|
143
|
+
{0x0E,0x11,0x01,0x02,0x04,0x08,0x1F}, {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
|
|
144
|
+
{0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
|
|
145
|
+
{0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
|
|
146
|
+
{0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C},
|
|
147
|
+
{0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}, {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E},
|
|
148
|
+
{0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E},
|
|
149
|
+
{0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10},
|
|
150
|
+
{0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, {0x11,0x11,0x11,0x1F,0x11,0x11,0x11},
|
|
151
|
+
{0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, {0x07,0x02,0x02,0x02,0x02,0x12,0x0C},
|
|
152
|
+
{0x11,0x12,0x14,0x18,0x14,0x12,0x11}, {0x10,0x10,0x10,0x10,0x10,0x10,0x1F},
|
|
153
|
+
{0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, {0x11,0x19,0x15,0x13,0x11,0x11,0x11},
|
|
154
|
+
{0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10},
|
|
155
|
+
{0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11},
|
|
156
|
+
{0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}, {0x1F,0x04,0x04,0x04,0x04,0x04,0x04},
|
|
157
|
+
{0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x11,0x11,0x11,0x11,0x11,0x0A,0x04},
|
|
158
|
+
{0x11,0x11,0x11,0x15,0x15,0x15,0x0A}, {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11},
|
|
159
|
+
{0x11,0x11,0x0A,0x04,0x04,0x04,0x04}, {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F},
|
|
160
|
+
{0x00,0x00,0x00,0x1F,0x00,0x00,0x00},
|
|
161
|
+
};
|
|
85
162
|
|
|
86
|
-
|
|
87
|
-
static u16
|
|
163
|
+
/* ── GAME LOGIC (clay) — sprite masks (16 rows × 16 bits, bit15 leftmost) ── */
|
|
164
|
+
static const u16 ship_mask[16] = {
|
|
165
|
+
0x0180, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x07E0, 0x0FF0, 0x0FF0,
|
|
166
|
+
0x1FF8, 0x1FF8, 0x3FFC, 0x7FFE, 0xFFFF, 0xE187, 0xC003, 0x8001
|
|
167
|
+
};
|
|
168
|
+
static const u16 bullet_mask[16] = {
|
|
169
|
+
0x0000, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x07E0, 0x07E0, 0x07E0,
|
|
170
|
+
0x07E0, 0x07E0, 0x03C0, 0x03C0, 0x0180, 0x0000, 0x0000, 0x0000
|
|
171
|
+
};
|
|
172
|
+
static const u16 enemy_mask[16] = {
|
|
173
|
+
0x0000, 0x4002, 0x6006, 0x7FFE, 0x7FFE, 0xFDBF, 0xFFFF, 0xFFFF,
|
|
174
|
+
0xFFFF, 0x7FFE, 0x3FFC, 0x1FF8, 0x300C, 0x6006, 0x4002, 0x0000
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/* ── GAME LOGIC (clay) — the boss's LEFT half (32x32). 2 u16 per row
|
|
178
|
+
* (cols 0-15, cols 16-31). The right half is this art MIRRORED at upload
|
|
179
|
+
* time — symmetric bosses cost half the data. body = hull (colour 1);
|
|
180
|
+
* core = the glowing eye + cannon tips (colour 3, a subset of body). */
|
|
181
|
+
static const u16 boss_body[64] = {
|
|
182
|
+
0x0000,0x0000, 0x0000,0x001F, 0x0000,0x007F, 0x0000,0x00FF,
|
|
183
|
+
0x0000,0x01FF, 0x0000,0x7FFF, 0x0003,0xFFFF, 0x001F,0xFFFF,
|
|
184
|
+
0x007F,0xFFFF, 0x01FF,0xFFFF, 0x07FF,0xFFFF, 0x0FFF,0xFFFF,
|
|
185
|
+
0x1FFF,0xFFFF, 0x1FFF,0xFFFF, 0x3FFF,0xFFFF, 0x3FFF,0xFFFF,
|
|
186
|
+
0x3FFF,0xFFFF, 0x3FFF,0xFFFF, 0x3FFF,0xFFFF, 0x3FFF,0xFFFF,
|
|
187
|
+
0x3FFF,0xFFFF, 0x3FFF,0xFFFF, 0x3FFF,0xFFFF, 0x1FFF,0xFFFF,
|
|
188
|
+
0x1FFF,0xEFFF, 0x0FF3,0xEFFF, 0x07E0,0x6FFF, 0x0000,0x01FF,
|
|
189
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
|
|
190
|
+
};
|
|
191
|
+
static const u16 boss_core[64] = {
|
|
192
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
|
|
193
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
|
|
194
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x000F,
|
|
195
|
+
0x0000,0x001F, 0x0000,0x003F, 0x0000,0x003F, 0x0000,0x003F,
|
|
196
|
+
0x0000,0x003F, 0x0000,0x003F, 0x03C0,0x003F, 0x03C0,0x001F,
|
|
197
|
+
0x07E0,0x000F, 0x03C0,0x0000, 0x03C0,0x0000, 0x0000,0x0000,
|
|
198
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
|
|
199
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
|
|
200
|
+
};
|
|
88
201
|
|
|
89
|
-
/*
|
|
202
|
+
/* ── GAME LOGIC (clay) — tile/sprite builders ────────────────────────────── */
|
|
90
203
|
static void make_solid_tile(u16 *t, u8 ci) {
|
|
91
204
|
u8 r;
|
|
92
205
|
u8 p0 = (ci & 1) ? 0xFF : 0x00;
|
|
93
206
|
u8 p1 = (ci & 2) ? 0xFF : 0x00;
|
|
94
|
-
u8 p2 = (ci & 4) ? 0xFF : 0x00;
|
|
95
|
-
u8 p3 = (ci & 8) ? 0xFF : 0x00;
|
|
96
207
|
for (r = 0; r < 8; ++r) {
|
|
97
208
|
t[r] = (u16)(p0 | (p1 << 8));
|
|
98
|
-
t[r + 8] =
|
|
209
|
+
t[r + 8] = 0;
|
|
99
210
|
}
|
|
100
211
|
}
|
|
101
212
|
|
|
102
|
-
/*
|
|
103
|
-
static void
|
|
104
|
-
u8 r;
|
|
105
|
-
for (r = 0; r < 8; ++r) { t[r] = 0x00FF; t[r + 8] = 0x0000; } /* base = colour 1 */
|
|
106
|
-
/* star = colour 3 (planes 0+1) at row 2: set plane1 bit too for that row */
|
|
107
|
-
t[2] = (u16)(0x00FF | (0x04 << 8)); /* plane0 row + plane1 single pixel */
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/* upload one 16x16 sprite from a 16-row body mask in colour `ci` */
|
|
111
|
-
static void make_sprite(u16 vram, const u16 *body, u8 ci) {
|
|
213
|
+
/* one-colour 16x16 sprite cell from a 16-row mask */
|
|
214
|
+
static void make_sprite16(u16 vram, const u16 *mask, u8 ci) {
|
|
112
215
|
u8 r;
|
|
113
216
|
for (r = 0; r < 64; ++r) spr_buf[r] = 0;
|
|
114
217
|
for (r = 0; r < 16; ++r) {
|
|
115
|
-
if (ci & 1) spr_buf[r] =
|
|
116
|
-
if (ci & 2) spr_buf[r + 16] =
|
|
117
|
-
if (ci & 4) spr_buf[r + 32] = body[r]; /* plane2 */
|
|
118
|
-
if (ci & 8) spr_buf[r + 48] = body[r]; /* plane3 */
|
|
218
|
+
if (ci & 1) spr_buf[r] = mask[r]; /* plane 0 */
|
|
219
|
+
if (ci & 2) spr_buf[r + 16] = mask[r]; /* plane 1 */
|
|
119
220
|
}
|
|
120
221
|
load_tiles(vram, spr_buf, 64);
|
|
121
222
|
}
|
|
122
223
|
|
|
123
224
|
static void upload_font(void) {
|
|
124
|
-
u8 g, row, bits,
|
|
225
|
+
u8 g, row, bits, px;
|
|
125
226
|
for (g = 0; g < NUM_GLYPHS; ++g) {
|
|
126
227
|
for (row = 0; row < 16; ++row) tile_buf[row] = 0;
|
|
127
228
|
for (row = 0; row < 7; ++row) {
|
|
128
229
|
bits = FONT5x7[g][row];
|
|
129
|
-
|
|
130
|
-
if (bits & 0x10)
|
|
131
|
-
if (bits & 0x08)
|
|
132
|
-
if (bits & 0x04)
|
|
133
|
-
if (bits & 0x02)
|
|
134
|
-
if (bits & 0x01)
|
|
135
|
-
tile_buf[row] = (u16)
|
|
230
|
+
px = 0;
|
|
231
|
+
if (bits & 0x10) px |= 0x40;
|
|
232
|
+
if (bits & 0x08) px |= 0x20;
|
|
233
|
+
if (bits & 0x04) px |= 0x10;
|
|
234
|
+
if (bits & 0x02) px |= 0x08;
|
|
235
|
+
if (bits & 0x01) px |= 0x04;
|
|
236
|
+
tile_buf[row] = (u16)px;
|
|
136
237
|
}
|
|
137
238
|
load_tiles((u16)(FONT_VRAM + g * 16), tile_buf, 16);
|
|
138
239
|
}
|
|
139
240
|
}
|
|
140
241
|
|
|
242
|
+
/* mirror a 16-bit row (bit15 <-> bit0) for the boss's right half */
|
|
243
|
+
static u16 rev16(u16 v) {
|
|
244
|
+
u16 out = 0;
|
|
245
|
+
u8 i;
|
|
246
|
+
for (i = 0; i < 16; ++i) {
|
|
247
|
+
out <<= 1;
|
|
248
|
+
if (v & 1) out |= 1;
|
|
249
|
+
v >>= 1;
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
255
|
+
* LARGE-SPRITE PATTERN LAYOUT — the half of the boss trick that lives in
|
|
256
|
+
* VRAM. A 32x32 HuC6270 sprite is FOUR 16x16 cells (64 words each) stored
|
|
257
|
+
* consecutively in TL, TR, BL, BR order, and its SATB pattern code must be
|
|
258
|
+
* 4-ALIGNED (the hardware ignores the low 2 bits and adds them back as
|
|
259
|
+
* column/row). Get the order wrong and the boss renders scrambled — four
|
|
260
|
+
* recognizable quarters in the wrong places. The other half of the trick
|
|
261
|
+
* (the SATB attribute bits) is in push_sprites() below.
|
|
262
|
+
*
|
|
263
|
+
* requires: BOSS_VRAM >> 6 a multiple of 4; 8 consecutive free cells
|
|
264
|
+
* (512 words) at BOSS_VRAM; set_sprite_ex() from pce_video.c. */
|
|
265
|
+
static void upload_boss(void) {
|
|
266
|
+
u8 half, cr, cc, row;
|
|
267
|
+
u16 body_bits, core_bits;
|
|
268
|
+
u16 vram = BOSS_VRAM;
|
|
269
|
+
for (half = 0; half < 2; ++half) { /* 0 = left, 1 = right */
|
|
270
|
+
for (cr = 0; cr < 2; ++cr) { /* cell row (top/bottom) */
|
|
271
|
+
for (cc = 0; cc < 2; ++cc) { /* cell col (left/right) */
|
|
272
|
+
for (row = 0; row < 64; ++row) spr_buf[row] = 0;
|
|
273
|
+
for (row = 0; row < 16; ++row) {
|
|
274
|
+
u8 y = (u8)(cr * 16 + row);
|
|
275
|
+
if (half == 0) { /* left half: stored art */
|
|
276
|
+
body_bits = boss_body[y * 2 + cc];
|
|
277
|
+
core_bits = boss_core[y * 2 + cc];
|
|
278
|
+
} else { /* right half: mirrored */
|
|
279
|
+
body_bits = rev16(boss_body[y * 2 + (1 - cc)]);
|
|
280
|
+
core_bits = rev16(boss_core[y * 2 + (1 - cc)]);
|
|
281
|
+
}
|
|
282
|
+
/* hull pixels = colour 1 (plane0), eye/cannon core =
|
|
283
|
+
* colour 3 (planes 0+1) — core is a subset of body. */
|
|
284
|
+
spr_buf[row] = body_bits;
|
|
285
|
+
spr_buf[row + 16] = core_bits;
|
|
286
|
+
}
|
|
287
|
+
load_tiles(vram, spr_buf, 64);
|
|
288
|
+
vram += 64; /* next cell: TL,TR,BL,BR */
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
141
294
|
static void upload_art(void) {
|
|
142
|
-
/* ship: an upward-pointing arrow */
|
|
143
|
-
static const u16 ship[16] = {
|
|
144
|
-
0x0180, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x07E0, 0x0FF0, 0x0FF0,
|
|
145
|
-
0x1FF8, 0x1FF8, 0x3FFC, 0x7FFE, 0xFFFF, 0xE187, 0xC003, 0x8001
|
|
146
|
-
};
|
|
147
|
-
/* bullet: a small vertical pellet */
|
|
148
|
-
static const u16 bullet[16] = {
|
|
149
|
-
0x0000, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x07E0, 0x07E0, 0x07E0,
|
|
150
|
-
0x07E0, 0x07E0, 0x03C0, 0x03C0, 0x0180, 0x0000, 0x0000, 0x0000
|
|
151
|
-
};
|
|
152
|
-
/* enemy: a downward, blocky invader */
|
|
153
|
-
static const u16 enemy[16] = {
|
|
154
|
-
0x0000, 0x4002, 0x6006, 0x7FFE, 0x7FFE, 0xFDBF, 0xFFFF, 0xFFFF,
|
|
155
|
-
0xFFFF, 0x7FFE, 0x3FFC, 0x1FF8, 0x300C, 0x6006, 0x4002, 0x0000
|
|
156
|
-
};
|
|
157
295
|
upload_font();
|
|
158
296
|
make_solid_tile(tile_buf, 1); load_tiles(STAR0_VRAM, tile_buf, 16);
|
|
159
297
|
make_solid_tile(tile_buf, 2); load_tiles(STAR1_VRAM, tile_buf, 16);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
298
|
+
make_solid_tile(tile_buf, 1); tile_buf[2] |= 0x0010; tile_buf[2 + 8] = 0x0010;
|
|
299
|
+
load_tiles(STAR2_VRAM, tile_buf, 16); /* band + colour-3 star px */
|
|
300
|
+
make_sprite16(SHIP_VRAM, ship_mask, 1);
|
|
301
|
+
make_sprite16(BULLET_VRAM, bullet_mask, 1);
|
|
302
|
+
make_sprite16(ENEMY_VRAM, enemy_mask, 1);
|
|
303
|
+
upload_boss();
|
|
164
304
|
}
|
|
165
305
|
|
|
166
|
-
/*
|
|
306
|
+
/* ── GAME LOGIC (clay) — BAT text + starfield ─────────────────────────────── */
|
|
307
|
+
static void put_glyph(u8 col, u8 row, u8 glyph) {
|
|
308
|
+
u16 e = BAT_ENTRY(1, (u16)(FONT_VRAM + glyph * 16)); /* pal 1 = white */
|
|
309
|
+
vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
|
|
310
|
+
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
311
|
+
VDC_DATA_HI = (u8)(e >> 8);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
static void draw_text(u8 col, u8 row, const char *s) {
|
|
315
|
+
u8 c;
|
|
316
|
+
while ((c = (u8)*s++) != 0) {
|
|
317
|
+
u8 g = G_BLANK;
|
|
318
|
+
if (c >= '0' && c <= '9') g = (u8)(G_DIGIT + c - '0');
|
|
319
|
+
else if (c >= 'A' && c <= 'Z') g = (u8)(G_ALPHA + c - 'A');
|
|
320
|
+
else if (c == '-') g = G_DASH;
|
|
321
|
+
put_glyph(col++, row, g);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
static void draw_num5(u8 col, u8 row, u16 v) {
|
|
326
|
+
u8 i, d[5];
|
|
327
|
+
for (i = 0; i < 5; ++i) { d[i] = (u8)(v % 10); v /= 10; }
|
|
328
|
+
for (i = 0; i < 5; ++i) put_glyph((u8)(col + i), row, (u8)(G_DIGIT + d[4 - i]));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* banded starfield over the whole 32x32 BAT (two band colours + sparse
|
|
332
|
+
* twinkle tiles — the bands keep the screen from being one flat colour) */
|
|
167
333
|
static void draw_starfield(void) {
|
|
168
334
|
u8 r, c;
|
|
169
335
|
u16 e0 = BAT_ENTRY(0, STAR0_VRAM);
|
|
@@ -173,51 +339,118 @@ static void draw_starfield(void) {
|
|
|
173
339
|
for (r = 0; r < 32; ++r) {
|
|
174
340
|
vram_set_write_addr((u16)(BAT_VRAM + r * 32));
|
|
175
341
|
for (c = 0; c < 32; ++c) {
|
|
176
|
-
e = (r & 2) ? e1 : e0;
|
|
177
|
-
if (((r * 7 + c * 5) & 7) == 0) e = e2;
|
|
342
|
+
e = (r & 2) ? e1 : e0;
|
|
343
|
+
if (((r * 7 + c * 5) & 7) == 0) e = e2;
|
|
178
344
|
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
179
345
|
VDC_DATA_HI = (u8)(e >> 8);
|
|
180
346
|
}
|
|
181
347
|
}
|
|
182
348
|
}
|
|
183
349
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
350
|
+
/* HUD: row 1 = "SC 00000 HI 00000 SH 3" */
|
|
351
|
+
static void draw_hud_labels(void) {
|
|
352
|
+
draw_text(1, 1, "SC");
|
|
353
|
+
draw_text(12, 1, "HI");
|
|
354
|
+
draw_text(23, 1, "SH");
|
|
189
355
|
}
|
|
190
356
|
|
|
191
|
-
static void
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
357
|
+
static void draw_hud_numbers(void) {
|
|
358
|
+
draw_num5(4, 1, score);
|
|
359
|
+
draw_num5(15, 1, hiscore);
|
|
360
|
+
put_glyph(26, 1, (u8)(G_DIGIT + lives));
|
|
195
361
|
}
|
|
196
362
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
363
|
+
/* ── HARDWARE TRUTH: a bare HuCard CANNOT save a hi-score (in-session only) ──
|
|
364
|
+
* This was researched and corrected: earlier versions wrote the hi-score to
|
|
365
|
+
* BRAM ("backup RAM", bank $F7) and claimed it persisted across power cycles.
|
|
366
|
+
* That is NOT honest for a HuCard game. On REAL hardware a plain HuCard plugged
|
|
367
|
+
* into a base PC Engine / TurboGrafx-16 has NO backup RAM at all — BRAM exists
|
|
368
|
+
* ONLY when a peripheral is attached: the CD-ROM² System (2KB kept by a
|
|
369
|
+
* supercapacitor), the Tennokoe Bank HuCard, or the Memory Base 128. No
|
|
370
|
+
* commercial HuCard self-saved; they used PASSWORDS. (The often-cited Populous
|
|
371
|
+
* "ROMRAM" SRAM was the game's own working RAM, not a battery save.) An
|
|
372
|
+
* emulator like geargrafx exposes BRAM unconditionally, so the old code
|
|
373
|
+
* "worked" in emulation in a way the real machine never would.
|
|
374
|
+
*
|
|
375
|
+
* So this game keeps an IN-SESSION hi-score only (like the honest 2600/Lynx
|
|
376
|
+
* examples) — it survives game-overs within a power-on, resets to 0 on a cold
|
|
377
|
+
* boot. To make it ACTUALLY persist on real hardware you would target a
|
|
378
|
+
* peripheral: write to BRAM only after detecting one (and go through the System
|
|
379
|
+
* Card BIOS's 'HUBM' directory for CD saves), or move the game to a CD-ROM²
|
|
380
|
+
* build. Either is a real-hardware feature, not a property of the cartridge. */
|
|
381
|
+
static u16 hiscore_load(void) {
|
|
382
|
+
return 0; /* cold boot: no persistence on a bare HuCard */
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
static void hiscore_save(u16 v) {
|
|
386
|
+
(void)v; /* in-session only — nowhere to persist on real HW */
|
|
208
387
|
}
|
|
209
388
|
|
|
210
|
-
/*
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
389
|
+
/* ── GAME LOGIC (clay) — music: a 2-channel tune ticked once per frame ──────
|
|
390
|
+
* PSG channel plan: 5 = melody, 4 = bass, 2/3 = SFX (tones cut by sfx_timer).
|
|
391
|
+
* PCE frequency regs are DIVIDERS: pitch ≈ 3.58MHz / (32 × value), so a
|
|
392
|
+
* BIGGER number is a LOWER note. Note indices into NOTE_DIV below. */
|
|
393
|
+
enum { R = 0, A2N, C3, F3, G3, A3, B3, C4, D4, E4, F4, G4, A4, B4, C5, D5, E5 };
|
|
394
|
+
static const u16 NOTE_DIV[17] = {
|
|
395
|
+
0, 1017, 854, 641, 571, 508, 453, 427, 381, 339, 320, 285, 254, 226, 214, 190, 170
|
|
396
|
+
};
|
|
397
|
+
/* 16 melody steps + 8 bass steps (one bass note per 2 melody steps) */
|
|
398
|
+
static const u8 MEL_TITLE[16] = { C4,E4,G4,C5, B4,G4,E4,G4, A3,C4,E4,A4, G4,E4,D4,B3 };
|
|
399
|
+
static const u8 BAS_TITLE[8] = { C3,C3, A2N,A2N, F3,F3, G3,G3 };
|
|
400
|
+
static const u8 MEL_PLAY[16] = { E4,R,E4,G4, A4,G4,E4,D4, C4,D4,E4,G4, E4,D4,C4,R };
|
|
401
|
+
static const u8 BAS_PLAY[8] = { A2N,A2N, F3,F3, C3,C3, G3,G3 };
|
|
402
|
+
static const u8 MEL_OVER[16] = { C5,R,B4,R, A4,R,G4,R, E4,R,D4,R, C4,R,R,R };
|
|
403
|
+
|
|
404
|
+
static u8 music_song; /* reuses the ST_* ids */
|
|
405
|
+
static u8 music_step, music_timer, music_done;
|
|
406
|
+
|
|
407
|
+
static void music_set(u8 song) {
|
|
408
|
+
music_song = song;
|
|
409
|
+
music_step = 0;
|
|
410
|
+
music_timer = 0;
|
|
411
|
+
music_done = 0;
|
|
412
|
+
psg_off(4);
|
|
413
|
+
psg_off(5);
|
|
214
414
|
}
|
|
215
415
|
|
|
416
|
+
static void music_tick(void) {
|
|
417
|
+
const u8 *mel;
|
|
418
|
+
u8 n;
|
|
419
|
+
if (music_done) return;
|
|
420
|
+
if (music_timer == 0) {
|
|
421
|
+
mel = (music_song == ST_PLAY) ? MEL_PLAY
|
|
422
|
+
: (music_song == ST_OVER) ? MEL_OVER : MEL_TITLE;
|
|
423
|
+
n = mel[music_step & 15];
|
|
424
|
+
if (n != R) psg_tone(5, NOTE_DIV[n], 26);
|
|
425
|
+
else psg_off(5);
|
|
426
|
+
if (music_song != ST_OVER) { /* the game-over jingle has no bass */
|
|
427
|
+
n = ((music_step & 1) == 0)
|
|
428
|
+
? ((music_song == ST_PLAY) ? BAS_PLAY[(music_step >> 1) & 7]
|
|
429
|
+
: BAS_TITLE[(music_step >> 1) & 7])
|
|
430
|
+
: R;
|
|
431
|
+
if (n != R) psg_tone(4, NOTE_DIV[n], 20);
|
|
432
|
+
}
|
|
433
|
+
++music_step;
|
|
434
|
+
if (music_song == ST_OVER && music_step >= 16) { /* play once, stop */
|
|
435
|
+
music_done = 1;
|
|
436
|
+
psg_off(4);
|
|
437
|
+
psg_off(5);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
++music_timer;
|
|
441
|
+
if (music_timer >= 8) music_timer = 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* ── GAME LOGIC (clay) — helpers ──────────────────────────────────────────── */
|
|
216
445
|
static u16 next_rand(void) {
|
|
217
446
|
rng = (u16)(rng * 25173u + 13849u);
|
|
218
447
|
return rng;
|
|
219
448
|
}
|
|
220
449
|
|
|
450
|
+
static u8 aabb(u16 ax, u16 ay, u16 aw, u16 ah, u16 bx, u16 by, u16 bw, u16 bh) {
|
|
451
|
+
return (u8)(ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by);
|
|
452
|
+
}
|
|
453
|
+
|
|
221
454
|
static void fire(void) {
|
|
222
455
|
u8 i;
|
|
223
456
|
for (i = 0; i < MAX_BULLETS; ++i) {
|
|
@@ -225,123 +458,316 @@ static void fire(void) {
|
|
|
225
458
|
bullets[i].x = player.x;
|
|
226
459
|
bullets[i].y = (u16)(player.y - 10);
|
|
227
460
|
bullets[i].alive = 1;
|
|
228
|
-
psg_tone(2, 0x180, 31);
|
|
461
|
+
psg_tone(2, 0x180, 31);
|
|
229
462
|
sfx_timer = 4;
|
|
230
463
|
return;
|
|
231
464
|
}
|
|
232
465
|
}
|
|
233
466
|
}
|
|
234
467
|
|
|
235
|
-
static void
|
|
468
|
+
static void spawn_enemy(u16 x, u16 y) {
|
|
236
469
|
u8 i;
|
|
237
470
|
for (i = 0; i < MAX_ENEMIES; ++i) {
|
|
238
471
|
if (!enemies[i].alive) {
|
|
239
|
-
enemies[i].x =
|
|
240
|
-
enemies[i].y =
|
|
472
|
+
enemies[i].x = x;
|
|
473
|
+
enemies[i].y = y;
|
|
241
474
|
enemies[i].alive = 1;
|
|
242
475
|
return;
|
|
243
476
|
}
|
|
244
477
|
}
|
|
245
478
|
}
|
|
246
479
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
vce_set_color(3, PCE_RGB(7, 7, 7)); /* BG c3: star white */
|
|
257
|
-
vce_set_color(256, PCE_RGB(0, 0, 0)); /* spr pal0 transparent */
|
|
258
|
-
vce_set_color(257, PCE_RGB(2, 6, 7)); /* spr pal0 c1: cyan ship */
|
|
259
|
-
vce_set_color(272, PCE_RGB(0, 0, 0)); /* spr pal1 transparent */
|
|
260
|
-
vce_set_color(273, PCE_RGB(7, 7, 0)); /* spr pal1 c1: yellow bullet */
|
|
261
|
-
vce_set_color(288, PCE_RGB(0, 0, 0)); /* spr pal2 transparent */
|
|
262
|
-
vce_set_color(289, PCE_RGB(7, 1, 1)); /* spr pal2 c1: red enemy */
|
|
480
|
+
/* ── GAME LOGIC (clay) — screen painters (full repaint per state change) ── */
|
|
481
|
+
static void paint_title(void) {
|
|
482
|
+
draw_starfield();
|
|
483
|
+
draw_text((u8)((32 - (sizeof(GAME_TITLE) - 1)) / 2), 8, GAME_TITLE);
|
|
484
|
+
draw_text(10, 14, "PRESS RUN");
|
|
485
|
+
draw_text(11, 18, "HI");
|
|
486
|
+
draw_num5(14, 18, hiscore);
|
|
487
|
+
draw_text(7, 22, "BOSS EVERY 10 KILLS");
|
|
488
|
+
}
|
|
263
489
|
|
|
264
|
-
|
|
490
|
+
static void paint_field(void) {
|
|
265
491
|
draw_starfield();
|
|
266
|
-
|
|
492
|
+
draw_hud_labels();
|
|
493
|
+
draw_hud_numbers();
|
|
494
|
+
}
|
|
267
495
|
|
|
268
|
-
|
|
496
|
+
static void start_game(void) {
|
|
497
|
+
u8 i;
|
|
269
498
|
for (i = 0; i < MAX_BULLETS; ++i) bullets[i].alive = 0;
|
|
270
499
|
for (i = 0; i < MAX_ENEMIES; ++i) enemies[i].alive = 0;
|
|
500
|
+
player.x = 120; player.y = 192; player.alive = 1;
|
|
501
|
+
lives = START_LIVES;
|
|
271
502
|
score = 0;
|
|
503
|
+
level = 0;
|
|
504
|
+
kills = 0;
|
|
505
|
+
invuln = 0;
|
|
506
|
+
fire_cd = 0;
|
|
272
507
|
spawn_timer = 0;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
508
|
+
boss_active = 0;
|
|
509
|
+
boss_flash = 0;
|
|
510
|
+
paint_field();
|
|
511
|
+
music_set(ST_PLAY);
|
|
512
|
+
state = ST_PLAY;
|
|
513
|
+
}
|
|
277
514
|
|
|
278
|
-
|
|
279
|
-
|
|
515
|
+
static void game_over(void) {
|
|
516
|
+
if (score > hiscore) {
|
|
517
|
+
hiscore = score;
|
|
518
|
+
hiscore_save(hiscore); /* in-session only (no save on a bare HuCard) */
|
|
519
|
+
}
|
|
520
|
+
draw_text(11, 12, "GAME OVER");
|
|
521
|
+
draw_text(10, 14, "PRESS RUN");
|
|
522
|
+
music_set(ST_OVER);
|
|
523
|
+
state = ST_OVER;
|
|
524
|
+
}
|
|
280
525
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
526
|
+
static void boss_enter(void) {
|
|
527
|
+
boss_active = 1;
|
|
528
|
+
boss_x = 96;
|
|
529
|
+
boss_y = 24;
|
|
530
|
+
boss_dir = 1;
|
|
531
|
+
boss_hp = (u8)(10 + level * 4);
|
|
532
|
+
if (boss_hp > 30) boss_hp = 30;
|
|
533
|
+
boss_shot_timer = 0;
|
|
534
|
+
}
|
|
285
535
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
536
|
+
static void boss_die(void) {
|
|
537
|
+
boss_active = 0;
|
|
538
|
+
boss_flash = 0;
|
|
539
|
+
if (score < 60000u) score += 500;
|
|
540
|
+
++level;
|
|
541
|
+
kills = 0;
|
|
542
|
+
hud_dirty = 1;
|
|
543
|
+
psg_tone(3, 0x600, 31); /* low rumble */
|
|
544
|
+
sfx_timer = 24;
|
|
545
|
+
}
|
|
293
546
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
547
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
548
|
+
* SPRITE STAGING + THE SATB DMA. The VDC never reads your RAM: sprites live
|
|
549
|
+
* in its INTERNAL sprite attribute table, refreshed by a DMA you schedule by
|
|
550
|
+
* writing R19 (satb_dma() does the copy + the R19 write; the transfer itself
|
|
551
|
+
* happens at the next vblank). So the per-frame contract is:
|
|
552
|
+
* waitvsync() → restage EVERY slot → satb_dma()
|
|
553
|
+
* Stage during vblank — satb_dma() also streams 256 words through the VWR
|
|
554
|
+
* port, and doing that mid-display tears sprite pattern fetches.
|
|
555
|
+
*
|
|
556
|
+
* THE BOSS (the PCE signature): one logical object, TWO SATB entries. Each
|
|
557
|
+
* half is a 32x32 sprite — SPR_CGX_32|SPR_CGY_32 in the attribute word —
|
|
558
|
+
* placed at (boss_x, boss_y) and (boss_x+32, boss_y). They move as one unit
|
|
559
|
+
* because ONE pair of variables drives both entries; nothing else keeps
|
|
560
|
+
* them glued. A 64x32 boss this way costs 2 sprites of the 64-sprite budget
|
|
561
|
+
* (and 4 of the 16-sprites-per-scanline budget) — the same object on a
|
|
562
|
+
* 8x8/8x16-sprite machine costs 16+. CGY goes to 64 if you want a 32x64
|
|
563
|
+
* tower from a SINGLE entry (SPR_CGY_64, 8-aligned pattern).
|
|
564
|
+
*
|
|
565
|
+
* requires: set_sprite_ex() + the 4-aligned boss cells from upload_boss(). */
|
|
566
|
+
static void push_sprites(void) {
|
|
567
|
+
u8 i;
|
|
568
|
+
/* ship (slot 0) — flickers while invulnerable by parking on odd frames */
|
|
569
|
+
if (player.alive && !(invuln & 2)) set_sprite(SLOT_SHIP, player.x, player.y, SHIP_PAT, PAL_SHIP);
|
|
570
|
+
else set_sprite(SLOT_SHIP, player.x, OFFSCREEN_Y, SHIP_PAT, PAL_SHIP);
|
|
571
|
+
for (i = 0; i < MAX_BULLETS; ++i)
|
|
572
|
+
set_sprite((u8)(SLOT_BULLET + i), bullets[i].x,
|
|
573
|
+
bullets[i].alive ? bullets[i].y : OFFSCREEN_Y, BULLET_PAT, PAL_BULLET);
|
|
574
|
+
for (i = 0; i < MAX_ENEMIES; ++i)
|
|
575
|
+
set_sprite((u8)(SLOT_ENEMY + i), enemies[i].x,
|
|
576
|
+
enemies[i].alive ? enemies[i].y : OFFSCREEN_Y, ENEMY_PAT, PAL_ENEMY);
|
|
577
|
+
if (boss_active) {
|
|
578
|
+
u8 pal = boss_flash ? PAL_ENEMY : PAL_BOSS; /* hit = red flash */
|
|
579
|
+
set_sprite_ex(SLOT_BOSS_L, boss_x, boss_y, BOSSL_PAT, pal,
|
|
580
|
+
SPR_CGX_32 | SPR_CGY_32);
|
|
581
|
+
set_sprite_ex(SLOT_BOSS_R, (u16)(boss_x + 32), boss_y, BOSSR_PAT, pal,
|
|
582
|
+
SPR_CGX_32 | SPR_CGY_32);
|
|
583
|
+
} else {
|
|
584
|
+
set_sprite_ex(SLOT_BOSS_L, 0, OFFSCREEN_Y, BOSSL_PAT, PAL_BOSS, SPR_CGX_32 | SPR_CGY_32);
|
|
585
|
+
set_sprite_ex(SLOT_BOSS_R, 0, OFFSCREEN_Y, BOSSR_PAT, PAL_BOSS, SPR_CGX_32 | SPR_CGY_32);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/* twinkle: rewrite the star tile's pixel row every 16 frames — animation
|
|
590
|
+
* without touching the BAT (one 16-word upload in vblank) */
|
|
591
|
+
static void twinkle(void) {
|
|
592
|
+
u8 phase;
|
|
593
|
+
++twinkle_timer;
|
|
594
|
+
if ((twinkle_timer & 15) != 0) return;
|
|
595
|
+
phase = (u8)((twinkle_timer >> 4) & 3);
|
|
596
|
+
make_solid_tile(tile_buf, 1);
|
|
597
|
+
tile_buf[phase * 2] |= 0x0010;
|
|
598
|
+
tile_buf[phase * 2 + 8] = 0x0010;
|
|
599
|
+
load_tiles(STAR2_VRAM, tile_buf, 16);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/* ── GAME LOGIC (clay) — the per-state updates ────────────────────────────── */
|
|
603
|
+
static void hit_ship(void) {
|
|
604
|
+
if (invuln) return;
|
|
605
|
+
psg_tone(3, 0x500, 31);
|
|
606
|
+
sfx_timer = 16;
|
|
607
|
+
if (lives > 0) --lives;
|
|
608
|
+
hud_dirty = 1;
|
|
609
|
+
if (lives == 0) {
|
|
610
|
+
game_over();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
invuln = 90;
|
|
614
|
+
player.x = 120;
|
|
615
|
+
player.y = 192;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
static void update_play(void) {
|
|
619
|
+
u8 i, j;
|
|
620
|
+
|
|
621
|
+
/* ship */
|
|
622
|
+
if (pad & PCE_JOY_LEFT) { if (player.x > 2) player.x -= 3; }
|
|
623
|
+
if (pad & PCE_JOY_RIGHT) { if (player.x < 238) player.x += 3; }
|
|
624
|
+
if (pad & PCE_JOY_UP) { if (player.y > SHIP_MIN_Y) player.y -= 2; }
|
|
625
|
+
if (pad & PCE_JOY_DOWN) { if (player.y < 200) player.y += 2; }
|
|
626
|
+
if ((pad & PCE_JOY_I) && fire_cd == 0) { fire(); fire_cd = 8; }
|
|
627
|
+
if (fire_cd) --fire_cd;
|
|
628
|
+
if (invuln) --invuln;
|
|
629
|
+
|
|
630
|
+
/* bullets */
|
|
631
|
+
for (i = 0; i < MAX_BULLETS; ++i) {
|
|
632
|
+
if (!bullets[i].alive) continue;
|
|
633
|
+
if (bullets[i].y < 14) { bullets[i].alive = 0; continue; }
|
|
634
|
+
bullets[i].y -= 5;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/* enemies: drift down, faster each level */
|
|
638
|
+
for (i = 0; i < MAX_ENEMIES; ++i) {
|
|
639
|
+
if (!enemies[i].alive) continue;
|
|
640
|
+
enemies[i].y += (u16)(1 + (level >> 1));
|
|
641
|
+
if (enemies[i].y >= 224) enemies[i].alive = 0;
|
|
642
|
+
}
|
|
300
643
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
644
|
+
if (boss_active) {
|
|
645
|
+
/* the boss sways as ONE unit; drones spawn from its eye */
|
|
646
|
+
boss_x += boss_dir ? 1 : -1;
|
|
647
|
+
if (boss_x >= 184) boss_dir = 0;
|
|
648
|
+
if (boss_x <= 8) boss_dir = 1;
|
|
649
|
+
if (boss_flash) --boss_flash;
|
|
650
|
+
++boss_shot_timer;
|
|
651
|
+
if (boss_shot_timer >= (u8)(70 - level * 8)) {
|
|
652
|
+
boss_shot_timer = 0;
|
|
653
|
+
spawn_enemy((u16)(boss_x + 24), (u16)(boss_y + 28));
|
|
306
654
|
}
|
|
655
|
+
} else {
|
|
656
|
+
++spawn_timer;
|
|
657
|
+
if (spawn_timer >= (u8)(40 - level * 4)) {
|
|
658
|
+
spawn_timer = 0;
|
|
659
|
+
spawn_enemy((u16)(8 + (next_rand() >> 8) % 224), 16);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
307
662
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
sfx_timer = 6;
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
663
|
+
/* bullets vs enemies + boss */
|
|
664
|
+
for (i = 0; i < MAX_BULLETS; ++i) {
|
|
665
|
+
if (!bullets[i].alive) continue;
|
|
666
|
+
for (j = 0; j < MAX_ENEMIES; ++j) {
|
|
667
|
+
if (!enemies[j].alive) continue;
|
|
668
|
+
if (aabb(bullets[i].x, bullets[i].y, 14, 14,
|
|
669
|
+
enemies[j].x, enemies[j].y, 14, 14)) {
|
|
670
|
+
bullets[i].alive = 0;
|
|
671
|
+
enemies[j].alive = 0;
|
|
672
|
+
if (score < 60000u) score += 10;
|
|
673
|
+
++kills;
|
|
674
|
+
hud_dirty = 1;
|
|
675
|
+
psg_tone(3, 0x040, 31);
|
|
676
|
+
sfx_timer = 6;
|
|
677
|
+
break;
|
|
326
678
|
}
|
|
327
679
|
}
|
|
680
|
+
if (!bullets[i].alive) continue;
|
|
681
|
+
if (boss_active &&
|
|
682
|
+
aabb(bullets[i].x, bullets[i].y, 14, 14, boss_x, boss_y, 64, 30)) {
|
|
683
|
+
bullets[i].alive = 0;
|
|
684
|
+
boss_flash = 4;
|
|
685
|
+
psg_tone(3, 0x090, 29);
|
|
686
|
+
sfx_timer = 4;
|
|
687
|
+
if (--boss_hp == 0) boss_die();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/* enemies vs ship */
|
|
692
|
+
for (i = 0; i < MAX_ENEMIES; ++i) {
|
|
693
|
+
if (!enemies[i].alive) continue;
|
|
694
|
+
if (aabb(enemies[i].x, enemies[i].y, 14, 14, player.x, player.y, 14, 14)) {
|
|
695
|
+
enemies[i].alive = 0;
|
|
696
|
+
hit_ship();
|
|
697
|
+
if (state != ST_PLAY) return;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/* the next boss */
|
|
702
|
+
if (!boss_active && kills >= 10) boss_enter();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
void main(void) {
|
|
706
|
+
u8 newpad;
|
|
707
|
+
|
|
708
|
+
_pce_keep[0] = 0; /* see the EMPTY-BSS TRAP note in pce_hw.h */
|
|
709
|
+
|
|
710
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
711
|
+
* Init order: palette → VRAM uploads → BAT paint → joypad → display ON.
|
|
712
|
+
* disp_enable() also sets the VBlank IRQ bit — without it waitvsync()
|
|
713
|
+
* never returns and the game freezes on its first frame. */
|
|
714
|
+
/* BG sub-pal 0: starfield. BG sub-pal 1: HUD/text (white on band). */
|
|
715
|
+
vce_set_color(0, PCE_RGB(0, 0, 1)); /* backdrop: near-black blue */
|
|
716
|
+
vce_set_color(1, PCE_RGB(0, 0, 3)); /* band A: deep space */
|
|
717
|
+
vce_set_color(2, PCE_RGB(1, 1, 4)); /* band B: lighter space */
|
|
718
|
+
vce_set_color(3, PCE_RGB(7, 7, 7)); /* star pixel: white */
|
|
719
|
+
vce_set_color(17, PCE_RGB(7, 7, 7)); /* text: white */
|
|
720
|
+
/* sprite sub-palettes (256 + pal*16 + index) */
|
|
721
|
+
vce_set_color(257, PCE_RGB(2, 6, 7)); /* pal0 c1: ship cyan */
|
|
722
|
+
vce_set_color(273, PCE_RGB(7, 7, 0)); /* pal1 c1: bullet yellow */
|
|
723
|
+
vce_set_color(289, PCE_RGB(7, 1, 1)); /* pal2 c1: enemy red */
|
|
724
|
+
vce_set_color(290, PCE_RGB(7, 1, 1));
|
|
725
|
+
vce_set_color(291, PCE_RGB(7, 5, 2)); /* pal2 c3: red-flash highlight */
|
|
726
|
+
vce_set_color(305, PCE_RGB(4, 2, 7)); /* pal3 c1: boss hull violet */
|
|
727
|
+
vce_set_color(307, PCE_RGB(7, 6, 1)); /* pal3 c3: boss eye amber */
|
|
328
728
|
|
|
329
|
-
|
|
729
|
+
upload_art();
|
|
730
|
+
|
|
731
|
+
hiscore = hiscore_load(); /* always 0 — no persistence on a bare HuCard */
|
|
732
|
+
state = ST_TITLE;
|
|
733
|
+
paint_title();
|
|
734
|
+
music_set(ST_TITLE);
|
|
735
|
+
|
|
736
|
+
pce_joy_init();
|
|
737
|
+
disp_enable();
|
|
738
|
+
|
|
739
|
+
for (;;) {
|
|
740
|
+
waitvsync();
|
|
741
|
+
|
|
742
|
+
/* vblank work first: sprites + SATB DMA + queued BAT/VRAM writes */
|
|
743
|
+
push_sprites();
|
|
744
|
+
satb_dma();
|
|
745
|
+
if (hud_dirty && state != ST_TITLE) { draw_hud_numbers(); hud_dirty = 0; }
|
|
746
|
+
twinkle();
|
|
747
|
+
|
|
748
|
+
music_tick();
|
|
330
749
|
if (sfx_timer) {
|
|
331
750
|
--sfx_timer;
|
|
332
751
|
if (sfx_timer == 0) { psg_off(2); psg_off(3); }
|
|
333
752
|
}
|
|
334
753
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
754
|
+
pad = pce_joy_read();
|
|
755
|
+
newpad = (u8)(pad & ~prev_pad);
|
|
756
|
+
prev_pad = pad;
|
|
757
|
+
|
|
758
|
+
if (state == ST_TITLE) {
|
|
759
|
+
if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) start_game();
|
|
760
|
+
continue;
|
|
340
761
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
762
|
+
if (state == ST_OVER) {
|
|
763
|
+
/* freeze the final frame; RUN (or I) returns to the title */
|
|
764
|
+
if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) {
|
|
765
|
+
state = ST_TITLE;
|
|
766
|
+
paint_title();
|
|
767
|
+
music_set(ST_TITLE);
|
|
768
|
+
}
|
|
769
|
+
continue;
|
|
344
770
|
}
|
|
345
|
-
|
|
771
|
+
update_play();
|
|
346
772
|
}
|
|
347
773
|
}
|