romdevtools 0.71.1 → 0.84.1
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 +19 -16
- package/CHANGELOG.md +184 -0
- package/README.md +5 -5
- package/examples/README.md +1 -0
- package/examples/gametank/templates/gt_draw.h +91 -0
- package/examples/gametank/templates/gt_hud.h +89 -0
- package/examples/gametank/templates/gt_palette.h +59 -0
- package/examples/gametank/templates/gt_sound.h +90 -0
- package/examples/gametank/templates/gt_sprites.h +53 -0
- package/examples/gametank/templates/platformer.c +318 -0
- package/examples/gametank/templates/puzzle.c +295 -0
- package/examples/gametank/templates/racing.c +139 -0
- package/examples/gametank/templates/shmup.c +277 -0
- package/examples/gametank/templates/sports.c +119 -0
- package/package.json +24 -22
- package/src/cores/capabilities.js +24 -0
- package/src/cores/registry.js +7 -1
- package/src/host/LibretroHost.js +90 -17
- package/src/host/callbacks.js +20 -2
- package/src/host/cpu-state.js +17 -0
- package/src/host/gametank-acp-state.js +53 -0
- package/src/http/skill-doc.js +10 -6
- package/src/mcp/tools/audio.js +1 -1
- package/src/mcp/tools/cart-parts.js +76 -0
- package/src/mcp/tools/lifecycle.js +1 -1
- package/src/mcp/tools/platform-tools.js +9 -1
- package/src/platforms/gametank/lib/gt/audio/acp_image.bin +0 -0
- package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.c +55 -0
- package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.h +34 -0
- package/src/platforms/gametank/lib/gt/audio/audio_fw.asm +223 -0
- package/src/platforms/gametank/lib/gt/audio/audio_fw_blob.s +8 -0
- package/src/platforms/gametank/lib/gt/audio/instruments.c +79 -0
- package/src/platforms/gametank/lib/gt/audio/instruments.h +25 -0
- package/src/platforms/gametank/lib/gt/audio/music.c +370 -0
- package/src/platforms/gametank/lib/gt/audio/music.h +35 -0
- package/src/platforms/gametank/lib/gt/audio/note_numbers.h +132 -0
- package/src/platforms/gametank/lib/gt/audio/scaled_sines.raw +0 -0
- package/src/platforms/gametank/lib/gt/audio/sine_256_-63_63.bin +0 -0
- package/src/platforms/gametank/lib/gt/banking.c +29 -0
- package/src/platforms/gametank/lib/gt/banking.h +8 -0
- package/src/platforms/gametank/lib/gt/banking2.s +41 -0
- package/src/platforms/gametank/lib/gt/crt0.s +102 -0
- package/src/platforms/gametank/lib/gt/draw_logo.s +55 -0
- package/src/platforms/gametank/lib/gt/feature/persist/persist.c +103 -0
- package/src/platforms/gametank/lib/gt/feature/persist/persist.h +13 -0
- package/src/platforms/gametank/lib/gt/feature/random/random.c +40 -0
- package/src/platforms/gametank/lib/gt/feature/random/random.h +18 -0
- package/src/platforms/gametank/lib/gt/feature/text/text.c +111 -0
- package/src/platforms/gametank/lib/gt/feature/text/text.h +25 -0
- package/src/platforms/gametank/lib/gt/gametank.c +12 -0
- package/src/platforms/gametank/lib/gt/gametank.h +80 -0
- package/src/platforms/gametank/lib/gt/gametank_logo.inc +393 -0
- package/src/platforms/gametank/lib/gt/gen/assets/assets_index.h +9 -0
- package/src/platforms/gametank/lib/gt/gen/assets/sdk_default.h +5 -0
- package/src/platforms/gametank/lib/gt/gen/bank_nums.h +11 -0
- package/src/platforms/gametank/lib/gt/gen/modules_enabled.h +4 -0
- package/src/platforms/gametank/lib/gt/gen/modules_enabled.inc +4 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_direct.c +132 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_direct.h +75 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_queue.c +177 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_queue.h +35 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_util.s +157 -0
- package/src/platforms/gametank/lib/gt/gfx/gfx_sys.c +43 -0
- package/src/platforms/gametank/lib/gt/gfx/gfx_sys.h +46 -0
- package/src/platforms/gametank/lib/gt/gfx/sprites.c +216 -0
- package/src/platforms/gametank/lib/gt/gfx/sprites.h +41 -0
- package/src/platforms/gametank/lib/gt/init.c +9 -0
- package/src/platforms/gametank/lib/gt/input.c +26 -0
- package/src/platforms/gametank/lib/gt/input.h +20 -0
- package/src/platforms/gametank/lib/gt/interrupt.s +67 -0
- package/src/platforms/gametank/lib/gt/vectors.s +14 -0
- package/src/platforms/gametank/lib/gt/wait.s +53 -0
- package/src/playtest/playtest.js +174 -26
- package/src/toolchains/arm-none-eabi-gcc/gcc.js +39 -188
- package/src/toolchains/asar/asar.js +10 -15
- package/src/toolchains/cc65/cc65.js +82 -92
- package/src/toolchains/cc65/da65.js +12 -17
- package/src/toolchains/cc65/preset-resolver.js +13 -2
- package/src/toolchains/cc65/presets/gametank/gametank.h +80 -0
- package/src/toolchains/cc65/presets/gametank/sdk.cfg +32 -0
- package/src/toolchains/cc65/presets/gametank/sdk.crt0.s +61 -0
- package/src/toolchains/cc65/presets/gametank/sdk.vectors.s +11 -0
- package/src/toolchains/cc65/presets/gametank/single-bank.cfg +28 -0
- package/src/toolchains/cc65/presets/gametank/single-bank.crt0.s +71 -0
- package/src/toolchains/cc65/presets/gametank/single-bank.vectors.s +12 -0
- package/src/toolchains/common/c-build.js +109 -0
- package/src/toolchains/common/gcc-toolchain.js +164 -0
- package/src/toolchains/common/wasm-tool.js +101 -0
- package/src/toolchains/dasm/dasm.js +12 -18
- package/src/toolchains/gba-c/gba-c.js +253 -305
- package/src/toolchains/genesis-c/genesis-c.js +196 -225
- package/src/toolchains/index.js +58 -4
- package/src/toolchains/m68k-elf-gcc/gcc.js +39 -202
- package/src/toolchains/mips-c/mips-c.js +68 -78
- package/src/toolchains/mips-elf-gcc/gcc.js +55 -118
- package/src/toolchains/rgbds/rgbds.js +7 -19
- package/src/toolchains/sdcc/sdcc.js +35 -26
- package/src/toolchains/sh-c/sh-c.js +44 -52
- package/src/toolchains/sh-elf-gcc/gcc.js +55 -110
- package/src/toolchains/sjasm/sjasm.js +10 -14
- package/src/toolchains/snes-c/snes-c.js +125 -150
- package/src/toolchains/tcc816/tcc816.js +10 -15
- package/src/toolchains/vasm68k/vasm68k.js +11 -16
- package/src/toolchains/wladx/wladx.js +5 -17
- package/src/toolchains/z80/binutils.js +5 -11
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* ── gt_palette.h — GameTank palette (read DIRECTLY from the core's table) ─────
|
|
2
|
+
*
|
|
3
|
+
* These indices were read straight out of the core's active palette table
|
|
4
|
+
* (vendor/gametank_palette.h, the CAPTURE sub-palette that palette_select points
|
|
5
|
+
* at) — NOT measured from rendered output (that sampling was unreliable and gave
|
|
6
|
+
* wrong, muddy/gray results). The rgb in each comment is the EXACT table entry.
|
|
7
|
+
*
|
|
8
|
+
* The GameTank palette is non-obvious: index != color, and it has FOUR sub-
|
|
9
|
+
* palettes; the core uses CAPTURE (palette_select = 256). An index that looks
|
|
10
|
+
* vibrant in one sub-palette is gray in another, which is why a naive measurement
|
|
11
|
+
* produced gray everywhere. Use THESE indices.
|
|
12
|
+
*/
|
|
13
|
+
#ifndef GT_PALETTE_H
|
|
14
|
+
#define GT_PALETTE_H
|
|
15
|
+
|
|
16
|
+
/* near-blacks / backgrounds */
|
|
17
|
+
#define GT_NIGHT 0xC8 /* rgb(4,32,23) — near-black */
|
|
18
|
+
#define GT_DKBLUE 0xB1 /* rgb(16,52,103) — deep blue backdrop */
|
|
19
|
+
#define GT_DKGREEN 0xF8 /* rgb(0,48,0) — dark green */
|
|
20
|
+
|
|
21
|
+
/* greys / white */
|
|
22
|
+
#define GT_WHITE 0xA7 /* rgb(184,184,184) — light grey (use as white) */
|
|
23
|
+
#define GT_LTGREY 0xA7 /* rgb(184,184,184) */
|
|
24
|
+
#define GT_GREY 0xC4 /* rgb(115,115,115) — mid grey */
|
|
25
|
+
|
|
26
|
+
/* greens */
|
|
27
|
+
#define GT_GREEN 0xFF /* rgb(133,208,102) — bright green */
|
|
28
|
+
#define GT_DKGRN 0xFB /* rgb(44,117,11) — dark green */
|
|
29
|
+
#define GT_LIME 0x1F /* rgb(185,197,65) — yellow-green */
|
|
30
|
+
|
|
31
|
+
/* cyans / teals */
|
|
32
|
+
#define GT_CYAN 0xDF /* rgb(114,205,184) — aqua */
|
|
33
|
+
#define GT_TEAL 0xDC /* rgb(45,136,115) — teal */
|
|
34
|
+
|
|
35
|
+
/* blues */
|
|
36
|
+
#define GT_SKY 0xBF /* rgb(135,192,255) — light sky blue */
|
|
37
|
+
#define GT_BLUE 0xBD /* rgb(90,146,222) — mid blue */
|
|
38
|
+
#define GT_NAVY 0xBB /* rgb(44,100,176) — navy */
|
|
39
|
+
|
|
40
|
+
/* indigos / violets / purples */
|
|
41
|
+
#define GT_INDIGO 0x9C /* rgb(116,104,235) — periwinkle */
|
|
42
|
+
#define GT_VIOLET 0x9B /* rgb(95,82,212) — violet */
|
|
43
|
+
#define GT_PURPLE 0x7D /* rgb(192,116,222) — purple */
|
|
44
|
+
|
|
45
|
+
/* pinks / magentas */
|
|
46
|
+
#define GT_PINK 0x7F /* rgb(237,162,255) — light pink */
|
|
47
|
+
#define GT_MAGENTA 0x7D /* rgb(192,116,222) — magenta */
|
|
48
|
+
|
|
49
|
+
/* roses / reds */
|
|
50
|
+
#define GT_ROSE 0x5E /* rgb(234,140,162) — rose */
|
|
51
|
+
#define GT_RED 0x5A /* rgb(142,51,72) — dark red (the palette's reddest) */
|
|
52
|
+
|
|
53
|
+
/* golds / oranges / browns / yellow */
|
|
54
|
+
#define GT_GOLD 0x3F /* rgb(237,178,98) — bright gold */
|
|
55
|
+
#define GT_ORANGE 0x3D /* rgb(191,134,53) — orange */
|
|
56
|
+
#define GT_BROWN 0x3B /* rgb(146,89,9) — brown */
|
|
57
|
+
#define GT_YELLOW 0x3F /* rgb(237,178,98) — (gold doubles as yellow) */
|
|
58
|
+
|
|
59
|
+
#endif /* GT_PALETTE_H */
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/* ── gt_sound.h — simple SFX for the romdev GameTank examples (no music) ──────
|
|
2
|
+
*
|
|
3
|
+
* One-shot sound effects on the ACP FM synth — NO song engine. The SDK music
|
|
4
|
+
* player (play_song/tick_music) drove a continuous buzz on un-keyed channels and
|
|
5
|
+
* bogged the frame; for examples, punchy SFX (shoot / hit / explode) are clearer
|
|
6
|
+
* and dead simple. Each gt_sfx() keys a note on one FM channel at a chosen
|
|
7
|
+
* amplitude; gt_sfx_tick() (call once/frame) fades active SFX so they don't ring
|
|
8
|
+
* forever. No per-frame song parsing, no buzz.
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* gt_sfx_tick(); once per frame (fades out SFX envelopes)
|
|
12
|
+
* gt_sfx(GT_SFX_SHOOT); fire a shoot blip
|
|
13
|
+
* gt_sfx(GT_SFX_HIT); a hit/score blip
|
|
14
|
+
* gt_sfx(GT_SFX_EXPLODE); a low explosion
|
|
15
|
+
*/
|
|
16
|
+
#ifndef GT_SOUND_H
|
|
17
|
+
#define GT_SOUND_H
|
|
18
|
+
#include "music.h"
|
|
19
|
+
#include "instruments.h"
|
|
20
|
+
#include "audio_coprocessor.h" /* AMPLITUDE, PITCH_MSB/LSB, set_audio_param, flush_audio_params */
|
|
21
|
+
|
|
22
|
+
/* SFX presets: { note, FM channel, start amplitude, fade-per-frame }. */
|
|
23
|
+
#define GT_SFX_SHOOT 0
|
|
24
|
+
#define GT_SFX_HIT 1
|
|
25
|
+
#define GT_SFX_EXPLODE 2
|
|
26
|
+
#define GT_SFX_COIN 3
|
|
27
|
+
#define GT_SFX_JUMP 4
|
|
28
|
+
|
|
29
|
+
/* per-channel fade state (channel 0..3). */
|
|
30
|
+
static unsigned char gt_sfx_amp[4];
|
|
31
|
+
static unsigned char gt_sfx_fade[4];
|
|
32
|
+
static unsigned char gt_sfx_started;
|
|
33
|
+
|
|
34
|
+
static void gt_sfx_init(void) {
|
|
35
|
+
unsigned char c;
|
|
36
|
+
init_music(); /* brings up the note/param plumbing (no song) */
|
|
37
|
+
for (c = 0; c < 4; c++) {
|
|
38
|
+
/* ch0/1 = GUITAR (shoot/hit blips), ch2 = SNARE (explode), ch3 = PIANO — a clean
|
|
39
|
+
* mellow tone for pleasant pickups (coins). GUITAR on high notes sounds metallic. */
|
|
40
|
+
unsigned char instr = (c == 2) ? INSTR_IDX_SNARE
|
|
41
|
+
: (c == 3) ? INSTR_IDX_PIANO
|
|
42
|
+
: INSTR_IDX_GUITAR;
|
|
43
|
+
load_instrument(c, get_instrument_ptr(instr));
|
|
44
|
+
gt_sfx_amp[c] = 0; gt_sfx_fade[c] = 0;
|
|
45
|
+
}
|
|
46
|
+
gt_sfx_started = 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* fade active SFX channels toward silence. Call ONCE per frame. */
|
|
50
|
+
static void gt_sfx_tick(void) {
|
|
51
|
+
unsigned char c, op;
|
|
52
|
+
if (!gt_sfx_started) gt_sfx_init();
|
|
53
|
+
for (c = 0; c < 4; c++) {
|
|
54
|
+
if (gt_sfx_amp[c]) {
|
|
55
|
+
gt_sfx_amp[c] = (gt_sfx_amp[c] > gt_sfx_fade[c]) ? gt_sfx_amp[c] - gt_sfx_fade[c] : 0;
|
|
56
|
+
op = c << 2;
|
|
57
|
+
set_audio_param(AMPLITUDE + op, (gt_sfx_amp[c] >> 1) + 128);
|
|
58
|
+
set_audio_param(AMPLITUDE + op+1, (gt_sfx_amp[c] >> 1) + 128);
|
|
59
|
+
set_audio_param(AMPLITUDE + op+2, (gt_sfx_amp[c] >> 1) + 128);
|
|
60
|
+
set_audio_param(AMPLITUDE + op+3, (gt_sfx_amp[c] >> 1) + 128);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
flush_audio_params();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* fire a one-shot SFX. id picks the note/channel/amplitude/fade. */
|
|
67
|
+
static void gt_sfx(unsigned char id) {
|
|
68
|
+
unsigned char note, ch, amp, fade, op;
|
|
69
|
+
if (!gt_sfx_started) gt_sfx_init();
|
|
70
|
+
switch (id) {
|
|
71
|
+
case GT_SFX_SHOOT: note = 64; ch = 0; amp = 0x50; fade = 12; break;
|
|
72
|
+
case GT_SFX_HIT: note = 52; ch = 1; amp = 0x60; fade = 10; break;
|
|
73
|
+
case GT_SFX_EXPLODE: note = 30; ch = 2; amp = 0x70; fade = 5; break;
|
|
74
|
+
case GT_SFX_COIN: note = 76; ch = 3; amp = 0x44; fade = 16; break; /* PIANO ch, bright + quick */
|
|
75
|
+
case GT_SFX_JUMP: note = 60; ch = 0; amp = 0x50; fade = 14; break;
|
|
76
|
+
default: note = 60; ch = 0; amp = 0x50; fade = 12; break;
|
|
77
|
+
}
|
|
78
|
+
op = ch << 2;
|
|
79
|
+
set_note(op, note);
|
|
80
|
+
gt_sfx_amp[ch] = amp; gt_sfx_fade[ch] = fade;
|
|
81
|
+
set_audio_param(AMPLITUDE + op, (amp >> 1) + 128);
|
|
82
|
+
set_audio_param(AMPLITUDE + op+1, (amp >> 1) + 128);
|
|
83
|
+
set_audio_param(AMPLITUDE + op+2, (amp >> 1) + 128);
|
|
84
|
+
set_audio_param(AMPLITUDE + op+3, (amp >> 1) + 128);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* compatibility shim: games still call gt_music_tick() each frame → just fades SFX. */
|
|
88
|
+
#define gt_music_tick() gt_sfx_tick()
|
|
89
|
+
|
|
90
|
+
#endif /* GT_SOUND_H */
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* ── gt_sprites.h — runtime pixel-art sprites in GRAM (no asset pipeline) ──────
|
|
2
|
+
*
|
|
3
|
+
* Real colored sprite art without the SDK's .bmp asset pipeline: define a sprite
|
|
4
|
+
* as a flat array of PALETTE-INDEX bytes (0 = transparent) and gt_load_sprite()
|
|
5
|
+
* copies it into a GRAM page, then queue_draw_sprite blits a rect FROM that GRAM
|
|
6
|
+
* to the framebuffer (per-pixel color, transparency on index 0).
|
|
7
|
+
*
|
|
8
|
+
* This mirrors the SDK's load_spritesheet() EXACTLY (the only correct GRAM-write
|
|
9
|
+
* path) but copies raw bytes instead of inflatemem-decompressing them, so no
|
|
10
|
+
* asset build step is needed:
|
|
11
|
+
* - flagsMirror = 0 (plain mode — NOT DMA_CPU_TO_VRAM; that mode corrupts it)
|
|
12
|
+
* - bank_reg = bankflip | GRAM_PAGE(ramBank) (select the GRAM page)
|
|
13
|
+
* - then plain CPU writes to vram[] ($4000) land in that GRAM page, row-major.
|
|
14
|
+
* (My earlier version used direct_prepare_array_mode/DMA_CPU_TO_VRAM + the wrong
|
|
15
|
+
* bank → the sprite read uninitialized GRAM and showed as noise.)
|
|
16
|
+
*
|
|
17
|
+
* GRAM page geometry is 128 wide. Lay sprites out on a grid in the page and pass
|
|
18
|
+
* each one's (gx,gy,w,h). Colors are SDK-draw-path palette indices — see
|
|
19
|
+
* gt_palette.h. Call gt_load_sprite() ONCE per sprite at init.
|
|
20
|
+
*/
|
|
21
|
+
#ifndef GT_SPRITES_H
|
|
22
|
+
#define GT_SPRITES_H
|
|
23
|
+
#include "gametank.h"
|
|
24
|
+
#include "sprites.h"
|
|
25
|
+
#include "draw_queue.h"
|
|
26
|
+
#include "banking.h"
|
|
27
|
+
|
|
28
|
+
typedef struct GtSprite { unsigned char gx, gy, w, h; } GtSprite;
|
|
29
|
+
|
|
30
|
+
/* copy a w*h block of palette-index bytes into GRAM page 0 at (gx,gy). Mirrors
|
|
31
|
+
* load_spritesheet's GRAM setup; restores draw state after. */
|
|
32
|
+
static void gt_load_sprite(const unsigned char *px, unsigned char gx, unsigned char gy,
|
|
33
|
+
unsigned char w, unsigned char h) {
|
|
34
|
+
unsigned char x, y;
|
|
35
|
+
unsigned char oldFlags = flagsMirror;
|
|
36
|
+
unsigned char oldBanks = banksMirror;
|
|
37
|
+
flagsMirror = 0; /* plain mode (the SDK's setup) */
|
|
38
|
+
*dma_flags = flagsMirror;
|
|
39
|
+
banksMirror = bankflip | GRAM_PAGE(0); /* GRAM page 0 */
|
|
40
|
+
*bank_reg = banksMirror;
|
|
41
|
+
for (y = 0; y < h; y++)
|
|
42
|
+
for (x = 0; x < w; x++)
|
|
43
|
+
vram[(unsigned int)(gy + y) * 128 + (gx + x)] = px[(unsigned int)y * w + x];
|
|
44
|
+
flagsMirror = oldFlags; /* restore */
|
|
45
|
+
banksMirror = oldBanks;
|
|
46
|
+
*dma_flags = flagsMirror;
|
|
47
|
+
*bank_reg = banksMirror;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* blit a loaded sprite to (x,y) via the draw queue (gram slot 0). */
|
|
51
|
+
#define gt_blit(x, y, S) queue_draw_sprite((x), (y), (S).w, (S).h, (S).gx, (S).gy, 0)
|
|
52
|
+
|
|
53
|
+
#endif /* GT_SPRITES_H */
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/* ── platformer.c — GameTank side-scrolling platformer (complete example) ─────
|
|
2
|
+
*
|
|
3
|
+
* A COMPLETE, working game on the bundled GameTank SDK draw-queue runtime: title
|
|
4
|
+
* screen, a runner with a 3-frame WALK animation, real gravity + jumping, scrolling
|
|
5
|
+
* platforms to land on, collectible coins (with a pleasant pickup chime), score +
|
|
6
|
+
* lives, fall-death, and a restart loop. The GameTank's framebuffer makes a side-
|
|
7
|
+
* scroller EASY — no tilemap to stream, no hardware scroll register: the world is
|
|
8
|
+
* an array of platforms, you subtract a camera offset, and redraw the visible rects.
|
|
9
|
+
*
|
|
10
|
+
* FORK THIS. Markers:
|
|
11
|
+
* HARDWARE IDIOM (load-bearing) — redraw the whole frame as blitter rects each
|
|
12
|
+
* frame; the camera is a subtract on world-x. The hero is the ONLY GRAM sprite
|
|
13
|
+
* blit per frame (coins are cheap rects) — too many sprite blits overrun the
|
|
14
|
+
* vblank window and the draw queue silently DROPS rects (platforms flicker /
|
|
15
|
+
* vanish). Background slabs are clamped to width/height 127, never 128 (a
|
|
16
|
+
* full-screen-dimension box is dropped — see gt_draw.h).
|
|
17
|
+
* GAME LOGIC (clay) — gravity, jump height, level layout, scoring: tune freely.
|
|
18
|
+
*
|
|
19
|
+
* PHYSICS GOTCHAS baked in (each was a real bug):
|
|
20
|
+
* - World x (PLAT_X / COIN_X / cam / hx_world) is unsigned INT — char wraps at
|
|
21
|
+
* 255 and a platform's collision drifts away from where it's drawn.
|
|
22
|
+
* - Vertical position math is done in a SIGNED temp + clamped to 0: hy is an
|
|
23
|
+
* unsigned char, so a strong jump that pushes it below 0 would wrap to ~250 and
|
|
24
|
+
* trip the fall-death check (the "jump high → snap back to ground" bug).
|
|
25
|
+
* - Landing is SWEPT (feet crossed the platform top this frame), not an
|
|
26
|
+
* instantaneous thin-overlap test — a fast fall would otherwise tunnel through.
|
|
27
|
+
*
|
|
28
|
+
* CONTROLS: ←/→ run · A or Up = jump · A/START to begin. SCREEN: 128x128. 1 player.
|
|
29
|
+
*/
|
|
30
|
+
#include "gametank.h"
|
|
31
|
+
#include "draw_queue.h"
|
|
32
|
+
#include "input.h"
|
|
33
|
+
#include "gt_palette.h"
|
|
34
|
+
#include "gt_draw.h"
|
|
35
|
+
#include "gt_sprites.h"
|
|
36
|
+
#include "gt_hud.h"
|
|
37
|
+
#include "gt_sound.h"
|
|
38
|
+
|
|
39
|
+
/* ── palette (verified gt_palette.h) ── */
|
|
40
|
+
#define C_SKY1 GT_DKBLUE /* sky gradient: top */
|
|
41
|
+
#define C_SKY2 GT_BLUE
|
|
42
|
+
#define C_SKY3 GT_SKY /* sky near the ground */
|
|
43
|
+
#define C_GROUND GT_BROWN
|
|
44
|
+
#define C_GRASS GT_GREEN
|
|
45
|
+
#define C_PLAT GT_ORANGE
|
|
46
|
+
#define C_COIN GT_GOLD
|
|
47
|
+
#define C_HUD GT_WHITE
|
|
48
|
+
#define C_LIFE GT_GREEN
|
|
49
|
+
|
|
50
|
+
/* ── hero sprite art (12x14): cyan body, white face, gold boots. THREE frames —
|
|
51
|
+
* stand + two walk poses (legs swap) — cycled while running so he looks alive. ── */
|
|
52
|
+
#define HERO_W 12
|
|
53
|
+
#define HERO_H 14
|
|
54
|
+
/* shared top 12 rows (head + torso); only the legs (last 2 rows) differ per frame. */
|
|
55
|
+
#define HERO_TOP \
|
|
56
|
+
0,0,0,GT_BROWN,GT_BROWN,GT_BROWN,GT_BROWN,GT_BROWN,0,0,0,0, \
|
|
57
|
+
0,0,GT_BROWN,GT_BROWN,GT_BROWN,GT_BROWN,GT_BROWN,GT_BROWN,GT_BROWN,0,0,0, \
|
|
58
|
+
0,0,0,GT_ROSE,GT_ROSE,GT_ROSE,GT_ROSE,GT_ROSE,0,0,0,0, \
|
|
59
|
+
0,0,GT_ROSE,GT_WHITE,GT_ROSE,GT_ROSE,GT_WHITE,GT_ROSE,0,0,0,0, \
|
|
60
|
+
0,0,GT_ROSE,GT_ROSE,GT_ROSE,GT_ROSE,GT_ROSE,GT_ROSE,0,0,0,0, \
|
|
61
|
+
0,0,0,GT_ROSE,GT_RED,GT_RED,GT_ROSE,0,0,0,0,0, \
|
|
62
|
+
0,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,0,0,0, \
|
|
63
|
+
GT_CYAN,GT_CYAN,GT_SKY,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_SKY,GT_CYAN,GT_CYAN,0,0, \
|
|
64
|
+
GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,0,0, \
|
|
65
|
+
0,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,GT_CYAN,0,0,0
|
|
66
|
+
/* frame 0: standing — legs together */
|
|
67
|
+
static const unsigned char ART_HERO0[HERO_W*HERO_H] = {
|
|
68
|
+
HERO_TOP,
|
|
69
|
+
0,0,GT_CYAN,GT_CYAN,0,0,GT_CYAN,GT_CYAN,0,0,0,0,
|
|
70
|
+
0,0,GT_GOLD,GT_GOLD,0,0,GT_GOLD,GT_GOLD,0,0,0,0,
|
|
71
|
+
0,GT_GOLD,GT_GOLD,GT_GOLD,0,0,GT_GOLD,GT_GOLD,GT_GOLD,0,0,0,
|
|
72
|
+
};
|
|
73
|
+
/* frame 1: walk — left leg forward */
|
|
74
|
+
static const unsigned char ART_HERO1[HERO_W*HERO_H] = {
|
|
75
|
+
HERO_TOP,
|
|
76
|
+
0,GT_CYAN,GT_CYAN,0,0,0,GT_CYAN,GT_CYAN,0,0,0,0,
|
|
77
|
+
GT_GOLD,GT_GOLD,GT_GOLD,0,0,0,0,GT_GOLD,GT_GOLD,0,0,0,
|
|
78
|
+
GT_GOLD,GT_GOLD,0,0,0,0,0,0,GT_GOLD,GT_GOLD,0,0,
|
|
79
|
+
};
|
|
80
|
+
/* frame 2: walk — right leg forward (mirror of 1) */
|
|
81
|
+
static const unsigned char ART_HERO2[HERO_W*HERO_H] = {
|
|
82
|
+
HERO_TOP,
|
|
83
|
+
0,GT_CYAN,GT_CYAN,0,0,0,GT_CYAN,GT_CYAN,0,0,0,0,
|
|
84
|
+
0,GT_GOLD,GT_GOLD,0,0,0,0,GT_GOLD,GT_GOLD,GT_GOLD,0,0,
|
|
85
|
+
0,0,GT_GOLD,GT_GOLD,0,0,0,0,0,GT_GOLD,GT_GOLD,0,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
static GtSprite SPR_HERO0, SPR_HERO1, SPR_HERO2;
|
|
89
|
+
static void load_art(void) {
|
|
90
|
+
/* 3 hero frames on their own GRAM rows. Coins are drawn as cheap rects (no sprite),
|
|
91
|
+
* which keeps the per-frame GRAM-blit count to just ONE (the hero) — sprite blits
|
|
92
|
+
* are the expensive part of the frame and too many overrun vblank (dropped draws). */
|
|
93
|
+
SPR_HERO0.gx = 0; SPR_HERO0.gy = 0; SPR_HERO0.w = HERO_W; SPR_HERO0.h = HERO_H;
|
|
94
|
+
SPR_HERO1.gx = 0; SPR_HERO1.gy = 16; SPR_HERO1.w = HERO_W; SPR_HERO1.h = HERO_H;
|
|
95
|
+
SPR_HERO2.gx = 0; SPR_HERO2.gy = 32; SPR_HERO2.w = HERO_W; SPR_HERO2.h = HERO_H;
|
|
96
|
+
gt_load_sprite(ART_HERO0, SPR_HERO0.gx, SPR_HERO0.gy, HERO_W, HERO_H);
|
|
97
|
+
gt_load_sprite(ART_HERO1, SPR_HERO1.gx, SPR_HERO1.gy, HERO_W, HERO_H);
|
|
98
|
+
gt_load_sprite(ART_HERO2, SPR_HERO2.gx, SPR_HERO2.gy, HERO_W, HERO_H);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#define GROUND_Y 110
|
|
102
|
+
#define GRAVITY 1
|
|
103
|
+
#define JUMP_V 12
|
|
104
|
+
|
|
105
|
+
/* world platforms: x (world), y, w. ⚠ PLAT_X / COIN_X MUST be unsigned int — world
|
|
106
|
+
* coords run past 255 and an unsigned char would WRAP (270 → 14), putting a platform
|
|
107
|
+
* somewhere it isn't drawn, so the hero lands on / falls through empty air. */
|
|
108
|
+
/* Platform tops (PLAT_Y) must sit a clear gap ABOVE the ground so the hero (14 tall,
|
|
109
|
+
* feet at GROUND_Y=110 → head at 96) can JUMP UP onto them rather than walk into them
|
|
110
|
+
* from the ground. Lowest here is y=84 (26px of clear air above the floor); they step
|
|
111
|
+
* up from there. */
|
|
112
|
+
#define N_PLAT 7
|
|
113
|
+
static const unsigned int PLAT_X[N_PLAT] = { 26, 70, 118, 160, 204, 248, 290 };
|
|
114
|
+
static const unsigned char PLAT_Y[N_PLAT] = { 84, 68, 84, 56, 72, 60, 84 };
|
|
115
|
+
static const unsigned char PLAT_W[N_PLAT] = { 32, 28, 30, 26, 28, 26, 32 };
|
|
116
|
+
/* coins sit just ABOVE each platform's surface (centered on it), so you grab one by
|
|
117
|
+
* landing on or running across that platform — reachable, not floating out of reach.
|
|
118
|
+
* COIN_Y = PLAT_Y - 11 (a coin's height above the deck); COIN_X = platform center. */
|
|
119
|
+
#define N_COIN 6
|
|
120
|
+
static const unsigned int COIN_X[N_COIN] = { 38, 80, 130, 170, 214, 258 };
|
|
121
|
+
static const unsigned char COIN_Y[N_COIN] = { 73, 57, 73, 45, 61, 49 };
|
|
122
|
+
|
|
123
|
+
static unsigned int cam; /* world-x of screen left edge */
|
|
124
|
+
static unsigned char hx_screen; /* hero x ON screen (kept ~center) */
|
|
125
|
+
static unsigned int hx_world; /* hero x in world */
|
|
126
|
+
static unsigned char hy; /* hero y (top) */
|
|
127
|
+
static signed char vy; /* vertical velocity */
|
|
128
|
+
static unsigned char on_ground;
|
|
129
|
+
static unsigned char coin_got[N_COIN];
|
|
130
|
+
static unsigned int score;
|
|
131
|
+
static unsigned char lives;
|
|
132
|
+
static unsigned char walk_t, walk_f; /* hero walk-cycle timer + frame (0,1,0,2) */
|
|
133
|
+
|
|
134
|
+
/* ── coin pickup chime: the classic RISING two-note sparkle (low → high), played on
|
|
135
|
+
* the PIANO channel (3) for a clean mellow ding — NOT the harsh guitar, and NOT the
|
|
136
|
+
* same note re-hit (that re-trigger mid-decay is what made it clang like a dropped
|
|
137
|
+
* pot). gt_sfx fires the 1st (lower) note; coin_chime_tick() fires the 2nd (higher)
|
|
138
|
+
* a few frames later by keying ch3 directly with a fresh, soft amplitude. ── */
|
|
139
|
+
#define COIN_CH 3
|
|
140
|
+
#define COIN_NOTE2 80 /* 2nd note, higher than the COIN preset's 76 */
|
|
141
|
+
static unsigned char coin_chime; /* frames left until the 2nd note, 0 = idle */
|
|
142
|
+
static void coin_pickup_sound(void) { gt_sfx(GT_SFX_COIN); coin_chime = 4; }
|
|
143
|
+
static void coin_chime_tick(void) {
|
|
144
|
+
if (coin_chime) {
|
|
145
|
+
coin_chime--;
|
|
146
|
+
if (coin_chime == 0) { /* fire the 2nd (higher) note on the PIANO ch */
|
|
147
|
+
unsigned char op = COIN_CH << 2;
|
|
148
|
+
set_note(op, COIN_NOTE2);
|
|
149
|
+
gt_sfx_amp[COIN_CH] = 0x44; gt_sfx_fade[COIN_CH] = 18; /* soft, quick fade */
|
|
150
|
+
set_audio_param(AMPLITUDE + op, (0x44 >> 1) + 128);
|
|
151
|
+
set_audio_param(AMPLITUDE + op+1, (0x44 >> 1) + 128);
|
|
152
|
+
set_audio_param(AMPLITUDE + op+2, (0x44 >> 1) + 128);
|
|
153
|
+
set_audio_param(AMPLITUDE + op+3, (0x44 >> 1) + 128);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static void reset_level(void) {
|
|
159
|
+
unsigned char i;
|
|
160
|
+
cam = 0; hx_world = 20; hy = GROUND_Y - 14; vy = 0; on_ground = 1;
|
|
161
|
+
for (i = 0; i < N_COIN; i++) coin_got[i] = 0;
|
|
162
|
+
}
|
|
163
|
+
static void reset_game(void) { score = 0; lives = 3; reset_level(); }
|
|
164
|
+
|
|
165
|
+
static unsigned char overlap(unsigned int ax, unsigned char ay, unsigned char aw, unsigned char ah,
|
|
166
|
+
unsigned int bx, unsigned char by, unsigned char bw, unsigned char bh) {
|
|
167
|
+
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* sky + ground. IDIOM (anti-flicker): only gt_clear paints a flicker-free fill —
|
|
171
|
+
* every extra full-width queue_draw_box flickers on its TOP scanline (the blitter
|
|
172
|
+
* leaves that row inconsistent between the two double-buffer pages). So keep the
|
|
173
|
+
* background to gt_clear + a SINGLE ground box, and hide that one seam under the
|
|
174
|
+
* grass-topped platforms / hero feet. (No multi-band haze — it just adds seams.) */
|
|
175
|
+
static void draw_world_bg(void) {
|
|
176
|
+
gt_clear(C_SKY1); /* sky: deep blue */
|
|
177
|
+
/* ⚠ width MUST be <= 127 — the GameTank blitter drops a box whose width is 128
|
|
178
|
+
* (a full-screen-wide box wraps/rejects). Always 127, never 128. */
|
|
179
|
+
queue_draw_box(0, GROUND_Y, 127, 128 - GROUND_Y, C_GROUND); /* ground slab */
|
|
180
|
+
queue_draw_box(0, GROUND_Y, 127, 2, C_GRASS); /* grass cap (one shared seam) */
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
static unsigned char title(void) {
|
|
184
|
+
gt_start_reset();
|
|
185
|
+
while (1) {
|
|
186
|
+
draw_world_bg();
|
|
187
|
+
queue_draw_box(28, 30, 72, 12, C_PLAT); /* banner */
|
|
188
|
+
gt_blit(58, 70, SPR_HERO0);
|
|
189
|
+
queue_draw_box(41, 81, 5, 5, C_COIN); queue_draw_box(81, 81, 5, 5, C_COIN); /* coins */
|
|
190
|
+
queue_clear_border(C_SKY1);
|
|
191
|
+
gt_present();
|
|
192
|
+
gt_music_tick();
|
|
193
|
+
update_inputs();
|
|
194
|
+
if (gt_start_pressed()) return 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
void main(void) {
|
|
199
|
+
unsigned char i;
|
|
200
|
+
load_art();
|
|
201
|
+
reset_game();
|
|
202
|
+
|
|
203
|
+
for (;;) {
|
|
204
|
+
title();
|
|
205
|
+
reset_game();
|
|
206
|
+
|
|
207
|
+
for (;;) {
|
|
208
|
+
update_inputs();
|
|
209
|
+
|
|
210
|
+
/* run: hero advances through the world; camera follows, hero stays mid-screen */
|
|
211
|
+
if (player1_buttons & INPUT_MASK_RIGHT) { hx_world += 2; }
|
|
212
|
+
if ((player1_buttons & INPUT_MASK_LEFT) && hx_world > 2) { hx_world -= 2; }
|
|
213
|
+
/* jump */
|
|
214
|
+
if ((player1_new_buttons & (INPUT_MASK_A | INPUT_MASK_UP)) && on_ground) { vy = -JUMP_V; on_ground = 0; }
|
|
215
|
+
|
|
216
|
+
/* gravity + vertical move. hy is UNSIGNED, so compute the new y in a SIGNED temp:
|
|
217
|
+
* a strong jump can push y below 0, and `hy += vy` would WRAP to ~250 → the
|
|
218
|
+
* "hy > 124" fall-death check fires and resets the run (the "jump high → snap to
|
|
219
|
+
* ground" bug). Clamp the top to 0. Remember the OLD feet for swept landing. */
|
|
220
|
+
{
|
|
221
|
+
signed int foot_prev = (signed int)hy + 14; /* feet before moving */
|
|
222
|
+
signed int ny;
|
|
223
|
+
vy += GRAVITY;
|
|
224
|
+
if (vy > 10) vy = 10;
|
|
225
|
+
ny = (signed int)hy + vy;
|
|
226
|
+
if (ny < 0) { ny = 0; vy = 0; } /* bonked the top of the screen */
|
|
227
|
+
hy = (unsigned char)ny;
|
|
228
|
+
|
|
229
|
+
/* land — SWEPT, so a fast fall can't tunnel through a thin platform top. The
|
|
230
|
+
* hero lands when, while falling (vy>=0), his feet were ABOVE a platform's top
|
|
231
|
+
* last frame and are now AT/BELOW it, and he's horizontally over that platform. */
|
|
232
|
+
on_ground = 0;
|
|
233
|
+
if (hy + 14 >= GROUND_Y) { hy = GROUND_Y - 14; vy = 0; on_ground = 1; }
|
|
234
|
+
if (vy >= 0) {
|
|
235
|
+
signed int foot = (signed int)hy + 14;
|
|
236
|
+
for (i = 0; i < N_PLAT; i++) {
|
|
237
|
+
unsigned int px = PLAT_X[i];
|
|
238
|
+
signed int top = (signed int)PLAT_Y[i];
|
|
239
|
+
if (hx_world + 10 > px && hx_world < px + PLAT_W[i] /* horizontally over it */
|
|
240
|
+
&& foot_prev <= top && foot >= top) { /* crossed the top */
|
|
241
|
+
hy = (unsigned char)(top - 14); vy = 0; on_ground = 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/* fall off the bottom = lose a life (only when actually falling, not at the top) */
|
|
247
|
+
if (hy > 124 && vy > 0) { if (--lives == 0) goto dead; reset_level(); continue; }
|
|
248
|
+
|
|
249
|
+
/* coins — collect on overlap: score + a pleasant "ding-ding" chime. */
|
|
250
|
+
for (i = 0; i < N_COIN; i++) if (!coin_got[i] &&
|
|
251
|
+
overlap(hx_world, hy, 10, 14, COIN_X[i], COIN_Y[i], 8, 8)) {
|
|
252
|
+
coin_got[i] = 1; score += 25; coin_pickup_sound();
|
|
253
|
+
}
|
|
254
|
+
coin_chime_tick(); /* sequence the 2nd blip of the coin chime */
|
|
255
|
+
|
|
256
|
+
/* camera: keep hero ~45px from the left */
|
|
257
|
+
cam = (hx_world > 45) ? hx_world - 45 : 0;
|
|
258
|
+
hx_screen = (unsigned char)(hx_world - cam);
|
|
259
|
+
|
|
260
|
+
/* hero walk animation: cycle frames 0→1→0→2 while moving, stand still otherwise. */
|
|
261
|
+
if (player1_buttons & (INPUT_MASK_LEFT | INPUT_MASK_RIGHT)) {
|
|
262
|
+
if (++walk_t >= 6) { walk_t = 0; walk_f++; if (walk_f >= 4) walk_f = 0; }
|
|
263
|
+
} else { walk_f = 0; walk_t = 0; }
|
|
264
|
+
|
|
265
|
+
/* ── redraw the frame; world rects minus the camera ── */
|
|
266
|
+
draw_world_bg();
|
|
267
|
+
for (i = 0; i < N_PLAT; i++) {
|
|
268
|
+
unsigned int px = PLAT_X[i];
|
|
269
|
+
unsigned char pw = PLAT_W[i];
|
|
270
|
+
/* on screen only when fully within [cam, cam+128). For a platform straddling
|
|
271
|
+
* the LEFT edge, px-cam would underflow — clamp the screen x to 0 and trim the
|
|
272
|
+
* width so the DRAWN rect matches the WORLD rect the collision uses (no desync). */
|
|
273
|
+
if (px + pw > cam && px < cam + 128) {
|
|
274
|
+
unsigned char sx, sw;
|
|
275
|
+
if (px >= cam) { sx = (unsigned char)(px - cam); sw = pw; }
|
|
276
|
+
else { sx = 0; sw = (unsigned char)(pw - (cam - px)); } /* left-clipped */
|
|
277
|
+
if (sx + sw > 128) sw = (unsigned char)(128 - sx); /* right-clip */
|
|
278
|
+
gt_rect(sx, PLAT_Y[i], sw, 5, C_PLAT);
|
|
279
|
+
gt_rect(sx, PLAT_Y[i], sw, 2, C_GRASS); /* grassy top */
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/* coins as cheap RECTS (a gold ring) — not GRAM sprites — so the only expensive
|
|
283
|
+
* sprite blit per frame is the hero; that's what keeps the queue inside vblank. */
|
|
284
|
+
for (i = 0; i < N_COIN; i++) if (!coin_got[i]) {
|
|
285
|
+
unsigned int cx = COIN_X[i];
|
|
286
|
+
/* coins float above the platforms against the sky. Drawn as a bright YELLOW
|
|
287
|
+
* disc with a white sparkle + dark outline so they read clearly and don't
|
|
288
|
+
* blend into the orange platforms. Only drawn fully on-screen. */
|
|
289
|
+
if (cx >= cam && cx < cam + 120) {
|
|
290
|
+
unsigned char dx = (unsigned char)(cx - cam);
|
|
291
|
+
unsigned char cy = COIN_Y[i];
|
|
292
|
+
gt_rect(dx + 1, cy, 6, 8, C_COIN); /* body (rounded) */
|
|
293
|
+
gt_rect(dx, cy + 1, 8, 6, C_COIN);
|
|
294
|
+
gt_rect(dx + 2, cy + 1, 2, 3, GT_WHITE); /* bright shine */
|
|
295
|
+
gt_rect(dx + 4, cy + 4, 3, 3, GT_ORANGE); /* shaded edge */
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/* the single sprite blit: the hero, animated */
|
|
299
|
+
gt_blit(hx_screen, hy,
|
|
300
|
+
(walk_f == 1) ? SPR_HERO1 : (walk_f == 3) ? SPR_HERO2 : SPR_HERO0);
|
|
301
|
+
queue_draw_box(1, 7, 126, 7, GT_NAVY); /* HUD bar (in play area, y>=7) */
|
|
302
|
+
hud_number(score, 30, 8, 2, C_HUD);
|
|
303
|
+
hud_pips(lives, 100, 8, 5, C_LIFE);
|
|
304
|
+
gt_present();
|
|
305
|
+
gt_music_tick();
|
|
306
|
+
}
|
|
307
|
+
dead:
|
|
308
|
+
while (1) {
|
|
309
|
+
draw_world_bg();
|
|
310
|
+
queue_draw_box(20, 48, 88, 30, GT_NAVY);
|
|
311
|
+
hud_number(score, 76, 56, 3, C_HUD);
|
|
312
|
+
gt_present();
|
|
313
|
+
gt_music_tick();
|
|
314
|
+
update_inputs();
|
|
315
|
+
if (gt_start_pressed()) break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|