diff --git a/PERF.md b/PERF.md index c30ee0f..7df8d34 100644 --- a/PERF.md +++ b/PERF.md @@ -1,39 +1,70 @@ +# JoeyLib cross-port performance (UBER benchmark) + +Regenerated 2026-06-30. ST/Amiga/DOS columns are fresh measurements from +the headless harness (`scripts/bench-atarist.sh` Hatari, `bench-amiga.sh` +FS-UAE, `bench-dos.sh` DOSBox). The IIgs column is the perf reference: the +generic+override redesign was bit-identical on IIgs (MAME `iigs-verify` +checksum 0817FE), so its codegen — and thus its ops/sec — is unchanged from +the last IIgs capture. Every non-IIgs cell is a ratio (port / IIgs); the +directive is that every target meet or beat the IIgs floor (>= 1.0x). + +Sprite ops now meet or beat the IIgs on BOTH 68k targets after this session's +work (compiled DRAW emitters + raw-plane-byte save/restore); they were the +dominant violations (down to 0.13x). The ST column is honest for the first +time — the ST port was crashing on first present (user-mode I/O poke) until +the `Super(0L)` fix in jlpInit. + +Genuine remaining sub-1.0x cells and why: + * **ST jlDrawCircle (0.61-0.65x)** — inherent planar-outline cost: a 4-plane + word RMW per sparse outline pixel (8 memory accesses) vs the IIgs single + nibble, amplified by ST video-DMA contention. Cycle-bound; ~0.80x ceiling + even fully optimized. Not winnable in this format. + * **Amiga jlFillCircle (0.82x)** — already asm span-fill (long-store middles); + closing the gap needs a blitter area-fill rewrite (risk: the blitter has + wedged the system before). + * **ST jlSpriteSaveAndDraw (0.97x)** — ST runs save+draw as two dispatched + ops; the IIgs fuses them. Marginal. + * **ST jlTileFill (0.98x), Amiga jlFillRect 80x80 (0.95x)** — within noise. + * **ST jlFillRect 320x200 (0.53x)** — ARTIFACT: the IIgs reference (60) is the + SEI-inflated full-screen reading (see the ceiling note below; honest ~28). + Against the honest reference ST is ~1.1x. Same caveat for surfaceClear / + stagePresent full. + | Op | IIgs (ops/sec) | Amiga (vs IIGS) | Atari ST (vs IIGS) | DOS (vs IIGS) | | --- | --- | --- | --- | --- | -| jlSurfaceClear | 28 | 2.54x | 1.11x | 638.57x | -| jlPaletteSet | 678 | 8.68x | 4.23x | 26.13x | -| jlScbSetRange | 1005 | 3.66x | 1.86x | 11.16x | -| jlDrawPixel | 1755 | 1.85x | 1.01x | 9.70x | -| jlDrawLine H | 682 | 2.19x | 1.22x | 19.26x | -| jlDrawLine V | 90 | 1.67x | 1.11x | 115.30x | -| jlDrawLine diag | 35 | 1.14x | 1.06x | 261.49x | -| jlDrawRect 100x100 | 75 | 1.57x | 1.04x | 250.77x | -| jlDrawCircle r=16 | 232 | 1.21x | **0.65x** | 71.24x | -| jlDrawCircle r=80 | 56 | 1.16x | **0.61x** | 310.93x | -| jlFillRect 16x16 | 450 | 1.24x | 1.04x | 39.38x | -| jlFillRect 80x80 | 75 | **0.95x** | 1.28x | 206.73x | -| jlFillRect 320x200 | 60 | **0.93x** | **0.43x** | 184.62x | -| jlFillCircle r=40 | 38 | **0.97x** | 1.39x | 347.24x | -| jlSamplePixel | 1916 | 3.48x | 1.92x | 18.04x | -| jlTileFill | 1252 | 1.73x | **0.93x** | 10.44x | -| jlTileCopy | 997 | 1.80x | 1.02x | 16.96x | -| jlTileCopyMasked | 498 | 1.76x | 1.08x | 28.55x | -| jlTilePaste | 1106 | 1.94x | 1.09x | 13.53x | -| jlTileSnap | 1473 | 2.26x | 1.28x | 9.44x | -| jlSpriteSaveUnder | 528 | 2.21x | 1.29x | 20.14x | -| jlSpriteDraw | 438 | 1.82x | 1.28x | 38.39x | -| jlSpriteRestoreUnder | 487 | 2.00x | 1.11x | 36.71x | -| jlSpriteSaveAndDraw | 277 | 1.82x | 1.06x | 72.79x | -| jlStagePresent full | 42 | 6.31x | 1.40x | 373.83x | -| jlInputPoll | 273 | 13.90x | 4.19x | 57.15x | -| jlKeyDown | 3382 | 7.82x | 3.76x | 18.93x | -| jlKeyPressed | 3345 | 7.65x | 3.81x | 18.72x | -| jlMouseX | 4170 | 10.26x | 5.22x | 26.27x | -| joeyJoyConnected | 3378 | 7.76x | 3.71x | 18.95x | -| jlAudioFrameTick | 4106 | 7.99x | 3.04x | 12.37x | -| jlAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x | -| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x | -| c2p full screen | - | - | - | - | +| jlSurfaceClear | 28 | 2.68x | 1.32x | 449.21x | +| jlPaletteSet | 678 | 9.09x | 4.27x | 22.01x | +| jlScbSetRange | 1005 | 3.62x | 1.86x | 14.20x | +| jlDrawPixel | 1755 | 2.04x | 1.18x | 9.40x | +| jlDrawLine H | 682 | 2.26x | 1.61x | 26.93x | +| jlDrawLine V | 90 | 1.70x | 1.14x | 145.83x | +| jlDrawLine diag | 35 | 1.14x | 1.06x | 475.37x | +| jlDrawRect 100x100 | 75 | 1.67x | 1.16x | 206.49x | +| jlDrawCircle r=16 | 232 | 1.20x | **0.65x** | 64.79x | +| jlDrawCircle r=80 | 56 | 1.16x | **0.61x** | 301.25x | +| jlFillRect 16x16 | 450 | 1.24x | 1.11x | 36.04x | +| jlFillRect 80x80 | 75 | **0.95x** | 1.33x | 205.09x | +| jlFillRect 320x200 | 60 | 1.13x | **0.53x** | 188.92x | +| jlFillCircle r=40 | 38 | **0.82x** | 1.39x | 398.24x | +| jlSamplePixel | 1916 | 3.42x | 1.92x | 18.04x | +| jlTileFill | 1252 | 1.74x | **0.98x** | 10.60x | +| jlTileCopy | 997 | 1.80x | 1.04x | 7.88x | +| jlTileCopyMasked | 498 | 1.81x | 1.10x | 32.48x | +| jlTilePaste | 1106 | 1.99x | 1.12x | 14.64x | +| jlTileSnap | 1473 | 2.29x | 1.53x | 9.96x | +| jlSpriteSaveUnder | 528 | 1.05x | 1.29x | 23.69x | +| jlSpriteDraw | 438 | 2.09x | 1.09x | 23.85x | +| jlSpriteRestoreUnder | 487 | 1.08x | 1.11x | 34.83x | +| jlSpriteSaveAndDraw | 277 | 1.23x | **0.97x** | 53.79x | +| jlStagePresent full | 42 | 7.81x | 1.10x | 412.17x | +| jlInputPoll | 273 | 13.94x | 4.19x | 37.87x | +| jlKeyDown | 3382 | 7.75x | 3.76x | 18.94x | +| jlKeyPressed | 3345 | 7.66x | 3.81x | 18.73x | +| jlMouseX | 4170 | 10.40x | 5.22x | 26.30x | +| joeyJoyConnected | 3378 | 7.71x | 3.71x | 18.96x | +| jlAudioFrameTick | 4106 | 8.00x | 2.50x | 12.42x | +| jlAudioIsPlayingMod | 3536 | 9.30x | 4.33x | 26.78x | +| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.42x | 1.31x | 47.12x | > **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves > 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both @@ -43,6 +74,5 @@ > clock was perturbed. The clock is `jlFrameCount` = the VBL-interrupt- > driven GetTick, so an op that holds `SEI` while it runs can over- > report its ops/sec. Treat full-screen IIgs numbers above ~29 as -> inflated and cross-check against the cycle model. (`jlFillRect 320x200` -> = 60 and `jlStagePresent full` = 42 in the table above predate the -> Phase 0 SEI removal and are inflated; expect ~25-28 after re-measure.) +> inflated and cross-check against the cycle model. + diff --git a/examples/spacetaxi/VERIFIED.md b/examples/spacetaxi/VERIFIED.md index 030ec15..aaa7ed4 100644 --- a/examples/spacetaxi/VERIFIED.md +++ b/examples/spacetaxi/VERIFIED.md @@ -40,7 +40,8 @@ plus the inline scripts run during verification sessions. | `$71CE` (fares done) INCs ONLY at `$6BD6` | grep finds only one INC (`$6BD6`) and one STA (`$5EE9 = 0` at game init). | | `$7213` (fare target) is player-selectable 1..4 at title | Writers: `$48AF` sets to 1 (default); `$5310-$5328` is a title-screen joystick UI -- RIGHT INCs, LEFT DECs, ANDed with `#$03` to clamp 0..3 (= 1..4 with +1 baseline). | | Game-end goes to `$5EC4` (gameInitContinue, restarts to title) | `$6BE1: JMP $602F`, `$602F: JMP $5EC4`. So Space Taxi's "level complete" just resets the game -- no inter-level progression in the original. | -| `romToLevel.py` bug: levels A/O/R had fareCount=0 (uncompletable) | Verified by parsing all 24 .dat files. Fixed: single-pad levels now emit `(0, 0)` self-loop fare. | +| `romToLevel.py` `fareCount` and the `(0,0)` self-loop were BOTH fabricated | Deep disassembly (padHoverSetup `$6537`, padDetect `$64CC`, `$6CB8`) proves there is NO static fare list: spawn pad AND destination are RNG-chosen at runtime (`$4080`) in `[1..padCount]`; when all pads are used the destination is the `$0B` "UP PLEASE!" transporter sentinel. Fare count = `$7213` (the GAME-VARIATIONS "cabbies" setting), global not per-level. FIX: `romToLevel` emits `fareCount=0`; the port (`stPassenger.c`) ports the runtime RNG selection. Empirically verified in DOSBox: land->board->"up please!"->fly up through the top-wall transporter->fare delivered (+5)->advance to level B. | +| `romToLevel.py` pad tileX/tileY had NO VIC origin subtraction (pads off-field) | Pad slot bytes are VIC sprite-register coords. FIX (verified exact on 4 pads across scenes A/B, matched to live screen RAM): `tileX=(x1-24)//8`, `tileY=(padRow-36)//8` (36 = sprite-Y origin 50 - 14px landing-gear). Level A pad: was tileX13/tileY25(off-field), now tileX10/tileY21 (on the rendered pad). Also the taxi spawn was hardcoded `(18,2)`; real spawn is `$7D5B-5F` (level A = tile 30,3). | | Full `$5F40` main-loop body verified: 25 JSRs in exact order matching MECHANICS.md | With stubs for KERNAL `$FFE1`, `$$404B` (vblWait), `$44EF` (waitTickSpin), `$44D2` (waitVoicesIdle), and `$7D65 = $80` so `$6BE7` doesn't early-exit: 716 instructions, 25 depth-1 JSRs captured in documented order. | | `$4BF8` (CIA1 DDR setup) is called every main-loop iteration, not "once" | Verified by trace: appears in the depth-1 chain at PC `$5FB5`. Setting CIA DDRs each frame is wasteful but harmless. | | Emulator now covers all opcodes used by the main-loop body | Added INC/DEC absx/zpx, ASL/LSR/ROL/ROR zpx/absx, LDA/STA/AND/ORA/EOR/ADC/SBC/CMP indx, CPX/CPY zp. The main loop runs to completion through these instructions. | diff --git a/examples/spacetaxi/assets/genPlaceholderArt.py b/examples/spacetaxi/assets/genPlaceholderArt.py index 35fc635..d9c0f75 100644 --- a/examples/spacetaxi/assets/genPlaceholderArt.py +++ b/examples/spacetaxi/assets/genPlaceholderArt.py @@ -32,9 +32,9 @@ PALETTE_RGB = [ ] -def new_canvas(): - """Returns a fresh 320x200 indexed PIL image with the palette installed.""" - img = Image.new('P', (320, 200), 0) +def new_canvas(w=320, h=200): + """Returns a fresh w x h indexed PIL image with the palette installed.""" + img = Image.new('P', (w, h), 0) flat = [] for r, g, b in PALETTE_RGB: flat.extend([r, g, b]) @@ -120,11 +120,13 @@ def gen_tilebank(): def gen_sprites(): """sprites.png: 320x200, layout: - y= 0..23 : taxi cels (4 frames of 24x24 starting at x=0) - y= 24..39 : passenger walk cels (4 frames of 16x16 at x=0) - Remaining rows free for future content. + y= 0..23 : taxi cels 0-3 (drive) + 4-8 (transporter shrink) + y= 24..47 : passenger walk cels (row 1) + y= 48..71 : flame row (left blank; real cels come from raw.bin) + Sheet is exactly the 9x3 grid of 24x24 cels (216x72) so assetbake's + cell grid divides evenly. """ - img = new_canvas() + img = new_canvas(9 * 24, 3 * 24) # 216x72: 9 cols x 3 rows of 24x24 d = ImageDraw.Draw(img) # ---- Taxi cels: simple side-view "pod with thruster" ---- @@ -149,6 +151,24 @@ def gen_sprites(): (x+15, flame_top), (x+12, flame_bot)], fill=flame_color) + # ---- Taxi shrink cels (transporter warp): the cab receding into the + # distance, 5 progressively smaller sizes, centered in the 24x24 cell. + # Placed in the free row-0 slots (cels 4..8). No flame -- it is warping + # away, not thrusting. The renderer draws one per warp step ($E2..$E9 + # on the C64 = 8 shrink cels; the port approximates with 5 sizes). + for i in range(5): + cx = (4 + i) * 24 + 12 + scale = 1.0 - i * 0.19 # 1.0, 0.81, 0.62, 0.43, 0.24 + bw = max(3, int(round(19 * scale))) + bh = max(2, int(round(12 * scale))) + x0 = cx - bw // 2 + y0 = 12 - bh // 2 + # Pod body (gray, white outline) + d.rectangle([x0, y0, x0 + bw, y0 + bh], fill=7, outline=15) + # Cockpit window (blue) while big enough to show + if bw >= 8: + d.rectangle([x0 + bw - 5, y0 + 1, x0 + bw - 1, y0 + 3], fill=8) + # ---- Passenger cels: small stick figure, walk cycle ---- # Centered in 16x16, walking right. for cel in range(4): @@ -179,6 +199,12 @@ def gen_font(): Authored manually for the printable subset (0x20..0x7E). Each glyph cell is at (ascii%40, ascii/40) when ascii<128. Unfilled cells stay black (treated as TILE_NO_GLYPH at runtime). + + WARNING: this generator is UPPERCASE-ONLY (lowercase maps to the + uppercase glyphs). The COMMITTED font.png has been hand-extended with + real lowercase glyphs, so running this clobbers it -- the HUD reverts + to all-caps. Regenerate SPRITES only (gen_sprites) unless you also add + the lowercase glyphs here first; then `git checkout` font.png. """ # A compact 5-pixel-wide bitmap font, drawn in 7 rows with 1px # padding on all sides (so 5x7 active inside an 8x8 cell, baseline diff --git a/examples/spacetaxi/assets/sprites/sprites.png b/examples/spacetaxi/assets/sprites/sprites.png index 9411d66..83a1e6a 100644 Binary files a/examples/spacetaxi/assets/sprites/sprites.png and b/examples/spacetaxi/assets/sprites/sprites.png differ diff --git a/examples/spacetaxi/assets/tiles/tilebank0.png b/examples/spacetaxi/assets/tiles/tilebank0.png new file mode 100644 index 0000000..0dc7bb4 Binary files /dev/null and b/examples/spacetaxi/assets/tiles/tilebank0.png differ diff --git a/examples/spacetaxi/generated/amiga/levels/level01.dat b/examples/spacetaxi/generated/amiga/levels/level01.dat index 18889fc..f8a6f9a 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level01.dat and b/examples/spacetaxi/generated/amiga/levels/level01.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level02.dat b/examples/spacetaxi/generated/amiga/levels/level02.dat index dd590ea..dea5903 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level02.dat and b/examples/spacetaxi/generated/amiga/levels/level02.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level03.dat b/examples/spacetaxi/generated/amiga/levels/level03.dat index 40c531a..9b5a703 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level03.dat and b/examples/spacetaxi/generated/amiga/levels/level03.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level04.dat b/examples/spacetaxi/generated/amiga/levels/level04.dat index 0115781..74993ec 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level04.dat and b/examples/spacetaxi/generated/amiga/levels/level04.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level05.dat b/examples/spacetaxi/generated/amiga/levels/level05.dat index 0b5268d..8467bf0 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level05.dat and b/examples/spacetaxi/generated/amiga/levels/level05.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level06.dat b/examples/spacetaxi/generated/amiga/levels/level06.dat index 8cac764..d232194 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level06.dat and b/examples/spacetaxi/generated/amiga/levels/level06.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level07.dat b/examples/spacetaxi/generated/amiga/levels/level07.dat index b0114fa..805246c 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level07.dat and b/examples/spacetaxi/generated/amiga/levels/level07.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level08.dat b/examples/spacetaxi/generated/amiga/levels/level08.dat index 18842ba..3d6d273 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level08.dat and b/examples/spacetaxi/generated/amiga/levels/level08.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level09.dat b/examples/spacetaxi/generated/amiga/levels/level09.dat index 28f3d58..fc9246e 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level09.dat and b/examples/spacetaxi/generated/amiga/levels/level09.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level10.dat b/examples/spacetaxi/generated/amiga/levels/level10.dat index c67d49c..c4717cc 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level10.dat and b/examples/spacetaxi/generated/amiga/levels/level10.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level11.dat b/examples/spacetaxi/generated/amiga/levels/level11.dat index 635f902..4c3ce87 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level11.dat and b/examples/spacetaxi/generated/amiga/levels/level11.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level12.dat b/examples/spacetaxi/generated/amiga/levels/level12.dat index 13417e5..6a5ba48 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level12.dat and b/examples/spacetaxi/generated/amiga/levels/level12.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level13.dat b/examples/spacetaxi/generated/amiga/levels/level13.dat index 1b8046a..459177b 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level13.dat and b/examples/spacetaxi/generated/amiga/levels/level13.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level14.dat b/examples/spacetaxi/generated/amiga/levels/level14.dat index 651e35e..4a4374a 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level14.dat and b/examples/spacetaxi/generated/amiga/levels/level14.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level15.dat b/examples/spacetaxi/generated/amiga/levels/level15.dat index 957a058..f091239 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level15.dat and b/examples/spacetaxi/generated/amiga/levels/level15.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level16.dat b/examples/spacetaxi/generated/amiga/levels/level16.dat index d7057b9..e6471a5 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level16.dat and b/examples/spacetaxi/generated/amiga/levels/level16.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level17.dat b/examples/spacetaxi/generated/amiga/levels/level17.dat index 194b264..4aa266c 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level17.dat and b/examples/spacetaxi/generated/amiga/levels/level17.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level18.dat b/examples/spacetaxi/generated/amiga/levels/level18.dat index a283dc5..2303e0d 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level18.dat and b/examples/spacetaxi/generated/amiga/levels/level18.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level19.dat b/examples/spacetaxi/generated/amiga/levels/level19.dat index c7dfc6e..76c0dd3 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level19.dat and b/examples/spacetaxi/generated/amiga/levels/level19.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level20.dat b/examples/spacetaxi/generated/amiga/levels/level20.dat index ac96165..0c70215 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level20.dat and b/examples/spacetaxi/generated/amiga/levels/level20.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level21.dat b/examples/spacetaxi/generated/amiga/levels/level21.dat index a342bd3..cd178e5 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level21.dat and b/examples/spacetaxi/generated/amiga/levels/level21.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level22.dat b/examples/spacetaxi/generated/amiga/levels/level22.dat index 1ce7d95..89df634 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level22.dat and b/examples/spacetaxi/generated/amiga/levels/level22.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level23.dat b/examples/spacetaxi/generated/amiga/levels/level23.dat index e83b4c4..ccf8623 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level23.dat and b/examples/spacetaxi/generated/amiga/levels/level23.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/level24.dat b/examples/spacetaxi/generated/amiga/levels/level24.dat index f623c48..a499a94 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/level24.dat and b/examples/spacetaxi/generated/amiga/levels/level24.dat differ diff --git a/examples/spacetaxi/generated/amiga/levels/title.dat b/examples/spacetaxi/generated/amiga/levels/title.dat index 0773fae..9887b56 100644 Binary files a/examples/spacetaxi/generated/amiga/levels/title.dat and b/examples/spacetaxi/generated/amiga/levels/title.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level01.dat b/examples/spacetaxi/generated/atarist/levels/level01.dat index 18889fc..f8a6f9a 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level01.dat and b/examples/spacetaxi/generated/atarist/levels/level01.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level02.dat b/examples/spacetaxi/generated/atarist/levels/level02.dat index dd590ea..dea5903 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level02.dat and b/examples/spacetaxi/generated/atarist/levels/level02.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level03.dat b/examples/spacetaxi/generated/atarist/levels/level03.dat index 40c531a..9b5a703 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level03.dat and b/examples/spacetaxi/generated/atarist/levels/level03.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level04.dat b/examples/spacetaxi/generated/atarist/levels/level04.dat index 0115781..74993ec 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level04.dat and b/examples/spacetaxi/generated/atarist/levels/level04.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level05.dat b/examples/spacetaxi/generated/atarist/levels/level05.dat index 0b5268d..8467bf0 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level05.dat and b/examples/spacetaxi/generated/atarist/levels/level05.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level06.dat b/examples/spacetaxi/generated/atarist/levels/level06.dat index 8cac764..d232194 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level06.dat and b/examples/spacetaxi/generated/atarist/levels/level06.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level07.dat b/examples/spacetaxi/generated/atarist/levels/level07.dat index b0114fa..805246c 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level07.dat and b/examples/spacetaxi/generated/atarist/levels/level07.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level08.dat b/examples/spacetaxi/generated/atarist/levels/level08.dat index 18842ba..3d6d273 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level08.dat and b/examples/spacetaxi/generated/atarist/levels/level08.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level09.dat b/examples/spacetaxi/generated/atarist/levels/level09.dat index 28f3d58..fc9246e 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level09.dat and b/examples/spacetaxi/generated/atarist/levels/level09.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level10.dat b/examples/spacetaxi/generated/atarist/levels/level10.dat index c67d49c..c4717cc 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level10.dat and b/examples/spacetaxi/generated/atarist/levels/level10.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level11.dat b/examples/spacetaxi/generated/atarist/levels/level11.dat index 635f902..4c3ce87 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level11.dat and b/examples/spacetaxi/generated/atarist/levels/level11.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level12.dat b/examples/spacetaxi/generated/atarist/levels/level12.dat index 13417e5..6a5ba48 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level12.dat and b/examples/spacetaxi/generated/atarist/levels/level12.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level13.dat b/examples/spacetaxi/generated/atarist/levels/level13.dat index 1b8046a..459177b 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level13.dat and b/examples/spacetaxi/generated/atarist/levels/level13.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level14.dat b/examples/spacetaxi/generated/atarist/levels/level14.dat index 651e35e..4a4374a 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level14.dat and b/examples/spacetaxi/generated/atarist/levels/level14.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level15.dat b/examples/spacetaxi/generated/atarist/levels/level15.dat index 957a058..f091239 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level15.dat and b/examples/spacetaxi/generated/atarist/levels/level15.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level16.dat b/examples/spacetaxi/generated/atarist/levels/level16.dat index d7057b9..e6471a5 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level16.dat and b/examples/spacetaxi/generated/atarist/levels/level16.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level17.dat b/examples/spacetaxi/generated/atarist/levels/level17.dat index 194b264..4aa266c 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level17.dat and b/examples/spacetaxi/generated/atarist/levels/level17.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level18.dat b/examples/spacetaxi/generated/atarist/levels/level18.dat index a283dc5..2303e0d 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level18.dat and b/examples/spacetaxi/generated/atarist/levels/level18.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level19.dat b/examples/spacetaxi/generated/atarist/levels/level19.dat index c7dfc6e..76c0dd3 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level19.dat and b/examples/spacetaxi/generated/atarist/levels/level19.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level20.dat b/examples/spacetaxi/generated/atarist/levels/level20.dat index ac96165..0c70215 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level20.dat and b/examples/spacetaxi/generated/atarist/levels/level20.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level21.dat b/examples/spacetaxi/generated/atarist/levels/level21.dat index a342bd3..cd178e5 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level21.dat and b/examples/spacetaxi/generated/atarist/levels/level21.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level22.dat b/examples/spacetaxi/generated/atarist/levels/level22.dat index 1ce7d95..89df634 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level22.dat and b/examples/spacetaxi/generated/atarist/levels/level22.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level23.dat b/examples/spacetaxi/generated/atarist/levels/level23.dat index e83b4c4..ccf8623 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level23.dat and b/examples/spacetaxi/generated/atarist/levels/level23.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/level24.dat b/examples/spacetaxi/generated/atarist/levels/level24.dat index f623c48..a499a94 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/level24.dat and b/examples/spacetaxi/generated/atarist/levels/level24.dat differ diff --git a/examples/spacetaxi/generated/atarist/levels/title.dat b/examples/spacetaxi/generated/atarist/levels/title.dat index 0773fae..9887b56 100644 Binary files a/examples/spacetaxi/generated/atarist/levels/title.dat and b/examples/spacetaxi/generated/atarist/levels/title.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level01.dat b/examples/spacetaxi/generated/dos/levels/level01.dat index 18889fc..f8a6f9a 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level01.dat and b/examples/spacetaxi/generated/dos/levels/level01.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level02.dat b/examples/spacetaxi/generated/dos/levels/level02.dat index dd590ea..dea5903 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level02.dat and b/examples/spacetaxi/generated/dos/levels/level02.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level03.dat b/examples/spacetaxi/generated/dos/levels/level03.dat index 40c531a..9b5a703 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level03.dat and b/examples/spacetaxi/generated/dos/levels/level03.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level04.dat b/examples/spacetaxi/generated/dos/levels/level04.dat index 0115781..74993ec 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level04.dat and b/examples/spacetaxi/generated/dos/levels/level04.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level05.dat b/examples/spacetaxi/generated/dos/levels/level05.dat index 0b5268d..8467bf0 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level05.dat and b/examples/spacetaxi/generated/dos/levels/level05.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level06.dat b/examples/spacetaxi/generated/dos/levels/level06.dat index 8cac764..d232194 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level06.dat and b/examples/spacetaxi/generated/dos/levels/level06.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level07.dat b/examples/spacetaxi/generated/dos/levels/level07.dat index b0114fa..805246c 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level07.dat and b/examples/spacetaxi/generated/dos/levels/level07.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level08.dat b/examples/spacetaxi/generated/dos/levels/level08.dat index 18842ba..3d6d273 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level08.dat and b/examples/spacetaxi/generated/dos/levels/level08.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level09.dat b/examples/spacetaxi/generated/dos/levels/level09.dat index 28f3d58..fc9246e 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level09.dat and b/examples/spacetaxi/generated/dos/levels/level09.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level10.dat b/examples/spacetaxi/generated/dos/levels/level10.dat index c67d49c..c4717cc 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level10.dat and b/examples/spacetaxi/generated/dos/levels/level10.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level11.dat b/examples/spacetaxi/generated/dos/levels/level11.dat index 635f902..4c3ce87 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level11.dat and b/examples/spacetaxi/generated/dos/levels/level11.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level12.dat b/examples/spacetaxi/generated/dos/levels/level12.dat index 13417e5..6a5ba48 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level12.dat and b/examples/spacetaxi/generated/dos/levels/level12.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level13.dat b/examples/spacetaxi/generated/dos/levels/level13.dat index 1b8046a..459177b 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level13.dat and b/examples/spacetaxi/generated/dos/levels/level13.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level14.dat b/examples/spacetaxi/generated/dos/levels/level14.dat index 651e35e..4a4374a 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level14.dat and b/examples/spacetaxi/generated/dos/levels/level14.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level15.dat b/examples/spacetaxi/generated/dos/levels/level15.dat index 957a058..f091239 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level15.dat and b/examples/spacetaxi/generated/dos/levels/level15.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level16.dat b/examples/spacetaxi/generated/dos/levels/level16.dat index d7057b9..e6471a5 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level16.dat and b/examples/spacetaxi/generated/dos/levels/level16.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level17.dat b/examples/spacetaxi/generated/dos/levels/level17.dat index 194b264..4aa266c 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level17.dat and b/examples/spacetaxi/generated/dos/levels/level17.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level18.dat b/examples/spacetaxi/generated/dos/levels/level18.dat index a283dc5..2303e0d 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level18.dat and b/examples/spacetaxi/generated/dos/levels/level18.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level19.dat b/examples/spacetaxi/generated/dos/levels/level19.dat index c7dfc6e..76c0dd3 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level19.dat and b/examples/spacetaxi/generated/dos/levels/level19.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level20.dat b/examples/spacetaxi/generated/dos/levels/level20.dat index ac96165..0c70215 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level20.dat and b/examples/spacetaxi/generated/dos/levels/level20.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level21.dat b/examples/spacetaxi/generated/dos/levels/level21.dat index a342bd3..cd178e5 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level21.dat and b/examples/spacetaxi/generated/dos/levels/level21.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level22.dat b/examples/spacetaxi/generated/dos/levels/level22.dat index 1ce7d95..89df634 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level22.dat and b/examples/spacetaxi/generated/dos/levels/level22.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level23.dat b/examples/spacetaxi/generated/dos/levels/level23.dat index e83b4c4..ccf8623 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level23.dat and b/examples/spacetaxi/generated/dos/levels/level23.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/level24.dat b/examples/spacetaxi/generated/dos/levels/level24.dat index f623c48..a499a94 100644 Binary files a/examples/spacetaxi/generated/dos/levels/level24.dat and b/examples/spacetaxi/generated/dos/levels/level24.dat differ diff --git a/examples/spacetaxi/generated/dos/levels/title.dat b/examples/spacetaxi/generated/dos/levels/title.dat index 0773fae..9887b56 100644 Binary files a/examples/spacetaxi/generated/dos/levels/title.dat and b/examples/spacetaxi/generated/dos/levels/title.dat differ diff --git a/examples/spacetaxi/generated/dos/sprites/sprites.spr b/examples/spacetaxi/generated/dos/sprites/sprites.spr index 8dd31fe..90ef8db 100644 Binary files a/examples/spacetaxi/generated/dos/sprites/sprites.spr and b/examples/spacetaxi/generated/dos/sprites/sprites.spr differ diff --git a/examples/spacetaxi/generated/dos/tiles/tilebank0.tbk b/examples/spacetaxi/generated/dos/tiles/tilebank0.tbk new file mode 100644 index 0000000..5b8edfc Binary files /dev/null and b/examples/spacetaxi/generated/dos/tiles/tilebank0.tbk differ diff --git a/examples/spacetaxi/mkstlevel/mkstlevel.c b/examples/spacetaxi/mkstlevel/mkstlevel.c index ba1afa4..57c0f65 100644 --- a/examples/spacetaxi/mkstlevel/mkstlevel.c +++ b/examples/spacetaxi/mkstlevel/mkstlevel.c @@ -1,5 +1,5 @@ // mkstlevel: convert a human-readable text level definition into the -// STL2 binary format read by examples/spacetaxi/stLevel.c. +// STL3 binary format read by examples/spacetaxi/stLevel.c. // // Usage: // mkstlevel input.txt output.dat @@ -15,8 +15,10 @@ // @accel [optional, default 14 14] // @gravity [optional, default 0 1; yGrav signed] // @vicColor [optional, default all 0] -// @pad [up to 10 of these] -// @fare [up to 16 of these] +// @pad [standX] [up to 10 of these; +// letter is a readability label only (not stored); +// standX = passenger stand column in pixels, optional +// (defaults to the pad's left edge, tileX*8)] // @tilemap -- followed by exactly 25 lines of 40 chars each // @colormap -- followed by exactly 25 lines of 40 chars each // @@ -45,25 +47,18 @@ #define ST_TILEMAP_W 40 #define ST_PLAYFIELD_ROWS 25 #define ST_MAX_PADS 10 -#define ST_MAX_FARES 16 #define ST_NAME_MAX 23 #define LINE_MAX 256 typedef struct { - uint8_t letter; uint8_t tileX; uint8_t tileY; uint8_t tileW; + uint8_t standX; // passenger stand column, screen pixels } PadT; -typedef struct { - uint8_t spawnPad; - uint8_t destPad; -} FareT; - - typedef struct { char name[ST_NAME_MAX + 1]; uint8_t tileBank; @@ -85,15 +80,12 @@ typedef struct { uint8_t sprite1Color; uint8_t padCount; PadT pads[ST_MAX_PADS]; - uint8_t fareCount; - FareT fares[ST_MAX_FARES]; uint8_t tilemap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS]; uint8_t colormap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS]; } LevelT; static int charToIndex(int c, int *outIdx); -static int findPadByLetter(const LevelT *L, uint8_t letter); static int parseGrid(FILE *fp, const char *gridName, uint8_t *outCells, int *outLine); static int parseLevel(FILE *fp, LevelT *out); static int readLine(FILE *fp, char *buf, int n); @@ -113,17 +105,6 @@ static int charToIndex(int c, int *outIdx) { } -static int findPadByLetter(const LevelT *L, uint8_t letter) { - uint8_t i; - for (i = 0; i < L->padCount; i++) { - if (L->pads[i].letter == letter) { - return (int)i; - } - } - return -1; -} - - // Reads one line; returns >0 on success, 0 on EOF, -1 on error. // Strips trailing newline. Lines starting with '#' are returned with // the leading '#' so the caller can decide whether to skip them. @@ -311,11 +292,14 @@ static int parseLevel(FILE *fp, LevelT *out) { out->sprite0Color = (uint8_t)sc0; out->sprite1Color = (uint8_t)sc1; } else if (strncmp(line, "@pad ", 5) == 0) { - char letter = '\0'; + char letter = '\0'; // kept in the syntax for readability only int px = 0; int py = 0; int pw = 0; - if (sscanf(line + 5, " %c %d %d %d", &letter, &px, &py, &pw) != 4) { + int sx = -1; // optional passenger stand X (pixels) + int got = sscanf(line + 5, " %c %d %d %d %d", + &letter, &px, &py, &pw, &sx); + if (got != 4 && got != 5) { fprintf(stderr, "mkstlevel: line %d: bad pad\n", lineNo); return 0; } @@ -324,36 +308,13 @@ static int parseLevel(FILE *fp, LevelT *out) { lineNo, ST_MAX_PADS); return 0; } - out->pads[out->padCount].letter = (uint8_t)letter; out->pads[out->padCount].tileX = (uint8_t)px; out->pads[out->padCount].tileY = (uint8_t)py; out->pads[out->padCount].tileW = (uint8_t)pw; + // Default the passenger stand column to the pad's left edge. + out->pads[out->padCount].standX = + (uint8_t)((sx >= 0) ? sx : (px * 8)); out->padCount++; - } else if (strncmp(line, "@fare ", 6) == 0) { - char src = '\0'; - char dst = '\0'; - int srcIdx; - int dstIdx; - if (sscanf(line + 6, " %c %c", &src, &dst) != 2) { - fprintf(stderr, "mkstlevel: line %d: bad fare\n", lineNo); - return 0; - } - srcIdx = findPadByLetter(out, (uint8_t)src); - dstIdx = findPadByLetter(out, (uint8_t)dst); - if (srcIdx < 0 || dstIdx < 0) { - fprintf(stderr, - "mkstlevel: line %d: fare references unknown pad (src '%c' dst '%c')\n", - lineNo, src, dst); - return 0; - } - if (out->fareCount >= ST_MAX_FARES) { - fprintf(stderr, "mkstlevel: line %d: too many fares (max %d)\n", - lineNo, ST_MAX_FARES); - return 0; - } - out->fares[out->fareCount].spawnPad = (uint8_t)srcIdx; - out->fares[out->fareCount].destPad = (uint8_t)dstIdx; - out->fareCount++; } else if (strcmp(line, "@tilemap") == 0) { if (!parseGrid(fp, "tilemap", out->tilemap, &lineNo)) { return 0; @@ -403,7 +364,7 @@ static int writeOut(const LevelT *L, const char *path) { return 0; } if (fputc('S', fp) == EOF || fputc('T', fp) == EOF || - fputc('L', fp) == EOF || fputc('2', fp) == EOF) { + fputc('L', fp) == EOF || fputc('3', fp) == EOF) { goto fail; } nameLen = (uint8_t)strlen(L->name); @@ -440,22 +401,14 @@ static int writeOut(const LevelT *L, const char *path) { goto fail; } for (i = 0; i < L->padCount; i++) { - if (fputc(L->pads[i].letter, fp) == EOF || - fputc(L->pads[i].tileX, fp) == EOF || + if (fputc(L->pads[i].tileX, fp) == EOF || fputc(L->pads[i].tileY, fp) == EOF || - fputc(L->pads[i].tileW, fp) == EOF) { - goto fail; - } - } - if (fputc(L->fareCount, fp) == EOF) { - goto fail; - } - for (i = 0; i < L->fareCount; i++) { - if (fputc(L->fares[i].spawnPad, fp) == EOF || - fputc(L->fares[i].destPad, fp) == EOF) { + fputc(L->pads[i].tileW, fp) == EOF || + fputc(L->pads[i].standX, fp) == EOF) { goto fail; } } + // STL3 stores no fares -- fares are RNG-chosen at runtime. cells = (size_t)ST_TILEMAP_W * ST_PLAYFIELD_ROWS; if (fwrite(L->tilemap, 1, cells, fp) != cells) { goto fail; @@ -493,7 +446,7 @@ int main(int argc, char **argv) { if (!writeOut(&L, argv[2])) { return 1; } - printf("mkstlevel: wrote %s (name=\"%s\", pads=%u, fares=%u)\n", - argv[2], L.name, (unsigned)L.padCount, (unsigned)L.fareCount); + printf("mkstlevel: wrote %s (name=\"%s\", pads=%u)\n", + argv[2], L.name, (unsigned)L.padCount); return 0; } diff --git a/examples/spacetaxi/spacetaxi.c b/examples/spacetaxi/spacetaxi.c index f4ce554..45cd5f4 100644 --- a/examples/spacetaxi/spacetaxi.c +++ b/examples/spacetaxi/spacetaxi.c @@ -163,18 +163,75 @@ int main(void) { switch (game.state) { case ST_STATE_TITLE: { - // C64 title only listens for UP / DOWN / FIRE per the - // baked joystick instructions ("UP FOR HIGH SCORES, - // DOWN FOR INSTRUCTIONS, FIRE BUTTON TO BEGIN"). The - // fare-count selector ($5310-$5328) lives on a separate - // options/menu scene at $5295 (header strings) and - // $52E1/$533C (the "1 2 3 4" digit row), not the main - // title. UP / DOWN / options screen are TODO; for now - // the title only honors FIRE -> begin. + // C64 title listens for UP (high scores), DOWN + // (instructions), and FIRE. FIRE on the title does NOT + // start gameplay -- it advances to the "GAME VARIATIONS" + // options menu ($5295, reached via $4750 -> JSR $5295), + // where the player selects the cabbie count before the + // game begins. (UP/DOWN high-scores/instructions screens + // are still TODO.) + { + int8_t jy = jlJoystickY(JOYSTICK_0); + if (jlKeyDown(KEY_UP) || jy < -32) { + game.state = ST_STATE_HIGHSCORES; + break; + } + if (jlKeyDown(KEY_DOWN) || jy > 32) { + game.state = ST_STATE_INSTRUCTIONS; + break; + } + } + if (jlKeyPressed(KEY_SPACE) || + jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) { + game.fareTarget = 1u; // C64 default ($48AF sets 1) + game.state = ST_STATE_OPTIONS; + } + break; + } + case ST_STATE_HIGHSCORES: + case ST_STATE_INSTRUCTIONS: + // Both are title sub-screens; FIRE returns to the title. + // The screens cleared the stage, so force a full title + // tilemap repaint on the way back. + if (jlKeyPressed(KEY_SPACE) || + jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) { + stRenderLevelChanged(); + game.state = ST_STATE_TITLE; + } + break; + case ST_STATE_OPTIONS: + { + // "GAME VARIATIONS" menu ($5295). LEFT/RIGHT cycle the + // cabbie count 1..4 (C64 stores 0..3 in $7213 and wraps + // with AND #$03 -- so it rolls 4->1 and 1->4); FIRE + // confirms and starts the game. LEFT/RIGHT are edge- + // triggered so one press = one step. + static bool prevLeft = false; + static bool prevRight = false; + int8_t jx = jlJoystickX(JOYSTICK_0); + bool left = jlKeyDown(KEY_LEFT) || jx < -32; + bool right = jlKeyDown(KEY_RIGHT) || jx > 32; + uint8_t sel = (uint8_t)((game.fareTarget - 1u) & 3u); + + if (right && !prevRight) { + sel = (uint8_t)((sel + 1u) & 3u); + } + if (left && !prevLeft) { + sel = (uint8_t)((sel - 1u) & 3u); + } + game.fareTarget = (uint8_t)(sel + 1u); + prevLeft = left; + prevRight = right; + if (jlKeyPressed(KEY_SPACE) || jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) { game.score = 0; - game.lives = 5; + // "SELECT NUMBER OF CABBIES" ($7213) sets the number of + // taxi LIVES, not a fare quota: gFareCounter increments + // only on the crash path ($6BD6), and the game ends when + // crashes reach the cabbie count. Deliveries never end + // the game; you advance screens by flying up. + game.lives = game.fareTarget; game.levelIndex = 0; if (!loadLevelByIndex(&game, 0)) { jlLogF("spacetaxi: ! cannot load level 0 (DATA/levels/level01.dat)"); @@ -182,15 +239,24 @@ int main(void) { goto shutdown; } stEngineReset(&game); - // C64 goes straight from title to gameplay; no - // intermediate "PRESS FIRE TO START" screen. game.state = ST_STATE_PLAYING; } break; } case ST_STATE_PLAYING: { - bool wasLanded = game.taxi.landed; + bool wasLanded; + // Transporter shrink-warp in progress: suspend input and + // physics, just advance the recede animation; when it + // finishes, deliver any "UP PLEASE" fare and change screens. + if (game.taxi.warpFrame > 0u) { + if (stEngineWarpTick(&game)) { + stPassengerTransporterExit(&game); + game.state = ST_STATE_LEVEL_DONE; + } + break; + } + wasLanded = game.taxi.landed; applyInput(&game); stEngineTick(&game); stPassengerTick(&game); @@ -199,6 +265,13 @@ int main(void) { if (!wasLanded && game.taxi.landed) { stAudioSfxLand(); } + // Flying up through the top-wall transporter exits the + // screen: begin the shrink-warp (which delivers a carried + // "UP PLEASE" fare and advances the screen when it ends). + if (game.taxi.crashTicks == 0u && + stEngineInTransporter(&game)) { + stEngineStartWarp(&game); + } break; } case ST_STATE_LEVEL_DONE: diff --git a/examples/spacetaxi/spacetaxi.h b/examples/spacetaxi/spacetaxi.h index 5ae4f1b..7bbb62b 100644 --- a/examples/spacetaxi/spacetaxi.h +++ b/examples/spacetaxi/spacetaxi.h @@ -39,6 +39,13 @@ #define ST_TILEMAP_H 25u #define ST_TILE_PIXELS 8u +// The tilemap stores raw C64 screen-RAM character codes. The empty +// (non-colliding) playfield cell is the space character, $20. Every +// other code is non-blank background -- on the C64 it latches a sprite- +// background collision ($D01F). romToLevel.py uses BG=0x20 as the empty +// marker when extracting screen RAM. +#define ST_TILE_EMPTY 0x20u + // Display field is 320x200 (JoeyLib's SURFACE_WIDTH x SURFACE_HEIGHT). // 40 tiles x 8 px = 320, 25 tiles x 8 px = 200. The bottom 3 rows are // the HUD band (score / lives / level / current-fare strip). The top @@ -53,10 +60,28 @@ // Maximum stuff. Tuned for fitting the smallest target (IIgs): // 10 pads is the highest count in any canonical Space Taxi level // (D and M each have 10 pads; everything else <= 9) -// 2 active passenger sprites covers waiting + carrying +// Exactly ONE fare exists at a time. Verified: the C64 spawns a new +// fare only while its state machine (gDeathStage) is idle, so +// waiting-vs-carrying is the single passenger's `onboard` flag -- NOT +// two passengers. (A prior "waiting + carrying = 2 sprites" note was +// a misconception / false second source of truth.) #define ST_MAX_PADS 10u -#define ST_MAX_PASSENGERS 2u -#define ST_MAX_FARES 16u +#define ST_MAX_PASSENGERS 1u + +// Passenger destination sentinel: "UP PLEASE!" -- the fare wants to be +// flown UP through the top-wall transporter opening rather than to a +// landing pad. This is the C64's $0B destination ($6CB8), used on +// single-pad screens (and when all pads are consumed). Any real pad +// index is 0..ST_MAX_PADS-1, so 0xFE can't collide. +#define ST_DEST_TRANSPORTER 0xFEu + +// The transporter opening is a fixed 4-column gap ($67 tiles) in the top +// wall, at columns 18..21 in every canonical Space Taxi screen (verified +// across the extracted tilemaps). Flying the taxi up through it exits the +// screen. +#define ST_TRANSPORTER_COL_LO 18u +#define ST_TRANSPORTER_COL_HI 22u // exclusive +#define ST_TILE_TRANSPORTER 0x67u // Fixed-point taxi physics: position and velocity are int16_t in // units of 1/16 px, so the taxi can drift fractionally and the @@ -84,6 +109,9 @@ typedef enum { ST_STATE_TITLE = 0, + ST_STATE_HIGHSCORES, // UP on title -> "THE IMMORTAL CABBIES" ($4C1F) + ST_STATE_INSTRUCTIONS, // DOWN on title -> author/about screen ($556A) + ST_STATE_OPTIONS, // "GAME VARIATIONS" menu ($5295): pick cabbie count ST_STATE_PLAYING, ST_STATE_LEVEL_DONE, ST_STATE_GAME_OVER @@ -97,19 +125,13 @@ typedef enum { typedef struct { - uint8_t letter; // 'A'..'H' identifier (which pad number) uint8_t tileX; // landing surface left edge (tile coord) uint8_t tileY; // landing surface row (tile coord) uint8_t tileW; // pad width in tiles + uint8_t standX; // passenger stand X (screen px; slot byte6-24) } StPadT; -typedef struct { - uint8_t spawnPad; // pad index where they appear - uint8_t destPad; // pad index they want to go to -} StFareT; - - typedef struct { char name[24]; // level display name ("UP & DOWN", etc.) uint8_t tileBankId; // which tile asset (0 = default bank) @@ -150,8 +172,6 @@ typedef struct { uint8_t sprite1Color; // $D028 sprite 1 (flame) uint8_t padCount; StPadT pads[ST_MAX_PADS]; - uint8_t fareCount; - StFareT fares[ST_MAX_FARES]; // Tilemap: tile-index per cell (row-major). uint8_t tilemap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS]; // Palette slot per cell -- which surface palette index a cell uses. @@ -180,6 +200,10 @@ typedef struct { // branch"). Reaches 0 -> respawn (or game-over). Mirrors the // C64's $6B24/$6B4C phase-2/3 timing. uint8_t crashTicks; + // Transporter shrink-warp progress (0 = not warping; 1..64 = the + // takeoff/shrink animation $5C91/$5CB9). During the warp the taxi + // recedes up through the opening; the screen advances when it ends. + uint8_t warpFrame; } StTaxiT; @@ -203,10 +227,10 @@ typedef struct { uint32_t score; uint8_t lives; uint8_t levelIndex; - // Player-selectable fare target (1..4). Mirrors C64 $7213, set on - // the title screen via LEFT/RIGHT joystick (see $5310-$5328 in - // MECHANICS.md). Used to cap the per-level fare count: the engine - // delivers min(level.fareCount, game.fareTarget) before advancing. + // Player-selectable fare quota (1..4). Mirrors C64 $7213 (the + // GAME-VARIATIONS "cabbies" count), set on the title screen via + // LEFT/RIGHT joystick ($5310-$5328). This is the GLOBAL number of + // fares to deliver before the game completes -- NOT per-level. uint8_t fareTarget; } StGameT; @@ -232,9 +256,21 @@ void stRenderDrawText(jlSurfaceT *stage, uint8_t bx, uint8_t by, const char *s); void stEngineReset(StGameT *game); void stEngineTick(StGameT *game); +// True when the taxi has flown up into the top-wall transporter opening +// (used to trigger a screen exit). +bool stEngineInTransporter(const StGameT *game); +// Begin the transporter shrink-warp (taxi.warpFrame 0 -> 1). +void stEngineStartWarp(StGameT *game); +// Advance the shrink-warp one frame; returns true when it completes. +bool stEngineWarpTick(StGameT *game); +// Current shrink step 0..7 (0 = full size, 7 = smallest) for rendering. +uint8_t stEngineWarpStep(const StGameT *game); void stPassengerReset(StGameT *game); void stPassengerTick(StGameT *game); +// Called when the taxi exits up through the transporter: delivers a +// carried "UP PLEASE" fare (if any) before the screen advances. +void stPassengerTransporterExit(StGameT *game); void stAudioInit(void); void stAudioShutdown(void); diff --git a/examples/spacetaxi/stEngine.c b/examples/spacetaxi/stEngine.c index 56273a6..2191f7e 100644 --- a/examples/spacetaxi/stEngine.c +++ b/examples/spacetaxi/stEngine.c @@ -86,12 +86,22 @@ static bool isSolidAt(const StLevelT *level, int16_t px, int16_t py) { return true; // top/bottom out-of-field treated as walls } tile = level->tilemap[ty * ST_TILEMAP_W + tx]; - // Tile-index convention (authored by the level designer): - // 0 = empty space - // 1..63 = solid (walls, platforms, structure) - // 64..127 = landing-pad surfaces (also solid for collision) - // 128+ = decorative non-solid (lights, signs, etc.) - return tile != 0u && tile < 128u; + // The tilemap holds raw C64 screen-RAM character codes (romToLevel.py + // copies screen RAM directly; the empty cell is the space character + // $20, BG=0x20 in the extractor -- confirmed by the .dat histogram + // where char 32 dominates the tilemap). On the C64 a sprite-vs- + // background collision ($D01F) latches on ANY non-blank background + // cell the cab overlaps, so a cell is solid iff its char code is not + // the space character. (The earlier "1..63 solid / 64..127 pad / + // 128+ decorative" convention was invented and is wrong against the + // real data -- it treated char 32, the empty playfield, as solid, + // which crashes the cab the instant it descends into open air.) + // + // The transporter opening ($67, the 4-tile top-wall gap) is the one + // non-space tile the cab may pass through: it's the screen exit. It + // only ever occurs at row 0 cols 18..21, so treating it as passable + // everywhere is safe. + return tile != ST_TILE_EMPTY && tile != ST_TILE_TRANSPORTER; } @@ -297,7 +307,15 @@ void stEngineTick(StGameT *game) { if (t->vy > 0) { if (isSolidAt(L, centerX, feetY)) { - if (onLandingPad(L, centerX, feetY, &landingPad)) { + // The C64 pad detector ($645C) only registers a landing on a + // SLOW descent: it gates on the Y-velocity high byte being + // zero (vy < 1 px/frame). A fast descent fails the gate, so + // the pad surface acts as solid background and the cab + // crashes into it -- this is the "land gently or die" + // mechanic. In the port's 8-bit sub-pixel scale that high- + // byte-zero test is vy < ST_SUBPIXEL (256). + bool slowDescent = (t->vy < ST_SUBPIXEL); + if (slowDescent && onLandingPad(L, centerX, feetY, &landingPad)) { ny = (int32_t)(feetY / ST_TILE_PIXELS) * ST_TILE_PIXELS * ST_SUBPIXEL - (int32_t)ST_TAXI_H_PX * ST_SUBPIXEL; t->vx = 0; @@ -337,3 +355,81 @@ void stEngineTick(StGameT *game) { t->y = ny; reflectAtEdges(t); } + + +bool stEngineInTransporter(const StGameT *game) { + const StTaxiT *t = &game->taxi; + int16_t topY; + int16_t centerCol; + + // The cab has exited when its top has risen into row 0 while its + // center sits within the transporter columns (18..21). The opening + // tile ($67) is passable (see isSolidAt), so the cab can ascend the + // empty channel and reach row 0 only through the gap -- the walls + // stop it everywhere else. + topY = (int16_t)(t->y >> ST_SUBPIXEL_SHIFT); + centerCol = (int16_t)(((t->x >> ST_SUBPIXEL_SHIFT) + ST_TAXI_W_PX / 2) + / ST_TILE_PIXELS); + // Fire once the cab has risen into the opening (top within rows 0-1). + // The walls block every column but the 18-21 gap, so reaching here at + // all means the cab is exiting through the transporter. + return topY < (int16_t)(2 * ST_TILE_PIXELS) && + centerCol >= (int16_t)ST_TRANSPORTER_COL_LO && + centerCol < (int16_t)ST_TRANSPORTER_COL_HI; +} + + +// Transporter shrink-warp ($5C91 takeoff + $5CB9 pad2pad). The C64 walks +// the cab sprite through shrink cels $E2..$E9 (8 cels / 7 steps) while +// nudging it +1px right / -1px up per step, with a cadence that slows as +// it recedes: 4 frames/step for the first steps, 16 frames/step for the +// last. Step boundaries (cumulative frames): 4,8,12,16,32,48,64. Total 64 +// frames (~1.1s). The port has no shrink cels, so the renderer scales a +// block by stEngineWarpStep instead. +#define ST_WARP_TOTAL_FRAMES 64u + +static const uint8_t kWarpStepFrame[8] = { 0u, 4u, 8u, 12u, 16u, 32u, 48u, 64u }; + + +uint8_t stEngineWarpStep(const StGameT *game) { + uint8_t frame = game->taxi.warpFrame; + uint8_t step = 0u; + uint8_t k; + + for (k = 0u; k < 8u; k++) { + if (frame >= kWarpStepFrame[k]) { + step = k; + } + } + return step; +} + + +void stEngineStartWarp(StGameT *game) { + game->taxi.warpFrame = 1u; + game->taxi.vx = 0; + game->taxi.vy = 0; + game->taxi.thrusting = false; + stAudioSfxCrash(); // the C64 sets voice-3 noise ($81) on warp start +} + + +bool stEngineWarpTick(StGameT *game) { + StTaxiT *t = &game->taxi; + uint8_t before; + uint8_t after; + + before = stEngineWarpStep(game); + t->warpFrame++; + after = stEngineWarpStep(game); + // Nudge +1px right / -1px up on each step boundary (net +7/-7). + if (after != before) { + t->x += (int32_t)ST_SUBPIXEL; + t->y -= (int32_t)ST_SUBPIXEL; + } + if (t->warpFrame >= (uint8_t)ST_WARP_TOTAL_FRAMES) { + t->warpFrame = 0u; + return true; + } + return false; +} diff --git a/examples/spacetaxi/stHud.c b/examples/spacetaxi/stHud.c index b47259c..529e1bd 100644 --- a/examples/spacetaxi/stHud.c +++ b/examples/spacetaxi/stHud.c @@ -54,25 +54,25 @@ void stHudDraw(jlSurfaceT *stage, const StGameT *game) { stRenderDrawText(stage, col, (uint8_t)ST_HUD_ROW, game->level.name); } - // Active fare display: where the current passenger wants to go - // (or "PAD x WANTS y" when one is still waiting to be picked up). + // Active-fare message, matching the C64 HUD ($6C1E "PAD PLEASE", + // $6C2B " UP PLEASE!"): a waiting fare hails ("HEY TAXI!"); once + // aboard it announces its destination by pad NUMBER (index+1, per + // $671E which draws the destination index + '0'), or "UP PLEASE!" + // when the exit is the transporter. for (i = 0u; i < ST_MAX_PASSENGERS; i++) { const StPassengerT *p = &game->passengers[i]; - if (p->active && p->onboard && - p->destPad < game->level.padCount) { - char dest = (char)game->level.pads[p->destPad].letter; - snprintf(buf, sizeof(buf), "FARE %c", dest); - stRenderDrawText(stage, 0u, (uint8_t)(ST_HUD_ROW + 1u), buf); - break; + if (!p->active) { + continue; } - if (p->active && !p->onboard && - p->currentPad < game->level.padCount) { - char src = (char)game->level.pads[p->currentPad].letter; - char dest = (p->destPad < game->level.padCount) - ? (char)game->level.pads[p->destPad].letter : '?'; - snprintf(buf, sizeof(buf), "PAD %c WANTS %c", src, dest); - stRenderDrawText(stage, 0u, (uint8_t)(ST_HUD_ROW + 1u), buf); - break; + if (!p->onboard) { + snprintf(buf, sizeof(buf), "HEY TAXI!"); + } else if (p->destPad == ST_DEST_TRANSPORTER) { + snprintf(buf, sizeof(buf), "UP PLEASE!"); + } else { + snprintf(buf, sizeof(buf), "PAD %u PLEASE!", + (unsigned)(p->destPad + 1u)); } + stRenderDrawText(stage, 0u, (uint8_t)(ST_HUD_ROW + 1u), buf); + break; } } diff --git a/examples/spacetaxi/stLevel.c b/examples/spacetaxi/stLevel.c index c496ad6..84a6666 100644 --- a/examples/spacetaxi/stLevel.c +++ b/examples/spacetaxi/stLevel.c @@ -14,13 +14,13 @@ #include "spacetaxi.h" -// STL2 adds the per-level physics templates (xAccel/yAccel/xGrav/yGrav) -// and the full VIC color block ($7D00-$7D08). Drops the patience byte -// from each fare entry (Space Taxi proper has no patience timeout). +// STL3: per-pad record is (tileX, tileY, tileW, standX) where standX is the +// passenger stand column in screen pixels; the STL2 per-pad letter and the +// whole fare list are gone (fares are RNG-chosen at runtime, not stored). #define ST_LEVEL_MAGIC0 'S' #define ST_LEVEL_MAGIC1 'T' #define ST_LEVEL_MAGIC2 'L' -#define ST_LEVEL_MAGIC3 '2' +#define ST_LEVEL_MAGIC3 '3' static bool readByte(FILE *fp, uint8_t *out) { @@ -108,22 +108,10 @@ bool stLevelLoad(StLevelT *out, const char *path) { return false; } for (i = 0u; i < out->padCount; i++) { - if (!readByte(fp, &out->pads[i].letter) || - !readByte(fp, &out->pads[i].tileX) || + if (!readByte(fp, &out->pads[i].tileX) || !readByte(fp, &out->pads[i].tileY) || - !readByte(fp, &out->pads[i].tileW)) { - fclose(fp); - return false; - } - } - - if (!readByte(fp, &out->fareCount) || out->fareCount > ST_MAX_FARES) { - fclose(fp); - return false; - } - for (i = 0u; i < out->fareCount; i++) { - if (!readByte(fp, &out->fares[i].spawnPad) || - !readByte(fp, &out->fares[i].destPad)) { + !readByte(fp, &out->pads[i].tileW) || + !readByte(fp, &out->pads[i].standX)) { fclose(fp); return false; } diff --git a/examples/spacetaxi/stPassenger.c b/examples/spacetaxi/stPassenger.c index e6f167d..fa94900 100644 --- a/examples/spacetaxi/stPassenger.c +++ b/examples/spacetaxi/stPassenger.c @@ -1,20 +1,26 @@ -// Space Taxi -- passenger AI. +// Space Taxi -- passenger AI (dynamic fare model). // -// A passenger has three states: -// 1. Waiting on a pad (active, !onboard) - cycles walk animation. -// Boards when the taxi lands on their pad. -// 2. In the cab (active, onboard) - invisible; followed by -// the taxi until it touches the destination pad. -// 3. Done (!active) - waiting to be replaced -// by the next fare slot. +// The C64 game has NO static per-level fare list (verified: the prior +// port's "circular A->B->..->A" fares were fabricated). Both the SPAWN +// pad and the DELIVERY destination are chosen at RUNTIME by RNG: // -// When a passenger lands at their destination, the next fare in the -// level's fareCount is spawned. When the level's fares are exhausted, -// the level is done. +// * padHoverSetup ($6537) picks a random free pad in [1..padCount] for +// the passenger to appear on, and again for where they want to go. +// * When all pads are consumed (single-pad screens, notably level A), +// the destination becomes the "UP PLEASE!" sentinel ($0B, $6CB8): +// the fare is delivered by flying UP through the top-wall +// transporter opening. +// * "PAD n PLEASE" is drawn from the runtime destination index+'0' +// ($671E: gActiveSpriteIdx + $30); pads are numeric, not lettered. +// +// This port keeps ONE active fare at a time (spawn -> board -> deliver -> +// spawn next), a faithful reduction of the C64's multi-slot queue. The +// RNG here is a small port-side LCG (the exact C64 RNG sequence isn't +// observable and depends on live seed state); it only needs to pick a +// plausible free pad each fare. // // Scoring is flat-rate per the C64 original ($43B9 BCD blob added at -// success-stage 6 / $6742): 500 points per delivered fare. The earlier -// "tip by patience" formula was a port-side invention. +// success-stage 6 / $6742): a delivered fare adds ST_FARE_SCORE. #include @@ -27,190 +33,209 @@ #define ST_WALK_FRAMES 4u #define ST_WALK_TICKS_PER_CEL 6u -// Verified via emulator trace of $4354 (BCD-add) with the $43B9 blob -// against an all-blank HUD: the only HUD position modified is index 3 -// (the ones-digit of the integer part in the C64's DDDD.DD layout), -// where digit '5' gets added. So a basic fare delivered = +5 score. -// See stuff/spacetaxi/trace.py and the run output for proof. -// -// Two other blobs exist but their callers haven't been traced: -// $43C1 = +95 (called from $5D03 after a per-passenger counter -// decrement -- looks like a bonus event) -// $43C9 = +50 (called from $5C82 in the $5BBB-dispatched anim -- -// possibly a "fare-on-takeoff" or "tip" bonus) +// Verified via emulator trace of $4354 (BCD-add) with the $43B9 blob: +// a basic delivered fare adds '5' at the ones digit of the DDDD.DD HUD +// score. See stuff/spacetaxi/trace.py. #define ST_FARE_SCORE 5u +// Spawn cadence (C64 idle ticker $660B): while no fare is active, each +// frame rolls rand%100 and spawns on roll < 3, else counts a 100-frame +// cap down to a forced spawn -- a fresh fare appears within ~1-2 seconds. +#define ST_SPAWN_ROLL_THRESH 3u +#define ST_SPAWN_CAP_FRAMES 100 -// Per-port passenger queue cursor. Reset by stPassengerReset whenever a -// new level (or restart-after-game-over) needs to begin from fare 0. -static uint8_t gNextFareIdx; + +static uint32_t gRng; static uint8_t gWalkTick; +// Frames until the next fare is forced to spawn (mirrors gDeathStage0Rng). +static int16_t gSpawnCountdown; +// Pad deliveries completed on the current screen. Destination pads are +// "used up" as fares are delivered (C64 gSpriteSlots/gFareSlotCount); once +// only the last pad would remain free, the next fare wants "UP PLEASE!" +// (fly up to the next screen), which is what makes each screen finite. +static uint8_t gDeliveredThisScreen; -static uint8_t effectiveFareTarget(const StGameT *game); -static void spawnNextFare(StGameT *game); +static void deliverFare(StGameT *game, StPassengerT *p); +static void pickDestination(StGameT *game, StPassengerT *p); +static void placeOnPad(StPassengerT *p, const StLevelT *level, uint8_t padIdx); +static uint8_t rngPad(uint8_t n); +static void spawnPassenger(StGameT *game); +static void walkPassenger(StGameT *game, StPassengerT *p); -// Apply the title-screen fare-count selector ($7213 in the C64) as an -// upper cap on this level's fare count. The level data sets the -// maximum (e.g. 8 for a long delivery chain); player picks 1..4 on -// the title to control session length. The lesser of the two wins. -static uint8_t effectiveFareTarget(const StGameT *game) { - uint8_t levelMax = game->level.fareCount; - uint8_t sel = game->fareTarget; - if (sel == 0u) { - return levelMax; - } - return (levelMax < sel) ? levelMax : sel; +static void deliverFare(StGameT *game, StPassengerT *p) { + game->score += ST_FARE_SCORE; + stAudioSfxDropoff(); + p->active = false; + gDeliveredThisScreen++; + // The next fare appears after the spawn cadence (see stPassengerTick), + // not instantly. + gSpawnCountdown = ST_SPAWN_CAP_FRAMES; } -static void spawnNextFare(StGameT *game) { - const StFareT *fare; - const StPadT *pad; - StPassengerT *slot; - uint8_t i; +// On boarding, choose where the fare wants to go. Destination pads get +// used up as the screen's fares are delivered: once delivering would leave +// no fresh pad (single-pad screens, or after padCount-1 deliveries), the +// fare wants "UP PLEASE!" -- the player flies up to the next screen. +static void pickDestination(StGameT *game, StPassengerT *p) { + uint8_t pc = game->level.padCount; + uint8_t d; - if (gNextFareIdx >= effectiveFareTarget(game)) { + if (pc <= 1u || gDeliveredThisScreen >= (uint8_t)(pc - 1u)) { + p->destPad = ST_DEST_TRANSPORTER; return; } - // Find an idle slot. - slot = NULL; - for (i = 0u; i < ST_MAX_PASSENGERS; i++) { - if (!game->passengers[i].active) { - slot = &game->passengers[i]; - break; + d = rngPad(pc); + if (d == p->currentPad) { + d = (uint8_t)((d + 1u) % pc); + } + p->destPad = d; +} + + +// Stand the passenger at the pad's real stand column (pad->standX, the +// C64 slot byte6 = gPassenger1Col). pad->tileY is the contact-surface +// row; the fare's feet sit on it, so the sprite top-left is two tiles up. +static void placeOnPad(StPassengerT *p, const StLevelT *level, uint8_t padIdx) { + const StPadT *pad = &level->pads[padIdx]; + + p->currentPad = padIdx; + p->x = (int16_t)pad->standX; + p->y = (int16_t)((pad->tileY - ST_PASSENGER_H_TILES) * ST_TILE_PIXELS); +} + + +// Small LCG -> uniform pad index in [0, n-1]. n==0 guarded to 0. +static uint8_t rngPad(uint8_t n) { + if (n == 0u) { + return 0u; + } + gRng = gRng * 1103515245u + 12345u; + return (uint8_t)(((gRng >> 16) & 0x7FFFu) % n); +} + + +static void spawnPassenger(StGameT *game) { + StPassengerT *p = &game->passengers[0]; + uint8_t padIdx; + + if (game->level.padCount == 0u) { + return; + } + padIdx = rngPad(game->level.padCount); + + p->active = true; + p->onboard = false; + p->walkPhase = 0u; + p->walkDir = 0u; + p->destPad = 0u; + placeOnPad(p, &game->level, padIdx); +} + + +// Idle back-and-forth walk across the pad while waiting to be picked up. +// The C64 passenger AI wasn't fully traced; this gives each waiting fare +// some motion instead of standing still. +static void walkPassenger(StGameT *game, StPassengerT *p) { + const StPadT *pad; + int16_t padXMin; + int16_t padXMax; + + p->walkPhase = (uint8_t)((p->walkPhase + 1u) % ST_WALK_FRAMES); + if (p->currentPad >= game->level.padCount) { + return; + } + pad = &game->level.pads[p->currentPad]; + padXMin = (int16_t)(pad->tileX * ST_TILE_PIXELS); + padXMax = (int16_t)((pad->tileX + pad->tileW) * ST_TILE_PIXELS - ST_PASSENGER_W_PX); + if (padXMax <= padXMin) { + return; // pad too narrow to walk on + } + if (p->walkDir == 0u) { + p->x++; + if (p->x >= padXMax) { + p->x = padXMax; + p->walkDir = 1u; + } + } else { + p->x--; + if (p->x <= padXMin) { + p->x = padXMin; + p->walkDir = 0u; } } - if (slot == NULL) { - return; - } - fare = &game->level.fares[gNextFareIdx++]; - if (fare->spawnPad >= game->level.padCount) { - return; - } - pad = &game->level.pads[fare->spawnPad]; - - slot->active = true; - slot->onboard = false; - slot->currentPad = fare->spawnPad; - slot->destPad = fare->destPad; - slot->walkPhase = 0u; - slot->walkDir = 0u; - slot->x = (int16_t)(pad->tileX * ST_TILE_PIXELS); - // Pad's tileY is the landing-surface row (top of the pad block). - // Place the passenger so their feet sit on that surface -- - // top-left Y is (tileY - passengerHeightInTiles) * tilePixels. - slot->y = (int16_t)((pad->tileY - ST_PASSENGER_H_TILES) * ST_TILE_PIXELS); } void stPassengerReset(StGameT *game) { uint8_t i; - gNextFareIdx = 0u; - gWalkTick = 0u; + gWalkTick = 0u; + gDeliveredThisScreen = 0u; + gSpawnCountdown = ST_SPAWN_CAP_FRAMES; + // Deterministic per-screen seed: varies the pad sequence level to + // level without needing a wall-clock source. + gRng = 0x2545F491u ^ ((uint32_t)game->levelIndex * 2654435761u); for (i = 0u; i < ST_MAX_PASSENGERS; i++) { game->passengers[i].active = false; } - // Seed the first fare for the level. Subsequent fares spawn on - // successful drop-off via spawnNextFare inside stPassengerTick. - spawnNextFare(game); + // The screen starts empty; the first fare appears after the spawn + // cadence in stPassengerTick. } void stPassengerTick(StGameT *game) { - StPassengerT *p; + StPassengerT *p = &game->passengers[0]; StTaxiT *t = &game->taxi; - uint8_t i; - bool atDest; - // Walk-cel cycling: advance every ST_WALK_TICKS_PER_CEL host frames - // for all waiting passengers in lockstep. Also step horizontally - // one pixel per cel advance so passengers walk back-and-forth - // across their pad. The C64 game's passenger AI was never fully - // traced -- this is a simplification giving each waiting fare - // some idle motion instead of standing in place. - if (++gWalkTick >= ST_WALK_TICKS_PER_CEL) { - gWalkTick = 0u; - for (i = 0u; i < ST_MAX_PASSENGERS; i++) { - StPassengerT *p = &game->passengers[i]; - const StPadT *pad; - int16_t padXMin; - int16_t padXMax; - if (!p->active || p->onboard) { - continue; + if (!p->active) { + // Idle: a fresh fare appears after the C64 spawn cadence. Never + // spawn mid-crash (the C64 gates the idle ticker on + // gCollisionPhase == 0). + if (t->crashTicks == 0u) { + if (gSpawnCountdown > 0) { + gSpawnCountdown--; } - p->walkPhase = (uint8_t)((p->walkPhase + 1u) % ST_WALK_FRAMES); - if (p->currentPad >= game->level.padCount) { - continue; - } - pad = &game->level.pads[p->currentPad]; - padXMin = (int16_t)(pad->tileX * ST_TILE_PIXELS); - padXMax = (int16_t)((pad->tileX + pad->tileW) * ST_TILE_PIXELS - - ST_PASSENGER_W_PX); - if (padXMax <= padXMin) { - continue; // pad too narrow to walk on - } - if (p->walkDir == 0u) { - p->x++; - if (p->x >= padXMax) { - p->x = padXMax; - p->walkDir = 1u; - } - } else { - p->x--; - if (p->x <= padXMin) { - p->x = padXMin; - p->walkDir = 0u; - } + if (rngPad(100u) < ST_SPAWN_ROLL_THRESH || gSpawnCountdown <= 0) { + spawnPassenger(game); } } + return; } - for (i = 0u; i < ST_MAX_PASSENGERS; i++) { - p = &game->passengers[i]; - if (!p->active) { - continue; + if (!p->onboard) { + // Walk in place while waiting. + if (++gWalkTick >= ST_WALK_TICKS_PER_CEL) { + gWalkTick = 0u; + walkPassenger(game, p); } - - if (!p->onboard) { - // Board if the taxi has landed on their pad. - if (t->landed && t->onPad == p->currentPad) { - p->onboard = true; - stAudioSfxPickup(); - } - } else { - // Onboard: deliver to destination pad. - if (p->destPad >= game->level.padCount) { - p->active = false; - continue; - } - atDest = t->landed && - t->onPad < game->level.padCount && - t->onPad == p->destPad; - if (atDest) { - game->score += ST_FARE_SCORE; - stAudioSfxDropoff(); - p->active = false; - spawnNextFare(game); - } + // Board when the taxi has landed on their pad. + if (t->landed && t->onPad == p->currentPad) { + p->onboard = true; + pickDestination(game, p); + stAudioSfxPickup(); } - } - - // If every fare is exhausted AND no active passenger remains, - // the level is complete. - if (gNextFareIdx >= effectiveFareTarget(game)) { - bool anyActive = false; - for (i = 0u; i < ST_MAX_PASSENGERS; i++) { - if (game->passengers[i].active) { - anyActive = true; - break; - } - } - if (!anyActive) { - game->state = ST_STATE_LEVEL_DONE; + } else if (p->destPad != ST_DEST_TRANSPORTER) { + // Deliver by landing on the destination pad. (UP-PLEASE fares are + // delivered on the transporter fly-up; see stPassengerTransporterExit.) + if (t->landed && + t->onPad < game->level.padCount && + t->onPad == p->destPad) { + deliverFare(game, p); } } } + + +void stPassengerTransporterExit(StGameT *game) { + StPassengerT *p = &game->passengers[0]; + + if (p->active && p->onboard && p->destPad == ST_DEST_TRANSPORTER) { + game->score += ST_FARE_SCORE; + stAudioSfxDropoff(); + p->active = false; + } +} diff --git a/examples/spacetaxi/stRender.c b/examples/spacetaxi/stRender.c index 8bad0c0..c0fd1aa 100644 --- a/examples/spacetaxi/stRender.c +++ b/examples/spacetaxi/stRender.c @@ -70,6 +70,11 @@ #define ST_TAXI_W_PX 24 #define ST_TAXI_H_PX 24 #define ST_TAXI_CEL_COUNT 4 +// Transporter shrink-warp cels: the cab receding, in the free row-0 slots +// right after the 4 drive cels. The C64 walks 8 shrink cels ($E2..$E9); +// the port approximates with 5 sizes and maps the 8 warp steps onto them. +#define ST_SPRITE_WARP_FIRST (ST_SPRITE_TAXI_FIRST + ST_TAXI_CEL_COUNT) +#define ST_WARP_CEL_COUNT 5 // Flame placeholder fallback (only used if real sprite-2 cel is // missing): a small bright rectangle below the cab while thrusting. @@ -111,6 +116,7 @@ typedef struct { uint8_t currentBankId; // which tbank%u.tbk is loaded (0xFF = none) bool bankLoaded; jlSpriteT *taxiCels[ST_TAXI_CEL_COUNT]; + jlSpriteT *warpCels[ST_WARP_CEL_COUNT]; jlSpriteT *flameCels[ST_FLAME_CEL_COUNT]; jlSpriteBackupT flameBackup; uint8_t flameBackupMem[ST_SPRITE_BACKUP_BYTES]; @@ -268,8 +274,20 @@ typedef struct { int32_t demoY; int16_t demoVx; int16_t demoVy; + // Logo flip-book + color cycle. The "SPACE TAXI" logo is 103 cells + // of char $84; the C64 ($4827) copies one of the flip frames + // $85..$88 over char $84's bitmap every 4th frame, walking the + // ping-pong table $48A7 = {1,2,3,4,3,2} so each dot rotates end over + // end, and advances the 8-color cycle $489C when the index hits 4. + uint8_t logoFlipDiv; // mirrors $48A6 (mod-4 frame divider) + uint8_t logoFlipIdx; // mirrors $48A5 (0..5 ping-pong index) + uint8_t logoColorIdx; // mirrors $48A4 (0..7 color cycle index) } StTitleStateT; +// The single animated logo character. Its bitmap is flip-booked through +// the four frames that follow it in the charset ($85..$88). +#define ST_LOGO_CHAR 0x84u + static StTitleStateT gTitle; static void titleReset(void); @@ -464,6 +482,63 @@ static void titleReset(void) { gTitle.scriptTimer = kTitleDemoScript[1]; gTitle.demoX = (int32_t)ST_TITLE_CAB_SX_INIT * ST_SUBPIXEL; gTitle.demoY = (int32_t)ST_TITLE_CAB_SY_INIT * ST_SUBPIXEL; + // Force the first frame to be an animation step (3 -> 0 on first ++) + // so the logo paints its first flip frame + color immediately. + gTitle.logoFlipDiv = 3u; +} + + +// $4827 logo flip-book + $4861 color cycle. The title's "SPACE TAXI" +// logo is 103 cells of char $84 (verified in the title dump). Every 4th +// frame ($4827: INC $48A6 / AND #$03 / BEQ) the C64 copies one flip +// frame ($85..$88) over char $84's bitmap, walking the ping-pong table +// $48A7 = {1,2,3,4,3,2} -> frames $85,$86,$87,$88,$87,$86 so each dot +// rotates end over end; when the index reaches 4 it advances the 8-entry +// color cycle $489C = {red,orange,yellow,green,blue,lt-blue,cyan,purple} +// (C64 codes, which are this port's palette indices). We reproduce it by +// re-pasting every $84 cell with the current flip-frame tile + color. +static void titleAnimateLogo(jlSurfaceT *stage, const StLevelT *level) { + static const uint8_t kFlip[6] = { 1u, 2u, 3u, 4u, 3u, 2u }; // $48A7 + static const uint8_t kColor[8] = { 0x02u, 0x08u, 0x07u, 0x05u, + 0x06u, 0x0Eu, 0x03u, 0x04u }; // $489C + const jlTileT *tile; + uint8_t ch; + uint8_t fg; + uint8_t bx; + uint8_t by; + size_t i; + + // $4827: only step the flip-book every 4th frame. + gTitle.logoFlipDiv = (uint8_t)((gTitle.logoFlipDiv + 1u) & 3u); + if (gTitle.logoFlipDiv != 0u) { + return; + } + + // Paint the CURRENT flip frame + color (C64 displays table[$48A5] + // this frame, then advances), then repaint every $84 logo cell. + ch = (uint8_t)(ST_LOGO_CHAR + kFlip[gTitle.logoFlipIdx]); + fg = kColor[gTitle.logoColorIdx]; + tile = gRender.tileValid[ch] ? &gRender.tiles[ch] : NULL; + if (tile != NULL) { + for (by = 0u; by < ST_PLAYFIELD_ROWS; by++) { + for (bx = 0u; bx < ST_TILEMAP_W; bx++) { + i = (size_t)by * ST_TILEMAP_W + bx; + if (level->tilemap[i] == ST_LOGO_CHAR) { + jlTilePasteMono(stage, bx, by, tile, fg, level->bgColor); + } + } + } + } + + // $484D INC $48A5 + $4853 gate: wrap the ping-pong index at 6, and + // advance the color cycle when it reaches 4. + gTitle.logoFlipIdx++; + if (gTitle.logoFlipIdx == 6u) { + gTitle.logoFlipIdx = 0u; + } + if (gTitle.logoFlipIdx == 4u) { + gTitle.logoColorIdx = (uint8_t)((gTitle.logoColorIdx + 1u) & 7u); + } } @@ -671,9 +746,10 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) { char buf[32]; // Title screen: render the title tilemap (extracted from the C64 - // game's screen RAM at $0400). The "SPACE TAXI" logo stays at - // its captured color -- the C64 ran a rainbow cycle here, but - // we keep it static so the per-frame logo repaint cost is zero. + // game's screen RAM at $0400). The "SPACE TAXI" logo is animated by + // titleAnimateLogo below -- the C64 flip-books char $84's bitmap + // ($4827) so each dot rotates end over end while an 8-color cycle + // ($4861) runs; both were missing before. if (game->state == ST_STATE_TITLE) { // Restore the area saved under the prev-frame sprites BEFORE // anything else paints. Without this, jlSpriteDraw leaves the @@ -701,6 +777,10 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) { // (e.g., transitioned from gameplay back to title) -- otherwise // it's a fast no-op. stRenderLevel(stage, &game->level); + // Flip-book + color-cycle the "SPACE TAXI" logo (char $84 cells) + // over the freshly-painted tilemap, BEFORE the demo sprites + // save-under and draw, so the sprites overlay the live logo. + titleAnimateLogo(stage, &game->level); // No "fares per game" overlay on the title: the C64 has a // separate options/menu scene driven by $5295 (header text) // and $52E1 / $533C (the "1 2 3 4" digit row at row 2 col 28 @@ -775,6 +855,68 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) { jlStagePresent(); return; } + if (game->state == ST_STATE_HIGHSCORES) { + // "THE IMMORTAL CABBIES" high-score screen ($4C1F). Header + + // the game's default 8-entry table, transcribed from $4A89 + // (score string + 15-char name). Persistent scoring isn't wired + // up, so we show the original default table. + static const char *kHighScores[8] = { + "3877.56 MICHAEL PLATE", + "1809.51 MICHAEL PLATE", + "1179.87 ANDREAS PLATE", + " 892.65 MICHAEL PLATE", + " 685.37 ANDREAS PLATE", + " 629.45 MICHAEL PLATE", + " 569.50 MICHAEL PLATE", + " 504.97 ANDREAS PLATE", + }; + uint8_t i; + jlSurfaceClear(stage, 0u); + stRenderDrawText(stage, 10u, 1u, "THE IMMORTAL CABBIES"); + for (i = 0u; i < 8u; i++) { + stRenderDrawText(stage, 9u, (uint8_t)(4u + i * 2u), kHighScores[i]); + } + stRenderDrawText(stage, 10u, 22u, "PRESS FIRE TO RETURN"); + jlWaitVBL(); + jlStagePresent(); + return; + } + if (game->state == ST_STATE_INSTRUCTIONS) { + // Author/about screen ($556A, reached via DOWN). Text transcribed + // verbatim from $56D4-$57A6. + jlSurfaceClear(stage, 0u); + stRenderDrawText(stage, 1u, 3u, "THIS PROGRAM DEVELOPED AND WRITTEN BY"); + stRenderDrawText(stage, 12u, 5u, "JOHN F. KUTCHER"); + stRenderDrawText(stage, 15u, 7u, "11/21/65"); + stRenderDrawText(stage, 4u, 10u, "CURRENTLY, AS OF JANUARY 1984,"); + stRenderDrawText(stage, 5u, 12u, "HE IS ATTENDING JOHNS HOPKINS"); + stRenderDrawText(stage, 6u, 14u, "UNIVERSITY IN BALTIMORE, MD"); + stRenderDrawText(stage, 1u, 18u, "ALSO TRY RESCUE SQUAD BY JOHN KUTCHER"); + stRenderDrawText(stage, 10u, 22u, "PRESS FIRE TO RETURN"); + jlWaitVBL(); + jlStagePresent(); + return; + } + if (game->state == ST_STATE_OPTIONS) { + // "GAME VARIATIONS" menu ($5295). Text + positions transcribed + // from the C64: header (13,0), prompt (1,2), digits (28,2), + // instructions (5,4) and (19,5). The C64 recolors the selected + // digit via color RAM ($D86C + sel*2); we mark it with a + // highlight box behind the digit cell (row 2, col 28 + sel*2). + uint8_t sel = (uint8_t)((game->fareTarget - 1u) & 3u); // 0..3 + int16_t hx = (int16_t)((28 + (int)sel * 2) * (int)ST_TILE_PIXELS); + jlSurfaceClear(stage, 0u); + jlFillRect(stage, hx, (int16_t)(2 * ST_TILE_PIXELS), + (int16_t)ST_TILE_PIXELS, (int16_t)ST_TILE_PIXELS, 6u); + stRenderDrawText(stage, 13u, 0u, "GAME VARIATIONS"); + stRenderDrawText(stage, 1u, 2u, "SELECT NUMBER OF CABBIES:"); + stRenderDrawText(stage, 28u, 2u, "1 2 3 4"); + stRenderDrawText(stage, 5u, 4u, "USE JOYSTICK: LEFT OR RIGHT,"); + stRenderDrawText(stage, 19u, 5u, "FIRE TO SELECT"); + jlWaitVBL(); + jlStagePresent(); + return; + } if (game->state == ST_STATE_GAME_OVER) { jlSurfaceClear(stage, 0u); stRenderDrawText(stage, 15u, 8u, "GAME OVER"); @@ -828,7 +970,22 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) { if (gRender.taxiCels[taxiCel] == NULL) { taxiCel = 0u; } - if (gRender.taxiCels[taxiCel] != NULL) { + if (game->taxi.warpFrame > 0u) { + // Transporter shrink-warp: draw the cab-receding cel for this warp + // step (the C64 walks shrink cels $E2..$E9; the port has 5 sizes, + // one selected per step). Uses the taxi's save-under so it undraws + // cleanly next frame -- no tilemap repaint needed. + uint8_t step = stEngineWarpStep(game); /* 0..7 */ + uint8_t wcel = (uint8_t)((uint16_t)step * ST_WARP_CEL_COUNT / 8u); + if (wcel >= ST_WARP_CEL_COUNT) { + wcel = (uint8_t)(ST_WARP_CEL_COUNT - 1u); + } + if (gRender.warpCels[wcel] != NULL) { + jlSpriteSaveAndDraw(stage, gRender.warpCels[wcel], px, py, + &gRender.taxiBackup); + gRender.taxiHasBackup = true; + } + } else if (gRender.taxiCels[taxiCel] != NULL) { // Save-under + draw. Backup is replayed at the start of next // frame above to undraw cleanly. jlSpriteSaveAndDraw(stage, gRender.taxiCels[taxiCel], px, py, @@ -946,6 +1103,9 @@ static bool loadSpriteSheet(void) { for (i = 0u; i < ST_TAXI_CEL_COUNT; i++) { gRender.taxiCels[i] = cels[ST_SPRITE_TAXI_FIRST + i]; } + for (i = 0u; i < ST_WARP_CEL_COUNT; i++) { + gRender.warpCels[i] = cels[ST_SPRITE_WARP_FIRST + i]; + } for (i = 0u; i < ST_PASSENGER_CEL_COUNT; i++) { gRender.passengerCels[i] = cels[ST_SPRITE_PASS_FIRST + i]; } @@ -964,6 +1124,12 @@ static void destroySprites(void) { gRender.taxiCels[i] = NULL; } } + for (i = 0u; i < ST_WARP_CEL_COUNT; i++) { + if (gRender.warpCels[i] != NULL) { + jlSpriteDestroy(gRender.warpCels[i]); + gRender.warpCels[i] = NULL; + } + } for (i = 0u; i < ST_PASSENGER_CEL_COUNT; i++) { if (gRender.passengerCels[i] != NULL) { jlSpriteDestroy(gRender.passengerCels[i]);