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,133 +1,432 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* PC Engine "platformer" — a side-scrolling platformer scaffold.
|
|
1
|
+
/* ── main.c — PC Engine side-scrolling platformer (complete example game) ─────
|
|
3
2
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* A COMPLETE, working game — title screen, 1P mode and 2P ALTERNATING-TURNS
|
|
4
|
+
* mode (arcade-classic: players swap on death; each player has their own
|
|
5
|
+
* score and own 3 lives; player 2 plays on the SECOND pad), coins + distance
|
|
6
|
+
* scoring, in-session hi-score (a bare HuCard can't save — see the hi-score
|
|
7
|
+
* note below), music + SFX, and TWO of
|
|
8
|
+
* the PC Engine's signature features working together:
|
|
9
|
+
* - HARDWARE BG SCROLL: a world wider than one screen scrolled with the
|
|
10
|
+
* VDC's BXR register (zero per-frame tilemap rewrites once a column is
|
|
11
|
+
* painted) — the smoothest, cheapest scroll of any 8-bit machine.
|
|
12
|
+
* - LARGE MULTI-CELL SPRITES: the hero is a 32x32 HuC6270 sprite from ONE
|
|
13
|
+
* SATB entry (four 16x16 cells, 4-aligned pattern) — the kind of big,
|
|
14
|
+
* readable character the NES needs 4+ hardware sprites to draw.
|
|
8
15
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
16
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
17
|
+
* very different one. The markers tell you what's what:
|
|
18
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented PCE footgun; reshape
|
|
19
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
20
|
+
* GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
|
|
21
|
+
* freely.
|
|
14
22
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* What depends on what:
|
|
24
|
+
* pce_hw.h / pce_video.c / pce_input.c / pce_sound.c — the helper lib
|
|
25
|
+
* (VDC/VCE/PSG register dances + joypad). The HARDWARE IDIOM markers in
|
|
26
|
+
* pce_video.c say which parts are load-bearing.
|
|
27
|
+
* cc65's pce crt0 + pce.lib are auto-linked; the 'rom32k' linker preset
|
|
28
|
+
* (applied automatically to example projects) gives a 32KB HuCard.
|
|
19
29
|
*
|
|
20
|
-
*
|
|
30
|
+
* 2P, honestly: the stock PC Engine has ONE controller port; 2P needs a
|
|
31
|
+
* TurboTap. The geargrafx core implements the TurboTap and the romdev host
|
|
32
|
+
* now force-ENABLES it (PLATFORM_CORE_OPTIONS pce: geargrafx_turbotap), so a
|
|
33
|
+
* second pad's input reaches the game on pad slot 2 — verified by driving
|
|
34
|
+
* port-1 input and seeing P2 move. So this game ships REAL 2P alternating
|
|
35
|
+
* turns. (On real hardware the player plugs a TurboTap and a second pad.)
|
|
36
|
+
*
|
|
37
|
+
* Frame budget (NTSC, 60fps, 7.16MHz 65C02-class CPU): player physics + a
|
|
38
|
+
* two-column ground probe + (3 coins + 2 spikes) of AABB + a 256-word SATB
|
|
39
|
+
* copy in vblank + at most one streamed BAT column fit comfortably in one
|
|
40
|
+
* frame. Hardware scroll (BXR) is free; rewriting the whole tilemap per frame
|
|
41
|
+
* would NOT fit — column streaming is why this scrolls smoothly.
|
|
21
42
|
*/
|
|
22
43
|
#include <pce.h>
|
|
23
|
-
#include <stdint.h> /*
|
|
44
|
+
#include <stdint.h> /* int16_t/int32_t for sub-pixel physics + camera */
|
|
45
|
+
#include <joystick.h> /* JOY_2 + joy_read for the 2nd pad (TurboTap port 1) */
|
|
24
46
|
#include "pce_hw.h"
|
|
25
47
|
|
|
26
|
-
/*
|
|
27
|
-
|
|
28
|
-
#define
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
49
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
50
|
+
#define GAME_TITLE "GLADE DASH"
|
|
51
|
+
|
|
52
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
53
|
+
* VRAM map (WORD addresses — the VDC is a 16-bit-word machine; an 8x8 tile is
|
|
54
|
+
* 16 words, a 16x16 sprite cell is 64). Sprites and BG tiles share one 64KB
|
|
55
|
+
* VRAM, so lay it out ONCE and keep the SATB out of pattern space:
|
|
56
|
+
* $0000 BAT (32x32 background map — matches vdc_init's VDC_MWR setting)
|
|
57
|
+
* $1000 font glyphs (38 tiles: blank, 0-9, A-Z, dash)
|
|
58
|
+
* $1400 BG scenery tiles (sky, dirt, grass, slab, hud band)
|
|
59
|
+
* $1800 16x16 sprite cells: coin, spike
|
|
60
|
+
* $1900 PLAYER pattern cells — 4-ALIGNED cell index (32x32 large sprite)
|
|
61
|
+
* $7F00 shadow SATB destination (satb_dma copies it here, VDC reads it) */
|
|
62
|
+
#define BAT_VRAM 0x0000
|
|
63
|
+
#define FONT_VRAM 0x1000
|
|
64
|
+
#define SKY_VRAM 0x1400 /* solid colour 1 — sky */
|
|
65
|
+
#define DIRT_VRAM 0x1410 /* solid colour 2 — ground body */
|
|
66
|
+
#define GRASS_VRAM 0x1420 /* colour-3 lip over colour-2 body */
|
|
67
|
+
#define SLAB_VRAM 0x1430 /* colour-3 thin one-way platform */
|
|
68
|
+
#define HUDBAND_VRAM 0x1440 /* solid colour 2 — band behind the HUD text */
|
|
69
|
+
#define COIN_VRAM 0x1800 /* 16x16 sprite cell */
|
|
70
|
+
#define SPIKE_VRAM 0x1840 /* 16x16 sprite cell */
|
|
71
|
+
#define PLAYER_VRAM 0x1900 /* 4 cells (TL,TR,BL,BR) — 4-aligned (see idiom) */
|
|
32
72
|
|
|
33
73
|
#define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
|
|
34
74
|
|
|
35
|
-
/*
|
|
36
|
-
#define
|
|
37
|
-
#define
|
|
38
|
-
#define
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
/* Sprite pattern codes = VRAM >> 6 (the 16x16 cell index). */
|
|
76
|
+
#define COIN_PAT (COIN_VRAM >> 6)
|
|
77
|
+
#define SPIKE_PAT (SPIKE_VRAM >> 6)
|
|
78
|
+
#define PLAYER_PAT (PLAYER_VRAM >> 6) /* 0x64 — multiple of 4 */
|
|
79
|
+
|
|
80
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
81
|
+
* SATB slot plan (slot order is also priority: LOWER slot wins overlaps on
|
|
82
|
+
* the HuC6270):
|
|
83
|
+
* 0 player (a 32x32 large sprite — ONE SATB entry)
|
|
84
|
+
* 1-3 coins
|
|
85
|
+
* 4-5 spikes
|
|
86
|
+
* Everything else stays parked off-screen. */
|
|
87
|
+
#define SLOT_PLAYER 0
|
|
88
|
+
#define SLOT_COIN 1
|
|
89
|
+
#define SLOT_SPIKE 4
|
|
90
|
+
#define NUM_COINS 3
|
|
91
|
+
#define NUM_SPIKES 2
|
|
92
|
+
#define OFFSCREEN_Y 0x1F0 /* park unused sprites below the display */
|
|
93
|
+
|
|
94
|
+
#define PAL_PLAYER 0
|
|
95
|
+
#define PAL_COIN 1
|
|
96
|
+
#define PAL_SPIKE 2
|
|
97
|
+
|
|
98
|
+
/* ── GAME LOGIC (clay) — the world ───────────────────────────────────────────
|
|
99
|
+
* A 96-cell (768px) world, wider than the 256px screen. The BAT is a 32x32
|
|
100
|
+
* virtual map that WRAPS at 256px, so a wider world needs COLUMN STREAMING:
|
|
101
|
+
* each time the camera crosses an 8px boundary we rewrite the BAT column about
|
|
102
|
+
* to scroll into view with the next world column's tiles. Rows are 8px:
|
|
103
|
+
* ground_row[c] — BAT row of the grass top, 0xFF = pit.
|
|
104
|
+
* plat_row[c] — BAT row of a one-way slab, 0 = none.
|
|
105
|
+
* Playfield rows are 3..27 (rows 0-2 sit under the HUD). */
|
|
106
|
+
#define WORLD_COLS 96
|
|
107
|
+
#define WORLD_W (WORLD_COLS * 8)
|
|
108
|
+
#define SCREEN_W 256
|
|
109
|
+
#define VIS_ROWS 28 /* 224-line display = 28 rows */
|
|
110
|
+
#define NO_GROUND 0xFF
|
|
111
|
+
#define GROUND_R 25 /* default ground surface row (y = 200) */
|
|
112
|
+
#define HUD_ROWS 3 /* rows 0-2 reserved for HUD (drawn fixed) */
|
|
113
|
+
|
|
114
|
+
static const u8 ground_row[WORLD_COLS] = {
|
|
115
|
+
25,25,25,25,25,25,25,25, /* start runway */
|
|
116
|
+
25,25,25,25, NO_GROUND,NO_GROUND, 25,25, /* pit 1 (16px) */
|
|
117
|
+
25,25,25,25,25,25,25,25,
|
|
118
|
+
25,25, NO_GROUND,NO_GROUND,NO_GROUND, 25,25,25, /* pit 2 (24px) */
|
|
119
|
+
25,25,25,25,25,25,25,25,
|
|
120
|
+
25,25,25, NO_GROUND,NO_GROUND, 25,25,25, /* pit 3 (16px) */
|
|
121
|
+
25,25,25,25,25,25,25,25,
|
|
122
|
+
25, NO_GROUND,NO_GROUND,NO_GROUND, 25,25,25,25, /* pit 4 (24px) */
|
|
123
|
+
25,25,25,25,25,25,25,25,
|
|
124
|
+
25,25,25,25, NO_GROUND,NO_GROUND, 25,25, /* pit 5 (16px) */
|
|
125
|
+
25,25,25,25,25,25,25,25,
|
|
126
|
+
25,25,25,25,25,25,25,25, /* run-out to the loop */
|
|
127
|
+
};
|
|
128
|
+
static const u8 plat_row[WORLD_COLS] = {
|
|
129
|
+
0,0,0,0, 21,21,21, 0, /* warm-up slab */
|
|
130
|
+
0,0, 19,19,19, 0,0,0, /* bridge over pit 1 */
|
|
131
|
+
0,0,0, 18,18, 0,0,0,
|
|
132
|
+
0, 20,20, 0,0,0, 0,0, /* hop near pit 2 */
|
|
133
|
+
0,0,0, 17,17,17, 0,0, /* high ledge */
|
|
134
|
+
0,0, 19,19, 0,0,0,0, /* over pit 3 */
|
|
135
|
+
21,21, 0,0,0, 19,19, 0,
|
|
136
|
+
0, 18,18,18, 0,0,0,0, /* over pit 4 */
|
|
137
|
+
0,0,0, 20,20, 0,0,0,
|
|
138
|
+
0,0, 19,19, 0,0,0,0, /* over pit 5 */
|
|
139
|
+
0, 21,21,21, 0,0,0,0,
|
|
140
|
+
0,0,0,0,0,0,0,0,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
typedef struct { int16_t x, y; u8 alive; } Obj;
|
|
144
|
+
|
|
145
|
+
/* ── GAME LOGIC (clay) — physics + tuning (Q4.4 fixed point: 16 = 1 px) ── */
|
|
146
|
+
#define GRAVITY 10
|
|
147
|
+
#define JUMP_VEL (-104) /* ~36px apex (~4.5 tiles) — clears a pit */
|
|
148
|
+
#define MAX_VY 64 /* terminal 4 px/frame — MUST stay under 5:
|
|
149
|
+
* the landing probe's +4 window can't *
|
|
150
|
+
* catch a faster fall (tunnelling) */
|
|
151
|
+
#define MOVE 34 /* px/16 per frame walk + scroll speed */
|
|
152
|
+
#define SCROLL_WALL 120 /* px: past this the world scrolls, not you */
|
|
153
|
+
#define GROUND_TOP (GROUND_R * 8)
|
|
154
|
+
#define SPIKE_Y 192
|
|
155
|
+
#define START_LIVES 3
|
|
156
|
+
|
|
157
|
+
static int16_t px; /* player screen x (px) */
|
|
158
|
+
static int16_t py_q44; /* player y, Q4.4 — gravity adds <1 px/frame
|
|
159
|
+
* near the apex; integer y would stick */
|
|
160
|
+
static int16_t vy_q44;
|
|
161
|
+
static u8 on_ground;
|
|
162
|
+
static int16_t camX, lastCamCol; /* world scroll (px) + last streamed column */
|
|
163
|
+
static u8 dist_sub; /* sub-counter: 64 px scrolled = +1 point */
|
|
164
|
+
static Obj coins[NUM_COINS];
|
|
165
|
+
static Obj spikes[NUM_SPIKES];
|
|
166
|
+
|
|
167
|
+
/* Players: index 0 = P1 (pad 1), 1 = P2 (pad 2 — alternating turns). Each has
|
|
168
|
+
* own score + own lives; the HUD shows the CURRENT player's numbers. */
|
|
169
|
+
static u8 two_player;
|
|
170
|
+
static u8 cur_player;
|
|
171
|
+
static u8 p_lives[2];
|
|
172
|
+
static u16 p_score[2];
|
|
173
|
+
static u16 hiscore;
|
|
174
|
+
static u8 turn_pause; /* freeze frames after a turn change */
|
|
175
|
+
static u16 rng = 0xC0DE;
|
|
176
|
+
|
|
177
|
+
static u8 pad, prev_pad; /* CURRENT-player pad this frame */
|
|
178
|
+
static u8 sfx_timer;
|
|
179
|
+
static u8 hud_dirty;
|
|
180
|
+
static u8 anim_frame; /* player walk-cycle phase */
|
|
181
|
+
|
|
182
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
183
|
+
#define ST_TITLE 0
|
|
184
|
+
#define ST_PLAY 1
|
|
185
|
+
#define ST_OVER 2
|
|
186
|
+
static u8 state;
|
|
187
|
+
|
|
188
|
+
static u16 tile_buf[16]; /* scratch for one 8x8 tile */
|
|
189
|
+
static u16 spr_buf[64]; /* scratch for one 16x16 sprite cell */
|
|
190
|
+
|
|
191
|
+
/* ── GAME LOGIC (clay) — 5x7 glyph font: blank, 0-9, A-Z, dash ──────────────
|
|
192
|
+
* Each glyph is 7 rows of 5 bits (bit4 = leftmost). upload_font() expands
|
|
193
|
+
* them into 8x8 1-plane tiles; drawn with BG sub-palette 1 (white). */
|
|
194
|
+
#define G_BLANK 0
|
|
195
|
+
#define G_DIGIT 1 /* '0'..'9' -> glyphs 1..10 */
|
|
196
|
+
#define G_ALPHA 11 /* 'A'..'Z' -> glyphs 11..36 */
|
|
197
|
+
#define G_DASH 37
|
|
198
|
+
#define NUM_GLYPHS 38
|
|
199
|
+
|
|
200
|
+
static const u8 FONT5x7[NUM_GLYPHS][7] = {
|
|
201
|
+
{0,0,0,0,0,0,0},
|
|
202
|
+
{0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
|
|
203
|
+
{0x0E,0x11,0x01,0x02,0x04,0x08,0x1F}, {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
|
|
204
|
+
{0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
|
|
205
|
+
{0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
|
|
206
|
+
{0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C},
|
|
207
|
+
{0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}, {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E},
|
|
208
|
+
{0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E},
|
|
209
|
+
{0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10},
|
|
210
|
+
{0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, {0x11,0x11,0x11,0x1F,0x11,0x11,0x11},
|
|
211
|
+
{0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, {0x07,0x02,0x02,0x02,0x02,0x12,0x0C},
|
|
212
|
+
{0x11,0x12,0x14,0x18,0x14,0x12,0x11}, {0x10,0x10,0x10,0x10,0x10,0x10,0x1F},
|
|
213
|
+
{0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, {0x11,0x19,0x15,0x13,0x11,0x11,0x11},
|
|
214
|
+
{0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10},
|
|
215
|
+
{0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11},
|
|
216
|
+
{0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}, {0x1F,0x04,0x04,0x04,0x04,0x04,0x04},
|
|
217
|
+
{0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x11,0x11,0x11,0x11,0x11,0x0A,0x04},
|
|
218
|
+
{0x11,0x11,0x11,0x15,0x15,0x15,0x0A}, {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11},
|
|
219
|
+
{0x11,0x11,0x0A,0x04,0x04,0x04,0x04}, {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F},
|
|
220
|
+
{0x00,0x00,0x00,0x1F,0x00,0x00,0x00},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/* ── GAME LOGIC (clay) — the 32x32 hero, two walk frames (32 rows × 32 bits).
|
|
224
|
+
* Two u16 per row (cols 0-15, cols 16-31). body = colour 1 (plane0), the
|
|
225
|
+
* face/cap accents = colour 3 (planes 0+1, a subset of body). A round forest
|
|
226
|
+
* sprite (think a bounding critter) — big and readable, the PCE's strength. */
|
|
227
|
+
static const u16 hero_body_a[64] = {
|
|
228
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0007,0xE000, 0x001F,0xF800,
|
|
229
|
+
0x003F,0xFC00, 0x007F,0xFE00, 0x00FF,0xFF00, 0x01FF,0xFF80,
|
|
230
|
+
0x01FF,0xFF80, 0x03FF,0xFFC0, 0x03FF,0xFFC0, 0x03FF,0xFFC0,
|
|
231
|
+
0x07FF,0xFFE0, 0x07FF,0xFFE0, 0x07FF,0xFFE0, 0x07FF,0xFFE0,
|
|
232
|
+
0x07FF,0xFFE0, 0x07FF,0xFFE0, 0x03FF,0xFFC0, 0x03FF,0xFFC0,
|
|
233
|
+
0x01FF,0xFF80, 0x01FF,0xFF80, 0x00FF,0xFF00, 0x007F,0xFE00,
|
|
234
|
+
0x003F,0xFC00, 0x003C,0x3C00, 0x0078,0x1E00, 0x0070,0x0E00,
|
|
235
|
+
0x00E0,0x0700, 0x01C0,0x0380, 0x0380,0x01C0, 0x0700,0x00E0,
|
|
236
|
+
};
|
|
237
|
+
static const u16 hero_body_b[64] = {
|
|
238
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0007,0xE000, 0x001F,0xF800,
|
|
239
|
+
0x003F,0xFC00, 0x007F,0xFE00, 0x00FF,0xFF00, 0x01FF,0xFF80,
|
|
240
|
+
0x01FF,0xFF80, 0x03FF,0xFFC0, 0x03FF,0xFFC0, 0x03FF,0xFFC0,
|
|
241
|
+
0x07FF,0xFFE0, 0x07FF,0xFFE0, 0x07FF,0xFFE0, 0x07FF,0xFFE0,
|
|
242
|
+
0x07FF,0xFFE0, 0x07FF,0xFFE0, 0x03FF,0xFFC0, 0x03FF,0xFFC0,
|
|
243
|
+
0x01FF,0xFF80, 0x01FF,0xFF80, 0x00FF,0xFF00, 0x007F,0xFE00,
|
|
244
|
+
0x003F,0xFC00, 0x001F,0xF800, 0x003C,0x3C00, 0x0038,0x1C00,
|
|
245
|
+
0x0070,0x0E00, 0x00E0,0x0700, 0x01C0,0x0380, 0x0380,0x01C0,
|
|
246
|
+
};
|
|
247
|
+
/* eyes/cap accent (colour 3) — same for both frames, near the top of the head */
|
|
248
|
+
static const u16 hero_face[64] = {
|
|
249
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
|
|
250
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0030,0x0C00,
|
|
251
|
+
0x0078,0x1E00, 0x0078,0x1E00, 0x0030,0x0C00, 0x0000,0x0000,
|
|
252
|
+
0x0000,0x0000, 0x0000,0x0000, 0x00C0,0x0300, 0x00C0,0x0300,
|
|
253
|
+
0x0070,0x0E00, 0x003F,0xFC00, 0x0000,0x0000, 0x0000,0x0000,
|
|
254
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
|
|
255
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
|
|
256
|
+
0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000, 0x0000,0x0000,
|
|
57
257
|
};
|
|
58
|
-
#define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
|
|
59
258
|
|
|
60
|
-
/*
|
|
61
|
-
static
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
static u16
|
|
66
|
-
|
|
259
|
+
/* ── GAME LOGIC (clay) — 16x16 sprite masks (16 rows × 16 bits, bit15 left) ── */
|
|
260
|
+
static const u16 coin_mask[16] = {
|
|
261
|
+
0x0000, 0x07E0, 0x1FF8, 0x3C3C, 0x381C, 0x73CE, 0x77EE, 0x77EE,
|
|
262
|
+
0x77EE, 0x77EE, 0x73CE, 0x381C, 0x3C3C, 0x1FF8, 0x07E0, 0x0000
|
|
263
|
+
};
|
|
264
|
+
static const u16 spike_mask[16] = {
|
|
265
|
+
0x0000, 0x0000, 0x0180, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x07E0,
|
|
266
|
+
0x0FF0, 0x0FF0, 0x1FF8, 0x1FF8, 0x3FFC, 0x7FFE, 0xFFFF, 0xFFFF
|
|
267
|
+
};
|
|
67
268
|
|
|
269
|
+
/* ── GAME LOGIC (clay) — tile/sprite builders ────────────────────────────── */
|
|
68
270
|
static void make_solid_tile(u16 *t, u8 ci) {
|
|
69
271
|
u8 r;
|
|
70
272
|
u8 p0 = (ci & 1) ? 0xFF : 0x00;
|
|
71
273
|
u8 p1 = (ci & 2) ? 0xFF : 0x00;
|
|
72
|
-
u8 p2 = (ci & 4) ? 0xFF : 0x00;
|
|
73
|
-
u8 p3 = (ci & 8) ? 0xFF : 0x00;
|
|
74
274
|
for (r = 0; r < 8; ++r) {
|
|
75
275
|
t[r] = (u16)(p0 | (p1 << 8));
|
|
76
|
-
t[r + 8] =
|
|
276
|
+
t[r + 8] = 0;
|
|
77
277
|
}
|
|
78
278
|
}
|
|
79
279
|
|
|
80
|
-
/*
|
|
81
|
-
static void
|
|
82
|
-
make_solid_tile(t, 2);
|
|
83
|
-
/* rows 0,1: set plane0 too
|
|
84
|
-
t[
|
|
85
|
-
|
|
280
|
+
/* grass: colour-2 body with a colour-3 lip on the top 2 rows */
|
|
281
|
+
static void make_grass_tile(u16 *t) {
|
|
282
|
+
make_solid_tile(t, 2); /* body = colour 2 (plane1) */
|
|
283
|
+
t[0] |= 0x00FF; /* rows 0,1: set plane0 too → colour 3 */
|
|
284
|
+
t[1] |= 0x00FF;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* one-way slab: a colour-3 bar on the TOP 4 rows only (you jump up through
|
|
288
|
+
* the transparent bottom) */
|
|
289
|
+
static void make_slab_tile(u16 *t) {
|
|
290
|
+
u8 r;
|
|
291
|
+
for (r = 0; r < 16; ++r) t[r] = 0;
|
|
292
|
+
for (r = 0; r < 4; ++r) { t[r] = 0x00FF; t[r + 8] = 0x00FF; } /* colour 3 */
|
|
86
293
|
}
|
|
87
294
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
0x07E0, 0x0FF0, 0x1FF8, 0x1818, 0x1FF8, 0x1FF8, 0x3FFC, 0x7FFE,
|
|
91
|
-
0x7FFE, 0x7FFE, 0x3FFC, 0x1FF8, 0x0E70, 0x0C30, 0x0C30, 0x1818
|
|
92
|
-
};
|
|
93
|
-
static const u16 eyes[16] = {
|
|
94
|
-
0x0000, 0x0000, 0x0000, 0x0000, 0x0990, 0x0990, 0x0000, 0x0000,
|
|
95
|
-
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
|
|
96
|
-
};
|
|
295
|
+
/* one-colour 16x16 sprite cell from a 16-row mask */
|
|
296
|
+
static void make_sprite16(u16 vram, const u16 *mask, u8 ci) {
|
|
97
297
|
u8 r;
|
|
98
298
|
for (r = 0; r < 64; ++r) spr_buf[r] = 0;
|
|
99
299
|
for (r = 0; r < 16; ++r) {
|
|
100
|
-
spr_buf[r] =
|
|
101
|
-
spr_buf[r + 16] =
|
|
300
|
+
if (ci & 1) spr_buf[r] = mask[r]; /* plane 0 */
|
|
301
|
+
if (ci & 2) spr_buf[r + 16] = mask[r]; /* plane 1 */
|
|
102
302
|
}
|
|
103
|
-
load_tiles(
|
|
303
|
+
load_tiles(vram, spr_buf, 64);
|
|
104
304
|
}
|
|
105
305
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
306
|
+
static void upload_font(void) {
|
|
307
|
+
u8 g, row, bits, px2;
|
|
308
|
+
for (g = 0; g < NUM_GLYPHS; ++g) {
|
|
309
|
+
for (row = 0; row < 16; ++row) tile_buf[row] = 0;
|
|
310
|
+
for (row = 0; row < 7; ++row) {
|
|
311
|
+
bits = FONT5x7[g][row];
|
|
312
|
+
px2 = 0;
|
|
313
|
+
if (bits & 0x10) px2 |= 0x40;
|
|
314
|
+
if (bits & 0x08) px2 |= 0x20;
|
|
315
|
+
if (bits & 0x04) px2 |= 0x10;
|
|
316
|
+
if (bits & 0x02) px2 |= 0x08;
|
|
317
|
+
if (bits & 0x01) px2 |= 0x04;
|
|
318
|
+
tile_buf[row] = (u16)px2;
|
|
319
|
+
}
|
|
320
|
+
load_tiles((u16)(FONT_VRAM + g * 16), tile_buf, 16);
|
|
116
321
|
}
|
|
117
|
-
return 0;
|
|
118
322
|
}
|
|
119
323
|
|
|
120
|
-
/*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
324
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
325
|
+
* LARGE-SPRITE PATTERN LAYOUT — the half of the big-hero trick that lives in
|
|
326
|
+
* VRAM. A 32x32 HuC6270 sprite is FOUR 16x16 cells (64 words each) stored
|
|
327
|
+
* consecutively in TL, TR, BL, BR order, and its SATB pattern code must be
|
|
328
|
+
* 4-ALIGNED (the hardware ignores the low 2 bits and adds them back as
|
|
329
|
+
* column/row). Get the order wrong and the hero renders scrambled — four
|
|
330
|
+
* recognizable quarters in the wrong places. The other half of the trick
|
|
331
|
+
* (the SATB attribute bits) is in push_sprites() below.
|
|
332
|
+
*
|
|
333
|
+
* `body` selects the walk frame (hero_body_a / hero_body_b). `face` is the
|
|
334
|
+
* colour-3 accent shared by both. We upload BOTH frames' worth of cells when
|
|
335
|
+
* the walk phase flips — cheap (256 words) and only on phase change.
|
|
336
|
+
*
|
|
337
|
+
* requires: PLAYER_VRAM >> 6 a multiple of 4; 4 consecutive free cells
|
|
338
|
+
* (256 words) at PLAYER_VRAM; set_sprite_ex() from pce_video.c. */
|
|
339
|
+
static void upload_hero(const u16 *body) {
|
|
340
|
+
u8 cr, cc, row;
|
|
341
|
+
u16 body_bits, face_bits;
|
|
342
|
+
u16 vram = PLAYER_VRAM;
|
|
343
|
+
for (cr = 0; cr < 2; ++cr) { /* cell row (top/bottom) */
|
|
344
|
+
for (cc = 0; cc < 2; ++cc) { /* cell col (left/right) */
|
|
345
|
+
for (row = 0; row < 64; ++row) spr_buf[row] = 0;
|
|
346
|
+
for (row = 0; row < 16; ++row) {
|
|
347
|
+
u8 y = (u8)(cr * 16 + row);
|
|
348
|
+
body_bits = body[y * 2 + cc];
|
|
349
|
+
face_bits = hero_face[y * 2 + cc];
|
|
350
|
+
/* body pixels = colour 1 (plane0); face accents = colour 3
|
|
351
|
+
* (planes 0+1) — the accent is a subset of the body. */
|
|
352
|
+
spr_buf[row] = body_bits;
|
|
353
|
+
spr_buf[row + 16] = face_bits;
|
|
354
|
+
}
|
|
355
|
+
load_tiles(vram, spr_buf, 64);
|
|
356
|
+
vram += 64; /* next cell: TL,TR,BL,BR */
|
|
357
|
+
}
|
|
129
358
|
}
|
|
130
|
-
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
static void upload_art(void) {
|
|
362
|
+
upload_font();
|
|
363
|
+
make_solid_tile(tile_buf, 1); load_tiles(SKY_VRAM, tile_buf, 16);
|
|
364
|
+
make_solid_tile(tile_buf, 2); load_tiles(DIRT_VRAM, tile_buf, 16);
|
|
365
|
+
make_grass_tile(tile_buf); load_tiles(GRASS_VRAM, tile_buf, 16);
|
|
366
|
+
make_slab_tile(tile_buf); load_tiles(SLAB_VRAM, tile_buf, 16);
|
|
367
|
+
make_solid_tile(tile_buf, 2); load_tiles(HUDBAND_VRAM, tile_buf, 16);
|
|
368
|
+
make_sprite16(COIN_VRAM, coin_mask, 1);
|
|
369
|
+
make_sprite16(SPIKE_VRAM, spike_mask, 1);
|
|
370
|
+
upload_hero(hero_body_a);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* ── GAME LOGIC (clay) — BAT text + level paint ─────────────────────────────── */
|
|
374
|
+
static void put_glyph(u8 col, u8 row, u8 glyph) {
|
|
375
|
+
u16 e = BAT_ENTRY(1, (u16)(FONT_VRAM + glyph * 16)); /* pal 1 = white */
|
|
376
|
+
vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
|
|
377
|
+
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
378
|
+
VDC_DATA_HI = (u8)(e >> 8);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
static void draw_text(u8 col, u8 row, const char *s) {
|
|
382
|
+
u8 c;
|
|
383
|
+
while ((c = (u8)*s++) != 0) {
|
|
384
|
+
u8 g = G_BLANK;
|
|
385
|
+
if (c >= '0' && c <= '9') g = (u8)(G_DIGIT + c - '0');
|
|
386
|
+
else if (c >= 'A' && c <= 'Z') g = (u8)(G_ALPHA + c - 'A');
|
|
387
|
+
else if (c == '-') g = G_DASH;
|
|
388
|
+
put_glyph(col++, row, g);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
static void draw_num5(u8 col, u8 row, u16 v) {
|
|
393
|
+
u8 i, d[5];
|
|
394
|
+
for (i = 0; i < 5; ++i) { d[i] = (u8)(v % 10); v /= 10; }
|
|
395
|
+
for (i = 0; i < 5; ++i) put_glyph((u8)(col + i), row, (u8)(G_DIGIT + d[4 - i]));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
399
|
+
* HARDWARE BG SCROLL via BXR + COLUMN STREAMING — the PCE's smoothest trick.
|
|
400
|
+
* The BAT is a 32x32 (256px) virtual map that WRAPS, and the VDC's R7 (BXR)
|
|
401
|
+
* shifts the whole background horizontally with ZERO CPU per pixel. For a
|
|
402
|
+
* world WIDER than 256px we stream: as the camera advances, each BAT column
|
|
403
|
+
* about to wrap into view is rewritten with the next world column's tiles.
|
|
404
|
+
* Paint a column ONCE when it enters; from then on the scroll is free.
|
|
405
|
+
*
|
|
406
|
+
* THE HUD CAVEAT: BXR scrolls the ENTIRE background, including the top rows.
|
|
407
|
+
* The PCE has no hardware "window" plane (the Genesis trick), and no built-in
|
|
408
|
+
* raster split (the SMS/NES trick) in this minimal lib. So we keep the HUD
|
|
409
|
+
* readable by drawing it into the BAT rows 0-2 EVERY column we stream — the
|
|
410
|
+
* HUD text scrolls with the world, but because it's repainted into each fresh
|
|
411
|
+
* column it appears continuous across the whole top of the screen. A fancier
|
|
412
|
+
* fork can add a raster IRQ to reset BXR mid-frame for a truly fixed HUD; see
|
|
413
|
+
* TROUBLESHOOTING. For a clean teaching scaffold this "painted band" HUD is
|
|
414
|
+
* honest and flicker-free.
|
|
415
|
+
*
|
|
416
|
+
* requires: BXR written every frame (we do, in the loop); each world column
|
|
417
|
+
* painted exactly once as it enters; the BAT 32x32 (vdc_init's MWR). */
|
|
418
|
+
static u16 bat_entry_for(int16_t worldCol, u8 row) {
|
|
419
|
+
u8 g = ground_row[worldCol];
|
|
420
|
+
if (row < HUD_ROWS) return BAT_ENTRY(0, HUDBAND_VRAM); /* HUD band */
|
|
421
|
+
if (row < VIS_ROWS) {
|
|
422
|
+
if (plat_row[worldCol] && row == plat_row[worldCol])
|
|
423
|
+
return BAT_ENTRY(0, SLAB_VRAM); /* one-way slab */
|
|
424
|
+
if (g != NO_GROUND) {
|
|
425
|
+
if (row == g) return BAT_ENTRY(0, GRASS_VRAM); /* grass top */
|
|
426
|
+
if (row > g) return BAT_ENTRY(0, DIRT_VRAM); /* ground body */
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return BAT_ENTRY(0, SKY_VRAM); /* sky backdrop */
|
|
131
430
|
}
|
|
132
431
|
|
|
133
432
|
/* Write one world column into its wrapped BAT column. */
|
|
@@ -137,149 +436,519 @@ static void paint_column(int16_t worldCol) {
|
|
|
137
436
|
if (worldCol < 0 || worldCol >= WORLD_COLS) return;
|
|
138
437
|
ntCol = (u8)(worldCol & 31);
|
|
139
438
|
for (row = 0; row < 32; ++row) {
|
|
140
|
-
|
|
141
|
-
e = cell_is_top(worldCol, row)
|
|
142
|
-
? BAT_ENTRY(0, WALLTOP_VRAM)
|
|
143
|
-
: BAT_ENTRY(0, WALL_VRAM);
|
|
144
|
-
} else {
|
|
145
|
-
e = BAT_ENTRY(0, SKY_VRAM);
|
|
146
|
-
}
|
|
439
|
+
e = bat_entry_for(worldCol, row);
|
|
147
440
|
vram_set_write_addr((u16)(BAT_VRAM + row * 32 + ntCol));
|
|
148
441
|
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
149
442
|
VDC_DATA_HI = (u8)(e >> 8);
|
|
150
443
|
}
|
|
151
444
|
}
|
|
152
445
|
|
|
153
|
-
|
|
446
|
+
/* Repaint the first 32 columns (one screen) from scratch — used when (re)entering
|
|
447
|
+
* the level so the visible window is correct before the first scroll. */
|
|
448
|
+
static void paint_screen_from(int16_t firstCol) {
|
|
154
449
|
int16_t c;
|
|
155
|
-
for (c =
|
|
450
|
+
for (c = firstCol; c < firstCol + 32; ++c)
|
|
451
|
+
if (c >= 0 && c < WORLD_COLS) paint_column(c);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* Fill the whole 32x32 BAT with sky (title / game-over backdrop). */
|
|
455
|
+
static void paint_flat_sky(void) {
|
|
456
|
+
u8 r, c;
|
|
457
|
+
u16 e = BAT_ENTRY(0, SKY_VRAM);
|
|
458
|
+
for (r = 0; r < 32; ++r) {
|
|
459
|
+
vram_set_write_addr((u16)(BAT_VRAM + r * 32));
|
|
460
|
+
for (c = 0; c < 32; ++c) {
|
|
461
|
+
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
462
|
+
VDC_DATA_HI = (u8)(e >> 8);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* HUD: row 1 = "P1 x3 SC 00000 HI 00000". The HUD lives in the BAT band rows
|
|
468
|
+
* (painted into every streamed column), so writing the numbers once at the
|
|
469
|
+
* left of the BAT keeps them at screen-left while BXR scrolls (they reappear
|
|
470
|
+
* via the band but the live digits are what the player reads). */
|
|
471
|
+
static void draw_hud_numbers(void) {
|
|
472
|
+
put_glyph(1, 1, (u8)(G_ALPHA + ('P' - 'A')));
|
|
473
|
+
put_glyph(2, 1, (u8)(G_DIGIT + 1 + cur_player));
|
|
474
|
+
put_glyph(4, 1, (u8)(G_ALPHA + ('X' - 'A')));
|
|
475
|
+
put_glyph(5, 1, (u8)(G_DIGIT + p_lives[cur_player]));
|
|
476
|
+
draw_text(7, 1, "SC");
|
|
477
|
+
draw_num5(10, 1, p_score[cur_player]);
|
|
478
|
+
draw_text(17, 1, "HI");
|
|
479
|
+
draw_num5(20, 1, hiscore);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* ── HARDWARE TRUTH: a bare HuCard CANNOT save a hi-score (in-session only) ──
|
|
483
|
+
* This was researched and corrected: earlier versions wrote the hi-score to
|
|
484
|
+
* BRAM ("backup RAM", bank $F7) and claimed it persisted across power cycles.
|
|
485
|
+
* That is NOT honest for a HuCard game. On REAL hardware a plain HuCard plugged
|
|
486
|
+
* into a base PC Engine / TurboGrafx-16 has NO backup RAM at all — BRAM exists
|
|
487
|
+
* ONLY when a peripheral is attached: the CD-ROM² System (2KB kept by a
|
|
488
|
+
* supercapacitor), the Tennokoe Bank HuCard, or the Memory Base 128. No
|
|
489
|
+
* commercial HuCard self-saved; they used PASSWORDS. (The often-cited Populous
|
|
490
|
+
* "ROMRAM" SRAM was the game's own working RAM, not a battery save.) An
|
|
491
|
+
* emulator like geargrafx exposes BRAM unconditionally, so the old code
|
|
492
|
+
* "worked" in emulation in a way the real machine never would.
|
|
493
|
+
*
|
|
494
|
+
* So this game keeps an IN-SESSION hi-score only (like the honest 2600/Lynx
|
|
495
|
+
* examples) — it survives game-overs within a power-on, resets to 0 on a cold
|
|
496
|
+
* boot. To make it ACTUALLY persist on real hardware you would target a
|
|
497
|
+
* peripheral: write to BRAM only after detecting one (and go through the System
|
|
498
|
+
* Card BIOS's 'HUBM' directory for CD saves), or move the game to a CD-ROM²
|
|
499
|
+
* build. Either is a real-hardware feature, not a property of the cartridge. */
|
|
500
|
+
static u16 hiscore_load(void) {
|
|
501
|
+
return 0; /* cold boot: no persistence on a bare HuCard */
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
static void hiscore_save(u16 v) {
|
|
505
|
+
(void)v; /* in-session only — nowhere to persist on real HW */
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/* ── GAME LOGIC (clay) — music: a 2-channel tune ticked once per frame ──────
|
|
509
|
+
* PSG channel plan: 5 = melody, 4 = bass, 2/3 = SFX (tones cut by sfx_timer).
|
|
510
|
+
* PCE frequency regs are DIVIDERS: pitch ≈ 3.58MHz / (32 × value), so a
|
|
511
|
+
* BIGGER number is a LOWER note. Note indices into NOTE_DIV below. */
|
|
512
|
+
enum { R = 0, A2N, C3, F3, G3, A3, B3, C4, D4, E4, F4, G4, A4, B4, C5, D5, E5 };
|
|
513
|
+
static const u16 NOTE_DIV[17] = {
|
|
514
|
+
0, 1017, 854, 641, 571, 508, 453, 427, 381, 339, 320, 285, 254, 226, 214, 190, 170
|
|
515
|
+
};
|
|
516
|
+
/* 16 melody steps + 8 bass steps (one bass note per 2 melody steps) */
|
|
517
|
+
static const u8 MEL_TITLE[16] = { G4,C5,E5,C5, A4,C5,G4,E4, F4,A4,C5,A4, G4,E4,D4,C4 };
|
|
518
|
+
static const u8 BAS_TITLE[8] = { C3,C3, F3,F3, A2N,A2N, G3,G3 };
|
|
519
|
+
static const u8 MEL_PLAY[16] = { C4,E4,G4,E4, F4,A4,G4,E4, D4,F4,A4,G4, E4,G4,C5,R };
|
|
520
|
+
static const u8 BAS_PLAY[8] = { C3,C3, F3,F3, A2N,A2N, G3,G3 };
|
|
521
|
+
static const u8 MEL_OVER[16] = { C5,R,A4,R, G4,R,E4,R, D4,R,C4,R, A2N,R,R,R };
|
|
522
|
+
|
|
523
|
+
static u8 music_song; /* reuses the ST_* ids */
|
|
524
|
+
static u8 music_step, music_timer, music_done;
|
|
525
|
+
|
|
526
|
+
static void music_set(u8 song) {
|
|
527
|
+
music_song = song;
|
|
528
|
+
music_step = 0;
|
|
529
|
+
music_timer = 0;
|
|
530
|
+
music_done = 0;
|
|
531
|
+
psg_off(4);
|
|
532
|
+
psg_off(5);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
static void music_tick(void) {
|
|
536
|
+
const u8 *mel;
|
|
537
|
+
u8 n;
|
|
538
|
+
if (music_done) return;
|
|
539
|
+
if (music_timer == 0) {
|
|
540
|
+
mel = (music_song == ST_PLAY) ? MEL_PLAY
|
|
541
|
+
: (music_song == ST_OVER) ? MEL_OVER : MEL_TITLE;
|
|
542
|
+
n = mel[music_step & 15];
|
|
543
|
+
if (n != R) psg_tone(5, NOTE_DIV[n], 26);
|
|
544
|
+
else psg_off(5);
|
|
545
|
+
if (music_song != ST_OVER) { /* the game-over jingle has no bass */
|
|
546
|
+
n = ((music_step & 1) == 0)
|
|
547
|
+
? ((music_song == ST_PLAY) ? BAS_PLAY[(music_step >> 1) & 7]
|
|
548
|
+
: BAS_TITLE[(music_step >> 1) & 7])
|
|
549
|
+
: R;
|
|
550
|
+
if (n != R) psg_tone(4, NOTE_DIV[n], 20);
|
|
551
|
+
}
|
|
552
|
+
++music_step;
|
|
553
|
+
if (music_song == ST_OVER && music_step >= 16) { /* play once, stop */
|
|
554
|
+
music_done = 1;
|
|
555
|
+
psg_off(4);
|
|
556
|
+
psg_off(5);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
++music_timer;
|
|
560
|
+
if (music_timer >= 9) music_timer = 0;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* ── GAME LOGIC (clay) — helpers ──────────────────────────────────────────── */
|
|
564
|
+
static u8 random8(void) {
|
|
565
|
+
u16 r = rng;
|
|
566
|
+
r ^= r << 7;
|
|
567
|
+
r ^= r >> 9;
|
|
568
|
+
r ^= r << 8;
|
|
569
|
+
rng = r;
|
|
570
|
+
return (u8)r;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
static u8 dist8(int16_t a, int16_t b) {
|
|
574
|
+
int16_t d = (int16_t)(a - b);
|
|
575
|
+
if (d < 0) d = (int16_t)-d;
|
|
576
|
+
return (d > 255) ? 255 : (u8)d;
|
|
156
577
|
}
|
|
157
578
|
|
|
158
|
-
|
|
579
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
580
|
+
* SPRITE STAGING + THE SATB DMA. The VDC never reads your RAM: sprites live
|
|
581
|
+
* in its INTERNAL sprite attribute table, refreshed by a DMA you schedule by
|
|
582
|
+
* writing R19 (satb_dma() does the copy + the R19 write; the transfer itself
|
|
583
|
+
* happens at the next vblank). So the per-frame contract is:
|
|
584
|
+
* waitvsync() → restage EVERY slot → satb_dma()
|
|
585
|
+
* Stage during vblank — satb_dma() also streams 256 words through the VWR
|
|
586
|
+
* port, and doing that mid-display tears sprite pattern fetches.
|
|
587
|
+
*
|
|
588
|
+
* THE HERO (a PCE signature): ONE 32x32 SATB entry — SPR_CGX_32|SPR_CGY_32 in
|
|
589
|
+
* the attribute word — for a big, readable character. CGX goes 32, CGY goes
|
|
590
|
+
* 32 (or 64 for a 32x64 tower from a single entry). The NES needs 4 hardware
|
|
591
|
+
* sprites (and the per-scanline budget) for the same thing.
|
|
592
|
+
*
|
|
593
|
+
* requires: set_sprite_ex() + the 4-aligned hero cells from upload_hero(). */
|
|
594
|
+
static void push_sprites(void) {
|
|
159
595
|
u8 i;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
596
|
+
int16_t player_y = (int16_t)(py_q44 >> 4);
|
|
597
|
+
int16_t sx = (int16_t)(px - 8); /* center the 32-wide sprite */
|
|
598
|
+
/* hero (slot 0) — 32x32 large sprite; blink during the turn breather */
|
|
599
|
+
if (state == ST_PLAY && (turn_pause == 0 || (turn_pause & 4)))
|
|
600
|
+
set_sprite_ex(SLOT_PLAYER, (u16)sx, (u16)player_y, PLAYER_PAT, PAL_PLAYER,
|
|
601
|
+
SPR_CGX_32 | SPR_CGY_32);
|
|
602
|
+
else
|
|
603
|
+
set_sprite_ex(SLOT_PLAYER, 0, OFFSCREEN_Y, PLAYER_PAT, PAL_PLAYER,
|
|
604
|
+
SPR_CGX_32 | SPR_CGY_32);
|
|
605
|
+
for (i = 0; i < NUM_COINS; ++i) {
|
|
606
|
+
u8 vis = (state == ST_PLAY) && coins[i].alive &&
|
|
607
|
+
coins[i].x >= 0 && coins[i].x < SCREEN_W;
|
|
608
|
+
set_sprite((u8)(SLOT_COIN + i), vis ? (u16)coins[i].x : 0,
|
|
609
|
+
vis ? (u16)coins[i].y : OFFSCREEN_Y, COIN_PAT, PAL_COIN);
|
|
610
|
+
}
|
|
611
|
+
for (i = 0; i < NUM_SPIKES; ++i) {
|
|
612
|
+
u8 vis = (state == ST_PLAY) && spikes[i].alive &&
|
|
613
|
+
spikes[i].x >= 0 && spikes[i].x < SCREEN_W;
|
|
614
|
+
set_sprite((u8)(SLOT_SPIKE + i), vis ? (u16)spikes[i].x : 0,
|
|
615
|
+
vis ? (u16)spikes[i].y : OFFSCREEN_Y, SPIKE_PAT, PAL_SPIKE);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/* ── GAME LOGIC (clay) — coins + spikes (sprite objects in the world) ── */
|
|
620
|
+
static const int16_t coin_heights[4] = { 176, 152, 120, 144 };
|
|
621
|
+
static void respawn_coin(u8 i) {
|
|
622
|
+
coins[i].x = (int16_t)(SCREEN_W + 8 + (random8() & 31)); /* enter right */
|
|
623
|
+
coins[i].y = coin_heights[random8() & 3];
|
|
624
|
+
coins[i].alive = 1;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
static void try_spawn_spike(u8 i) {
|
|
628
|
+
/* Anchor only over ground: an inactive spike rolls a low per-frame chance
|
|
629
|
+
* and only spawns if the world column entering at the right edge has
|
|
630
|
+
* ground under it (never floats over a pit). */
|
|
631
|
+
int16_t c = (int16_t)(((camX + SCREEN_W + 8) >> 3));
|
|
632
|
+
if (c < 0 || c >= WORLD_COLS) return;
|
|
633
|
+
if (ground_row[c] == NO_GROUND) return;
|
|
634
|
+
if (random8() > 4) return;
|
|
635
|
+
spikes[i].x = (int16_t)(SCREEN_W + 8);
|
|
636
|
+
spikes[i].y = SPIKE_Y;
|
|
637
|
+
spikes[i].alive = 1;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/* ── GAME LOGIC (clay) — landing probe against the column map ──────────────
|
|
641
|
+
* One-way platforms, arcade-classic style: only catch the player while
|
|
642
|
+
* FALLING through a narrow window at the surface: top-1 (the standing snap
|
|
643
|
+
* parks feet exactly at top, and gravity's sub-pixel trickle doesn't move the
|
|
644
|
+
* integer y every frame — without the -1 slack the player "stands" with
|
|
645
|
+
* on_ground=0 most frames, so jumps only register on lucky frames) through
|
|
646
|
+
* top+4 (so a fast fall can't step over it). */
|
|
647
|
+
static int16_t land_top(int16_t c, int16_t feet) {
|
|
648
|
+
u8 r;
|
|
649
|
+
int16_t top;
|
|
650
|
+
if (c < 0 || c >= WORLD_COLS) return 0;
|
|
651
|
+
r = plat_row[c];
|
|
652
|
+
if (r) {
|
|
653
|
+
top = (int16_t)(r << 3);
|
|
654
|
+
if (feet + 1 >= top && feet <= top + 4) return top;
|
|
655
|
+
}
|
|
656
|
+
r = ground_row[c];
|
|
657
|
+
if (r != NO_GROUND) {
|
|
658
|
+
top = (int16_t)(r << 3);
|
|
659
|
+
if (feet + 1 >= top && feet <= top + 4) return top;
|
|
164
660
|
}
|
|
165
661
|
return 0;
|
|
166
662
|
}
|
|
167
663
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
vce_set_color(1, PCE_RGB(2, 4, 7)); /* BG c1: sky */
|
|
179
|
-
vce_set_color(2, PCE_RGB(3, 2, 1)); /* BG c2: brown platform */
|
|
180
|
-
vce_set_color(3, PCE_RGB(1, 6, 1)); /* BG c3: green grassy top */
|
|
181
|
-
vce_set_color(256, PCE_RGB(0, 0, 0)); /* spr transparent */
|
|
182
|
-
vce_set_color(257, PCE_RGB(7, 1, 1)); /* spr c1: red body */
|
|
183
|
-
vce_set_color(258, PCE_RGB(7, 7, 7)); /* spr c2: white eyes */
|
|
664
|
+
/* ── GAME LOGIC (clay) — screen painters (full repaint per state change) ── */
|
|
665
|
+
static void paint_title(void) {
|
|
666
|
+
paint_flat_sky();
|
|
667
|
+
draw_text((u8)((32 - (sizeof(GAME_TITLE) - 1)) / 2), 7, GAME_TITLE);
|
|
668
|
+
draw_text(10, 13, "1P RUN - I");
|
|
669
|
+
draw_text(10, 15, "2P TURNS - II");
|
|
670
|
+
draw_text(11, 19, "HI");
|
|
671
|
+
draw_num5(14, 19, hiscore);
|
|
672
|
+
draw_text(6, 23, "JUMP PITS GRAB COINS");
|
|
673
|
+
}
|
|
184
674
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
675
|
+
static void paint_over(void) {
|
|
676
|
+
paint_flat_sky();
|
|
677
|
+
draw_text(11, 9, "GAME OVER");
|
|
678
|
+
draw_text(10, 12, "P1");
|
|
679
|
+
draw_num5(14, 12, p_score[0]);
|
|
680
|
+
if (two_player) {
|
|
681
|
+
draw_text(10, 14, "P2");
|
|
682
|
+
draw_num5(14, 14, p_score[1]);
|
|
683
|
+
}
|
|
684
|
+
draw_text(10, 17, "HI");
|
|
685
|
+
draw_num5(14, 17, hiscore);
|
|
686
|
+
draw_text(8, 21, "RUN - TITLE");
|
|
687
|
+
}
|
|
189
688
|
|
|
190
|
-
|
|
689
|
+
/* ── GAME LOGIC (clay) — start a turn / a run ── */
|
|
690
|
+
static void begin_turn(void) {
|
|
691
|
+
u8 i;
|
|
692
|
+
px = 24;
|
|
693
|
+
py_q44 = (int16_t)((GROUND_TOP - 16) << 4);
|
|
694
|
+
vy_q44 = 0;
|
|
695
|
+
on_ground = 1;
|
|
696
|
+
camX = 0;
|
|
697
|
+
lastCamCol = 0;
|
|
698
|
+
dist_sub = 0;
|
|
699
|
+
coins[0].x = 120; coins[0].y = 176; coins[0].alive = 1;
|
|
700
|
+
coins[1].x = 200; coins[1].y = 152; coins[1].alive = 1;
|
|
701
|
+
coins[2].x = 248; coins[2].y = 120; coins[2].alive = 1;
|
|
702
|
+
for (i = 0; i < NUM_SPIKES; ++i) spikes[i].alive = 0;
|
|
703
|
+
spikes[0].x = 160; spikes[0].y = SPIKE_Y; spikes[0].alive = 1;
|
|
704
|
+
spikes[1].x = 232; spikes[1].y = SPIKE_Y; spikes[1].alive = 1;
|
|
705
|
+
turn_pause = 30; /* "P1/P2 ready" breather flash */
|
|
706
|
+
prev_pad = 0xFF; /* swallow held buttons */
|
|
707
|
+
paint_screen_from(0); /* repaint the visible window */
|
|
708
|
+
draw_hud_numbers();
|
|
709
|
+
vdc_set_reg(VDC_BXR, 0);
|
|
710
|
+
}
|
|
191
711
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
712
|
+
static void start_game(u8 players) {
|
|
713
|
+
two_player = players;
|
|
714
|
+
cur_player = 0;
|
|
715
|
+
p_score[0] = p_score[1] = 0;
|
|
716
|
+
p_lives[0] = START_LIVES;
|
|
717
|
+
p_lives[1] = players ? START_LIVES : 0;
|
|
718
|
+
begin_turn();
|
|
719
|
+
music_set(ST_PLAY);
|
|
720
|
+
psg_tone(2, 0x180, 28); sfx_timer = 6; /* start blip */
|
|
721
|
+
state = ST_PLAY;
|
|
722
|
+
}
|
|
197
723
|
|
|
198
|
-
|
|
199
|
-
|
|
724
|
+
static void game_over(void) {
|
|
725
|
+
u16 best = p_score[0];
|
|
726
|
+
if (two_player && p_score[1] > best) best = p_score[1];
|
|
727
|
+
if (best > hiscore) {
|
|
728
|
+
hiscore = best;
|
|
729
|
+
hiscore_save(hiscore); /* in-session only (no save on a bare HuCard) */
|
|
730
|
+
}
|
|
731
|
+
vdc_set_reg(VDC_BXR, 0); /* unscroll for the flat screen */
|
|
732
|
+
paint_over();
|
|
733
|
+
music_set(ST_OVER);
|
|
734
|
+
state = ST_OVER;
|
|
735
|
+
}
|
|
200
736
|
|
|
201
|
-
|
|
202
|
-
|
|
737
|
+
/* ── GAME LOGIC (clay) — death + alternating-turn handoff ── */
|
|
738
|
+
static void kill_player(void) {
|
|
739
|
+
u8 other;
|
|
740
|
+
psg_tone(3, 0x500, 31); /* death rumble */
|
|
741
|
+
sfx_timer = 16;
|
|
742
|
+
if (p_lives[cur_player] > 0) --p_lives[cur_player];
|
|
743
|
+
if (two_player) {
|
|
744
|
+
other = (u8)(cur_player ^ 1);
|
|
745
|
+
if (p_lives[other] > 0) cur_player = other; /* swap turns */
|
|
746
|
+
else if (p_lives[cur_player] == 0) { game_over(); return; }
|
|
747
|
+
} else if (p_lives[0] == 0) {
|
|
748
|
+
game_over();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
begin_turn();
|
|
752
|
+
}
|
|
203
753
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
754
|
+
/* ── GAME LOGIC (clay) — the per-frame play update ────────────────────────── */
|
|
755
|
+
static void update_play(void) {
|
|
756
|
+
u8 i;
|
|
757
|
+
int16_t delta, y8, feet, c0, c1, top, sx;
|
|
758
|
+
int16_t camCol;
|
|
759
|
+
int32_t np;
|
|
760
|
+
|
|
761
|
+
if (turn_pause) { --turn_pause; return; }
|
|
762
|
+
|
|
763
|
+
/* horizontal move; past SCROLL_WALL the world scrolls instead of the
|
|
764
|
+
* player (the camera never scrolls back — the classic one-way camera). */
|
|
765
|
+
delta = 0;
|
|
766
|
+
if (pad & PCE_JOY_RIGHT) {
|
|
767
|
+
if (px < SCROLL_WALL) px = (int16_t)(px + (MOVE >> 4) + 1);
|
|
768
|
+
else {
|
|
769
|
+
int16_t adv = (int16_t)((MOVE >> 4) + 1);
|
|
770
|
+
if (camX + adv <= WORLD_W - SCREEN_W) { camX = (int16_t)(camX + adv); delta = adv; }
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if ((pad & PCE_JOY_LEFT) && px > 8) px = (int16_t)(px - ((MOVE >> 4) + 1));
|
|
211
774
|
|
|
212
|
-
|
|
775
|
+
/* jump (button I), only when grounded */
|
|
776
|
+
if ((pad & PCE_JOY_I) && !(prev_pad & PCE_JOY_I) && on_ground) {
|
|
777
|
+
vy_q44 = JUMP_VEL;
|
|
778
|
+
on_ground = 0;
|
|
779
|
+
psg_tone(2, 0x200, 26); sfx_timer = 6;
|
|
780
|
+
}
|
|
213
781
|
|
|
214
|
-
|
|
215
|
-
|
|
782
|
+
/* stream the columns entering from the right as the camera advances */
|
|
783
|
+
camCol = (int16_t)(camX >> 3);
|
|
784
|
+
while (camCol > lastCamCol) { lastCamCol++; paint_column((int16_t)(lastCamCol + 31)); }
|
|
216
785
|
|
|
217
|
-
|
|
218
|
-
|
|
786
|
+
/* smooth pixel scroll via the BG X register — the whole point */
|
|
787
|
+
vdc_set_reg(VDC_BXR, (u16)camX);
|
|
219
788
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (
|
|
789
|
+
/* world objects drift left as the level scrolls (world-anchored) */
|
|
790
|
+
if (delta) {
|
|
791
|
+
dist_sub = (u8)(dist_sub + delta);
|
|
792
|
+
if (dist_sub >= 64) {
|
|
793
|
+
dist_sub = (u8)(dist_sub - 64);
|
|
794
|
+
++p_score[cur_player];
|
|
795
|
+
hud_dirty = 1;
|
|
796
|
+
}
|
|
797
|
+
for (i = 0; i < NUM_COINS; ++i) {
|
|
798
|
+
if (!coins[i].alive) continue;
|
|
799
|
+
coins[i].x = (int16_t)(coins[i].x - delta);
|
|
800
|
+
if (coins[i].x < -16) respawn_coin(i);
|
|
801
|
+
}
|
|
802
|
+
for (i = 0; i < NUM_SPIKES; ++i) {
|
|
803
|
+
if (!spikes[i].alive) continue;
|
|
804
|
+
spikes[i].x = (int16_t)(spikes[i].x - delta);
|
|
805
|
+
if (spikes[i].x < -16) spikes[i].alive = 0;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
for (i = 0; i < NUM_SPIKES; ++i)
|
|
809
|
+
if (!spikes[i].alive) try_spawn_spike(i);
|
|
810
|
+
|
|
811
|
+
/* physics: gravity + sub-pixel y */
|
|
812
|
+
vy_q44 = (int16_t)(vy_q44 + GRAVITY);
|
|
813
|
+
if (vy_q44 > MAX_VY) vy_q44 = MAX_VY;
|
|
814
|
+
np = (int32_t)py_q44 + (int32_t)vy_q44;
|
|
815
|
+
py_q44 = (int16_t)np;
|
|
816
|
+
y8 = (int16_t)(py_q44 >> 4);
|
|
817
|
+
|
|
818
|
+
/* fell into a pit (below the screen) → lose the turn */
|
|
819
|
+
if (y8 >= 216) { kill_player(); return; }
|
|
820
|
+
|
|
821
|
+
/* landing — probe the two world columns under the player's feet (feet =
|
|
822
|
+
* sprite bottom; the 32px sprite's feet are ~16px below its top y). */
|
|
823
|
+
if (vy_q44 >= 0) {
|
|
824
|
+
feet = (int16_t)(y8 + 16);
|
|
825
|
+
c0 = (int16_t)((camX + px) >> 3);
|
|
826
|
+
c1 = (int16_t)((camX + px + 7) >> 3);
|
|
827
|
+
top = land_top(c0, feet);
|
|
828
|
+
if (top == 0) top = land_top(c1, feet);
|
|
829
|
+
if (top) {
|
|
830
|
+
py_q44 = (int16_t)((top - 16) << 4);
|
|
831
|
+
vy_q44 = 0;
|
|
832
|
+
if (!on_ground) { psg_tone(3, 0x2A0, 22); sfx_timer = 3; }
|
|
833
|
+
on_ground = 1;
|
|
834
|
+
} else {
|
|
835
|
+
on_ground = 0;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
224
838
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
839
|
+
/* coins (collect) + spikes (death). AABB around the player center. */
|
|
840
|
+
sx = (int16_t)(px - 8);
|
|
841
|
+
for (i = 0; i < NUM_COINS; ++i) {
|
|
842
|
+
if (!coins[i].alive) continue;
|
|
843
|
+
if (dist8(coins[i].x, sx) < 18 && dist8(coins[i].y, y8) < 18) {
|
|
844
|
+
coins[i].alive = 0;
|
|
845
|
+
p_score[cur_player] += 10;
|
|
846
|
+
hud_dirty = 1;
|
|
847
|
+
psg_tone(2, 0x0D6, 31); sfx_timer = 6;
|
|
848
|
+
respawn_coin(i);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
for (i = 0; i < NUM_SPIKES; ++i) {
|
|
852
|
+
if (!spikes[i].alive) continue;
|
|
853
|
+
if (dist8(spikes[i].x, sx) < 14 && dist8(spikes[i].y, y8) < 16) {
|
|
854
|
+
kill_player();
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
229
858
|
|
|
230
|
-
|
|
231
|
-
|
|
859
|
+
/* walk-cycle animation: flip the hero frame every 8 px of camera/x travel */
|
|
860
|
+
if (delta || (pad & (PCE_JOY_LEFT | PCE_JOY_RIGHT))) {
|
|
861
|
+
++anim_frame;
|
|
862
|
+
if ((anim_frame & 7) == 0) upload_hero((anim_frame & 8) ? hero_body_b : hero_body_a);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
232
865
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
866
|
+
void main(void) {
|
|
867
|
+
u8 newpad, raw1, raw2;
|
|
868
|
+
|
|
869
|
+
_pce_keep[0] = 0; /* see the EMPTY-BSS TRAP note in pce_hw.h */
|
|
870
|
+
|
|
871
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
872
|
+
* Init order: palette → VRAM uploads → BAT paint → joypad → display ON.
|
|
873
|
+
* disp_enable() also sets the VBlank IRQ bit — without it waitvsync()
|
|
874
|
+
* never returns and the game freezes on its first frame. */
|
|
875
|
+
/* BG sub-pal 0: scenery. BG sub-pal 1: HUD/text (white). */
|
|
876
|
+
vce_set_color(0, PCE_RGB(1, 2, 5)); /* backdrop: dusk blue */
|
|
877
|
+
vce_set_color(1, PCE_RGB(2, 4, 7)); /* BG c1: sky */
|
|
878
|
+
vce_set_color(2, PCE_RGB(3, 2, 1)); /* BG c2: brown dirt */
|
|
879
|
+
vce_set_color(3, PCE_RGB(1, 6, 1)); /* BG c3: grassy green */
|
|
880
|
+
vce_set_color(17, PCE_RGB(7, 7, 7)); /* text: white */
|
|
881
|
+
/* sprite sub-palettes (256 + pal*16 + index) */
|
|
882
|
+
vce_set_color(257, PCE_RGB(7, 4, 1)); /* pal0 c1: hero orange body */
|
|
883
|
+
vce_set_color(259, PCE_RGB(7, 7, 4)); /* pal0 c3: hero face/cap accent */
|
|
884
|
+
vce_set_color(273, PCE_RGB(7, 7, 0)); /* pal1 c1: coin gold */
|
|
885
|
+
vce_set_color(289, PCE_RGB(7, 1, 1)); /* pal2 c1: spike danger red */
|
|
886
|
+
|
|
887
|
+
upload_art();
|
|
888
|
+
|
|
889
|
+
hiscore = hiscore_load(); /* always 0 — no persistence on a bare HuCard */
|
|
890
|
+
state = ST_TITLE;
|
|
891
|
+
paint_title();
|
|
892
|
+
music_set(ST_TITLE);
|
|
237
893
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
894
|
+
pce_joy_init();
|
|
895
|
+
disp_enable();
|
|
896
|
+
|
|
897
|
+
for (;;) {
|
|
898
|
+
waitvsync();
|
|
899
|
+
|
|
900
|
+
/* vblank work first: sprites + SATB DMA + queued HUD writes */
|
|
901
|
+
push_sprites();
|
|
902
|
+
satb_dma();
|
|
903
|
+
if (hud_dirty && state == ST_PLAY) { draw_hud_numbers(); hud_dirty = 0; }
|
|
904
|
+
|
|
905
|
+
music_tick();
|
|
906
|
+
if (sfx_timer) {
|
|
907
|
+
--sfx_timer;
|
|
908
|
+
if (sfx_timer == 0) { psg_off(2); psg_off(3); }
|
|
242
909
|
}
|
|
243
|
-
prev_pad = pad;
|
|
244
910
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
py = (int16_t)((p->y - 16) << 4);
|
|
264
|
-
vy = 0;
|
|
265
|
-
landed = 1;
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (!landed) py = (int16_t)np;
|
|
911
|
+
/* ── HARDWARE IDIOM (load-bearing) — 2P input via the TurboTap.
|
|
912
|
+
* pce_joy_read() reads pad 1 (slot 0). For pad 2 we read cc65's
|
|
913
|
+
* JOY_2 directly and translate it like pce_input.c does, so the
|
|
914
|
+
* CURRENT player's pad drives the game during their alternating turn.
|
|
915
|
+
* The host enables the TurboTap, so JOY_2 carries real port-1 input.
|
|
916
|
+
* On the title screen we always read pad 1 (the menu pad). */
|
|
917
|
+
raw1 = pce_joy_read();
|
|
918
|
+
if (state == ST_PLAY && cur_player == 1) {
|
|
919
|
+
raw2 = joy_read(JOY_2); /* cc65 raw mask for pad 2 */
|
|
920
|
+
pad = 0; /* translate like pce_input.c */
|
|
921
|
+
if (JOY_UP(raw2)) pad |= PCE_JOY_UP;
|
|
922
|
+
if (JOY_DOWN(raw2)) pad |= PCE_JOY_DOWN;
|
|
923
|
+
if (JOY_LEFT(raw2)) pad |= PCE_JOY_LEFT;
|
|
924
|
+
if (JOY_RIGHT(raw2)) pad |= PCE_JOY_RIGHT;
|
|
925
|
+
if (JOY_BTN_1(raw2)) pad |= PCE_JOY_I;
|
|
926
|
+
if (JOY_BTN_2(raw2)) pad |= PCE_JOY_II;
|
|
927
|
+
if (JOY_BTN_3(raw2)) pad |= PCE_JOY_SELECT;
|
|
928
|
+
if (JOY_BTN_4(raw2)) pad |= PCE_JOY_RUN;
|
|
270
929
|
} else {
|
|
271
|
-
|
|
930
|
+
pad = raw1;
|
|
272
931
|
}
|
|
273
|
-
|
|
932
|
+
newpad = (u8)(pad & ~prev_pad);
|
|
274
933
|
|
|
275
|
-
|
|
276
|
-
|
|
934
|
+
if (state == ST_TITLE) {
|
|
935
|
+
prev_pad = pad;
|
|
936
|
+
if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) start_game(0);
|
|
937
|
+
else if (newpad & PCE_JOY_II) start_game(1);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (state == ST_OVER) {
|
|
941
|
+
prev_pad = pad;
|
|
942
|
+
if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) {
|
|
943
|
+
state = ST_TITLE;
|
|
944
|
+
vdc_set_reg(VDC_BXR, 0);
|
|
945
|
+
paint_title();
|
|
946
|
+
music_set(ST_TITLE);
|
|
947
|
+
}
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
277
950
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (sx < 0) sx = 0;
|
|
281
|
-
if (sx > 240) sx = 240;
|
|
282
|
-
set_sprite(0, (u16)sx, (u16)(py >> 4), PLAYER_VRAM >> 6, 0);
|
|
283
|
-
satb_dma();
|
|
951
|
+
update_play();
|
|
952
|
+
prev_pad = pad;
|
|
284
953
|
}
|
|
285
954
|
}
|