Lots more work on the API and performance improvements.
This commit is contained in:
parent
add3f3dd0d
commit
a0931e8d16
90 changed files with 739 additions and 345 deletions
104
PERF.md
104
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) |
|
| Op | IIgs (ops/sec) | Amiga (vs IIGS) | Atari ST (vs IIGS) | DOS (vs IIGS) |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| jlSurfaceClear | 28 | 2.54x | 1.11x | 638.57x |
|
| jlSurfaceClear | 28 | 2.68x | 1.32x | 449.21x |
|
||||||
| jlPaletteSet | 678 | 8.68x | 4.23x | 26.13x |
|
| jlPaletteSet | 678 | 9.09x | 4.27x | 22.01x |
|
||||||
| jlScbSetRange | 1005 | 3.66x | 1.86x | 11.16x |
|
| jlScbSetRange | 1005 | 3.62x | 1.86x | 14.20x |
|
||||||
| jlDrawPixel | 1755 | 1.85x | 1.01x | 9.70x |
|
| jlDrawPixel | 1755 | 2.04x | 1.18x | 9.40x |
|
||||||
| jlDrawLine H | 682 | 2.19x | 1.22x | 19.26x |
|
| jlDrawLine H | 682 | 2.26x | 1.61x | 26.93x |
|
||||||
| jlDrawLine V | 90 | 1.67x | 1.11x | 115.30x |
|
| jlDrawLine V | 90 | 1.70x | 1.14x | 145.83x |
|
||||||
| jlDrawLine diag | 35 | 1.14x | 1.06x | 261.49x |
|
| jlDrawLine diag | 35 | 1.14x | 1.06x | 475.37x |
|
||||||
| jlDrawRect 100x100 | 75 | 1.57x | 1.04x | 250.77x |
|
| jlDrawRect 100x100 | 75 | 1.67x | 1.16x | 206.49x |
|
||||||
| jlDrawCircle r=16 | 232 | 1.21x | **0.65x** | 71.24x |
|
| jlDrawCircle r=16 | 232 | 1.20x | **0.65x** | 64.79x |
|
||||||
| jlDrawCircle r=80 | 56 | 1.16x | **0.61x** | 310.93x |
|
| jlDrawCircle r=80 | 56 | 1.16x | **0.61x** | 301.25x |
|
||||||
| jlFillRect 16x16 | 450 | 1.24x | 1.04x | 39.38x |
|
| jlFillRect 16x16 | 450 | 1.24x | 1.11x | 36.04x |
|
||||||
| jlFillRect 80x80 | 75 | **0.95x** | 1.28x | 206.73x |
|
| jlFillRect 80x80 | 75 | **0.95x** | 1.33x | 205.09x |
|
||||||
| jlFillRect 320x200 | 60 | **0.93x** | **0.43x** | 184.62x |
|
| jlFillRect 320x200 | 60 | 1.13x | **0.53x** | 188.92x |
|
||||||
| jlFillCircle r=40 | 38 | **0.97x** | 1.39x | 347.24x |
|
| jlFillCircle r=40 | 38 | **0.82x** | 1.39x | 398.24x |
|
||||||
| jlSamplePixel | 1916 | 3.48x | 1.92x | 18.04x |
|
| jlSamplePixel | 1916 | 3.42x | 1.92x | 18.04x |
|
||||||
| jlTileFill | 1252 | 1.73x | **0.93x** | 10.44x |
|
| jlTileFill | 1252 | 1.74x | **0.98x** | 10.60x |
|
||||||
| jlTileCopy | 997 | 1.80x | 1.02x | 16.96x |
|
| jlTileCopy | 997 | 1.80x | 1.04x | 7.88x |
|
||||||
| jlTileCopyMasked | 498 | 1.76x | 1.08x | 28.55x |
|
| jlTileCopyMasked | 498 | 1.81x | 1.10x | 32.48x |
|
||||||
| jlTilePaste | 1106 | 1.94x | 1.09x | 13.53x |
|
| jlTilePaste | 1106 | 1.99x | 1.12x | 14.64x |
|
||||||
| jlTileSnap | 1473 | 2.26x | 1.28x | 9.44x |
|
| jlTileSnap | 1473 | 2.29x | 1.53x | 9.96x |
|
||||||
| jlSpriteSaveUnder | 528 | 2.21x | 1.29x | 20.14x |
|
| jlSpriteSaveUnder | 528 | 1.05x | 1.29x | 23.69x |
|
||||||
| jlSpriteDraw | 438 | 1.82x | 1.28x | 38.39x |
|
| jlSpriteDraw | 438 | 2.09x | 1.09x | 23.85x |
|
||||||
| jlSpriteRestoreUnder | 487 | 2.00x | 1.11x | 36.71x |
|
| jlSpriteRestoreUnder | 487 | 1.08x | 1.11x | 34.83x |
|
||||||
| jlSpriteSaveAndDraw | 277 | 1.82x | 1.06x | 72.79x |
|
| jlSpriteSaveAndDraw | 277 | 1.23x | **0.97x** | 53.79x |
|
||||||
| jlStagePresent full | 42 | 6.31x | 1.40x | 373.83x |
|
| jlStagePresent full | 42 | 7.81x | 1.10x | 412.17x |
|
||||||
| jlInputPoll | 273 | 13.90x | 4.19x | 57.15x |
|
| jlInputPoll | 273 | 13.94x | 4.19x | 37.87x |
|
||||||
| jlKeyDown | 3382 | 7.82x | 3.76x | 18.93x |
|
| jlKeyDown | 3382 | 7.75x | 3.76x | 18.94x |
|
||||||
| jlKeyPressed | 3345 | 7.65x | 3.81x | 18.72x |
|
| jlKeyPressed | 3345 | 7.66x | 3.81x | 18.73x |
|
||||||
| jlMouseX | 4170 | 10.26x | 5.22x | 26.27x |
|
| jlMouseX | 4170 | 10.40x | 5.22x | 26.30x |
|
||||||
| joeyJoyConnected | 3378 | 7.76x | 3.71x | 18.95x |
|
| joeyJoyConnected | 3378 | 7.71x | 3.71x | 18.96x |
|
||||||
| jlAudioFrameTick | 4106 | 7.99x | 3.04x | 12.37x |
|
| jlAudioFrameTick | 4106 | 8.00x | 2.50x | 12.42x |
|
||||||
| jlAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
|
| jlAudioIsPlayingMod | 3536 | 9.30x | 4.33x | 26.78x |
|
||||||
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
|
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.42x | 1.31x | 47.12x |
|
||||||
| c2p full screen | - | - | - | - |
|
|
||||||
|
|
||||||
> **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves
|
> **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
|
> 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-
|
> clock was perturbed. The clock is `jlFrameCount` = the VBL-interrupt-
|
||||||
> driven GetTick, so an op that holds `SEI` while it runs can over-
|
> 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
|
> report its ops/sec. Treat full-screen IIgs numbers above ~29 as
|
||||||
> inflated and cross-check against the cycle model. (`jlFillRect 320x200`
|
> inflated and cross-check against the cycle model.
|
||||||
> = 60 and `jlStagePresent full` = 42 in the table above predate the
|
|
||||||
> Phase 0 SEI removal and are inflated; expect ~25-28 after re-measure.)
|
|
||||||
|
|
|
||||||
|
|
@ -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). |
|
| `$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). |
|
| `$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. |
|
| 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. |
|
| 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. |
|
| `$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. |
|
| 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. |
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,9 @@ PALETTE_RGB = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def new_canvas():
|
def new_canvas(w=320, h=200):
|
||||||
"""Returns a fresh 320x200 indexed PIL image with the palette installed."""
|
"""Returns a fresh w x h indexed PIL image with the palette installed."""
|
||||||
img = Image.new('P', (320, 200), 0)
|
img = Image.new('P', (w, h), 0)
|
||||||
flat = []
|
flat = []
|
||||||
for r, g, b in PALETTE_RGB:
|
for r, g, b in PALETTE_RGB:
|
||||||
flat.extend([r, g, b])
|
flat.extend([r, g, b])
|
||||||
|
|
@ -120,11 +120,13 @@ def gen_tilebank():
|
||||||
|
|
||||||
def gen_sprites():
|
def gen_sprites():
|
||||||
"""sprites.png: 320x200, layout:
|
"""sprites.png: 320x200, layout:
|
||||||
y= 0..23 : taxi cels (4 frames of 24x24 starting at x=0)
|
y= 0..23 : taxi cels 0-3 (drive) + 4-8 (transporter shrink)
|
||||||
y= 24..39 : passenger walk cels (4 frames of 16x16 at x=0)
|
y= 24..47 : passenger walk cels (row 1)
|
||||||
Remaining rows free for future content.
|
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)
|
d = ImageDraw.Draw(img)
|
||||||
|
|
||||||
# ---- Taxi cels: simple side-view "pod with thruster" ----
|
# ---- Taxi cels: simple side-view "pod with thruster" ----
|
||||||
|
|
@ -149,6 +151,24 @@ def gen_sprites():
|
||||||
(x+15, flame_top),
|
(x+15, flame_top),
|
||||||
(x+12, flame_bot)], fill=flame_color)
|
(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 ----
|
# ---- Passenger cels: small stick figure, walk cycle ----
|
||||||
# Centered in 16x16, walking right.
|
# Centered in 16x16, walking right.
|
||||||
for cel in range(4):
|
for cel in range(4):
|
||||||
|
|
@ -179,6 +199,12 @@ def gen_font():
|
||||||
Authored manually for the printable subset (0x20..0x7E).
|
Authored manually for the printable subset (0x20..0x7E).
|
||||||
Each glyph cell is at (ascii%40, ascii/40) when ascii<128.
|
Each glyph cell is at (ascii%40, ascii/40) when ascii<128.
|
||||||
Unfilled cells stay black (treated as TILE_NO_GLYPH at runtime).
|
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
|
# 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
|
# padding on all sides (so 5x7 active inside an 8x8 cell, baseline
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 437 B After Width: | Height: | Size: 1.1 KiB |
BIN
examples/spacetaxi/assets/tiles/tilebank0.png
Normal file
BIN
examples/spacetaxi/assets/tiles/tilebank0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
examples/spacetaxi/generated/dos/tiles/tilebank0.tbk
Normal file
BIN
examples/spacetaxi/generated/dos/tiles/tilebank0.tbk
Normal file
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
||||||
// mkstlevel: convert a human-readable text level definition into the
|
// 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:
|
// Usage:
|
||||||
// mkstlevel input.txt output.dat
|
// mkstlevel input.txt output.dat
|
||||||
|
|
@ -15,8 +15,10 @@
|
||||||
// @accel <xAccel> <yAccel> [optional, default 14 14]
|
// @accel <xAccel> <yAccel> [optional, default 14 14]
|
||||||
// @gravity <xGrav> <yGrav> [optional, default 0 1; yGrav signed]
|
// @gravity <xGrav> <yGrav> [optional, default 0 1; yGrav signed]
|
||||||
// @vicColor <bg1> <bg2> <bg3> <mc0> <mc1> <sc0> <sc1> [optional, default all 0]
|
// @vicColor <bg1> <bg2> <bg3> <mc0> <mc1> <sc0> <sc1> [optional, default all 0]
|
||||||
// @pad <letter> <tileX> <tileY> <tileW> [up to 10 of these]
|
// @pad <letter> <tileX> <tileY> <tileW> [standX] [up to 10 of these;
|
||||||
// @fare <spawnLetter> <destLetter> [up to 16 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
|
// @tilemap -- followed by exactly 25 lines of 40 chars each
|
||||||
// @colormap -- 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_TILEMAP_W 40
|
||||||
#define ST_PLAYFIELD_ROWS 25
|
#define ST_PLAYFIELD_ROWS 25
|
||||||
#define ST_MAX_PADS 10
|
#define ST_MAX_PADS 10
|
||||||
#define ST_MAX_FARES 16
|
|
||||||
#define ST_NAME_MAX 23
|
#define ST_NAME_MAX 23
|
||||||
#define LINE_MAX 256
|
#define LINE_MAX 256
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t letter;
|
|
||||||
uint8_t tileX;
|
uint8_t tileX;
|
||||||
uint8_t tileY;
|
uint8_t tileY;
|
||||||
uint8_t tileW;
|
uint8_t tileW;
|
||||||
|
uint8_t standX; // passenger stand column, screen pixels
|
||||||
} PadT;
|
} PadT;
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t spawnPad;
|
|
||||||
uint8_t destPad;
|
|
||||||
} FareT;
|
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char name[ST_NAME_MAX + 1];
|
char name[ST_NAME_MAX + 1];
|
||||||
uint8_t tileBank;
|
uint8_t tileBank;
|
||||||
|
|
@ -85,15 +80,12 @@ typedef struct {
|
||||||
uint8_t sprite1Color;
|
uint8_t sprite1Color;
|
||||||
uint8_t padCount;
|
uint8_t padCount;
|
||||||
PadT pads[ST_MAX_PADS];
|
PadT pads[ST_MAX_PADS];
|
||||||
uint8_t fareCount;
|
|
||||||
FareT fares[ST_MAX_FARES];
|
|
||||||
uint8_t tilemap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS];
|
uint8_t tilemap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS];
|
||||||
uint8_t colormap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS];
|
uint8_t colormap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS];
|
||||||
} LevelT;
|
} LevelT;
|
||||||
|
|
||||||
|
|
||||||
static int charToIndex(int c, int *outIdx);
|
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 parseGrid(FILE *fp, const char *gridName, uint8_t *outCells, int *outLine);
|
||||||
static int parseLevel(FILE *fp, LevelT *out);
|
static int parseLevel(FILE *fp, LevelT *out);
|
||||||
static int readLine(FILE *fp, char *buf, int n);
|
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.
|
// Reads one line; returns >0 on success, 0 on EOF, -1 on error.
|
||||||
// Strips trailing newline. Lines starting with '#' are returned with
|
// Strips trailing newline. Lines starting with '#' are returned with
|
||||||
// the leading '#' so the caller can decide whether to skip them.
|
// 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->sprite0Color = (uint8_t)sc0;
|
||||||
out->sprite1Color = (uint8_t)sc1;
|
out->sprite1Color = (uint8_t)sc1;
|
||||||
} else if (strncmp(line, "@pad ", 5) == 0) {
|
} else if (strncmp(line, "@pad ", 5) == 0) {
|
||||||
char letter = '\0';
|
char letter = '\0'; // kept in the syntax for readability only
|
||||||
int px = 0;
|
int px = 0;
|
||||||
int py = 0;
|
int py = 0;
|
||||||
int pw = 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);
|
fprintf(stderr, "mkstlevel: line %d: bad pad\n", lineNo);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -324,36 +308,13 @@ static int parseLevel(FILE *fp, LevelT *out) {
|
||||||
lineNo, ST_MAX_PADS);
|
lineNo, ST_MAX_PADS);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
out->pads[out->padCount].letter = (uint8_t)letter;
|
|
||||||
out->pads[out->padCount].tileX = (uint8_t)px;
|
out->pads[out->padCount].tileX = (uint8_t)px;
|
||||||
out->pads[out->padCount].tileY = (uint8_t)py;
|
out->pads[out->padCount].tileY = (uint8_t)py;
|
||||||
out->pads[out->padCount].tileW = (uint8_t)pw;
|
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++;
|
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) {
|
} else if (strcmp(line, "@tilemap") == 0) {
|
||||||
if (!parseGrid(fp, "tilemap", out->tilemap, &lineNo)) {
|
if (!parseGrid(fp, "tilemap", out->tilemap, &lineNo)) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -403,7 +364,7 @@ static int writeOut(const LevelT *L, const char *path) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (fputc('S', fp) == EOF || fputc('T', fp) == EOF ||
|
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;
|
goto fail;
|
||||||
}
|
}
|
||||||
nameLen = (uint8_t)strlen(L->name);
|
nameLen = (uint8_t)strlen(L->name);
|
||||||
|
|
@ -440,22 +401,14 @@ static int writeOut(const LevelT *L, const char *path) {
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
for (i = 0; i < L->padCount; i++) {
|
for (i = 0; i < L->padCount; i++) {
|
||||||
if (fputc(L->pads[i].letter, fp) == EOF ||
|
if (fputc(L->pads[i].tileX, fp) == EOF ||
|
||||||
fputc(L->pads[i].tileX, fp) == EOF ||
|
|
||||||
fputc(L->pads[i].tileY, fp) == EOF ||
|
fputc(L->pads[i].tileY, fp) == EOF ||
|
||||||
fputc(L->pads[i].tileW, fp) == EOF) {
|
fputc(L->pads[i].tileW, fp) == EOF ||
|
||||||
goto fail;
|
fputc(L->pads[i].standX, fp) == EOF) {
|
||||||
}
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// STL3 stores no fares -- fares are RNG-chosen at runtime.
|
||||||
cells = (size_t)ST_TILEMAP_W * ST_PLAYFIELD_ROWS;
|
cells = (size_t)ST_TILEMAP_W * ST_PLAYFIELD_ROWS;
|
||||||
if (fwrite(L->tilemap, 1, cells, fp) != cells) {
|
if (fwrite(L->tilemap, 1, cells, fp) != cells) {
|
||||||
goto fail;
|
goto fail;
|
||||||
|
|
@ -493,7 +446,7 @@ int main(int argc, char **argv) {
|
||||||
if (!writeOut(&L, argv[2])) {
|
if (!writeOut(&L, argv[2])) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
printf("mkstlevel: wrote %s (name=\"%s\", pads=%u, fares=%u)\n",
|
printf("mkstlevel: wrote %s (name=\"%s\", pads=%u)\n",
|
||||||
argv[2], L.name, (unsigned)L.padCount, (unsigned)L.fareCount);
|
argv[2], L.name, (unsigned)L.padCount);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -163,18 +163,75 @@ int main(void) {
|
||||||
switch (game.state) {
|
switch (game.state) {
|
||||||
case ST_STATE_TITLE:
|
case ST_STATE_TITLE:
|
||||||
{
|
{
|
||||||
// C64 title only listens for UP / DOWN / FIRE per the
|
// C64 title listens for UP (high scores), DOWN
|
||||||
// baked joystick instructions ("UP FOR HIGH SCORES,
|
// (instructions), and FIRE. FIRE on the title does NOT
|
||||||
// DOWN FOR INSTRUCTIONS, FIRE BUTTON TO BEGIN"). The
|
// start gameplay -- it advances to the "GAME VARIATIONS"
|
||||||
// fare-count selector ($5310-$5328) lives on a separate
|
// options menu ($5295, reached via $4750 -> JSR $5295),
|
||||||
// options/menu scene at $5295 (header strings) and
|
// where the player selects the cabbie count before the
|
||||||
// $52E1/$533C (the "1 2 3 4" digit row), not the main
|
// game begins. (UP/DOWN high-scores/instructions screens
|
||||||
// title. UP / DOWN / options screen are TODO; for now
|
// are still TODO.)
|
||||||
// the title only honors FIRE -> begin.
|
{
|
||||||
|
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) ||
|
if (jlKeyPressed(KEY_SPACE) ||
|
||||||
jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) {
|
jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) {
|
||||||
game.score = 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;
|
game.levelIndex = 0;
|
||||||
if (!loadLevelByIndex(&game, 0)) {
|
if (!loadLevelByIndex(&game, 0)) {
|
||||||
jlLogF("spacetaxi: ! cannot load level 0 (DATA/levels/level01.dat)");
|
jlLogF("spacetaxi: ! cannot load level 0 (DATA/levels/level01.dat)");
|
||||||
|
|
@ -182,15 +239,24 @@ int main(void) {
|
||||||
goto shutdown;
|
goto shutdown;
|
||||||
}
|
}
|
||||||
stEngineReset(&game);
|
stEngineReset(&game);
|
||||||
// C64 goes straight from title to gameplay; no
|
|
||||||
// intermediate "PRESS FIRE TO START" screen.
|
|
||||||
game.state = ST_STATE_PLAYING;
|
game.state = ST_STATE_PLAYING;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ST_STATE_PLAYING:
|
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);
|
applyInput(&game);
|
||||||
stEngineTick(&game);
|
stEngineTick(&game);
|
||||||
stPassengerTick(&game);
|
stPassengerTick(&game);
|
||||||
|
|
@ -199,6 +265,13 @@ int main(void) {
|
||||||
if (!wasLanded && game.taxi.landed) {
|
if (!wasLanded && game.taxi.landed) {
|
||||||
stAudioSfxLand();
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case ST_STATE_LEVEL_DONE:
|
case ST_STATE_LEVEL_DONE:
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,13 @@
|
||||||
#define ST_TILEMAP_H 25u
|
#define ST_TILEMAP_H 25u
|
||||||
#define ST_TILE_PIXELS 8u
|
#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).
|
// 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
|
// 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
|
// the HUD band (score / lives / level / current-fare strip). The top
|
||||||
|
|
@ -53,10 +60,28 @@
|
||||||
// Maximum stuff. Tuned for fitting the smallest target (IIgs):
|
// Maximum stuff. Tuned for fitting the smallest target (IIgs):
|
||||||
// 10 pads is the highest count in any canonical Space Taxi level
|
// 10 pads is the highest count in any canonical Space Taxi level
|
||||||
// (D and M each have 10 pads; everything else <= 9)
|
// (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_PADS 10u
|
||||||
#define ST_MAX_PASSENGERS 2u
|
#define ST_MAX_PASSENGERS 1u
|
||||||
#define ST_MAX_FARES 16u
|
|
||||||
|
// 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
|
// Fixed-point taxi physics: position and velocity are int16_t in
|
||||||
// units of 1/16 px, so the taxi can drift fractionally and the
|
// units of 1/16 px, so the taxi can drift fractionally and the
|
||||||
|
|
@ -84,6 +109,9 @@
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ST_STATE_TITLE = 0,
|
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_PLAYING,
|
||||||
ST_STATE_LEVEL_DONE,
|
ST_STATE_LEVEL_DONE,
|
||||||
ST_STATE_GAME_OVER
|
ST_STATE_GAME_OVER
|
||||||
|
|
@ -97,19 +125,13 @@ typedef enum {
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t letter; // 'A'..'H' identifier (which pad number)
|
|
||||||
uint8_t tileX; // landing surface left edge (tile coord)
|
uint8_t tileX; // landing surface left edge (tile coord)
|
||||||
uint8_t tileY; // landing surface row (tile coord)
|
uint8_t tileY; // landing surface row (tile coord)
|
||||||
uint8_t tileW; // pad width in tiles
|
uint8_t tileW; // pad width in tiles
|
||||||
|
uint8_t standX; // passenger stand X (screen px; slot byte6-24)
|
||||||
} StPadT;
|
} StPadT;
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t spawnPad; // pad index where they appear
|
|
||||||
uint8_t destPad; // pad index they want to go to
|
|
||||||
} StFareT;
|
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char name[24]; // level display name ("UP & DOWN", etc.)
|
char name[24]; // level display name ("UP & DOWN", etc.)
|
||||||
uint8_t tileBankId; // which tile asset (0 = default bank)
|
uint8_t tileBankId; // which tile asset (0 = default bank)
|
||||||
|
|
@ -150,8 +172,6 @@ typedef struct {
|
||||||
uint8_t sprite1Color; // $D028 sprite 1 (flame)
|
uint8_t sprite1Color; // $D028 sprite 1 (flame)
|
||||||
uint8_t padCount;
|
uint8_t padCount;
|
||||||
StPadT pads[ST_MAX_PADS];
|
StPadT pads[ST_MAX_PADS];
|
||||||
uint8_t fareCount;
|
|
||||||
StFareT fares[ST_MAX_FARES];
|
|
||||||
// Tilemap: tile-index per cell (row-major).
|
// Tilemap: tile-index per cell (row-major).
|
||||||
uint8_t tilemap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS];
|
uint8_t tilemap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS];
|
||||||
// Palette slot per cell -- which surface palette index a cell uses.
|
// 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
|
// branch"). Reaches 0 -> respawn (or game-over). Mirrors the
|
||||||
// C64's $6B24/$6B4C phase-2/3 timing.
|
// C64's $6B24/$6B4C phase-2/3 timing.
|
||||||
uint8_t crashTicks;
|
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;
|
} StTaxiT;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -203,10 +227,10 @@ typedef struct {
|
||||||
uint32_t score;
|
uint32_t score;
|
||||||
uint8_t lives;
|
uint8_t lives;
|
||||||
uint8_t levelIndex;
|
uint8_t levelIndex;
|
||||||
// Player-selectable fare target (1..4). Mirrors C64 $7213, set on
|
// Player-selectable fare quota (1..4). Mirrors C64 $7213 (the
|
||||||
// the title screen via LEFT/RIGHT joystick (see $5310-$5328 in
|
// GAME-VARIATIONS "cabbies" count), set on the title screen via
|
||||||
// MECHANICS.md). Used to cap the per-level fare count: the engine
|
// LEFT/RIGHT joystick ($5310-$5328). This is the GLOBAL number of
|
||||||
// delivers min(level.fareCount, game.fareTarget) before advancing.
|
// fares to deliver before the game completes -- NOT per-level.
|
||||||
uint8_t fareTarget;
|
uint8_t fareTarget;
|
||||||
} StGameT;
|
} StGameT;
|
||||||
|
|
||||||
|
|
@ -232,9 +256,21 @@ void stRenderDrawText(jlSurfaceT *stage, uint8_t bx, uint8_t by, const char *s);
|
||||||
|
|
||||||
void stEngineReset(StGameT *game);
|
void stEngineReset(StGameT *game);
|
||||||
void stEngineTick(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 stPassengerReset(StGameT *game);
|
||||||
void stPassengerTick(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 stAudioInit(void);
|
||||||
void stAudioShutdown(void);
|
void stAudioShutdown(void);
|
||||||
|
|
|
||||||
|
|
@ -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
|
return true; // top/bottom out-of-field treated as walls
|
||||||
}
|
}
|
||||||
tile = level->tilemap[ty * ST_TILEMAP_W + tx];
|
tile = level->tilemap[ty * ST_TILEMAP_W + tx];
|
||||||
// Tile-index convention (authored by the level designer):
|
// The tilemap holds raw C64 screen-RAM character codes (romToLevel.py
|
||||||
// 0 = empty space
|
// copies screen RAM directly; the empty cell is the space character
|
||||||
// 1..63 = solid (walls, platforms, structure)
|
// $20, BG=0x20 in the extractor -- confirmed by the .dat histogram
|
||||||
// 64..127 = landing-pad surfaces (also solid for collision)
|
// where char 32 dominates the tilemap). On the C64 a sprite-vs-
|
||||||
// 128+ = decorative non-solid (lights, signs, etc.)
|
// background collision ($D01F) latches on ANY non-blank background
|
||||||
return tile != 0u && tile < 128u;
|
// 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 (t->vy > 0) {
|
||||||
if (isSolidAt(L, centerX, feetY)) {
|
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
|
ny = (int32_t)(feetY / ST_TILE_PIXELS) * ST_TILE_PIXELS * ST_SUBPIXEL
|
||||||
- (int32_t)ST_TAXI_H_PX * ST_SUBPIXEL;
|
- (int32_t)ST_TAXI_H_PX * ST_SUBPIXEL;
|
||||||
t->vx = 0;
|
t->vx = 0;
|
||||||
|
|
@ -337,3 +355,81 @@ void stEngineTick(StGameT *game) {
|
||||||
t->y = ny;
|
t->y = ny;
|
||||||
reflectAtEdges(t);
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,25 +54,25 @@ void stHudDraw(jlSurfaceT *stage, const StGameT *game) {
|
||||||
stRenderDrawText(stage, col, (uint8_t)ST_HUD_ROW, game->level.name);
|
stRenderDrawText(stage, col, (uint8_t)ST_HUD_ROW, game->level.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active fare display: where the current passenger wants to go
|
// Active-fare message, matching the C64 HUD ($6C1E "PAD PLEASE",
|
||||||
// (or "PAD x WANTS y" when one is still waiting to be picked up).
|
// $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++) {
|
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
|
||||||
const StPassengerT *p = &game->passengers[i];
|
const StPassengerT *p = &game->passengers[i];
|
||||||
if (p->active && p->onboard &&
|
if (!p->active) {
|
||||||
p->destPad < game->level.padCount) {
|
continue;
|
||||||
char dest = (char)game->level.pads[p->destPad].letter;
|
}
|
||||||
snprintf(buf, sizeof(buf), "FARE %c", dest);
|
if (!p->onboard) {
|
||||||
stRenderDrawText(stage, 0u, (uint8_t)(ST_HUD_ROW + 1u), buf);
|
snprintf(buf, sizeof(buf), "HEY TAXI!");
|
||||||
break;
|
} 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));
|
||||||
}
|
}
|
||||||
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);
|
stRenderDrawText(stage, 0u, (uint8_t)(ST_HUD_ROW + 1u), buf);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,13 @@
|
||||||
#include "spacetaxi.h"
|
#include "spacetaxi.h"
|
||||||
|
|
||||||
|
|
||||||
// STL2 adds the per-level physics templates (xAccel/yAccel/xGrav/yGrav)
|
// STL3: per-pad record is (tileX, tileY, tileW, standX) where standX is the
|
||||||
// and the full VIC color block ($7D00-$7D08). Drops the patience byte
|
// passenger stand column in screen pixels; the STL2 per-pad letter and the
|
||||||
// from each fare entry (Space Taxi proper has no patience timeout).
|
// whole fare list are gone (fares are RNG-chosen at runtime, not stored).
|
||||||
#define ST_LEVEL_MAGIC0 'S'
|
#define ST_LEVEL_MAGIC0 'S'
|
||||||
#define ST_LEVEL_MAGIC1 'T'
|
#define ST_LEVEL_MAGIC1 'T'
|
||||||
#define ST_LEVEL_MAGIC2 'L'
|
#define ST_LEVEL_MAGIC2 'L'
|
||||||
#define ST_LEVEL_MAGIC3 '2'
|
#define ST_LEVEL_MAGIC3 '3'
|
||||||
|
|
||||||
|
|
||||||
static bool readByte(FILE *fp, uint8_t *out) {
|
static bool readByte(FILE *fp, uint8_t *out) {
|
||||||
|
|
@ -108,22 +108,10 @@ bool stLevelLoad(StLevelT *out, const char *path) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (i = 0u; i < out->padCount; i++) {
|
for (i = 0u; i < out->padCount; i++) {
|
||||||
if (!readByte(fp, &out->pads[i].letter) ||
|
if (!readByte(fp, &out->pads[i].tileX) ||
|
||||||
!readByte(fp, &out->pads[i].tileX) ||
|
|
||||||
!readByte(fp, &out->pads[i].tileY) ||
|
!readByte(fp, &out->pads[i].tileY) ||
|
||||||
!readByte(fp, &out->pads[i].tileW)) {
|
!readByte(fp, &out->pads[i].tileW) ||
|
||||||
fclose(fp);
|
!readByte(fp, &out->pads[i].standX)) {
|
||||||
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)) {
|
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,26 @@
|
||||||
// Space Taxi -- passenger AI.
|
// Space Taxi -- passenger AI (dynamic fare model).
|
||||||
//
|
//
|
||||||
// A passenger has three states:
|
// The C64 game has NO static per-level fare list (verified: the prior
|
||||||
// 1. Waiting on a pad (active, !onboard) - cycles walk animation.
|
// port's "circular A->B->..->A" fares were fabricated). Both the SPAWN
|
||||||
// Boards when the taxi lands on their pad.
|
// pad and the DELIVERY destination are chosen at RUNTIME by RNG:
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// When a passenger lands at their destination, the next fare in the
|
// * padHoverSetup ($6537) picks a random free pad in [1..padCount] for
|
||||||
// level's fareCount is spawned. When the level's fares are exhausted,
|
// the passenger to appear on, and again for where they want to go.
|
||||||
// the level is done.
|
// * 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
|
// Scoring is flat-rate per the C64 original ($43B9 BCD blob added at
|
||||||
// success-stage 6 / $6742): 500 points per delivered fare. The earlier
|
// success-stage 6 / $6742): a delivered fare adds ST_FARE_SCORE.
|
||||||
// "tip by patience" formula was a port-side invention.
|
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
|
|
@ -27,131 +33,125 @@
|
||||||
#define ST_WALK_FRAMES 4u
|
#define ST_WALK_FRAMES 4u
|
||||||
#define ST_WALK_TICKS_PER_CEL 6u
|
#define ST_WALK_TICKS_PER_CEL 6u
|
||||||
|
|
||||||
// Verified via emulator trace of $4354 (BCD-add) with the $43B9 blob
|
// 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
|
// a basic delivered fare adds '5' at the ones digit of the DDDD.DD HUD
|
||||||
// (the ones-digit of the integer part in the C64's DDDD.DD layout),
|
// score. See stuff/spacetaxi/trace.py.
|
||||||
// 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)
|
|
||||||
#define ST_FARE_SCORE 5u
|
#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 uint32_t gRng;
|
||||||
static uint8_t gNextFareIdx;
|
|
||||||
static uint8_t gWalkTick;
|
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 deliverFare(StGameT *game, StPassengerT *p);
|
||||||
static void spawnNextFare(StGameT *game);
|
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
|
static void deliverFare(StGameT *game, StPassengerT *p) {
|
||||||
// upper cap on this level's fare count. The level data sets the
|
game->score += ST_FARE_SCORE;
|
||||||
// maximum (e.g. 8 for a long delivery chain); player picks 1..4 on
|
stAudioSfxDropoff();
|
||||||
// the title to control session length. The lesser of the two wins.
|
p->active = false;
|
||||||
static uint8_t effectiveFareTarget(const StGameT *game) {
|
gDeliveredThisScreen++;
|
||||||
uint8_t levelMax = game->level.fareCount;
|
// The next fare appears after the spawn cadence (see stPassengerTick),
|
||||||
uint8_t sel = game->fareTarget;
|
// not instantly.
|
||||||
if (sel == 0u) {
|
gSpawnCountdown = ST_SPAWN_CAP_FRAMES;
|
||||||
return levelMax;
|
|
||||||
}
|
|
||||||
return (levelMax < sel) ? levelMax : sel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void spawnNextFare(StGameT *game) {
|
// On boarding, choose where the fare wants to go. Destination pads get
|
||||||
const StFareT *fare;
|
// used up as the screen's fares are delivered: once delivering would leave
|
||||||
const StPadT *pad;
|
// no fresh pad (single-pad screens, or after padCount-1 deliveries), the
|
||||||
StPassengerT *slot;
|
// fare wants "UP PLEASE!" -- the player flies up to the next screen.
|
||||||
uint8_t i;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// Find an idle slot.
|
d = rngPad(pc);
|
||||||
slot = NULL;
|
if (d == p->currentPad) {
|
||||||
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
|
d = (uint8_t)((d + 1u) % pc);
|
||||||
if (!game->passengers[i].active) {
|
|
||||||
slot = &game->passengers[i];
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
p->destPad = d;
|
||||||
}
|
}
|
||||||
if (slot == NULL) {
|
|
||||||
|
|
||||||
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
fare = &game->level.fares[gNextFareIdx++];
|
padIdx = rngPad(game->level.padCount);
|
||||||
if (fare->spawnPad >= game->level.padCount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pad = &game->level.pads[fare->spawnPad];
|
|
||||||
|
|
||||||
slot->active = true;
|
p->active = true;
|
||||||
slot->onboard = false;
|
p->onboard = false;
|
||||||
slot->currentPad = fare->spawnPad;
|
p->walkPhase = 0u;
|
||||||
slot->destPad = fare->destPad;
|
p->walkDir = 0u;
|
||||||
slot->walkPhase = 0u;
|
p->destPad = 0u;
|
||||||
slot->walkDir = 0u;
|
placeOnPad(p, &game->level, padIdx);
|
||||||
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) {
|
// Idle back-and-forth walk across the pad while waiting to be picked up.
|
||||||
uint8_t i;
|
// The C64 passenger AI wasn't fully traced; this gives each waiting fare
|
||||||
|
// some motion instead of standing still.
|
||||||
gNextFareIdx = 0u;
|
static void walkPassenger(StGameT *game, StPassengerT *p) {
|
||||||
gWalkTick = 0u;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void stPassengerTick(StGameT *game) {
|
|
||||||
StPassengerT *p;
|
|
||||||
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;
|
const StPadT *pad;
|
||||||
int16_t padXMin;
|
int16_t padXMin;
|
||||||
int16_t padXMax;
|
int16_t padXMax;
|
||||||
if (!p->active || p->onboard) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
p->walkPhase = (uint8_t)((p->walkPhase + 1u) % ST_WALK_FRAMES);
|
p->walkPhase = (uint8_t)((p->walkPhase + 1u) % ST_WALK_FRAMES);
|
||||||
if (p->currentPad >= game->level.padCount) {
|
if (p->currentPad >= game->level.padCount) {
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
pad = &game->level.pads[p->currentPad];
|
pad = &game->level.pads[p->currentPad];
|
||||||
padXMin = (int16_t)(pad->tileX * ST_TILE_PIXELS);
|
padXMin = (int16_t)(pad->tileX * ST_TILE_PIXELS);
|
||||||
padXMax = (int16_t)((pad->tileX + pad->tileW) * ST_TILE_PIXELS
|
padXMax = (int16_t)((pad->tileX + pad->tileW) * ST_TILE_PIXELS - ST_PASSENGER_W_PX);
|
||||||
- ST_PASSENGER_W_PX);
|
|
||||||
if (padXMax <= padXMin) {
|
if (padXMax <= padXMin) {
|
||||||
continue; // pad too narrow to walk on
|
return; // pad too narrow to walk on
|
||||||
}
|
}
|
||||||
if (p->walkDir == 0u) {
|
if (p->walkDir == 0u) {
|
||||||
p->x++;
|
p->x++;
|
||||||
|
|
@ -167,50 +167,75 @@ void stPassengerTick(StGameT *game) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
void stPassengerReset(StGameT *game) {
|
||||||
|
uint8_t i;
|
||||||
|
|
||||||
|
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++) {
|
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
|
||||||
p = &game->passengers[i];
|
game->passengers[i].active = false;
|
||||||
|
}
|
||||||
|
// The screen starts empty; the first fare appears after the spawn
|
||||||
|
// cadence in stPassengerTick.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void stPassengerTick(StGameT *game) {
|
||||||
|
StPassengerT *p = &game->passengers[0];
|
||||||
|
StTaxiT *t = &game->taxi;
|
||||||
|
|
||||||
if (!p->active) {
|
if (!p->active) {
|
||||||
continue;
|
// 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--;
|
||||||
|
}
|
||||||
|
if (rngPad(100u) < ST_SPAWN_ROLL_THRESH || gSpawnCountdown <= 0) {
|
||||||
|
spawnPassenger(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!p->onboard) {
|
if (!p->onboard) {
|
||||||
// Board if the taxi has landed on their pad.
|
// Walk in place while waiting.
|
||||||
|
if (++gWalkTick >= ST_WALK_TICKS_PER_CEL) {
|
||||||
|
gWalkTick = 0u;
|
||||||
|
walkPassenger(game, p);
|
||||||
|
}
|
||||||
|
// Board when the taxi has landed on their pad.
|
||||||
if (t->landed && t->onPad == p->currentPad) {
|
if (t->landed && t->onPad == p->currentPad) {
|
||||||
p->onboard = true;
|
p->onboard = true;
|
||||||
|
pickDestination(game, p);
|
||||||
stAudioSfxPickup();
|
stAudioSfxPickup();
|
||||||
}
|
}
|
||||||
} else {
|
} else if (p->destPad != ST_DEST_TRANSPORTER) {
|
||||||
// Onboard: deliver to destination pad.
|
// Deliver by landing on the destination pad. (UP-PLEASE fares are
|
||||||
if (p->destPad >= game->level.padCount) {
|
// delivered on the transporter fly-up; see stPassengerTransporterExit.)
|
||||||
p->active = false;
|
if (t->landed &&
|
||||||
continue;
|
|
||||||
}
|
|
||||||
atDest = t->landed &&
|
|
||||||
t->onPad < game->level.padCount &&
|
t->onPad < game->level.padCount &&
|
||||||
t->onPad == p->destPad;
|
t->onPad == p->destPad) {
|
||||||
if (atDest) {
|
deliverFare(game, p);
|
||||||
game->score += ST_FARE_SCORE;
|
|
||||||
stAudioSfxDropoff();
|
|
||||||
p->active = false;
|
|
||||||
spawnNextFare(game);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If every fare is exhausted AND no active passenger remains,
|
|
||||||
// the level is complete.
|
void stPassengerTransporterExit(StGameT *game) {
|
||||||
if (gNextFareIdx >= effectiveFareTarget(game)) {
|
StPassengerT *p = &game->passengers[0];
|
||||||
bool anyActive = false;
|
|
||||||
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
|
if (p->active && p->onboard && p->destPad == ST_DEST_TRANSPORTER) {
|
||||||
if (game->passengers[i].active) {
|
game->score += ST_FARE_SCORE;
|
||||||
anyActive = true;
|
stAudioSfxDropoff();
|
||||||
break;
|
p->active = false;
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!anyActive) {
|
|
||||||
game->state = ST_STATE_LEVEL_DONE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,11 @@
|
||||||
#define ST_TAXI_W_PX 24
|
#define ST_TAXI_W_PX 24
|
||||||
#define ST_TAXI_H_PX 24
|
#define ST_TAXI_H_PX 24
|
||||||
#define ST_TAXI_CEL_COUNT 4
|
#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
|
// Flame placeholder fallback (only used if real sprite-2 cel is
|
||||||
// missing): a small bright rectangle below the cab while thrusting.
|
// 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)
|
uint8_t currentBankId; // which tbank%u.tbk is loaded (0xFF = none)
|
||||||
bool bankLoaded;
|
bool bankLoaded;
|
||||||
jlSpriteT *taxiCels[ST_TAXI_CEL_COUNT];
|
jlSpriteT *taxiCels[ST_TAXI_CEL_COUNT];
|
||||||
|
jlSpriteT *warpCels[ST_WARP_CEL_COUNT];
|
||||||
jlSpriteT *flameCels[ST_FLAME_CEL_COUNT];
|
jlSpriteT *flameCels[ST_FLAME_CEL_COUNT];
|
||||||
jlSpriteBackupT flameBackup;
|
jlSpriteBackupT flameBackup;
|
||||||
uint8_t flameBackupMem[ST_SPRITE_BACKUP_BYTES];
|
uint8_t flameBackupMem[ST_SPRITE_BACKUP_BYTES];
|
||||||
|
|
@ -268,8 +274,20 @@ typedef struct {
|
||||||
int32_t demoY;
|
int32_t demoY;
|
||||||
int16_t demoVx;
|
int16_t demoVx;
|
||||||
int16_t demoVy;
|
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;
|
} 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 StTitleStateT gTitle;
|
||||||
|
|
||||||
static void titleReset(void);
|
static void titleReset(void);
|
||||||
|
|
@ -464,6 +482,63 @@ static void titleReset(void) {
|
||||||
gTitle.scriptTimer = kTitleDemoScript[1];
|
gTitle.scriptTimer = kTitleDemoScript[1];
|
||||||
gTitle.demoX = (int32_t)ST_TITLE_CAB_SX_INIT * ST_SUBPIXEL;
|
gTitle.demoX = (int32_t)ST_TITLE_CAB_SX_INIT * ST_SUBPIXEL;
|
||||||
gTitle.demoY = (int32_t)ST_TITLE_CAB_SY_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];
|
char buf[32];
|
||||||
|
|
||||||
// Title screen: render the title tilemap (extracted from the C64
|
// Title screen: render the title tilemap (extracted from the C64
|
||||||
// game's screen RAM at $0400). The "SPACE TAXI" logo stays at
|
// game's screen RAM at $0400). The "SPACE TAXI" logo is animated by
|
||||||
// its captured color -- the C64 ran a rainbow cycle here, but
|
// titleAnimateLogo below -- the C64 flip-books char $84's bitmap
|
||||||
// we keep it static so the per-frame logo repaint cost is zero.
|
// ($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) {
|
if (game->state == ST_STATE_TITLE) {
|
||||||
// Restore the area saved under the prev-frame sprites BEFORE
|
// Restore the area saved under the prev-frame sprites BEFORE
|
||||||
// anything else paints. Without this, jlSpriteDraw leaves the
|
// 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
|
// (e.g., transitioned from gameplay back to title) -- otherwise
|
||||||
// it's a fast no-op.
|
// it's a fast no-op.
|
||||||
stRenderLevel(stage, &game->level);
|
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
|
// No "fares per game" overlay on the title: the C64 has a
|
||||||
// separate options/menu scene driven by $5295 (header text)
|
// separate options/menu scene driven by $5295 (header text)
|
||||||
// and $52E1 / $533C (the "1 2 3 4" digit row at row 2 col 28
|
// 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();
|
jlStagePresent();
|
||||||
return;
|
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) {
|
if (game->state == ST_STATE_GAME_OVER) {
|
||||||
jlSurfaceClear(stage, 0u);
|
jlSurfaceClear(stage, 0u);
|
||||||
stRenderDrawText(stage, 15u, 8u, "GAME OVER");
|
stRenderDrawText(stage, 15u, 8u, "GAME OVER");
|
||||||
|
|
@ -828,7 +970,22 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) {
|
||||||
if (gRender.taxiCels[taxiCel] == NULL) {
|
if (gRender.taxiCels[taxiCel] == NULL) {
|
||||||
taxiCel = 0u;
|
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
|
// Save-under + draw. Backup is replayed at the start of next
|
||||||
// frame above to undraw cleanly.
|
// frame above to undraw cleanly.
|
||||||
jlSpriteSaveAndDraw(stage, gRender.taxiCels[taxiCel], px, py,
|
jlSpriteSaveAndDraw(stage, gRender.taxiCels[taxiCel], px, py,
|
||||||
|
|
@ -946,6 +1103,9 @@ static bool loadSpriteSheet(void) {
|
||||||
for (i = 0u; i < ST_TAXI_CEL_COUNT; i++) {
|
for (i = 0u; i < ST_TAXI_CEL_COUNT; i++) {
|
||||||
gRender.taxiCels[i] = cels[ST_SPRITE_TAXI_FIRST + 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++) {
|
for (i = 0u; i < ST_PASSENGER_CEL_COUNT; i++) {
|
||||||
gRender.passengerCels[i] = cels[ST_SPRITE_PASS_FIRST + i];
|
gRender.passengerCels[i] = cels[ST_SPRITE_PASS_FIRST + i];
|
||||||
}
|
}
|
||||||
|
|
@ -964,6 +1124,12 @@ static void destroySprites(void) {
|
||||||
gRender.taxiCels[i] = NULL;
|
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++) {
|
for (i = 0u; i < ST_PASSENGER_CEL_COUNT; i++) {
|
||||||
if (gRender.passengerCels[i] != NULL) {
|
if (gRender.passengerCels[i] != NULL) {
|
||||||
jlSpriteDestroy(gRender.passengerCels[i]);
|
jlSpriteDestroy(gRender.passengerCels[i]);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue