romdevtools 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +53 -43
- package/CHANGELOG.md +91 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +84 -8
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +131 -24
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +53 -10
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
|
@@ -1,7 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/* ── sports.c — Atari Lynx 1P-vs-CPU court game (complete example game) ───────
|
|
2
|
+
*
|
|
3
|
+
* A COMPLETE, working game — PULSE PARRY, a head-to-head court game (Pong
|
|
4
|
+
* lineage) fit to the Lynx's tiny 160x102 screen: title screen, 1P vs a
|
|
5
|
+
* beatable CPU, first-to-N match flow with a result screen, in-session
|
|
6
|
+
* record, MIKEY music + SFX, AND the Lynx's signature party trick:
|
|
7
|
+
* HARDWARE SPRITE SCALING. The ball is a Suzy scalable sprite that GROWS
|
|
8
|
+
* with its speed (a fast volley looms larger), and the result screen does a
|
|
9
|
+
* SCALE POP — a winner glyph swells then eases back — both pure-hardware
|
|
10
|
+
* "juice" that costs zero CPU pixel work.
|
|
11
|
+
*
|
|
12
|
+
* The game: you are the LEFT paddle; a CPU works the RIGHT. UP/DOWN move you.
|
|
13
|
+
* The ball rallies between you; the angle you return it at depends on where it
|
|
14
|
+
* strikes your paddle (centre = flat, edges = steep), and a ±1 PRNG "spin" on
|
|
15
|
+
* every return guarantees no rally loops forever. First side to WIN_SCORE
|
|
16
|
+
* takes the match → a result screen → back to the title.
|
|
17
|
+
*
|
|
18
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
19
|
+
* very different one. The markers tell you what's what:
|
|
20
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented Lynx footgun;
|
|
21
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
22
|
+
* GAME LOGIC (clay) — court art, ball physics, CPU skill, scoring rules:
|
|
23
|
+
* reshape freely.
|
|
24
|
+
*
|
|
25
|
+
* What depends on what:
|
|
26
|
+
* lynx_sfx.{h,c} — MIKEY 4-voice audio (voice 0 = paddle/score SFX, voice 1 =
|
|
27
|
+
* background melody, voice 2 = wall/whistle SFX, voice 3 = noise/miss).
|
|
28
|
+
* vendor/cc65/libsrc/lynx/ — the FULL cc65 Lynx driver source shipped into
|
|
29
|
+
* your project. The TGI driver (tgi/lynx-160-102-16.s) is REQUIRED
|
|
30
|
+
* reading when graphics misbehave: every TGI call is itself a Suzy
|
|
31
|
+
* sprite, and our scaled ball + result pop ride the same engine via
|
|
32
|
+
* tgi_ioctl(0).
|
|
33
|
+
*
|
|
34
|
+
* NO HARDWARE TILEMAP (read this — it is the platform's biggest "where's the
|
|
35
|
+
* court renderer?" surprise): the Lynx has NO background tilemap. Suzy is a
|
|
36
|
+
* SPRITE BLITTER, not a tile engine. So the court is drawn the honest way:
|
|
37
|
+
* the full-redraw TGI loop repaints the whole arena every frame as a stack
|
|
38
|
+
* of tgi_bar fills + tgi_line markings — cheap on a 160x102 screen. The
|
|
39
|
+
* paddles are flat bars; the ball is a Suzy SCALABLE sprite on top.
|
|
40
|
+
*
|
|
41
|
+
* PLAYERS: 1. This is a handheld — head-to-head on real hardware is ComLynx,
|
|
42
|
+
* a cable between TWO physical Lynx units. A single emulator instance has
|
|
43
|
+
* nobody on the other end of the cable, so this example is honestly 1P vs a
|
|
44
|
+
* CPU opponent (no fake "P2 VERSUS" that could never work here — contrast
|
|
45
|
+
* the NES sports donor, which has a real simultaneous-2P mode).
|
|
46
|
+
*
|
|
47
|
+
* SCREEN: 160x102. The system font is 8x8, so a full row of text is 20
|
|
48
|
+
* characters — the court + HUD are kept compact to fit.
|
|
49
|
+
*/
|
|
5
50
|
|
|
6
51
|
#include <tgi.h>
|
|
7
52
|
#include <joystick.h>
|
|
@@ -9,87 +54,466 @@
|
|
|
9
54
|
#include <stdint.h>
|
|
10
55
|
#include "lynx_sfx.h"
|
|
11
56
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#define
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
57
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
58
|
+
* name here automatically. Keep it <=16 chars of A-Z 0-9 space dash. */
|
|
59
|
+
#define GAME_TITLE "PULSE PARRY"
|
|
60
|
+
|
|
61
|
+
/* ── GAME LOGIC (clay — reshape freely) — court geometry (fits 160x102) ──────
|
|
62
|
+
* A full-width court with a slim HUD row across the top. COURT_TOP/BOT bound
|
|
63
|
+
* the ball vertically; the paddles ride the left/right edges. */
|
|
64
|
+
#define COURT_TOP 12 /* first playable pixel row */
|
|
65
|
+
#define COURT_BOT 100 /* first pixel row of the bottom rail */
|
|
66
|
+
#define PADDLE_H 20 /* paddle height in px (compact court) */
|
|
67
|
+
#define PADDLE_W 3
|
|
68
|
+
#define PADDLE_X1 5 /* you — left side */
|
|
69
|
+
#define PADDLE_X2 (159 - 5 - PADDLE_W) /* CPU — right side */
|
|
70
|
+
#define BALL_W 6 /* nominal ball footprint (1.0x sprite) */
|
|
71
|
+
#define WIN_SCORE 5 /* first to 5 takes the match */
|
|
72
|
+
|
|
73
|
+
/* Game states — the shell every example shares: title → play → result. */
|
|
74
|
+
#define ST_TITLE 0
|
|
75
|
+
#define ST_PLAY 1
|
|
76
|
+
#define ST_OVER 2
|
|
77
|
+
static uint8_t state;
|
|
78
|
+
|
|
79
|
+
/* ── GAME LOGIC (clay) — match state ── */
|
|
80
|
+
static int16_t p1y, p2y; /* paddle top Y */
|
|
81
|
+
static int16_t bx, by; /* ball top-left */
|
|
82
|
+
static int8_t bdx, bdy; /* ball velocity (px/frame) */
|
|
83
|
+
static uint8_t score_p1, score_p2;
|
|
84
|
+
static uint8_t serve_timer; /* freeze frames between points */
|
|
85
|
+
static uint8_t streak; /* current win streak vs CPU (this run) */
|
|
86
|
+
static uint8_t best_streak; /* in-session record — see end_match() */
|
|
87
|
+
static uint8_t new_record; /* result screen shows NEW RECORD */
|
|
88
|
+
static uint8_t p1_won; /* who took the match (result screen) */
|
|
89
|
+
static uint8_t prev_joy;
|
|
90
|
+
|
|
91
|
+
/* The result SCALE POP: when >0 the winner glyph draws swollen for a few
|
|
92
|
+
* frames (the SCALING signature), counting back down to the resting 1.0x. */
|
|
93
|
+
static uint8_t pop_timer;
|
|
94
|
+
#define POP_FRAMES 10
|
|
95
|
+
|
|
96
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call).
|
|
97
|
+
* A versus game NEEDS this: the Lynx is fully deterministic, so without a
|
|
98
|
+
* noise source two fixed strategies lock into an infinite rally loop (the
|
|
99
|
+
* exact same cycle, forever). rand8() is ticked once per play frame so
|
|
100
|
+
* identical game states a few seconds apart still diverge. */
|
|
101
|
+
static uint16_t rng = 0xC0A7;
|
|
102
|
+
static uint8_t rand8(void) {
|
|
103
|
+
uint16_t r = rng;
|
|
104
|
+
r ^= r << 7;
|
|
105
|
+
r ^= r >> 9;
|
|
106
|
+
r ^= r << 8;
|
|
107
|
+
rng = r;
|
|
108
|
+
return (uint8_t)r;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
112
|
+
* SUZY HARDWARE SPRITE SCALING — the Lynx signature. Suzy renders every
|
|
113
|
+
* sprite through a Sprite Control Block (SCB) it walks in cart/work RAM.
|
|
114
|
+
* Two SCB fields, HSIZE and VSIZE, are 8.8 fixed-point scale factors
|
|
115
|
+
* ($0100 = 1.0): the SAME 8x8 source pixels render at any size, every frame,
|
|
116
|
+
* for free. This game uses it two ways:
|
|
117
|
+
* - the BALL is a Suzy sprite whose 8.8 scale tracks its SPEED — a slow
|
|
118
|
+
* serve is a small dot, a fast volley looms larger — recomputed every
|
|
119
|
+
* frame, zero CPU pixel cost (Suzy scales while it blits);
|
|
120
|
+
* - the RESULT POP — for POP_FRAMES after a match ends, the winner glyph is
|
|
121
|
+
* redrawn at >1.0x then eased back to 1.0x, a pure-hardware "juice" flash.
|
|
122
|
+
*
|
|
123
|
+
* The SCB, field by field (this is cc65's SCB_REHV_PAL from <_suzy.h>):
|
|
124
|
+
* sprctl0 bits 7-6 = bits per pixel (11 = 4bpp), bits 2-0 = sprite TYPE.
|
|
125
|
+
* TYPE_NORMAL (4) draws pens 1-15 and treats pen 0 as
|
|
126
|
+
* TRANSPARENT — that's how the round ball sits over the court.
|
|
127
|
+
* sprctl1 bit 7 LITERAL (raw nybbles, no RLE) + bits 5-4 reload depth:
|
|
128
|
+
* REHV means "this SCB carries HPOS, VPOS, HSIZE, VSIZE". The
|
|
129
|
+
* reload bits ARE the struct layout — mismatch them and Suzy reads
|
|
130
|
+
* palette bytes as size words.
|
|
131
|
+
* sprcoll $20 = NO_COLLIDE. Ball/paddle collision is done in C on the court
|
|
132
|
+
* coordinates (the collision buffer knows nothing about gameplay).
|
|
133
|
+
* next pointer to the next SCB, 0 = end of chain (one blit per call).
|
|
134
|
+
* data sprite pixel data (LITERAL 4bpp format below).
|
|
135
|
+
* hpos/vpos signed SCREEN position of the sprite's top-left corner.
|
|
136
|
+
* hsize/vsize 8.8 scale — THE party trick, rewritten per draw.
|
|
137
|
+
* penpal[8] 16 nybbles mapping pixel values 0-15 → palette pens. We RECOLOUR
|
|
138
|
+
* the sprite per draw here (one 8x8 art block, any pen) by pointing
|
|
139
|
+
* the art's pixel value 1 at the wanted pen — no extra art.
|
|
140
|
+
*
|
|
141
|
+
* LITERAL 4bpp data format (hand-encodable): each sprite LINE is
|
|
142
|
+
* [offset byte][width/2 bytes of raw nybble pixels]
|
|
143
|
+
* where offset = 1 + bytes of pixel data; a final offset of 0 ends the sprite.
|
|
144
|
+
* 8 px @ 4bpp = 4 data bytes, so every line starts with 5.
|
|
145
|
+
*
|
|
146
|
+
* Drawing: tgi_sprite(&scb) → tgi_ioctl(0, &scb) — the TGI driver's
|
|
147
|
+
* documented escape hatch (see CONTROL in vendor/cc65/libsrc/lynx/tgi/
|
|
148
|
+
* lynx-160-102-16.s). It points Suzy's SCBNEXT at your SCB, aims VIDBAS at
|
|
149
|
+
* TGI's current DRAW page (so scaled sprites land in the same double-buffered
|
|
150
|
+
* frame as tgi_bar/tgi_outtextxy), fires SPRGO, and sleeps the CPU until
|
|
151
|
+
* SPRSYS reports the blit done.
|
|
152
|
+
*
|
|
153
|
+
* Requires: the cc65 crt0 Suzy init (already done before main()), and calls
|
|
154
|
+
* only between the tgi_busy() wait and tgi_updatedisplay() — i.e. while
|
|
155
|
+
* TGI's draw buffer is the blit target. Draw order = paint order: court
|
|
156
|
+
* fills first, scaled ball/glyph after, HUD text last.
|
|
157
|
+
*/
|
|
158
|
+
static SCB_REHV_PAL scb = {
|
|
159
|
+
BPP_4 | TYPE_NORMAL, /* sprctl0: 4bpp, pen 0 transparent */
|
|
160
|
+
LITERAL | REHV, /* sprctl1: literal data, HV+size SCB */
|
|
161
|
+
0x20, /* sprcoll: NO_COLLIDE */
|
|
162
|
+
0, /* next: single-SCB chain */
|
|
163
|
+
0, /* data: set per draw */
|
|
164
|
+
0, 0, /* hpos, vpos */
|
|
165
|
+
0x0100, 0x0100, /* hsize, vsize (8.8) */
|
|
166
|
+
{ 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF } /* identity pens */
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/* ── GAME LOGIC (clay) — 8x8 4bpp literal sprite art ────────────────────────
|
|
170
|
+
* A round ball in pixel value 1 (plus value $F = white glint). draw_sprite()
|
|
171
|
+
* recolours value 1 → the wanted pen via the SCB penpal, so one art block
|
|
172
|
+
* paints any colour. Each line: 5, then 4 nybble bytes; a final 0 byte ends
|
|
173
|
+
* the sprite. */
|
|
174
|
+
static unsigned char spr_ball[] = {
|
|
175
|
+
5, 0x00, 0x11, 0x10, 0x00, /* . . 1 1 1 . . . round ball body */
|
|
176
|
+
5, 0x01, 0x1F, 0xF1, 0x10, /* . 1 1 F F 1 1 . (white glint) */
|
|
177
|
+
5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
|
|
178
|
+
5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
|
|
179
|
+
5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
|
|
180
|
+
5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
|
|
181
|
+
5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . */
|
|
182
|
+
5, 0x00, 0x11, 0x10, 0x00, /* . . 1 1 1 . . . */
|
|
183
|
+
0
|
|
184
|
+
};
|
|
185
|
+
/* A chunky trophy/cup glyph for the result pop (pixel value 1 = body). */
|
|
186
|
+
static unsigned char spr_cup[] = {
|
|
187
|
+
5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . cup bowl */
|
|
188
|
+
5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . */
|
|
189
|
+
5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . */
|
|
190
|
+
5, 0x00, 0x11, 0x11, 0x00, /* . . 1 1 1 1 . . taper */
|
|
191
|
+
5, 0x00, 0x01, 0x10, 0x00, /* . . . 1 1 . . . stem */
|
|
192
|
+
5, 0x00, 0x01, 0x10, 0x00, /* . . . 1 1 . . . */
|
|
193
|
+
5, 0x00, 0x11, 0x11, 0x00, /* . . 1 1 1 1 . . base */
|
|
194
|
+
5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . */
|
|
195
|
+
0
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/* Draw an 8x8 literal sprite CENTERED on (cx,cy) at the given 8.8 scale,
|
|
199
|
+
* recoloured so art pixel value 1 paints `pen`. Centering matters: hpos/vpos
|
|
200
|
+
* are the TOP-LEFT, so a sprite scaled around its corner would slide as it
|
|
201
|
+
* grows — anchoring the centre keeps a growing ball reading as "coming at
|
|
202
|
+
* you", and the result pop as a uniform swell. */
|
|
203
|
+
static void draw_sprite(unsigned char *data, int cx, int cy, uint8_t pen, unsigned scale) {
|
|
204
|
+
unsigned w = (8u * scale) >> 8;
|
|
205
|
+
if (w == 0) w = 1;
|
|
206
|
+
scb.penpal[0] = (uint8_t)((0u << 4) | pen); /* val0=transparent, val1=pen */
|
|
207
|
+
scb.data = data;
|
|
208
|
+
scb.hsize = scale;
|
|
209
|
+
scb.vsize = scale;
|
|
210
|
+
scb.hpos = cx - (int)(w >> 1);
|
|
211
|
+
scb.vpos = cy - (int)(w >> 1);
|
|
212
|
+
tgi_sprite(&scb);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Ball scale tracks its speed: |bdx|+|bdy| (1..~5) maps onto 0.75x..1.6x.
|
|
216
|
+
* A faster volley genuinely looms larger — the HARDWARE scale is the speed
|
|
217
|
+
* read-out, not a decoration. */
|
|
218
|
+
static unsigned ball_scale(void) {
|
|
219
|
+
unsigned spd = (unsigned)((bdx < 0 ? -bdx : bdx) + (bdy < 0 ? -bdy : bdy));
|
|
220
|
+
if (spd > 6) spd = 6;
|
|
221
|
+
return 0x00C0u + spd * 0x0028u; /* 0.75x + 0.156x per speed unit */
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Current result-pop scale: 1.0x at rest, swelling to ~2.0x at the peak and
|
|
225
|
+
* easing back. POP drives the SCALING idiom on the result screen. */
|
|
226
|
+
#define POP_SCALE_PEAK 0x0200u /* 2.0x */
|
|
227
|
+
static unsigned pop_scale(void) {
|
|
228
|
+
if (pop_timer == 0) return 0x0100u;
|
|
229
|
+
return 0x0100u + ((unsigned)pop_timer * (POP_SCALE_PEAK - 0x0100u)) / POP_FRAMES;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* ── GAME LOGIC (clay) — score text (no sprintf: it drags in ~6KB) ── */
|
|
233
|
+
static char numbuf[6];
|
|
234
|
+
static char *fmt5(unsigned v) {
|
|
235
|
+
uint8_t i;
|
|
236
|
+
for (i = 0; i < 5; i++) { numbuf[4 - i] = (char)('0' + v % 10); v /= 10; }
|
|
237
|
+
numbuf[5] = 0;
|
|
238
|
+
return numbuf;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ── GAME LOGIC (clay) — serve: ball to centre, toward the chosen side ── */
|
|
242
|
+
static void serve_ball(uint8_t to_left) {
|
|
243
|
+
bx = 78;
|
|
244
|
+
by = 48;
|
|
245
|
+
bdx = to_left ? -2 : 2;
|
|
246
|
+
bdy = ((score_p1 + score_p2) & 1) ? -1 : 1; /* alternate the angle */
|
|
247
|
+
serve_timer = 30; /* half-second breather */
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* ── GAME LOGIC (clay) — paint the court (full redraw, every frame) ──────────
|
|
251
|
+
* No hardware tilemap, so the arena is bars + lines: grass fill, end zones,
|
|
252
|
+
* top/bottom rails, the white boundary + dashed centre net + centre circle.
|
|
253
|
+
* Layered tones keep any one colour comfortably under the render-health blank
|
|
254
|
+
* threshold (>=92% one colour reads as "blank"). */
|
|
255
|
+
static void draw_court(void) {
|
|
256
|
+
int16_t ny;
|
|
257
|
+
tgi_setcolor(COLOR_GREEN);
|
|
258
|
+
tgi_bar(0, 0, 159, 101); /* court grass */
|
|
259
|
+
tgi_setcolor(COLOR_LIGHTGREEN);
|
|
260
|
+
tgi_bar(0, COURT_TOP, 50, COURT_BOT - 1); /* left end zone */
|
|
261
|
+
tgi_bar(109, COURT_TOP, 159, COURT_BOT - 1); /* right end zone */
|
|
262
|
+
tgi_setcolor(COLOR_DARKGREY);
|
|
263
|
+
tgi_bar(0, 0, 159, COURT_TOP - 1); /* top HUD/rail band */
|
|
264
|
+
tgi_bar(0, COURT_BOT, 159, 101); /* bottom rail */
|
|
265
|
+
tgi_setcolor(COLOR_WHITE);
|
|
266
|
+
tgi_line(0, COURT_TOP, 159, COURT_TOP);
|
|
267
|
+
tgi_line(0, COURT_BOT, 159, COURT_BOT);
|
|
268
|
+
for (ny = COURT_TOP; ny < COURT_BOT; ny += 8)
|
|
269
|
+
tgi_bar(79, (unsigned)ny, 80,
|
|
270
|
+
(unsigned)(ny + 3 > COURT_BOT ? COURT_BOT : ny + 3)); /* net */
|
|
271
|
+
tgi_line(70, 46, 90, 46);
|
|
272
|
+
tgi_line(70, 66, 90, 66);
|
|
273
|
+
tgi_line(70, 46, 70, 66);
|
|
274
|
+
tgi_line(90, 46, 90, 66);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* Draw the two paddles. */
|
|
278
|
+
static void draw_paddles(void) {
|
|
279
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
280
|
+
tgi_bar(PADDLE_X1, (unsigned)p1y, PADDLE_X1 + PADDLE_W - 1,
|
|
281
|
+
(unsigned)(p1y + PADDLE_H - 1));
|
|
282
|
+
tgi_setcolor(COLOR_RED);
|
|
283
|
+
tgi_bar(PADDLE_X2, (unsigned)p2y, PADDLE_X2 + PADDLE_W - 1,
|
|
284
|
+
(unsigned)(p2y + PADDLE_H - 1));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ── GAME LOGIC (clay) — start a match ── */
|
|
288
|
+
static void start_match(void) {
|
|
289
|
+
p1y = 40; p2y = 40;
|
|
290
|
+
score_p1 = 0; score_p2 = 0;
|
|
291
|
+
new_record = 0;
|
|
292
|
+
prev_joy = 0xFF; /* the button that started the match shouldn't
|
|
293
|
+
* also count as the first frame's input */
|
|
294
|
+
sfx_tone(0, 80, 8); /* start chirp */
|
|
295
|
+
serve_ball(0);
|
|
296
|
+
state = ST_PLAY;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* ── GAME LOGIC (clay) — match over: result + record bookkeeping.
|
|
300
|
+
* Persistence choice: for a VERSUS game a raw hi-score is meaningless (every
|
|
301
|
+
* match ends 5-x), so we keep the longest CPU-beating win STREAK — the stat a
|
|
302
|
+
* returning player actually chases — in-session only (see the EEPROM note). */
|
|
303
|
+
static void end_match(void) {
|
|
304
|
+
p1_won = (score_p1 >= WIN_SCORE);
|
|
305
|
+
if (p1_won) {
|
|
306
|
+
++streak;
|
|
307
|
+
if (streak > best_streak) {
|
|
308
|
+
/* ── In-session record ONLY — and here's the honest why. Real Lynx
|
|
309
|
+
* carts persist via a 93Cxx serial EEPROM on the cart PCB (cc65 even
|
|
310
|
+
* ships lynx_eeprom_read/write for it; see vendor/cc65/libsrc/lynx/
|
|
311
|
+
* eeprom.s). PROBED: the bundled handy core emulates CEEPROM internally
|
|
312
|
+
* but its libretro build exposes NO save path — retro_get_memory(
|
|
313
|
+
* SAVE_RAM) returns NULL/size 0, so nothing survives host.hardReset()
|
|
314
|
+
* and a bit-banged round-trip reads back garbage under the WASM build.
|
|
315
|
+
* Wiring the EEPROM to SAVE_RAM is a future core round; until then a
|
|
316
|
+
* fake "save" would be lying. The record DOES survive title↔play cycles
|
|
317
|
+
* within one power-on. ── */
|
|
318
|
+
best_streak = streak;
|
|
319
|
+
new_record = 1;
|
|
320
|
+
}
|
|
321
|
+
sfx_tone(0, 60, 16); /* victory rise */
|
|
322
|
+
} else {
|
|
323
|
+
streak = 0; /* the streak dies with the loss */
|
|
324
|
+
sfx_tone(2, 220, 18); /* low defeat whistle */
|
|
325
|
+
sfx_noise(12);
|
|
326
|
+
}
|
|
327
|
+
pop_timer = POP_FRAMES; /* trigger the result SCALE POP */
|
|
328
|
+
state = ST_OVER;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* ── GAME LOGIC (clay) — one point scored ── */
|
|
332
|
+
static void score_point(uint8_t for_p1) {
|
|
333
|
+
if (for_p1) ++score_p1; else ++score_p2;
|
|
334
|
+
sfx_noise(6);
|
|
335
|
+
if (score_p1 >= WIN_SCORE || score_p2 >= WIN_SCORE) end_match();
|
|
336
|
+
else serve_ball(for_p1); /* winner of the point receives */
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ── GAME LOGIC (clay) — paddle hit: deflect by where the ball struck.
|
|
340
|
+
* Centre = flat-ish, edges = steep. A ±1 random "spin" on every return keeps
|
|
341
|
+
* rallies from repeating (see the PRNG note above), so an idle match (you
|
|
342
|
+
* never moving) still ENDS — the CPU eventually wins. */
|
|
343
|
+
static void deflect(int16_t paddle_y) {
|
|
344
|
+
int16_t rel = (by + BALL_W / 2) - (paddle_y + PADDLE_H / 2);
|
|
345
|
+
bdy = (int8_t)(rel >> 3);
|
|
346
|
+
bdy += (int8_t)((rand8() & 2) - 1); /* spin: -1 or +1 */
|
|
347
|
+
if (bdy > 3) bdy = 3;
|
|
348
|
+
if (bdy < -3) bdy = -3;
|
|
349
|
+
if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat ball */
|
|
350
|
+
sfx_tone(0, 70, 4);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* ── GAME LOGIC (clay) — HUD: scores + labels across the top band ── */
|
|
354
|
+
static void draw_hud(void) {
|
|
355
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
356
|
+
tgi_outtextxy(2, 2, "P1");
|
|
357
|
+
numbuf[0] = (char)('0' + score_p1); numbuf[1] = 0;
|
|
358
|
+
tgi_outtextxy(20, 2, numbuf);
|
|
359
|
+
tgi_setcolor(COLOR_RED);
|
|
360
|
+
tgi_outtextxy(136, 2, "CPU");
|
|
361
|
+
numbuf[0] = (char)('0' + score_p2); numbuf[1] = 0;
|
|
362
|
+
tgi_outtextxy(124, 2, numbuf);
|
|
363
|
+
tgi_setcolor(COLOR_LIGHTGREY);
|
|
364
|
+
tgi_outtextxy(56, 2, "WIN");
|
|
365
|
+
numbuf[0] = (char)('0' + WIN_SCORE); numbuf[1] = 0;
|
|
366
|
+
tgi_outtextxy(84, 2, numbuf);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* ── GAME LOGIC (clay) — per-state frames. Each runs INSIDE the canonical
|
|
370
|
+
* loop below: court already painted, tgi_updatedisplay not yet called. ── */
|
|
371
|
+
|
|
372
|
+
static unsigned attract_phase;
|
|
373
|
+
|
|
374
|
+
static void frame_title(uint8_t joy) {
|
|
375
|
+
/* attract: a lone ball in the title's clear zone pulses via the SCALING
|
|
376
|
+
* idiom — the same swell the speed-scaled ball + result pop use, shown off
|
|
377
|
+
* on the menu. */
|
|
378
|
+
unsigned t = attract_phase < 64 ? attract_phase : (127 - attract_phase);
|
|
379
|
+
unsigned s = 0x00C0u + (t * (0x0220u - 0x00C0u)) / 63u; /* 0.75x..2.13x */
|
|
380
|
+
attract_phase = (attract_phase + 2) & 127;
|
|
381
|
+
draw_sprite(spr_ball, 80, 34, COLOR_WHITE, s); /* breathing ball */
|
|
382
|
+
|
|
383
|
+
tgi_setcolor(COLOR_WHITE);
|
|
384
|
+
tgi_outtextxy(8, 18, GAME_TITLE);
|
|
385
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
386
|
+
tgi_outtextxy(48, 52, "PRESS A");
|
|
387
|
+
tgi_setcolor(COLOR_LIGHTGREY);
|
|
388
|
+
tgi_outtextxy(36, 66, "BEST ");
|
|
389
|
+
numbuf[0] = (char)('0' + (best_streak > 9 ? 9 : best_streak)); numbuf[1] = 0;
|
|
390
|
+
tgi_outtextxy(76, 66, numbuf);
|
|
391
|
+
tgi_outtextxy(20, 80, "1P VS CPU"); /* handheld honesty */
|
|
392
|
+
|
|
393
|
+
if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) start_match();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
static void frame_over(uint8_t joy) {
|
|
397
|
+
unsigned ps = pop_scale();
|
|
398
|
+
draw_paddles();
|
|
399
|
+
/* the SCALE POP: a winner glyph swells then eases back to 1.0x */
|
|
400
|
+
if (p1_won) draw_sprite(spr_cup, 80, 40, COLOR_YELLOW, ps);
|
|
401
|
+
else draw_sprite(spr_ball, 80, 40, COLOR_RED, ps);
|
|
402
|
+
if (pop_timer) pop_timer--;
|
|
403
|
+
|
|
404
|
+
tgi_setcolor(COLOR_DARKGREY);
|
|
405
|
+
tgi_bar(28, 54, 131, 96);
|
|
406
|
+
tgi_setcolor(COLOR_WHITE);
|
|
407
|
+
tgi_outtextxy(44, 58, "GAME OVER");
|
|
408
|
+
tgi_setcolor(p1_won ? COLOR_LIGHTGREEN : COLOR_RED);
|
|
409
|
+
tgi_outtextxy(40, 68, p1_won ? "YOU WIN" : "CPU WINS");
|
|
410
|
+
if (new_record) { tgi_setcolor(COLOR_YELLOW); tgi_outtextxy(36, 78, "NEW RECORD"); }
|
|
411
|
+
else { tgi_setcolor(COLOR_LIGHTGREY); tgi_outtextxy(44, 78, "A = TITLE"); }
|
|
412
|
+
tgi_setcolor(COLOR_LIGHTGREY);
|
|
413
|
+
tgi_outtextxy(40, 88, "SCORE ");
|
|
414
|
+
numbuf[0] = (char)('0' + score_p1); numbuf[1] = '-';
|
|
415
|
+
numbuf[2] = (char)('0' + score_p2); numbuf[3] = 0;
|
|
416
|
+
tgi_outtextxy(88, 88, numbuf);
|
|
417
|
+
|
|
418
|
+
if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) state = ST_TITLE;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
static void frame_play(uint8_t joy) {
|
|
422
|
+
/* ── draw: paddles, the SPEED-SCALED ball (Suzy), HUD ── */
|
|
423
|
+
draw_paddles();
|
|
424
|
+
draw_sprite(spr_ball, (int)bx + BALL_W / 2, (int)by + BALL_W / 2,
|
|
425
|
+
COLOR_WHITE, ball_scale());
|
|
426
|
+
draw_hud();
|
|
427
|
+
|
|
428
|
+
/* ── update ── */
|
|
429
|
+
rand8(); /* tick the noise source every play frame */
|
|
430
|
+
|
|
431
|
+
/* you — UP/DOWN, 2px/frame */
|
|
432
|
+
if ((joy & JOY_UP_MASK) && p1y > COURT_TOP) p1y -= 2;
|
|
433
|
+
if ((joy & JOY_DOWN_MASK) && p1y < COURT_BOT - PADDLE_H) p1y += 2;
|
|
434
|
+
|
|
435
|
+
/* CPU — chases the ball centre at 1px/frame (half your speed) with a small
|
|
436
|
+
* dead zone. Beatable by design: steep edge deflections outrun it. */
|
|
437
|
+
{
|
|
438
|
+
int16_t target = by + BALL_W / 2 - PADDLE_H / 2;
|
|
439
|
+
if (p2y + 2 < target && p2y < COURT_BOT - PADDLE_H) p2y += 1;
|
|
440
|
+
else if (p2y > target + 2 && p2y > COURT_TOP) p2y -= 1;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* ball frozen during the post-point serve pause */
|
|
444
|
+
if (serve_timer > 0) { --serve_timer; return; }
|
|
445
|
+
bx += bdx;
|
|
446
|
+
by += bdy;
|
|
447
|
+
|
|
448
|
+
/* rail bounce */
|
|
449
|
+
if (by < COURT_TOP) { by = COURT_TOP; bdy = -bdy; sfx_tone(2, 90, 3); }
|
|
450
|
+
if (by + BALL_W > COURT_BOT) { by = COURT_BOT - BALL_W; bdy = -bdy; sfx_tone(2, 90, 3); }
|
|
451
|
+
|
|
452
|
+
/* paddle collisions (direction-gated so the ball can't double-hit) */
|
|
453
|
+
if (bdx < 0
|
|
454
|
+
&& bx <= PADDLE_X1 + PADDLE_W && bx + BALL_W >= PADDLE_X1
|
|
455
|
+
&& by + BALL_W > p1y && by < p1y + PADDLE_H) {
|
|
456
|
+
bdx = -bdx; bx = PADDLE_X1 + PADDLE_W; deflect(p1y);
|
|
457
|
+
}
|
|
458
|
+
if (bdx > 0
|
|
459
|
+
&& bx + BALL_W >= PADDLE_X2 && bx <= PADDLE_X2 + PADDLE_W
|
|
460
|
+
&& by + BALL_W > p2y && by < p2y + PADDLE_H) {
|
|
461
|
+
bdx = -bdx; bx = PADDLE_X2 - BALL_W; deflect(p2y);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* off either side → point */
|
|
465
|
+
if (bx < -BALL_W) score_point(0); /* past you → CPU scores */
|
|
466
|
+
if (bx > 160) score_point(1); /* past CPU → you score */
|
|
467
|
+
}
|
|
19
468
|
|
|
20
469
|
void main(void) {
|
|
21
|
-
int16_t p1y = 40, p2y = 40, bx = 78, by = 48;
|
|
22
|
-
int8_t bdx = 2, bdy = 1;
|
|
23
470
|
uint8_t joy;
|
|
24
|
-
int16_t ny; /* loop var for the dashed centre net */
|
|
25
471
|
|
|
26
472
|
tgi_install(&lynx_160_102_16_tgi);
|
|
27
473
|
tgi_init();
|
|
28
474
|
joy_install(&lynx_stdjoy_joy);
|
|
29
|
-
sfx_init();
|
|
475
|
+
sfx_init(); /* MIKEY up; background melody starts on voice 1 */
|
|
476
|
+
|
|
477
|
+
state = ST_TITLE;
|
|
478
|
+
prev_joy = 0;
|
|
479
|
+
attract_phase = 0;
|
|
480
|
+
best_streak = 0;
|
|
481
|
+
streak = 0;
|
|
482
|
+
p1_won = 0;
|
|
30
483
|
|
|
31
484
|
for (;;) {
|
|
32
|
-
/*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
485
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
486
|
+
* CANONICAL LYNX GAME LOOP — full-redraw every frame, in this order:
|
|
487
|
+
* 1. while (tgi_busy()) { } — WAIT for the previous frame's page flip.
|
|
488
|
+
* Skipping this is the #1 "Lynx screen stays blank" trap: drawing
|
|
489
|
+
* while the swap is pending loses the frame.
|
|
490
|
+
* 2. Repaint the WHOLE scene with tgi_bar/tgi_line fills — NOT
|
|
491
|
+
* tgi_clear() (which can leave the framebuffer stale on this
|
|
492
|
+
* toolchain+emulator path). TGI double-buffers; the back buffer holds
|
|
493
|
+
* the frame from two flips ago, so partial redraws ghost. With no
|
|
494
|
+
* hardware tilemap, the COURT is repainted every frame.
|
|
495
|
+
* 3. Draw every object (every TGI call and every tgi_sprite() is a
|
|
496
|
+
* synchronous Suzy blit into the SAME draw page).
|
|
497
|
+
* 4. tgi_updatedisplay() — request the page flip at next VBL.
|
|
498
|
+
* 5. sfx_update() IMMEDIATELY after — MIKEY voice writes must land in
|
|
499
|
+
* vblank: handy reschedules its timer sweep on the spot when a voice
|
|
500
|
+
* CTL bit-3 write lands, and mid-frame that sweep can preempt an
|
|
501
|
+
* in-flight Suzy blit and eat sprites (the R57 bug — history in
|
|
502
|
+
* lynx_sfx.c). sfx_tone()/sfx_noise() only STAGE; sfx_update() is
|
|
503
|
+
* the hardware flush. */
|
|
36
504
|
while (tgi_busy()) { }
|
|
37
505
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
tgi_setcolor(COLOR_GREEN);
|
|
47
|
-
tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy()); /* court grass */
|
|
48
|
-
tgi_setcolor(COLOR_LIGHTGREEN);
|
|
49
|
-
tgi_bar(0, COURT_TOP, 52, COURT_BOT - 1); /* left end zone */
|
|
50
|
-
tgi_bar(107, COURT_TOP, 159, COURT_BOT - 1); /* right end zone */
|
|
51
|
-
tgi_setcolor(COLOR_DARKGREY);
|
|
52
|
-
tgi_bar(0, 0, 159, COURT_TOP - 1); /* top boards */
|
|
53
|
-
tgi_bar(0, COURT_BOT, 159, 101); /* bottom boards */
|
|
54
|
-
/* white court boundary + dashed centre net + centre circle */
|
|
55
|
-
tgi_setcolor(COLOR_WHITE);
|
|
56
|
-
tgi_line(0, COURT_TOP, 159, COURT_TOP);
|
|
57
|
-
tgi_line(0, COURT_BOT, 159, COURT_BOT);
|
|
58
|
-
for (ny = COURT_TOP; ny < COURT_BOT; ny += 8)
|
|
59
|
-
tgi_bar(79, (unsigned)ny, 80, (unsigned)(ny + 3 > COURT_BOT ? COURT_BOT : ny + 3));
|
|
60
|
-
tgi_line(70, 40, 90, 40);
|
|
61
|
-
tgi_line(70, 60, 90, 60);
|
|
62
|
-
tgi_line(70, 40, 70, 60);
|
|
63
|
-
tgi_line(90, 40, 90, 60);
|
|
64
|
-
|
|
65
|
-
/* Playtest: "needs better contrast" — yellow paddles + white ball pop
|
|
66
|
-
* against the green court far better than white-on-lightgreen +
|
|
67
|
-
* yellow-on-green did. */
|
|
68
|
-
tgi_setcolor(COLOR_YELLOW);
|
|
69
|
-
tgi_bar(PADDLE_X1, (unsigned)p1y, PADDLE_X1 + PADDLE_W - 1, (unsigned)(p1y + PADDLE_H - 1));
|
|
70
|
-
tgi_bar(PADDLE_X2, (unsigned)p2y, PADDLE_X2 + PADDLE_W - 1, (unsigned)(p2y + PADDLE_H - 1));
|
|
71
|
-
tgi_setcolor(COLOR_WHITE);
|
|
72
|
-
tgi_bar((unsigned)bx, (unsigned)by, (unsigned)(bx + BALL_SIZE - 1), (unsigned)(by + BALL_SIZE - 1));
|
|
506
|
+
draw_court();
|
|
507
|
+
|
|
508
|
+
joy = joy_read(JOY_1);
|
|
509
|
+
|
|
510
|
+
if (state == ST_TITLE) frame_title(joy);
|
|
511
|
+
else if (state == ST_PLAY) frame_play(joy);
|
|
512
|
+
else frame_over(joy);
|
|
513
|
+
|
|
73
514
|
tgi_updatedisplay();
|
|
74
515
|
sfx_update();
|
|
75
516
|
|
|
76
|
-
|
|
77
|
-
if (JOY_UP(joy) && p1y > COURT_TOP) p1y -= 2;
|
|
78
|
-
if (JOY_DOWN(joy) && p1y < COURT_BOT - PADDLE_H) p1y += 2;
|
|
79
|
-
|
|
80
|
-
/* AI */
|
|
81
|
-
if (p2y + PADDLE_H/2 < by && p2y < COURT_BOT - PADDLE_H) p2y++;
|
|
82
|
-
else if (p2y + PADDLE_H/2 > by && p2y > COURT_TOP) p2y--;
|
|
83
|
-
|
|
84
|
-
bx += bdx;
|
|
85
|
-
by += bdy;
|
|
86
|
-
if (by < COURT_TOP) { by = COURT_TOP; bdy = -bdy; sfx_tone(2, 90, 2); }
|
|
87
|
-
if (by + BALL_SIZE > COURT_BOT) { by = COURT_BOT - BALL_SIZE; bdy = -bdy; sfx_tone(2, 90, 2); }
|
|
88
|
-
if (bdx < 0 && bx <= PADDLE_X1 + PADDLE_W && bx + BALL_SIZE >= PADDLE_X1
|
|
89
|
-
&& by + BALL_SIZE > p1y && by < p1y + PADDLE_H) { bdx = -bdx; sfx_tone(0, 70, 3); }
|
|
90
|
-
if (bdx > 0 && bx + BALL_SIZE >= PADDLE_X2 && bx <= PADDLE_X2 + PADDLE_W
|
|
91
|
-
&& by + BALL_SIZE > p2y && by < p2y + PADDLE_H) { bdx = -bdx; sfx_tone(0, 70, 3); }
|
|
92
|
-
if (bx < -BALL_SIZE) { bx = 78; by = 48; bdx = 2; sfx_noise(20); }
|
|
93
|
-
if (bx > 160) { bx = 78; by = 48; bdx = -2; sfx_tone(0, 50, 12); }
|
|
517
|
+
prev_joy = joy;
|
|
94
518
|
}
|
|
95
519
|
}
|