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.
Files changed (105) hide show
  1. package/AGENTS.md +19 -16
  2. package/CHANGELOG.md +184 -0
  3. package/README.md +5 -5
  4. package/examples/README.md +1 -0
  5. package/examples/gametank/templates/gt_draw.h +91 -0
  6. package/examples/gametank/templates/gt_hud.h +89 -0
  7. package/examples/gametank/templates/gt_palette.h +59 -0
  8. package/examples/gametank/templates/gt_sound.h +90 -0
  9. package/examples/gametank/templates/gt_sprites.h +53 -0
  10. package/examples/gametank/templates/platformer.c +318 -0
  11. package/examples/gametank/templates/puzzle.c +295 -0
  12. package/examples/gametank/templates/racing.c +139 -0
  13. package/examples/gametank/templates/shmup.c +277 -0
  14. package/examples/gametank/templates/sports.c +119 -0
  15. package/package.json +24 -22
  16. package/src/cores/capabilities.js +24 -0
  17. package/src/cores/registry.js +7 -1
  18. package/src/host/LibretroHost.js +90 -17
  19. package/src/host/callbacks.js +20 -2
  20. package/src/host/cpu-state.js +17 -0
  21. package/src/host/gametank-acp-state.js +53 -0
  22. package/src/http/skill-doc.js +10 -6
  23. package/src/mcp/tools/audio.js +1 -1
  24. package/src/mcp/tools/cart-parts.js +76 -0
  25. package/src/mcp/tools/lifecycle.js +1 -1
  26. package/src/mcp/tools/platform-tools.js +9 -1
  27. package/src/platforms/gametank/lib/gt/audio/acp_image.bin +0 -0
  28. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.c +55 -0
  29. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.h +34 -0
  30. package/src/platforms/gametank/lib/gt/audio/audio_fw.asm +223 -0
  31. package/src/platforms/gametank/lib/gt/audio/audio_fw_blob.s +8 -0
  32. package/src/platforms/gametank/lib/gt/audio/instruments.c +79 -0
  33. package/src/platforms/gametank/lib/gt/audio/instruments.h +25 -0
  34. package/src/platforms/gametank/lib/gt/audio/music.c +370 -0
  35. package/src/platforms/gametank/lib/gt/audio/music.h +35 -0
  36. package/src/platforms/gametank/lib/gt/audio/note_numbers.h +132 -0
  37. package/src/platforms/gametank/lib/gt/audio/scaled_sines.raw +0 -0
  38. package/src/platforms/gametank/lib/gt/audio/sine_256_-63_63.bin +0 -0
  39. package/src/platforms/gametank/lib/gt/banking.c +29 -0
  40. package/src/platforms/gametank/lib/gt/banking.h +8 -0
  41. package/src/platforms/gametank/lib/gt/banking2.s +41 -0
  42. package/src/platforms/gametank/lib/gt/crt0.s +102 -0
  43. package/src/platforms/gametank/lib/gt/draw_logo.s +55 -0
  44. package/src/platforms/gametank/lib/gt/feature/persist/persist.c +103 -0
  45. package/src/platforms/gametank/lib/gt/feature/persist/persist.h +13 -0
  46. package/src/platforms/gametank/lib/gt/feature/random/random.c +40 -0
  47. package/src/platforms/gametank/lib/gt/feature/random/random.h +18 -0
  48. package/src/platforms/gametank/lib/gt/feature/text/text.c +111 -0
  49. package/src/platforms/gametank/lib/gt/feature/text/text.h +25 -0
  50. package/src/platforms/gametank/lib/gt/gametank.c +12 -0
  51. package/src/platforms/gametank/lib/gt/gametank.h +80 -0
  52. package/src/platforms/gametank/lib/gt/gametank_logo.inc +393 -0
  53. package/src/platforms/gametank/lib/gt/gen/assets/assets_index.h +9 -0
  54. package/src/platforms/gametank/lib/gt/gen/assets/sdk_default.h +5 -0
  55. package/src/platforms/gametank/lib/gt/gen/bank_nums.h +11 -0
  56. package/src/platforms/gametank/lib/gt/gen/modules_enabled.h +4 -0
  57. package/src/platforms/gametank/lib/gt/gen/modules_enabled.inc +4 -0
  58. package/src/platforms/gametank/lib/gt/gfx/draw_direct.c +132 -0
  59. package/src/platforms/gametank/lib/gt/gfx/draw_direct.h +75 -0
  60. package/src/platforms/gametank/lib/gt/gfx/draw_queue.c +177 -0
  61. package/src/platforms/gametank/lib/gt/gfx/draw_queue.h +35 -0
  62. package/src/platforms/gametank/lib/gt/gfx/draw_util.s +157 -0
  63. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.c +43 -0
  64. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.h +46 -0
  65. package/src/platforms/gametank/lib/gt/gfx/sprites.c +216 -0
  66. package/src/platforms/gametank/lib/gt/gfx/sprites.h +41 -0
  67. package/src/platforms/gametank/lib/gt/init.c +9 -0
  68. package/src/platforms/gametank/lib/gt/input.c +26 -0
  69. package/src/platforms/gametank/lib/gt/input.h +20 -0
  70. package/src/platforms/gametank/lib/gt/interrupt.s +67 -0
  71. package/src/platforms/gametank/lib/gt/vectors.s +14 -0
  72. package/src/platforms/gametank/lib/gt/wait.s +53 -0
  73. package/src/playtest/playtest.js +174 -26
  74. package/src/toolchains/arm-none-eabi-gcc/gcc.js +39 -188
  75. package/src/toolchains/asar/asar.js +10 -15
  76. package/src/toolchains/cc65/cc65.js +82 -92
  77. package/src/toolchains/cc65/da65.js +12 -17
  78. package/src/toolchains/cc65/preset-resolver.js +13 -2
  79. package/src/toolchains/cc65/presets/gametank/gametank.h +80 -0
  80. package/src/toolchains/cc65/presets/gametank/sdk.cfg +32 -0
  81. package/src/toolchains/cc65/presets/gametank/sdk.crt0.s +61 -0
  82. package/src/toolchains/cc65/presets/gametank/sdk.vectors.s +11 -0
  83. package/src/toolchains/cc65/presets/gametank/single-bank.cfg +28 -0
  84. package/src/toolchains/cc65/presets/gametank/single-bank.crt0.s +71 -0
  85. package/src/toolchains/cc65/presets/gametank/single-bank.vectors.s +12 -0
  86. package/src/toolchains/common/c-build.js +109 -0
  87. package/src/toolchains/common/gcc-toolchain.js +164 -0
  88. package/src/toolchains/common/wasm-tool.js +101 -0
  89. package/src/toolchains/dasm/dasm.js +12 -18
  90. package/src/toolchains/gba-c/gba-c.js +253 -305
  91. package/src/toolchains/genesis-c/genesis-c.js +196 -225
  92. package/src/toolchains/index.js +58 -4
  93. package/src/toolchains/m68k-elf-gcc/gcc.js +39 -202
  94. package/src/toolchains/mips-c/mips-c.js +68 -78
  95. package/src/toolchains/mips-elf-gcc/gcc.js +55 -118
  96. package/src/toolchains/rgbds/rgbds.js +7 -19
  97. package/src/toolchains/sdcc/sdcc.js +35 -26
  98. package/src/toolchains/sh-c/sh-c.js +44 -52
  99. package/src/toolchains/sh-elf-gcc/gcc.js +55 -110
  100. package/src/toolchains/sjasm/sjasm.js +10 -14
  101. package/src/toolchains/snes-c/snes-c.js +125 -150
  102. package/src/toolchains/tcc816/tcc816.js +10 -15
  103. package/src/toolchains/vasm68k/vasm68k.js +11 -16
  104. package/src/toolchains/wladx/wladx.js +5 -17
  105. 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
+ }