Massive optimizations. New music player. Serial support.
This commit is contained in:
parent
1442bf79a2
commit
d498a9f0b3
22 changed files with 1454 additions and 405 deletions
107
MECHANICS.md
107
MECHANICS.md
|
|
@ -572,9 +572,20 @@ in $4FCB, $6BE7). The actual title-screen DRAWING happens via
|
|||
sets their ptr to `$DA` ($3680), color to 7, then triggers song 8
|
||||
via `$CB02`. This is the **idle title state**.
|
||||
|
||||
`$4955` is the reverse: gate-off voice 1+2, noise off, load song
|
||||
from `$6F75,X` (X = $716B current music index), set $5E9D=0,
|
||||
JMP $5EC4. **Exit-to-title cleanup**.
|
||||
`$4955` is the reverse: gate-off voice 1+2, noise off, then load a
|
||||
disk file selected by `$6F75,X` (X = `$716B`) via the `$9936`
|
||||
fast-loader, set $5E9D=0, JMP $5EC4. **Exit-to-title cleanup**.
|
||||
|
||||
CORRECTED 2026-07-20 (demo-subsystem trace): the `$6F75,X` table and
|
||||
the `$9936` loader do NOT load music. `$6F75` holds a FILENAME DIGIT
|
||||
patched into the reversed "S0:SPACETAXI 0_/N0S" string, and the four
|
||||
"SPACETAXI 01..04" disk files are the **attract-demo recorded input
|
||||
streams** (RLE `[joystick mask][tick count]` pairs; H/T/W/X screen
|
||||
rotation, `$6F75` maps H=01, T=02, W=03, X=04; playback substitutes
|
||||
the recorded mask for `$DC00` input via the `$48F2` hook). `$716B`
|
||||
is the demo rotation index, not a music index. The music engine
|
||||
proper ($CB00-$CFFF, per-voice streams, freq tables) is unrelated to
|
||||
these files.
|
||||
|
||||
## Lives + death state
|
||||
|
||||
|
|
@ -955,7 +966,7 @@ check_note:
|
|||
AND #$7F ; mask high bit (effect flag)
|
||||
CMP #$61 ; note count = 97
|
||||
BCC normal_note
|
||||
JMP $CD56 ; effect command (out of scope for now)
|
||||
JMP $CD56 ; effect command (traced -- see "Effect commands" below)
|
||||
|
||||
normal_note:
|
||||
CLC; ADC $CB86,X ; transpose
|
||||
|
|
@ -965,8 +976,8 @@ LDA $CEF1,Y -> $D400,X ; voice freq LO
|
|||
LDA $CE90,Y -> $D401,X ; voice freq HI
|
||||
LDA $CBBF,X -> $D405,X ; attack/decay
|
||||
LDA $CBC0,X -> $D406,X ; sustain/release
|
||||
LDA $CBC1,X -> $D403,X ; pulse-width lo
|
||||
LDA $CBC2,X -> $D402,X ; pulse-width hi
|
||||
LDA $CBC1,X -> $D403,X ; pulse-width HI ($D403 is the PW hi register)
|
||||
LDA $CBC2,X -> $D402,X ; pulse-width LO
|
||||
LDA $CBC4,X ; filter flag
|
||||
PHP
|
||||
LDA $D417; EOR #$FF; ORA $CBAB,X; EOR #$FF
|
||||
|
|
@ -985,13 +996,85 @@ A=0; STA $CB83,X ; active flag = 0
|
|||
BEQ continue
|
||||
```
|
||||
|
||||
Frequency tables: `$CEF1-CF52` (97 entries hi), `$CE90-CEF0`
|
||||
(97 entries lo). Note codes are 1..97 indexing both tables.
|
||||
Frequency tables (CORRECTED 2026-07-20 against the trace above:
|
||||
the earlier version of this line had lo/hi swapped): `$CEF1-CF51`
|
||||
holds the 97 freq LO bytes (written to `$D400,X`), `$CE90-CEF0`
|
||||
holds the 97 freq HI bytes (written to `$D401,X`). Note codes are
|
||||
1..97 indexing both tables. Verified against raw.bin bytes:
|
||||
`$CE90: 00 01 01 01 ...` (small = hi bytes of low notes),
|
||||
`$CEF1: 00 0C 1C 2D 3F 51 ...` (lo bytes).
|
||||
|
||||
So the music is a simple **list-of-(note, optional-duration)** stream
|
||||
per voice, with per-voice transpose and ADSR/filter state stored
|
||||
adjacently. No subroutines, no jump tables -- just a flat stream
|
||||
terminated by $00.
|
||||
CORRECTED 2026-07-20 (full byte-level trace of `$CD0E-$CD55`): the
|
||||
stream is **one byte per note event**, NOT (note, duration) pairs.
|
||||
After the note plays, `$CD0E` re-reads the SAME byte at `($FC)` --
|
||||
the pointer has not advanced yet -- and `BMI $CD24` tests the NOTE
|
||||
byte's own bit 7 (the bit masked off by `AND #$7F` before the table
|
||||
lookup):
|
||||
|
||||
- bit 7 CLEAR: default duration. `$CB84,X = $CB9B,X + $A2 + 1`
|
||||
(next event = now + default duration + 1 jiffy), `$CB83,X = 0`.
|
||||
- bit 7 SET: staccato/gap mode using `$CBC5,X`: if `$CBC5,X` is
|
||||
negative it is first added to `$CB9B,X` (clamped at 0). The result
|
||||
is stored BOTH into `$CB84,X` (+`$A2`+1, the note-on time) AND via
|
||||
self-modifying code into the `SBC` immediate at `$CD3F`, and
|
||||
`$CB83,X = $CB9B,X - 2 - (that value)`... i.e. `$CB83,X` receives
|
||||
the GAP length. `$CB83,X` is therefore a gap counter, not a plain
|
||||
flag: when the tick at `$CC74` finds it nonzero, it gates the voice
|
||||
OFF and schedules the next (note-start) event at `now + $CB83,X`
|
||||
(`CLC ADC $A2`). "0 = on" in the state table above really means
|
||||
"no pending gap".
|
||||
|
||||
Then `$CD43` advances the track pointer by ONE byte (the note byte)
|
||||
and refreshes zp `$FC/$FD`. Flat stream, terminated by `$00` (end of
|
||||
track: gate off, pointer stays) or `$80` (end of song: pointer set to
|
||||
`$FFFF`, gate off).
|
||||
|
||||
### Effect commands (`$CD56`) -- traced 2026-07-20, previously "out of scope"
|
||||
|
||||
A stream byte whose masked value `(byte & $7F) >= $61` is an effect.
|
||||
`$CD56` re-reads the raw byte and transforms it with an
|
||||
ASL/PHP/ASL/ASL/PLP/ROR/LSR/LSR dance that computes
|
||||
`cmd = (bit7 << 5) | (byte & $1F)` -- so `$61-$7F` map to cmd
|
||||
`$01-$1F` and `$E1-$FF` map to cmd `$21-$3F`. Dispatch (compare
|
||||
chain, byte-exact; interpretive names are inference from the code
|
||||
shapes, marked *):
|
||||
|
||||
| cmd range | bytes | action |
|
||||
| --------- | ----- | ------ |
|
||||
| `> $30` | 3 | skip two operand bytes (unused command class in this build) |
|
||||
| `= $30` | 1+14 | *instrument load: copy the next 14 stream bytes into `$CBBF+`(voice window) -- ADSR, pulse width, waveform mask, filter state (`$CD71`, self-modified X operands) |
|
||||
| `$20-$2F` | 1 | *set repeat counter: `$CBAA+Y = cmd & $0F` where `Y = $CB85,X + $CBD6` (`$CE84` slot indexer) |
|
||||
| `$18` | 2 | set per-voice transpose `$CB86,X` from the next byte (`$CDB2`) |
|
||||
| `$10-$17` (not `$18`) | 2 | set per-voice default duration `$CB9B,X` from the next byte (`$CDC3`) |
|
||||
| `$0C-$0F` | 1 | decrement repeat counter (`SBC 1, AND $0F`) (`$CDD4`) |
|
||||
| `$08-$0B` | 1 | *loop-back: condition via `$CE68` (cmd&3: 2 = if counter zero, 3 = if counter nonzero, else always); taken -> pop `$CB95/96,Y` saved pointer (`DEC $CB85,X` twice) and continue there; a negative slot index instead resets `$CB85,X` and ends the song (`$CDE8`) |
|
||||
| `$04-$07` | 1 or 3 | *loop-start: condition false -> skip 2 operand bytes; true -> push `ptr+3` onto the `$CB95/96` pointer stack (`INC $CB85,X` twice) then fall into the jump below (`$CE17`) |
|
||||
| `$01-$03` | 3 | *relative jump: add the 16-bit LE operand (bytes at ptr+1/+2) to the track pointer (`$CE4A`) |
|
||||
|
||||
After most commands control returns to `$CC99` to process the next
|
||||
stream byte in the same tick (so command chains execute
|
||||
"immediately", consuming no time).
|
||||
|
||||
### Song loading -- `$4955` is a patch stub, the real loader is `$9936`
|
||||
|
||||
`$4955` only silences the SID (`$D404 = 0`, `$D40B = 0`,
|
||||
`$D412 = $80`), then: `LDX $716B` (music index), `LDA $6F75,X`,
|
||||
`STA $99A7`, `JSR $9936`. **`$6F75` is not a pointer table -- it
|
||||
holds a FILENAME CHARACTER** (observed bytes: index 7 -> `$31` '1',
|
||||
19 -> `$32` '2', 22 -> `$33` '3', 23 -> `$34` '4'), and `$99A7` is a
|
||||
character slot inside a REVERSED filename string at `$99A3+`
|
||||
("S0N/_0 IXATECAPS:0S" = "S0:SPACETAXI 0_/N0S" reversed; a second
|
||||
string at `$9990` is "SPACETAXI HI"). `$9936` is a self-modifying
|
||||
IEC fast-loader (raw `$FFA8` CIOUT / `$FFAE` UNLSN serial traffic)
|
||||
that pulls the song file off the disk. **The music DATA is not in
|
||||
the resident program at all: each song ships as its own disk file**
|
||||
-- the `SPACETAXI 01..04` / `SPACETAXI HI` PRGs (present in
|
||||
`stuff/spacetaxi/extract/`). Whatever song was resident when the
|
||||
raw.bin dump was taken is what sits in its load area there.
|
||||
|
||||
Timing base: `$A2` in the tick compare is the KERNAL jiffy clock low
|
||||
byte, so all durations are in PAL jiffies (50 Hz) -- `$CB84,X` is a
|
||||
"next event at this jiffy" target, not a countdown.
|
||||
|
||||
### Two reset variants
|
||||
|
||||
|
|
|
|||
103
assets/extractFlame.py
Normal file
103
assets/extractFlame.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/env python3
|
||||
# extractFlame.py -- wire the engine-flame sprite cels from the C64 ROM
|
||||
# image (stuff/spacetaxi/raw.bin) into the sprite sheet's flame row.
|
||||
#
|
||||
# The flame is C64 sprite 2, MULTICOLOR (verified: $D01C bit2 set). Its
|
||||
# per-direction cel is chosen from the pointer table at $6DB0 (indexed by
|
||||
# the direction mask $716A). Reading raw.bin at $6DB0 (2-byte load-addr
|
||||
# header -> +2) gives, and the port's kFlameCelByDirMask mirrors exactly:
|
||||
# mask 1 UP -> block $D8 mask 8 RIGHT -> block $D7
|
||||
# mask 2 DOWN -> block $D4 mask 9 UP+RIGHT -> block $D3
|
||||
# mask 4 LEFT -> block $D5 mask 10 DOWN+RIGHT-> block $D6
|
||||
# mask 5 UP+LEFT -> block $D2
|
||||
# mask 6 DOWN+LEFT -> block $D1
|
||||
# flameCels[0..7] load from .spr cels 18..25 (sheet row 2, cols 0..7), so
|
||||
# the cel order below is the kFlameCelByDirMask index order.
|
||||
#
|
||||
# Sprite data lives at block*64 in VIC bank 0 ($3000-$37FF). Each C64
|
||||
# multicolor sprite is 12 MC-pixels x 21 rows (3 bytes/row, 2 bits/pixel);
|
||||
# each MC pixel is 2 hi-res pixels, so it maps 1 MC pixel -> 2 of the
|
||||
# port's 24-wide 4bpp cel pixels. Colors are the live-level flame palette
|
||||
# ($7D05->$D025=$02 red = MC0, $7D06->$D026=$07 yellow = MC1; sprite-2
|
||||
# colour = orange), mapped to sprites.png's indexed palette.
|
||||
#
|
||||
# Patches ONLY the flame row (row 2) of sprites.png -- taxi (row 0),
|
||||
# passenger (row 1), and font.png are left untouched. Re-run the .spr
|
||||
# bake afterwards (make/<port>.mk STAXI_SPR rule).
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
sys.exit("extractFlame.py: needs Pillow (pip install pillow)")
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
RAW = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "raw.bin")
|
||||
PNG = os.path.join(HERE, "sprites", "sprites.png")
|
||||
|
||||
CEL = 24 # cel is 24x24 px
|
||||
LOAD_HDR = 2 # raw.bin 2-byte load-address header
|
||||
|
||||
# flameCels index -> C64 sprite block (from $6DB0 / kFlameCelByDirMask).
|
||||
FLAME_BLOCKS = [0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6]
|
||||
|
||||
# Multicolor 2-bit pixel -> palette index. The engine renders sprite
|
||||
# cels through kDefaultPalette (stRender.c), whose index IS the C64 VIC-II
|
||||
# colour code -- the sprite's own baked palette is ignored (loadSpriteSheet
|
||||
# passes NULL). So the C64 flame colours map DIRECTLY by code:
|
||||
# 00 transparent(0), 01 MC0=$D025=$02 red(2), 11 MC1=$D026=$07 yellow(7),
|
||||
# 10 sprite-2 colour -> orange(8) (barely used by these cels).
|
||||
# (NOT the sprites.png preview-palette indices, which differ.)
|
||||
MC_TO_INDEX = {0: 0, 1: 2, 2: 8, 3: 7}
|
||||
|
||||
FLAME_ROW_Y = 2 * CEL # sheet row 2 (flame row) top edge
|
||||
|
||||
|
||||
def loadRaw():
|
||||
with open(RAW, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def celFromBlock(raw, block):
|
||||
# Return a 24x24 list-of-rows of palette indices for one flame block.
|
||||
base = LOAD_HDR + block * 64
|
||||
cel = [[0] * CEL for _ in range(CEL)]
|
||||
for row in range(21): # C64 sprite is 21 rows tall
|
||||
for byteIdx in range(3): # 3 bytes/row
|
||||
b = raw[base + row * 3 + byteIdx]
|
||||
for pair in range(4): # 4 MC pixels per byte (MSB first)
|
||||
mc = (b >> ((3 - pair) * 2)) & 3
|
||||
idx = MC_TO_INDEX[mc]
|
||||
px = (byteIdx * 4 + pair) * 2 # MC pixel -> 2 cel pixels
|
||||
cel[row][px] = idx
|
||||
cel[row][px + 1] = idx
|
||||
return cel
|
||||
|
||||
|
||||
def main():
|
||||
raw = loadRaw()
|
||||
im = Image.open(PNG)
|
||||
if im.mode != "P":
|
||||
sys.exit(f"extractFlame.py: {PNG} is {im.mode}, expected indexed 'P'")
|
||||
px = im.load()
|
||||
nonEmpty = 0
|
||||
for celIdx, block in enumerate(FLAME_BLOCKS):
|
||||
cel = celFromBlock(raw, block)
|
||||
x0 = celIdx * CEL
|
||||
opaque = 0
|
||||
for y in range(CEL):
|
||||
for x in range(CEL):
|
||||
v = cel[y][x]
|
||||
px[x0 + x, FLAME_ROW_Y + y] = v
|
||||
if v != 0:
|
||||
opaque += 1
|
||||
nonEmpty += 1 if opaque else 0
|
||||
print(f" flameCels[{celIdx}] block ${block:02X}: {opaque} opaque px -> cel {18 + celIdx}")
|
||||
im.save(PNG)
|
||||
print(f"extractFlame: patched {nonEmpty}/8 flame cels into {os.path.relpath(PNG, HERE)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
63
assets/genDemoStreams.py
Normal file
63
assets/genDemoStreams.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env python3
|
||||
# Embed the C64 attract-demo recorded input streams (the four
|
||||
# "SPACETAXI 0x" disk files -- RLE [joystick mask][tick count] pairs;
|
||||
# mask bit0=UP bit1=DOWN bit2=LEFT bit3=RIGHT, bit7 always set, no
|
||||
# fire bit -- which is why every recorded ride ends in a crash: the
|
||||
# demo cab can never lower its gear) as C arrays for stDemoStreams.h.
|
||||
#
|
||||
# Every stream file is odd-length: the $9936 saver's end=$3F+3
|
||||
# off-by-one appends one dangling byte past the last complete pair,
|
||||
# trimmed here on all four.
|
||||
#
|
||||
# Rotation order is H -> W -> T -> X ($6FE2/$50C1 trace) with file
|
||||
# mapping H=01, T=02, W=03, X=04 and demo levels H=8, W=23, T=20,
|
||||
# X=24 (1-based).
|
||||
|
||||
import os
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
EXTRACT = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "extract")
|
||||
OUT = os.path.join(HERE, "..", "stDemoStreams.h")
|
||||
|
||||
# (file, symbol, 0-based level index, screen letter) in ROTATION order.
|
||||
STREAMS = [
|
||||
("SPACETAXI 01_N0S.prg", "kDemoStreamH", 7, "H"),
|
||||
("SPACETAXI 03_N0S.prg", "kDemoStreamW", 22, "W"),
|
||||
("SPACETAXI 02_N0S.prg", "kDemoStreamT", 19, "T"),
|
||||
("SPACETAXI 04_N0S.prg", "kDemoStreamX", 23, "X"),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
with open(OUT, "w", encoding="ascii") as f:
|
||||
f.write("// Generated by assets/genDemoStreams.py -- do not hand-edit.\n")
|
||||
f.write("// C64 attract-demo recorded input streams (see the generator\n")
|
||||
f.write("// for format notes). Rotation order H, W, T, X.\n\n")
|
||||
f.write("#ifndef ST_DEMO_STREAMS_H\n#define ST_DEMO_STREAMS_H\n\n")
|
||||
f.write("#include <stdint.h>\n\n")
|
||||
entries = []
|
||||
for fname, sym, level, letter in STREAMS:
|
||||
data = open(os.path.join(EXTRACT, fname), "rb").read()
|
||||
if len(data) % 2 == 1:
|
||||
data = data[:-1] # trim the saver's dangling byte
|
||||
f.write(f"// {letter}: {fname} ({len(data)} bytes after trim)\n")
|
||||
f.write(f"static const uint8_t {sym}[{len(data)}] = {{\n")
|
||||
for i in range(0, len(data), 12):
|
||||
row = ", ".join(f"0x{b:02X}" for b in data[i:i + 12])
|
||||
f.write(f" {row},\n")
|
||||
f.write("};\n\n")
|
||||
entries.append((sym, len(data), level))
|
||||
f.write("typedef struct {\n")
|
||||
f.write(" const uint8_t *stream;\n")
|
||||
f.write(" uint16_t length;\n")
|
||||
f.write(" uint8_t levelIndex; // 0-based\n")
|
||||
f.write("} StDemoEntryT;\n\n")
|
||||
f.write(f"static const StDemoEntryT kDemoRotation[{len(entries)}] = {{\n")
|
||||
for sym, ln, level in entries:
|
||||
f.write(f" {{ {sym}, {ln}u, {level}u }},\n")
|
||||
f.write("};\n\n#endif\n")
|
||||
print(f"wrote {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -119,13 +119,19 @@ def gen_tilebank():
|
|||
|
||||
|
||||
def gen_sprites():
|
||||
"""sprites.png: 320x200, layout:
|
||||
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.
|
||||
"""
|
||||
"""RETIRED 2026-07-20: sprites.png is now fully extracted from the
|
||||
C64 dump by stuff/spacetaxi/extractSprites.py (taxi $C1/$C0/$DC/
|
||||
$DD, passenger $C4-$CB/$D9, flame $6DB0 table, warp $E2-$E8 --
|
||||
216x96, 4 rows). Regenerating placeholder art here would clobber
|
||||
the real sheet, the same trap gen_font has with the hand-edited
|
||||
font.png. Refuses to run; use extractSprites.py instead."""
|
||||
print("gen_sprites: REFUSING -- sprites.png is real C64 art now.")
|
||||
print(" regenerate via: python3 stuff/spacetaxi/extractSprites.py "
|
||||
"stuff/spacetaxi/raw.bin examples/spacetaxi/assets/sprites/sprites.png")
|
||||
return
|
||||
|
||||
|
||||
def gen_sprites_retired():
|
||||
img = new_canvas(9 * 24, 3 * 24) # 216x72: 9 cols x 3 rows of 24x24
|
||||
d = ImageDraw.Draw(img)
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 128 B |
BIN
generated/amiga/sprites/sprites.spc
(Stored with Git LFS)
Normal file
BIN
generated/amiga/sprites/sprites.spc
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
BIN
generated/atarist/sprites/sprites.spc
(Stored with Git LFS)
Normal file
BIN
generated/atarist/sprites/sprites.spc
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
BIN
generated/dos/sprites/sprites.spc
(Stored with Git LFS)
Normal file
BIN
generated/dos/sprites/sprites.spc
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
BIN
generated/iigs/sprites/sprites.spc
(Stored with Git LFS)
Normal file
BIN
generated/iigs/sprites/sprites.spc
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
349
spacetaxi.c
349
spacetaxi.c
|
|
@ -19,33 +19,178 @@
|
|||
#include <string.h>
|
||||
|
||||
#include "spacetaxi.h"
|
||||
#include "stDemoStreams.h"
|
||||
|
||||
// The C64 runs its physics + game logic once per GAME TICK, and a game
|
||||
// tick is 2 video frames (the main loop waits on the raster twice per
|
||||
// iteration, $5F94 + $5FA3) = ~30 Hz NTSC. The port renders at each
|
||||
// platform's own frame rate (70 Hz DOS, 50 Hz ST/Amiga, ~15 fps IIgs),
|
||||
// so gameplay is stepped on a fixed 33 ms accumulator rather than once
|
||||
// per rendered frame. Without this the cab accelerates ~2.3x too fast
|
||||
// on DOS and half-speed on the IIgs -- every per-tick constant (accel
|
||||
// 14, gravity 1, the $7D8F templates, the demo RLE counts) is authored
|
||||
// for this 30 Hz cadence, so only stepping at 30 Hz reproduces the
|
||||
// original feel, identically on every port.
|
||||
#define ST_GAME_TICK_MS 33u
|
||||
// Cap catch-up ticks per rendered frame so a hitch (disk stall, cold
|
||||
// cache) drops backlog instead of spiraling; ~165 ms simulated max.
|
||||
#define ST_MAX_CATCHUP_TICKS 5u
|
||||
|
||||
|
||||
static void applyInput(StGameT *game);
|
||||
static void demoEnter(StGameT *game);
|
||||
static void demoExitToTitle(StGameT *game);
|
||||
static void demoTick(StGameT *game);
|
||||
static void demoTickInput(StGameT *game);
|
||||
static bool loadLevelByIndex(StGameT *game, uint8_t idx);
|
||||
static void playingTick(StGameT *game);
|
||||
|
||||
|
||||
// All 24 canonical Space Taxi levels (A..X), extracted from the C64
|
||||
// ROM via stuff/spacetaxi/romToLevel.py. level01 = scene 0 = "A",
|
||||
// level24 = scene 23 = "X".
|
||||
static const char *gLevelPaths[] = {
|
||||
"DATA/levels/level01.dat", "DATA/levels/level02.dat",
|
||||
"DATA/levels/level03.dat", "DATA/levels/level04.dat",
|
||||
"DATA/levels/level05.dat", "DATA/levels/level06.dat",
|
||||
"DATA/levels/level07.dat", "DATA/levels/level08.dat",
|
||||
"DATA/levels/level09.dat", "DATA/levels/level10.dat",
|
||||
"DATA/levels/level11.dat", "DATA/levels/level12.dat",
|
||||
"DATA/levels/level13.dat", "DATA/levels/level14.dat",
|
||||
"DATA/levels/level15.dat", "DATA/levels/level16.dat",
|
||||
"DATA/levels/level17.dat", "DATA/levels/level18.dat",
|
||||
"DATA/levels/level19.dat", "DATA/levels/level20.dat",
|
||||
"DATA/levels/level21.dat", "DATA/levels/level22.dat",
|
||||
"DATA/levels/level23.dat", "DATA/levels/level24.dat",
|
||||
"levels/level01.dat", "levels/level02.dat",
|
||||
"levels/level03.dat", "levels/level04.dat",
|
||||
"levels/level05.dat", "levels/level06.dat",
|
||||
"levels/level07.dat", "levels/level08.dat",
|
||||
"levels/level09.dat", "levels/level10.dat",
|
||||
"levels/level11.dat", "levels/level12.dat",
|
||||
"levels/level13.dat", "levels/level14.dat",
|
||||
"levels/level15.dat", "levels/level16.dat",
|
||||
"levels/level17.dat", "levels/level18.dat",
|
||||
"levels/level19.dat", "levels/level20.dat",
|
||||
"levels/level21.dat", "levels/level22.dat",
|
||||
"levels/level23.dat", "levels/level24.dat",
|
||||
};
|
||||
|
||||
#define ST_LEVEL_COUNT (sizeof(gLevelPaths) / sizeof(gLevelPaths[0]))
|
||||
|
||||
|
||||
// Attract-demo rotation cursor (mirrors $5143). Playback is stepped by
|
||||
// the main loop's fixed game-tick accumulator, so no wall-clock here.
|
||||
static uint8_t gDemoRotation;
|
||||
|
||||
|
||||
// Enter (or advance to the next screen of) the rolling attract demo:
|
||||
// pick the rotation slot, load its level, seed the recorded stream
|
||||
// ($6262 seeds mask/timer from the head), zero the demo score.
|
||||
static void demoEnter(StGameT *game) {
|
||||
const StDemoEntryT *e = &kDemoRotation[gDemoRotation & 3u];
|
||||
|
||||
gDemoRotation++;
|
||||
if (!loadLevelByIndex(game, e->levelIndex)) {
|
||||
demoExitToTitle(game);
|
||||
return;
|
||||
}
|
||||
stEngineReset(game);
|
||||
game->score = 0u;
|
||||
game->demoStream = e->stream;
|
||||
game->demoLen = e->length;
|
||||
game->demoMask = e->stream[0];
|
||||
game->demoTimer = e->stream[1];
|
||||
game->demoOff = 2u;
|
||||
game->demoEnded = false;
|
||||
game->state = ST_STATE_DEMO;
|
||||
}
|
||||
|
||||
|
||||
static void demoExitToTitle(StGameT *game) {
|
||||
if (stLevelLoad(&game->level, "levels/title.dat")) {
|
||||
stRenderLevelChanged();
|
||||
}
|
||||
stRenderTitleReset(); // replay the intro -> attract cycle
|
||||
stAudioSfxThrust(false);
|
||||
game->state = ST_STATE_TITLE;
|
||||
}
|
||||
|
||||
|
||||
// Recorded-input playback, substituting for applyInput ($48F2): DEC
|
||||
// the pair timer each game tick; at zero take the next [mask][count]
|
||||
// pair. Mask bits 0-3 = UP/DOWN/LEFT/RIGHT (bit 7 is always set in
|
||||
// the recordings, no fire bit -- the demo cab never lowers its gear,
|
||||
// which is why every recorded ride ends in a crash).
|
||||
static void demoTickInput(StGameT *game) {
|
||||
StTaxiT *t = &game->taxi;
|
||||
uint8_t mask;
|
||||
bool up;
|
||||
bool down;
|
||||
bool left;
|
||||
bool right;
|
||||
|
||||
// Advance the RLE stream by exactly one game tick (the caller runs
|
||||
// this once per fixed 33 ms step, in lockstep with stEngineTick).
|
||||
if (game->demoTimer > 0u) {
|
||||
game->demoTimer--;
|
||||
}
|
||||
if (game->demoTimer == 0u) {
|
||||
if ((uint16_t)(game->demoOff + 1u) < game->demoLen) {
|
||||
game->demoMask = game->demoStream[game->demoOff];
|
||||
game->demoTimer = game->demoStream[game->demoOff + 1u];
|
||||
game->demoOff = (uint16_t)(game->demoOff + 2u);
|
||||
} else {
|
||||
// The C64 has no end-of-stream check (every recorded ride
|
||||
// crashes first); hold idle until then.
|
||||
game->demoMask = 0x80u;
|
||||
game->demoTimer = 0xFFu;
|
||||
}
|
||||
}
|
||||
mask = game->demoMask;
|
||||
up = (mask & 0x01u) != 0u;
|
||||
down = (mask & 0x02u) != 0u;
|
||||
left = (mask & 0x04u) != 0u;
|
||||
right = (mask & 0x08u) != 0u;
|
||||
t->thrustDx = (int8_t)((right ? 1 : 0) - (left ? 1 : 0));
|
||||
t->thrustDy = (int8_t)((down ? 1 : 0) - (up ? 1 : 0));
|
||||
t->thrusting = (up || down || left || right);
|
||||
t->dirMask = (uint8_t)((up ? 1u : 0u) | (down ? 2u : 0u) |
|
||||
(left ? 4u : 0u) | (right ? 8u : 0u));
|
||||
if (left) {
|
||||
t->facing = ST_DIR_LEFT;
|
||||
} else if (right) {
|
||||
t->facing = ST_DIR_RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// One fixed game-tick of the attract demo: recorded input -> physics ->
|
||||
// passenger. The main loop runs this in lockstep with the 30 Hz clock.
|
||||
static void demoTick(StGameT *game) {
|
||||
demoTickInput(game);
|
||||
stEngineTick(game);
|
||||
stPassengerTick(game);
|
||||
}
|
||||
|
||||
|
||||
// One fixed game-tick of PLAYING: warp recede, or input -> physics ->
|
||||
// passenger, with the land-SFX edge and the transporter-exit trigger.
|
||||
// The thrust SFX is left to the caller (once per rendered frame).
|
||||
static void playingTick(StGameT *game) {
|
||||
bool wasLanded;
|
||||
|
||||
if (game->taxi.warpFrame > 0u) {
|
||||
if (stEngineWarpTick(game)) {
|
||||
stPassengerTransporterExit(game);
|
||||
game->state = ST_STATE_LEVEL_DONE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
wasLanded = game->taxi.landed;
|
||||
applyInput(game);
|
||||
stEngineTick(game);
|
||||
stPassengerTick(game);
|
||||
if (!wasLanded && game->taxi.landed) {
|
||||
stAudioSfxLand();
|
||||
}
|
||||
// Flying up through the top-wall transporter exits the screen:
|
||||
// begin the shrink-warp (delivers a carried "UP PLEASE" fare and
|
||||
// advances the screen when it ends).
|
||||
if (game->taxi.crashTicks == 0u && stEngineInTransporter(game)) {
|
||||
stEngineStartWarp(game);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void applyInput(StGameT *game) {
|
||||
StTaxiT *t = &game->taxi;
|
||||
int8_t jx;
|
||||
|
|
@ -66,15 +211,36 @@ static void applyInput(StGameT *game) {
|
|||
left = jlKeyDown(KEY_LEFT) || jx < -32;
|
||||
right = jlKeyDown(KEY_RIGHT) || jx > 32;
|
||||
|
||||
// C64 Space Taxi gameplay only acts on the 4 directional bits of
|
||||
// $DC00; the fire button is masked out except on menu/title
|
||||
// screens. (Confirmed via disassembly at $6040-$60D6 in mem0801:
|
||||
// the main input handler only loads X/Y velocity templates when
|
||||
// L/R or U/D are held, fire is never tested in this path.)
|
||||
// Menu fire-to-advance is handled by main()'s state switch.
|
||||
// FIRE is the LANDING GEAR ($63DD): a press EDGE while airborne
|
||||
// toggles the gear with an SFX; pressing UP while parked on a pad
|
||||
// is the takeoff, which retracts the gear ($65AD AND #$FE). (The
|
||||
// old comment here claiming "fire is never tested in this path"
|
||||
// was wrong -- $63F1-$6401 tests it every tick.)
|
||||
{
|
||||
bool fire = jlKeyDown(KEY_SPACE) || jlJoyDown(JOYSTICK_0, JOY_BUTTON_0);
|
||||
if (fire && !t->fireHeld && !t->landed) {
|
||||
t->gearDown = !t->gearDown;
|
||||
stAudioSfxGear();
|
||||
}
|
||||
t->fireHeld = fire;
|
||||
}
|
||||
if (up && t->landed) {
|
||||
t->gearDown = false; // takeoff retracts the gear
|
||||
}
|
||||
// Gear down strips lateral thrust BEFORE the direction mask is
|
||||
// built ($6190 AND #$13 runs upstream of the $60D6 mask store),
|
||||
// so the flame table never sees L/R with the gear down either.
|
||||
if (t->gearDown) {
|
||||
left = false;
|
||||
right = false;
|
||||
}
|
||||
t->thrustDx = (int8_t)((right ? 1 : 0) - (left ? 1 : 0));
|
||||
t->thrustDy = (int8_t)((down ? 1 : 0) - (up ? 1 : 0));
|
||||
t->thrusting = (up || down || left || right);
|
||||
// $716A-style mask for the flame cel table (1=UP 2=DOWN 4=LEFT
|
||||
// 8=RIGHT, same encoding kFlameCelByDirMask indexes).
|
||||
t->dirMask = (uint8_t)((up ? 1u : 0u) | (down ? 2u : 0u) |
|
||||
(left ? 4u : 0u) | (right ? 8u : 0u));
|
||||
|
||||
// Facing is purely cosmetic (sprite cel selection). Update when
|
||||
// horizontal thrust is commanded; keep last facing otherwise so
|
||||
|
|
@ -103,14 +269,26 @@ static bool loadLevelByIndex(StGameT *game, uint8_t idx) {
|
|||
int main(void) {
|
||||
jlConfigT config;
|
||||
jlSurfaceT *stage;
|
||||
StGameT game;
|
||||
// game embeds a full StLevelT (tilemap + colormap = ~2 KB). As a
|
||||
// stack local it held ~2 KB across the whole program, overflowing the
|
||||
// IIgs (llvm-mos) soft stack once the deep asset-load call chain ran
|
||||
// (stRenderInit -> loadSpriteSheet -> jlSpriteBankLoad -> fopen) and
|
||||
// silently crashing back to GS/OS. It is a single program-lifetime
|
||||
// instance, so static (BSS) is both correct and stack-friendly.
|
||||
static StGameT game;
|
||||
|
||||
// Sprite codegen arena: after Phase 11 (shared-walker rewrite of
|
||||
// the planar sprite path), sprites no longer consume arena bytes
|
||||
// -- the per-cel work happens inside halSpriteDrawPlanes /
|
||||
// halSpriteSavePlanes / halSpriteRestorePlanes as static lib code.
|
||||
// 32 KB is plenty for whatever other codegen needs are around.
|
||||
config.codegenBytes = 32UL * 1024;
|
||||
// Sprite codegen: the 26 drawn cels (taxi/warp/passenger/flame,
|
||||
// 24x24 = 3x3-tile) each compile to per-shift save/draw/restore asm.
|
||||
// Compiling them is what makes sprite blitting fast (the interpreter
|
||||
// costs ~200 ms/frame on the IIgs 65816). The arena is capped at one
|
||||
// 64 KB bank (codegenArena attrNoCross); 60 KB fits all 26 cels with
|
||||
// headroom. The old 32 KB was sized for procedural sprites only.
|
||||
config.codegenBytes = 60UL * 1024;
|
||||
config.audioBytes = 32UL * 1024; // music + SFX
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
@ -145,7 +323,7 @@ int main(void) {
|
|||
// capture. Falls through to a black field if the asset is
|
||||
// missing.
|
||||
jlLogF("spacetaxi: stLevelLoad title.dat ...");
|
||||
if (stLevelLoad(&game.level, "DATA/levels/title.dat")) {
|
||||
if (stLevelLoad(&game.level, "levels/title.dat")) {
|
||||
jlLogF("spacetaxi: title loaded, tilebank=%u",
|
||||
(unsigned)game.level.tileBankId);
|
||||
stRenderLevelChanged();
|
||||
|
|
@ -154,12 +332,39 @@ int main(void) {
|
|||
}
|
||||
jlLogFlush();
|
||||
|
||||
// Fixed-timestep gameplay clock. gTickAccumMs banks real elapsed
|
||||
// time; each frame consumes it in whole 30 Hz game ticks so physics
|
||||
// advance at the C64's rate regardless of the render frame rate.
|
||||
uint32_t gPrevTickMs = jlMillisElapsed();
|
||||
uint32_t gTickAccumMs = 0u;
|
||||
|
||||
for (;;) {
|
||||
uint8_t pendingTicks;
|
||||
uint8_t tick;
|
||||
|
||||
jlInputPoll();
|
||||
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||
break;
|
||||
}
|
||||
|
||||
{
|
||||
uint32_t nowMs = jlMillisElapsed();
|
||||
uint32_t want;
|
||||
gTickAccumMs += (uint32_t)(nowMs - gPrevTickMs);
|
||||
gPrevTickMs = nowMs;
|
||||
want = gTickAccumMs / ST_GAME_TICK_MS; // wide: no cast wrap
|
||||
if (want > ST_MAX_CATCHUP_TICKS) {
|
||||
// A long stall (level load, disk seek): resync to now
|
||||
// and skip the gap instead of spiraling. Zero the whole
|
||||
// accumulator so no backlog survives the truncated cast.
|
||||
want = ST_MAX_CATCHUP_TICKS;
|
||||
gTickAccumMs = 0u;
|
||||
} else {
|
||||
gTickAccumMs -= want * ST_GAME_TICK_MS; // keep sub-tick remainder
|
||||
}
|
||||
pendingTicks = (uint8_t)want; // guaranteed <= 5
|
||||
}
|
||||
|
||||
switch (game.state) {
|
||||
case ST_STATE_TITLE:
|
||||
{
|
||||
|
|
@ -185,7 +390,56 @@ int main(void) {
|
|||
jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) {
|
||||
game.fareTarget = 1u; // C64 default ($48AF sets 1)
|
||||
game.state = ST_STATE_OPTIONS;
|
||||
break;
|
||||
}
|
||||
// Advance the sparkle -> walk -> takeoff intro (and the
|
||||
// logo flip) on the fixed 30 Hz clock, so pre-game
|
||||
// timing is identical on every port.
|
||||
for (tick = 0u;
|
||||
tick < pendingTicks && game.state == ST_STATE_TITLE;
|
||||
tick++) {
|
||||
stRenderTitleAdvance();
|
||||
}
|
||||
// Intro finished (cab took off): roll the attract
|
||||
// demo, exactly like the C64's post-takeoff flow.
|
||||
if (stRenderTitleIntroDone()) {
|
||||
demoEnter(&game);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ST_STATE_DEMO:
|
||||
{
|
||||
// Any real input exits the attract loop ($5006 tests
|
||||
// $DC00 != $7F).
|
||||
int8_t jx = jlJoystickX(JOYSTICK_0);
|
||||
int8_t jy = jlJoystickY(JOYSTICK_0);
|
||||
if (jlKeyPressed(KEY_SPACE) ||
|
||||
jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0) ||
|
||||
jlKeyDown(KEY_UP) || jlKeyDown(KEY_DOWN) ||
|
||||
jlKeyDown(KEY_LEFT) || jlKeyDown(KEY_RIGHT) ||
|
||||
jx < -32 || jx > 32 || jy < -32 || jy > 32) {
|
||||
demoExitToTitle(&game);
|
||||
break;
|
||||
}
|
||||
if (game.demoEnded) {
|
||||
// The recorded ride crashed out; $6B57 skips the
|
||||
// life bookkeeping in demo mode -- back to title.
|
||||
demoExitToTitle(&game);
|
||||
break;
|
||||
}
|
||||
if (game.score != 0u) {
|
||||
// Fare delivered: advance the rotation ($501D ->
|
||||
// $50C6) without visiting the title.
|
||||
demoEnter(&game);
|
||||
break;
|
||||
}
|
||||
for (tick = 0u;
|
||||
tick < pendingTicks && game.state == ST_STATE_DEMO &&
|
||||
!game.demoEnded && game.score == 0u;
|
||||
tick++) {
|
||||
demoTick(&game);
|
||||
}
|
||||
stAudioSfxThrust(game.taxi.thrusting);
|
||||
break;
|
||||
}
|
||||
case ST_STATE_HIGHSCORES:
|
||||
|
|
@ -196,6 +450,7 @@ int main(void) {
|
|||
if (jlKeyPressed(KEY_SPACE) ||
|
||||
jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) {
|
||||
stRenderLevelChanged();
|
||||
stRenderTitleReset();
|
||||
game.state = ST_STATE_TITLE;
|
||||
}
|
||||
break;
|
||||
|
|
@ -244,36 +499,18 @@ int main(void) {
|
|||
break;
|
||||
}
|
||||
case ST_STATE_PLAYING:
|
||||
{
|
||||
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;
|
||||
// Advance physics at the fixed 30 Hz cadence -- each
|
||||
// playingTick handles the warp recede, physics, passenger,
|
||||
// land SFX, and transporter-exit trigger. Stop early if a
|
||||
// tick changed state (crash game-over, level done).
|
||||
for (tick = 0u;
|
||||
tick < pendingTicks && game.state == ST_STATE_PLAYING;
|
||||
tick++) {
|
||||
playingTick(&game);
|
||||
}
|
||||
break;
|
||||
}
|
||||
wasLanded = game.taxi.landed;
|
||||
applyInput(&game);
|
||||
stEngineTick(&game);
|
||||
stPassengerTick(&game);
|
||||
// Edge-trigger SFX from state deltas.
|
||||
// Continuous thrust SFX reflects the final tick's state.
|
||||
stAudioSfxThrust(game.taxi.thrusting);
|
||||
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:
|
||||
// No music to stop -- gameplay is silent. The C64 plays
|
||||
// a score-screen jingle (song 7 / song 6) between levels;
|
||||
|
|
@ -283,9 +520,10 @@ int main(void) {
|
|||
game.state = ST_STATE_PLAYING;
|
||||
} else {
|
||||
// ran out of levels -- back to title (player wins)
|
||||
if (stLevelLoad(&game.level, "DATA/levels/title.dat")) {
|
||||
if (stLevelLoad(&game.level, "levels/title.dat")) {
|
||||
stRenderLevelChanged();
|
||||
}
|
||||
stRenderTitleReset();
|
||||
game.state = ST_STATE_TITLE;
|
||||
}
|
||||
break;
|
||||
|
|
@ -294,9 +532,10 @@ int main(void) {
|
|||
// Restore the title tilemap so the press-start
|
||||
// overlay isn't sitting on top of the last-played
|
||||
// level's art.
|
||||
if (stLevelLoad(&game.level, "DATA/levels/title.dat")) {
|
||||
if (stLevelLoad(&game.level, "levels/title.dat")) {
|
||||
stRenderLevelChanged();
|
||||
}
|
||||
stRenderTitleReset();
|
||||
game.state = ST_STATE_TITLE;
|
||||
}
|
||||
break;
|
||||
|
|
@ -305,9 +544,13 @@ int main(void) {
|
|||
break;
|
||||
}
|
||||
|
||||
// Silence continuous thrust SFX whenever we're not actively
|
||||
// playing (title, level-intro, game-over, level-done).
|
||||
if (game.state != ST_STATE_PLAYING) {
|
||||
// Silence continuous thrust SFX whenever no cab is flying
|
||||
// under thrust (title, menus, game-over, level-done). The
|
||||
// attract DEMO is a recorded gameplay session and thrusts
|
||||
// audibly, so it keeps its own per-tick thrust SFX -- without
|
||||
// this exclusion the blanket silence overrode it and the demo
|
||||
// cab flamed in total silence.
|
||||
if (game.state != ST_STATE_PLAYING && game.state != ST_STATE_DEMO) {
|
||||
stAudioSfxThrust(false);
|
||||
}
|
||||
|
||||
|
|
|
|||
79
spacetaxi.h
79
spacetaxi.h
|
|
@ -20,8 +20,11 @@
|
|||
// Ensoniq DOC channels (IIgs). The dispatch lives in
|
||||
// stAudio.c with one entry point per voice command.
|
||||
//
|
||||
// input Joystick port 2 conventionally on the C64; we map
|
||||
// thrust to fire and direction to all four cardinals.
|
||||
// input Joystick port 2 conventionally on the C64: direction =
|
||||
// the four cardinals (thrust), FIRE = the LANDING GEAR
|
||||
// toggle while airborne ($63DD: press edge EORs $7197
|
||||
// bit 0 with an SFX; $6462 requires gear down to land;
|
||||
// $6190 strips left/right thrust while gear is down).
|
||||
// Keyboard fallback: arrows + space for hosts without
|
||||
// joysticks.
|
||||
//
|
||||
|
|
@ -96,15 +99,10 @@
|
|||
#define ST_SUBPIXEL_SHIFT 8
|
||||
#define ST_SUBPIXEL (1 << ST_SUBPIXEL_SHIFT)
|
||||
|
||||
// Taxi thrust cel cycling. The sprite asset
|
||||
// (assets/genPlaceholderArt.py) lays out 4 taxi cels:
|
||||
// cel 0 = idle (no thrust)
|
||||
// cel 1..3 = thrust-flame frames (cycled while input is held)
|
||||
// thrustFrame on StTaxiT counts 0..(ST_THRUST_CEL_COUNT *
|
||||
// ST_THRUST_CEL_TICKS - 1) while thrusting. Renderer divides by
|
||||
// ST_THRUST_CEL_TICKS to pick cel 1, 2, or 3.
|
||||
#define ST_THRUST_CEL_COUNT 3u
|
||||
#define ST_THRUST_CEL_TICKS 2u
|
||||
// Taxi cels are the real C64 art (extractSprites.py): the $C0/$C1
|
||||
// right-facing and $DC/$DD left-facing pairs with the $6A95 per-frame
|
||||
// low-bit flicker; the engine flame is the separate direction-indexed
|
||||
// sprite-2 cel set. Selection lives in stRender.c.
|
||||
|
||||
|
||||
typedef enum {
|
||||
|
|
@ -112,6 +110,7 @@ typedef enum {
|
|||
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_DEMO, // rolling attract demo (recorded input playback)
|
||||
ST_STATE_PLAYING,
|
||||
ST_STATE_LEVEL_DONE,
|
||||
ST_STATE_GAME_OVER
|
||||
|
|
@ -188,10 +187,19 @@ typedef struct {
|
|||
int16_t vx; // velocity accumulator (sub-pixel/frame)
|
||||
int16_t vy;
|
||||
StFacingE facing;
|
||||
uint8_t thrustFrame; // 0..1 parity bit driving cab-cel flicker
|
||||
bool thrusting; // any directional input held this frame
|
||||
int8_t thrustDx; // -1 left, 0 neutral, +1 right
|
||||
int8_t thrustDy; // -1 up, 0 neutral, +1 down
|
||||
// Held-direction bit mask, mirroring $716A's encoding (the index
|
||||
// into the $6DB0 flame cel table): 1=UP 2=DOWN 4=LEFT 8=RIGHT.
|
||||
// With gear down the left/right bits are stripped before this is
|
||||
// built, exactly like $6190 AND #$13 feeding $60D6.
|
||||
uint8_t dirMask;
|
||||
// Landing gear ($7197 bit 0). Toggled by a FIRE press edge while
|
||||
// airborne, retracted on takeoff ($65B0), REQUIRED down to land
|
||||
// ($6462) -- gear-up contact with a pad is a crash.
|
||||
bool gearDown;
|
||||
bool fireHeld; // previous-frame fire state (edge detect)
|
||||
bool landed; // sitting on a pad
|
||||
uint8_t onPad; // pad index if landed (0xFF if none)
|
||||
// Death-animation countdown. >0 means crashed -- engine keeps the
|
||||
|
|
@ -200,6 +208,9 @@ typedef struct {
|
|||
// branch"). Reaches 0 -> respawn (or game-over). Mirrors the
|
||||
// C64's $6B24/$6B4C phase-2/3 timing.
|
||||
uint8_t crashTicks;
|
||||
// Crash phase split ($6A72 fall vs $6B2A impact walk): false while
|
||||
// the debris tumbles, true once it hits the floor.
|
||||
bool crashImpacted;
|
||||
// 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.
|
||||
|
|
@ -207,15 +218,40 @@ typedef struct {
|
|||
} StTaxiT;
|
||||
|
||||
|
||||
// Passenger lifecycle phases (see the field comments in StPassengerT
|
||||
// for the C64 sequences each one mirrors).
|
||||
typedef enum {
|
||||
ST_PASS_BEAM_IN = 0, // materialize at standX ($CB..$C7)
|
||||
ST_PASS_WAIT, // stand + wave ($66D6 cycle)
|
||||
ST_PASS_WALK_TO_CAB, // walk to the captured taxi X
|
||||
ST_PASS_BOARD_OUT, // shrink into the cab ($C7..$CB)
|
||||
ST_PASS_RIDING, // aboard, invisible
|
||||
ST_PASS_DROP_IN, // delivery: materialize beside the cab
|
||||
ST_PASS_WALK_TO_PAD, // walk to the destination standX
|
||||
ST_PASS_DROP_OUT // shrink out -- fare complete
|
||||
} StPassPhaseE;
|
||||
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
bool onboard; // in the cab (true) or waiting at pad (false)
|
||||
uint8_t currentPad; // where they are if waiting
|
||||
uint8_t destPad;
|
||||
int16_t x; // pixel position (for waiting/walking)
|
||||
int16_t x; // pixel position
|
||||
int16_t y;
|
||||
uint8_t walkPhase; // animation cel (0..3)
|
||||
uint8_t walkDir; // 0 = walking right, 1 = walking left
|
||||
// Passenger lifecycle, per the traced sequences: materialize
|
||||
// ($CB..$C7 grow-in), wave in place ($66D6 cycle -- he NEVER
|
||||
// walks while waiting), walk to the landed cab ($6811 captures
|
||||
// the taxi X once; $C4/$C5 leftward, $C2/$C3 rightward, +/-2 px
|
||||
// per 3-tick step), shrink into the cab ($C7..$CB), ride, and
|
||||
// the delivery mirror (materialize at the cab, walk to standX,
|
||||
// shrink out).
|
||||
StPassPhaseE phase;
|
||||
int16_t targetX; // walk destination (captured once)
|
||||
// Step counter within the current phase; in ST_PASS_WAIT it is
|
||||
// the 0..3 wave-cycle index, in the walk phases its low bit is
|
||||
// the stride cel alternation.
|
||||
uint8_t waitPhase;
|
||||
} StPassengerT;
|
||||
|
||||
|
||||
|
|
@ -227,6 +263,15 @@ typedef struct {
|
|||
uint32_t score;
|
||||
uint8_t lives;
|
||||
uint8_t levelIndex;
|
||||
// Attract-demo playback state (ST_STATE_DEMO): the recorded RLE
|
||||
// input stream and its cursor, plus the crash-finished signal the
|
||||
// engine raises instead of decrementing lives ($6B57).
|
||||
const uint8_t *demoStream;
|
||||
uint16_t demoLen;
|
||||
uint16_t demoOff;
|
||||
uint8_t demoTimer;
|
||||
uint8_t demoMask;
|
||||
bool demoEnded;
|
||||
// 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
|
||||
|
|
@ -249,6 +294,9 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game);
|
|||
// after stLevelLoad even when the StLevelT pointer is unchanged --
|
||||
// stRenderLevel's dirty-cache compares pointers, not contents.
|
||||
void stRenderLevelChanged(void);
|
||||
void stRenderTitleAdvance(void);
|
||||
bool stRenderTitleIntroDone(void);
|
||||
void stRenderTitleReset(void);
|
||||
|
||||
// Draw an ASCII string into the stage at tile coords (bx, by) using
|
||||
// the loaded font asset. No-op if the font asset failed to load.
|
||||
|
|
@ -278,6 +326,7 @@ void stAudioFrameTick(void); // call once per host frame; counts down SFX
|
|||
void stAudioPlayMusic(uint8_t musicId);
|
||||
void stAudioStopMusic(void);
|
||||
void stAudioSfxThrust(bool on);
|
||||
void stAudioSfxGear(void);
|
||||
void stAudioSfxLand(void);
|
||||
void stAudioSfxPickup(void);
|
||||
void stAudioSfxDropoff(void);
|
||||
|
|
|
|||
11
stAudio.c
11
stAudio.c
|
|
@ -34,6 +34,8 @@
|
|||
#define ST_SFX_DROPOFF_TICKS 25u
|
||||
#define ST_SFX_LAND_HZ 90u
|
||||
#define ST_SFX_LAND_TICKS 12u
|
||||
#define ST_SFX_GEAR_HZ 140u // approximation -- $6410 table untraced
|
||||
#define ST_SFX_GEAR_TICKS 5u
|
||||
|
||||
// C64-matched SFX behavior (see MECHANICS.md "Sound (SID)"):
|
||||
//
|
||||
|
|
@ -191,6 +193,15 @@ void stAudioSfxThrust(bool on) {
|
|||
}
|
||||
|
||||
|
||||
void stAudioSfxGear(void) {
|
||||
// $63FC-$640C: the gear toggle fires SFX ptr $6410 through $42E9.
|
||||
// The exact $6410 waveform is untraced; a short low click stands
|
||||
// in until that SFX table is decoded -- marked approximation, not
|
||||
// ROM-derived.
|
||||
armSfx(ST_SFX_GEAR_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_GEAR_TICKS);
|
||||
}
|
||||
|
||||
|
||||
void stAudioSfxLand(void) {
|
||||
armSfx(ST_SFX_LAND_HZ, ST_SFX_DEFAULT_ATTEN, ST_SFX_LAND_TICKS);
|
||||
}
|
||||
|
|
|
|||
171
stDemoStreams.h
Normal file
171
stDemoStreams.h
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// Generated by assets/genDemoStreams.py -- do not hand-edit.
|
||||
// C64 attract-demo recorded input streams (see the generator
|
||||
// for format notes). Rotation order H, W, T, X.
|
||||
|
||||
#ifndef ST_DEMO_STREAMS_H
|
||||
#define ST_DEMO_STREAMS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// H: SPACETAXI 01_N0S.prg (296 bytes after trim)
|
||||
static const uint8_t kDemoStreamH[296] = {
|
||||
0x80, 0x17, 0x88, 0x0A, 0x80, 0x0A, 0x84, 0x04, 0x85, 0x03, 0x81, 0x03,
|
||||
0x80, 0x0A, 0x88, 0x04, 0x80, 0x01, 0x81, 0x0E, 0x80, 0x0D, 0x82, 0x02,
|
||||
0x80, 0x0A, 0x81, 0x09, 0x85, 0x07, 0x84, 0x05, 0x80, 0x02, 0x88, 0x05,
|
||||
0x8A, 0x04, 0x82, 0x03, 0x80, 0x05, 0x85, 0x10, 0x81, 0x08, 0x89, 0x02,
|
||||
0x88, 0x0E, 0x8A, 0x03, 0x88, 0x06, 0x81, 0x07, 0x80, 0x12, 0x82, 0x05,
|
||||
0x80, 0x04, 0x81, 0x11, 0x80, 0x04, 0x82, 0x04, 0x86, 0x09, 0x84, 0x02,
|
||||
0x95, 0x07, 0x85, 0x04, 0x81, 0x01, 0x80, 0x43, 0x81, 0x0A, 0x84, 0x05,
|
||||
0x86, 0x03, 0x82, 0x06, 0x80, 0x07, 0x81, 0x09, 0x80, 0x04, 0x88, 0x0B,
|
||||
0x80, 0x09, 0x84, 0x04, 0x80, 0x09, 0x81, 0x01, 0x89, 0x04, 0x80, 0x08,
|
||||
0x85, 0x04, 0x81, 0x01, 0x80, 0x05, 0x81, 0x15, 0x80, 0x0C, 0x82, 0x02,
|
||||
0x86, 0x07, 0x80, 0x05, 0x81, 0x09, 0x80, 0x13, 0x84, 0x01, 0x85, 0x03,
|
||||
0x80, 0x06, 0x88, 0x0B, 0x80, 0x16, 0x81, 0x0A, 0x80, 0x01, 0x88, 0x04,
|
||||
0x80, 0x0C, 0x81, 0x03, 0x85, 0x02, 0x84, 0x03, 0x85, 0x0C, 0x81, 0x01,
|
||||
0x80, 0x0D, 0x82, 0x07, 0x80, 0x06, 0x81, 0x02, 0x89, 0x0C, 0x88, 0x02,
|
||||
0x80, 0x05, 0x82, 0x04, 0x80, 0x0A, 0x81, 0x04, 0x85, 0x01, 0x84, 0x04,
|
||||
0x80, 0x0D, 0x81, 0x05, 0x89, 0x0E, 0x81, 0x01, 0x80, 0x11, 0x84, 0x17,
|
||||
0x80, 0x05, 0x81, 0x04, 0x80, 0x19, 0x81, 0x03, 0x89, 0x01, 0x88, 0x0E,
|
||||
0x80, 0x07, 0x84, 0x05, 0x80, 0x0C, 0x81, 0x0D, 0x80, 0x19, 0x88, 0x01,
|
||||
0x80, 0x02, 0x88, 0x04, 0x80, 0x1B, 0x88, 0x0A, 0x89, 0x0B, 0x81, 0x14,
|
||||
0x80, 0x02, 0x82, 0x03, 0x80, 0x05, 0x84, 0x03, 0x85, 0x09, 0x84, 0x05,
|
||||
0x85, 0x05, 0x84, 0x02, 0x86, 0x08, 0x84, 0x01, 0x80, 0x01, 0x81, 0x03,
|
||||
0x89, 0x12, 0x81, 0x08, 0x89, 0x01, 0x88, 0x07, 0x8A, 0x04, 0x82, 0x0F,
|
||||
0x80, 0x07, 0x81, 0x04, 0x80, 0x03, 0x82, 0x03, 0x86, 0x09, 0x84, 0x08,
|
||||
0x81, 0x03, 0x89, 0x06, 0x80, 0x01, 0x86, 0x08, 0x82, 0x01, 0x80, 0x03,
|
||||
0x88, 0x03, 0x80, 0x04, 0x95, 0x07, 0x85, 0x0A,
|
||||
};
|
||||
|
||||
// W: SPACETAXI 03_N0S.prg (334 bytes after trim)
|
||||
static const uint8_t kDemoStreamW[334] = {
|
||||
0x80, 0x0E, 0x82, 0x05, 0x8A, 0x01, 0x88, 0x0F, 0x89, 0x06, 0x81, 0x06,
|
||||
0x85, 0x0D, 0x84, 0x07, 0x86, 0x0C, 0x82, 0x01, 0x88, 0x09, 0x89, 0x02,
|
||||
0x81, 0x02, 0x80, 0x05, 0x88, 0x04, 0x80, 0x06, 0x84, 0x04, 0x80, 0x02,
|
||||
0x88, 0x13, 0x81, 0x01, 0x80, 0x01, 0x84, 0x0B, 0x85, 0x07, 0x80, 0x08,
|
||||
0x88, 0x14, 0x80, 0x02, 0x84, 0x0F, 0x80, 0x0D, 0x84, 0x05, 0x85, 0x03,
|
||||
0x80, 0x04, 0x88, 0x16, 0x81, 0x01, 0x80, 0x03, 0x84, 0x02, 0x86, 0x12,
|
||||
0x80, 0x09, 0x81, 0x18, 0x80, 0x0B, 0x81, 0x05, 0x85, 0x06, 0x84, 0x08,
|
||||
0x86, 0x0A, 0x84, 0x01, 0x80, 0x01, 0x81, 0x02, 0x89, 0x0B, 0x88, 0x0C,
|
||||
0x80, 0x17, 0x82, 0x01, 0x86, 0x0B, 0x84, 0x02, 0x85, 0x09, 0x81, 0x01,
|
||||
0x89, 0x01, 0x88, 0x1E, 0x80, 0x03, 0x84, 0x02, 0x85, 0x06, 0x95, 0x02,
|
||||
0x94, 0x04, 0x90, 0x01, 0x80, 0x09, 0x82, 0x06, 0x80, 0x07, 0x81, 0x04,
|
||||
0x80, 0x0E, 0x81, 0x01, 0x80, 0x38, 0x81, 0x07, 0x89, 0x01, 0x88, 0x05,
|
||||
0x80, 0x0C, 0x84, 0x01, 0x85, 0x05, 0x81, 0x01, 0x80, 0x10, 0x88, 0x04,
|
||||
0x8A, 0x0E, 0x88, 0x02, 0x89, 0x04, 0x81, 0x04, 0x85, 0x01, 0x84, 0x22,
|
||||
0x81, 0x03, 0x89, 0x0A, 0x81, 0x01, 0x80, 0x0E, 0x82, 0x08, 0x8A, 0x08,
|
||||
0x88, 0x09, 0x89, 0x02, 0x81, 0x07, 0x85, 0x03, 0x84, 0x03, 0x85, 0x0F,
|
||||
0x81, 0x02, 0x89, 0x06, 0x88, 0x04, 0x80, 0x02, 0x84, 0x06, 0x86, 0x03,
|
||||
0x82, 0x05, 0x80, 0x01, 0x89, 0x07, 0x88, 0x02, 0x80, 0x04, 0x86, 0x16,
|
||||
0x84, 0x02, 0x85, 0x03, 0x81, 0x04, 0x89, 0x09, 0x88, 0x05, 0x80, 0x11,
|
||||
0x88, 0x02, 0x8A, 0x0A, 0x82, 0x01, 0x80, 0x04, 0x84, 0x08, 0x85, 0x12,
|
||||
0x84, 0x06, 0x81, 0x01, 0x89, 0x07, 0x88, 0x07, 0x8A, 0x14, 0x82, 0x02,
|
||||
0x86, 0x01, 0x84, 0x03, 0x85, 0x07, 0x95, 0x03, 0x91, 0x02, 0x80, 0x0E,
|
||||
0x98, 0x04, 0x99, 0x01, 0x89, 0x06, 0x81, 0x03, 0x85, 0x02, 0x84, 0x03,
|
||||
0x86, 0x01, 0x82, 0x04, 0x8A, 0x01, 0x88, 0x03, 0x80, 0x05, 0x90, 0x01,
|
||||
0x94, 0x05, 0x84, 0x01, 0x80, 0x08, 0x84, 0x05, 0x80, 0x78, 0x81, 0x0D,
|
||||
0x80, 0x07, 0x82, 0x06, 0x80, 0x2D, 0x88, 0x08, 0x8A, 0x02, 0x88, 0x0C,
|
||||
0x80, 0x02, 0x84, 0x06, 0x85, 0x0F, 0x81, 0x02, 0x89, 0x04, 0x88, 0x07,
|
||||
0x8A, 0x0B, 0x82, 0x05, 0x86, 0x03, 0x84, 0x07, 0x85, 0x03,
|
||||
};
|
||||
|
||||
// T: SPACETAXI 02_N0S.prg (374 bytes after trim)
|
||||
static const uint8_t kDemoStreamT[374] = {
|
||||
0x89, 0x0B, 0x88, 0x03, 0x80, 0x04, 0x86, 0x05, 0x84, 0x07, 0x80, 0x01,
|
||||
0x81, 0x0A, 0x80, 0x06, 0x86, 0x02, 0x84, 0x02, 0x80, 0x09, 0x84, 0x04,
|
||||
0x80, 0x04, 0x81, 0x01, 0x89, 0x04, 0x81, 0x01, 0x80, 0x0B, 0x84, 0x02,
|
||||
0x80, 0x18, 0x81, 0x06, 0x80, 0x09, 0x88, 0x04, 0x80, 0x09, 0x81, 0x04,
|
||||
0x80, 0x0C, 0x81, 0x07, 0x80, 0x0E, 0x88, 0x05, 0x80, 0x07, 0x81, 0x05,
|
||||
0x85, 0x02, 0x81, 0x05, 0x85, 0x01, 0x84, 0x05, 0x80, 0x04, 0x81, 0x0B,
|
||||
0x80, 0x0B, 0x81, 0x0B, 0x80, 0x0A, 0x81, 0x09, 0x80, 0x06, 0x88, 0x0C,
|
||||
0x80, 0x0D, 0x84, 0x05, 0x80, 0x0C, 0x81, 0x07, 0x80, 0x05, 0x84, 0x03,
|
||||
0x80, 0x0D, 0x85, 0x04, 0x81, 0x0C, 0x80, 0x0C, 0x84, 0x03, 0x80, 0x06,
|
||||
0x81, 0x0C, 0x80, 0x0A, 0x81, 0x0A, 0x80, 0x06, 0x81, 0x03, 0x85, 0x04,
|
||||
0x81, 0x01, 0x80, 0x07, 0x88, 0x05, 0x8A, 0x04, 0x80, 0x04, 0x88, 0x05,
|
||||
0x89, 0x0B, 0x81, 0x06, 0x80, 0x07, 0x82, 0x05, 0x80, 0x03, 0x81, 0x08,
|
||||
0x80, 0x02, 0x84, 0x05, 0x86, 0x04, 0x82, 0x01, 0x80, 0x08, 0x85, 0x12,
|
||||
0x81, 0x09, 0x89, 0x04, 0x81, 0x03, 0x85, 0x09, 0x81, 0x02, 0x80, 0x13,
|
||||
0x81, 0x04, 0x80, 0x0D, 0x81, 0x02, 0x80, 0x06, 0x84, 0x04, 0x80, 0x04,
|
||||
0x81, 0x03, 0x80, 0x03, 0x81, 0x03, 0x80, 0x05, 0x82, 0x04, 0x80, 0x07,
|
||||
0x81, 0x0B, 0x80, 0x03, 0x86, 0x03, 0x82, 0x01, 0x80, 0x14, 0x81, 0x03,
|
||||
0x80, 0x06, 0x81, 0x05, 0x80, 0x0C, 0x82, 0x03, 0x80, 0x04, 0x81, 0x06,
|
||||
0x80, 0x07, 0x82, 0x02, 0x80, 0x06, 0x81, 0x04, 0x80, 0x07, 0x88, 0x05,
|
||||
0x80, 0x08, 0x88, 0x04, 0x80, 0x06, 0x91, 0x02, 0x95, 0x03, 0x85, 0x01,
|
||||
0x84, 0x02, 0x80, 0x04, 0x82, 0x05, 0x80, 0x41, 0x81, 0x05, 0x80, 0x02,
|
||||
0x84, 0x06, 0x80, 0x07, 0x82, 0x04, 0x80, 0x05, 0x81, 0x08, 0x80, 0x10,
|
||||
0x81, 0x02, 0x80, 0x06, 0x82, 0x01, 0x80, 0x0D, 0x88, 0x07, 0x80, 0x0C,
|
||||
0x84, 0x04, 0x80, 0x14, 0x88, 0x03, 0x80, 0x1E, 0x81, 0x0C, 0x89, 0x01,
|
||||
0x88, 0x05, 0x80, 0x08, 0x84, 0x05, 0x80, 0x09, 0x88, 0x08, 0x89, 0x0F,
|
||||
0x88, 0x03, 0x8A, 0x05, 0x82, 0x01, 0x80, 0x02, 0x81, 0x07, 0x85, 0x0C,
|
||||
0x81, 0x01, 0x80, 0x01, 0x88, 0x01, 0x8A, 0x02, 0x82, 0x02, 0x80, 0x03,
|
||||
0x84, 0x0C, 0x86, 0x04, 0x84, 0x02, 0x80, 0x17, 0x91, 0x05, 0x80, 0x07,
|
||||
0x82, 0x03, 0x80, 0x5D, 0x81, 0x08, 0x80, 0x05, 0x82, 0x05, 0x80, 0x04,
|
||||
0x81, 0x01, 0x85, 0x02, 0x84, 0x07, 0x81, 0x04, 0x80, 0x06, 0x88, 0x06,
|
||||
0x80, 0x0E, 0x81, 0x06, 0x80, 0x0D, 0x81, 0x0D, 0x84, 0x05, 0x85, 0x0F,
|
||||
0x81, 0x06,
|
||||
};
|
||||
|
||||
// X: SPACETAXI 04_N0S.prg (564 bytes after trim)
|
||||
static const uint8_t kDemoStreamX[564] = {
|
||||
0x84, 0x07, 0x85, 0x06, 0x84, 0x06, 0x80, 0x0A, 0x81, 0x05, 0x80, 0x0C,
|
||||
0x88, 0x07, 0x81, 0x05, 0x88, 0x08, 0x81, 0x07, 0x80, 0x07, 0x88, 0x04,
|
||||
0x8A, 0x02, 0x88, 0x01, 0x80, 0x0A, 0x81, 0x07, 0x80, 0x08, 0x81, 0x07,
|
||||
0x85, 0x01, 0x84, 0x06, 0x80, 0x0E, 0x81, 0x06, 0x80, 0x0C, 0x88, 0x01,
|
||||
0x80, 0x0C, 0x81, 0x07, 0x80, 0x0D, 0x88, 0x07, 0x80, 0x01, 0x81, 0x06,
|
||||
0x80, 0x06, 0x84, 0x03, 0x80, 0x08, 0x81, 0x02, 0x80, 0x08, 0x81, 0x05,
|
||||
0x80, 0x0B, 0x88, 0x04, 0x80, 0x05, 0x91, 0x06, 0x80, 0x44, 0x81, 0x07,
|
||||
0x85, 0x01, 0x84, 0x0C, 0x80, 0x0D, 0x81, 0x05, 0x80, 0x0C, 0x81, 0x05,
|
||||
0x80, 0x1A, 0x88, 0x0C, 0x80, 0x16, 0x89, 0x06, 0x81, 0x0A, 0x80, 0x07,
|
||||
0x81, 0x05, 0x80, 0x0C, 0x81, 0x01, 0x89, 0x02, 0x88, 0x01, 0x89, 0x08,
|
||||
0x88, 0x03, 0x8A, 0x07, 0x88, 0x03, 0x80, 0x05, 0x81, 0x09, 0x84, 0x09,
|
||||
0x85, 0x02, 0x81, 0x03, 0x80, 0x12, 0x81, 0x05, 0x80, 0x04, 0x84, 0x04,
|
||||
0x80, 0x06, 0x81, 0x05, 0x85, 0x01, 0x84, 0x08, 0x86, 0x04, 0x84, 0x03,
|
||||
0x94, 0x04, 0x95, 0x01, 0x85, 0x07, 0x81, 0x05, 0x85, 0x02, 0x84, 0x01,
|
||||
0x80, 0x02, 0x82, 0x02, 0x8A, 0x07, 0x82, 0x01, 0x80, 0x07, 0x90, 0x01,
|
||||
0x91, 0x04, 0x80, 0x0B, 0x81, 0x04, 0x80, 0x44, 0x81, 0x09, 0x89, 0x05,
|
||||
0x88, 0x07, 0x8A, 0x05, 0x82, 0x01, 0x80, 0x11, 0x81, 0x03, 0x80, 0x0A,
|
||||
0x81, 0x08, 0x80, 0x06, 0x81, 0x04, 0x80, 0x0C, 0x81, 0x05, 0x80, 0x05,
|
||||
0x84, 0x03, 0x86, 0x01, 0x82, 0x01, 0x80, 0x07, 0x84, 0x06, 0x80, 0x05,
|
||||
0x84, 0x0A, 0x80, 0x08, 0x89, 0x04, 0x81, 0x01, 0x80, 0x06, 0x84, 0x01,
|
||||
0x85, 0x06, 0x81, 0x01, 0x80, 0x0B, 0x81, 0x05, 0x80, 0x04, 0x81, 0x07,
|
||||
0x80, 0x07, 0x84, 0x03, 0x85, 0x04, 0x84, 0x02, 0x80, 0x06, 0x81, 0x04,
|
||||
0x88, 0x02, 0x80, 0x05, 0x88, 0x06, 0x80, 0x08, 0x81, 0x03, 0x80, 0x08,
|
||||
0x89, 0x05, 0x88, 0x03, 0x89, 0x01, 0x80, 0x0F, 0x84, 0x04, 0x80, 0x03,
|
||||
0x81, 0x02, 0x91, 0x03, 0x80, 0x22, 0x85, 0x0C, 0x81, 0x05, 0x80, 0x02,
|
||||
0x82, 0x07, 0x80, 0x02, 0x82, 0x04, 0x80, 0x07, 0x81, 0x03, 0x84, 0x08,
|
||||
0x85, 0x05, 0x81, 0x03, 0x80, 0x0E, 0x81, 0x06, 0x88, 0x02, 0x80, 0x05,
|
||||
0x88, 0x01, 0x89, 0x08, 0x88, 0x01, 0x80, 0x06, 0x82, 0x06, 0x80, 0x05,
|
||||
0x81, 0x05, 0x80, 0x09, 0x81, 0x04, 0x80, 0x06, 0x81, 0x07, 0x80, 0x08,
|
||||
0x81, 0x03, 0x80, 0x09, 0x82, 0x02, 0x8A, 0x01, 0x88, 0x02, 0x80, 0x07,
|
||||
0x88, 0x09, 0x80, 0x11, 0x89, 0x02, 0x81, 0x02, 0x80, 0x04, 0x81, 0x0A,
|
||||
0x80, 0x05, 0x81, 0x01, 0x89, 0x08, 0x81, 0x04, 0x88, 0x01, 0x8A, 0x01,
|
||||
0x82, 0x06, 0x80, 0x05, 0x81, 0x08, 0x80, 0x0B, 0x81, 0x05, 0x88, 0x04,
|
||||
0x89, 0x03, 0x81, 0x03, 0x80, 0x22, 0x84, 0x06, 0x85, 0x07, 0x84, 0x0A,
|
||||
0x85, 0x01, 0x81, 0x03, 0x91, 0x02, 0x90, 0x01, 0x80, 0x01, 0x84, 0x04,
|
||||
0x80, 0x85, 0x81, 0x05, 0x85, 0x01, 0x84, 0x06, 0x85, 0x05, 0x84, 0x0B,
|
||||
0x80, 0x1B, 0x89, 0x0D, 0x80, 0x08, 0x82, 0x04, 0x80, 0x0D, 0x81, 0x0B,
|
||||
0x80, 0x06, 0x88, 0x01, 0x89, 0x09, 0x81, 0x07, 0x84, 0x04, 0x80, 0x02,
|
||||
0x81, 0x03, 0x89, 0x05, 0x80, 0x05, 0x82, 0x09, 0x8A, 0x01, 0x88, 0x0D,
|
||||
0x8A, 0x03, 0x88, 0x0B, 0x89, 0x08, 0x81, 0x03, 0x85, 0x06, 0x81, 0x01,
|
||||
0x80, 0x03, 0x82, 0x05, 0x86, 0x06, 0x84, 0x07, 0x95, 0x07, 0x85, 0x04,
|
||||
0x80, 0x36, 0x81, 0x02, 0x85, 0x07, 0x84, 0x0F, 0x80, 0x13, 0x81, 0x04,
|
||||
0x89, 0x04, 0x88, 0x03, 0x80, 0x05, 0x81, 0x02, 0x80, 0x14, 0x81, 0x06,
|
||||
0x80, 0x08, 0x88, 0x0A, 0x80, 0x04, 0x88, 0x01, 0x89, 0x09, 0x80, 0x06,
|
||||
0x84, 0x04, 0x80, 0x0C, 0x84, 0x03, 0x80, 0x02, 0x81, 0x06, 0x80, 0x07,
|
||||
0x81, 0x08, 0x89, 0x02, 0x88, 0x05, 0x80, 0x07, 0x84, 0x02, 0x86, 0x05,
|
||||
0x80, 0x0D, 0x81, 0x0B, 0x80, 0x08, 0x88, 0x01, 0x89, 0x05, 0x80, 0x04,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *stream;
|
||||
uint16_t length;
|
||||
uint8_t levelIndex; // 0-based
|
||||
} StDemoEntryT;
|
||||
|
||||
static const StDemoEntryT kDemoRotation[4] = {
|
||||
{ kDemoStreamH, 296u, 7u },
|
||||
{ kDemoStreamW, 334u, 22u },
|
||||
{ kDemoStreamT, 374u, 19u },
|
||||
{ kDemoStreamX, 564u, 23u },
|
||||
};
|
||||
|
||||
#endif
|
||||
77
stEngine.c
77
stEngine.c
|
|
@ -50,15 +50,15 @@
|
|||
#define ST_TAXI_W_PX 24
|
||||
#define ST_TAXI_H_PX 24
|
||||
|
||||
// Death-animation duration. Matches the C64 sequence at $6A72/$6B24/
|
||||
// $6B4C: phase-1 keeps integrating Y velocity until the cab falls to
|
||||
// the floor (row >= $DA = 218), then phase-2 walks the sprite cels
|
||||
// $CC..$D1 with a rate that slows each step, then phase-3 holds for
|
||||
// 70 frames before the finalize (DEC lives, blank sprite). 120 host
|
||||
// frames here covers the full "fall + sit" sequence at the port's
|
||||
// physics scale. The cab keeps falling under gravity during the
|
||||
// countdown so the visual matches the original "watch it drop".
|
||||
#define ST_CRASH_ANIM_FRAMES 120u
|
||||
// Death-animation duration, in GAME TICKS (the death countdown runs
|
||||
// inside stEngineTick, now stepped at the fixed 30 Hz cadence). Matches
|
||||
// the C64 sequence at $6A72/$6B24/$6B4C: phase-1 integrates Y velocity
|
||||
// until the cab falls to the floor (row >= $DA), phase-2 walks cels
|
||||
// $CC..$D1 at a slowing rate, phase-3 holds ~70 video frames (~35 game
|
||||
// ticks). 60 game ticks (~2 s) comfortably covers fall + impact walk +
|
||||
// hold. The cab keeps falling under gravity during the countdown so the
|
||||
// visual matches the original "watch it drop".
|
||||
#define ST_CRASH_ANIM_FRAMES 60u
|
||||
|
||||
// Edge reflection at the visible playfield edges. The C64's $6AED
|
||||
// thresholds (col 23 left / col 65 in X-MSB high half) are in VIC
|
||||
|
|
@ -164,6 +164,11 @@ static void respawnTaxi(StGameT *game) {
|
|||
t->landed = false;
|
||||
t->onPad = 0xFFu;
|
||||
t->thrusting = false;
|
||||
// $6892 forces #$C0 on every respawn: gear up, facing right.
|
||||
t->gearDown = false;
|
||||
t->fireHeld = false;
|
||||
t->facing = ST_DIR_RIGHT;
|
||||
t->crashImpacted = false;
|
||||
// Boot any in-flight passenger out of the cab; a respawn doesn't
|
||||
// keep the fare. Waiting passengers (still on a pad) survive.
|
||||
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
|
||||
|
|
@ -226,11 +231,21 @@ void stEngineTick(StGameT *game) {
|
|||
if (t->vy < -ST_MAX_VY) { t->vy = -ST_MAX_VY; }
|
||||
t->y += t->vy;
|
||||
if (t->y > maxY) {
|
||||
// Floor hit: the C64's phase-1 -> phase-2 transition (row
|
||||
// $DA, $6ACC) -- the debris stops falling and the impact
|
||||
// cel walk plays out where it landed.
|
||||
t->y = maxY;
|
||||
t->vy = 0;
|
||||
t->crashImpacted = true;
|
||||
}
|
||||
t->crashTicks--;
|
||||
if (t->crashTicks == 0u) {
|
||||
if (game->state == ST_STATE_DEMO) {
|
||||
// $6B57-$6B5E: a demo crash skips the life bookkeeping
|
||||
// entirely; the main loop returns to the title.
|
||||
game->demoEnded = true;
|
||||
return;
|
||||
}
|
||||
if (game->lives > 0u) {
|
||||
game->lives--;
|
||||
}
|
||||
|
|
@ -256,19 +271,6 @@ void stEngineTick(StGameT *game) {
|
|||
t->vx = (int16_t)(t->vx + ax + (int16_t)L->xGrav);
|
||||
t->vy = (int16_t)(t->vy + ay + (int16_t)L->yGrav);
|
||||
|
||||
if (t->thrusting) {
|
||||
// Thrust-flame cel cycling. The asset (genPlaceholderArt.py)
|
||||
// authors 3 thrust-flame variants at cels 1..3. Advance the
|
||||
// counter every frame; cel selection in stRender divides by
|
||||
// ST_THRUST_CEL_TICKS so the visible animation runs at ~10 Hz.
|
||||
t->thrustFrame++;
|
||||
if (t->thrustFrame >= (uint8_t)(ST_THRUST_CEL_COUNT * ST_THRUST_CEL_TICKS)) {
|
||||
t->thrustFrame = 0u;
|
||||
}
|
||||
} else {
|
||||
t->thrustFrame = 0u;
|
||||
}
|
||||
|
||||
// Safety clamp on the velocity accumulator -- without this a long
|
||||
// fall under gravity (or sustained one-way thrust against no
|
||||
// collision) lets vx/vy wrap int16_t and reverse sign.
|
||||
|
|
@ -314,8 +316,12 @@ void stEngineTick(StGameT *game) {
|
|||
// 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).
|
||||
// $6462 additionally requires the GEAR DOWN: with the gear
|
||||
// up the pad detector aborts and the pad surface is just
|
||||
// lethal background -- fast descent OR gear-up = crash.
|
||||
bool slowDescent = (t->vy < ST_SUBPIXEL);
|
||||
if (slowDescent && onLandingPad(L, centerX, feetY, &landingPad)) {
|
||||
if (slowDescent && t->gearDown &&
|
||||
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;
|
||||
|
|
@ -335,6 +341,7 @@ void stEngineTick(StGameT *game) {
|
|||
t->vy = ST_CRASH_FALL_VY;
|
||||
t->thrusting = false;
|
||||
t->crashTicks = ST_CRASH_ANIM_FRAMES;
|
||||
t->crashImpacted = false;
|
||||
}
|
||||
} else {
|
||||
t->landed = false;
|
||||
|
|
@ -382,10 +389,20 @@ bool stEngineInTransporter(const StGameT *game) {
|
|||
// 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.
|
||||
// it recedes. $5CB9 INCs $5A66 and gates the step on (counter & mask):
|
||||
// mask $03 (every 4) for the first steps, then $0F (every 16) from $E5.
|
||||
//
|
||||
// UNIT PROOF (from raw.bin, so this is not re-litigated): the animation
|
||||
// runs in the loop at $5B85 -- JSR $5BBB (the animDispatcher, sole caller,
|
||||
// which advances $5A66 via $5CB9) is called ONCE per iteration, while the
|
||||
// single-frame raster wait $404B (LDA $D019 / AND #$01 / BEQ on line 252)
|
||||
// runs TWICE per iteration ($5BAC + $5BB2), exactly like the gameplay
|
||||
// loop's $5F94/$5FA3. So $5A66 advances once per 2 video frames = one
|
||||
// GAME TICK. The boundaries 4,8,12,16,32,48,64 and the total 64 are
|
||||
// therefore GAME TICKS: 64 ticks = 128 video frames = ~2.13 s NTSC. The
|
||||
// values stay in their native C64 units (stEngineWarpTick runs once per
|
||||
// game tick), unlike the port-invented crashTicks. 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 };
|
||||
|
|
@ -410,7 +427,10 @@ void stEngineStartWarp(StGameT *game) {
|
|||
game->taxi.vx = 0;
|
||||
game->taxi.vy = 0;
|
||||
game->taxi.thrusting = false;
|
||||
stAudioSfxCrash(); // the C64 sets voice-3 noise ($81) on warp start
|
||||
// $5CB0: the C64 gates SID voice-3 noise on ($D412 = $81) for the
|
||||
// warp. Voice 3's freq word is $0617 (set at the $4092 RNG init,
|
||||
// ~92 Hz shift rate) -- the darkest rumble, portable pitch 31.
|
||||
jlAudioNoise(31u, 4u);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -429,6 +449,7 @@ bool stEngineWarpTick(StGameT *game) {
|
|||
}
|
||||
if (t->warpFrame >= (uint8_t)ST_WARP_TOTAL_FRAMES) {
|
||||
t->warpFrame = 0u;
|
||||
jlAudioNoise(0u, 15u); // warp done: rumble off
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
4
stHud.c
4
stHud.c
|
|
@ -64,6 +64,10 @@ void stHudDraw(jlSurfaceT *stage, const StGameT *game) {
|
|||
if (!p->active) {
|
||||
continue;
|
||||
}
|
||||
if (p->phase == ST_PASS_DROP_IN || p->phase == ST_PASS_WALK_TO_PAD ||
|
||||
p->phase == ST_PASS_DROP_OUT) {
|
||||
break; // delivery in progress: no hail, no dest
|
||||
}
|
||||
if (!p->onboard) {
|
||||
snprintf(buf, sizeof(buf), "HEY TAXI!");
|
||||
} else if (p->destPad == ST_DEST_TRANSPORTER) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "joey/file.h"
|
||||
#include "spacetaxi.h"
|
||||
|
||||
|
||||
|
|
@ -47,7 +48,8 @@ bool stLevelLoad(StLevelT *out, const char *path) {
|
|||
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
fp = fopen(path, "rb");
|
||||
// path is a name relative to DATA/ (e.g. "levels/title.dat").
|
||||
fp = jlDataOpen(path, "rb");
|
||||
if (fp == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
181
stPassenger.c
181
stPassenger.c
|
|
@ -27,11 +27,13 @@
|
|||
#include "spacetaxi.h"
|
||||
|
||||
|
||||
#define ST_PASSENGER_W_PX 16
|
||||
#define ST_PASSENGER_H_PX 16
|
||||
#define ST_PASSENGER_H_TILES (ST_PASSENGER_H_PX / ST_TILE_PIXELS)
|
||||
#define ST_WALK_FRAMES 4u
|
||||
#define ST_WALK_TICKS_PER_CEL 6u
|
||||
// The waiting passenger's in-place wave (and every beam/walk step)
|
||||
// advances every 3 game ticks -- the C64's $715E == 3 reload. Since
|
||||
// stPassengerTick is now called once per fixed game tick (~30 Hz),
|
||||
// this is a plain tick counter, in lockstep with the physics.
|
||||
#define ST_WAVE_STEP_TICKS 3u
|
||||
|
||||
// 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
|
||||
|
|
@ -46,7 +48,7 @@
|
|||
|
||||
|
||||
static uint32_t gRng;
|
||||
static uint8_t gWalkTick;
|
||||
static uint8_t gWaveTicks; // game ticks since the last wave/step advance
|
||||
// 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
|
||||
|
|
@ -56,12 +58,22 @@ static int16_t gSpawnCountdown;
|
|||
static uint8_t gDeliveredThisScreen;
|
||||
|
||||
|
||||
static bool arrivedAtTarget(const StPassengerT *p);
|
||||
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);
|
||||
static void walkStride(StPassengerT *p);
|
||||
|
||||
|
||||
// Walk arrival test. The C64 aligns the stride with AND #$FE and
|
||||
// compares ((x ^ target) & ~1) == 0; the port's +/-2 window is the
|
||||
// same idea, robust to an odd captured taxi X.
|
||||
static bool arrivedAtTarget(const StPassengerT *p) {
|
||||
int16_t d = (int16_t)(p->x - p->targetX);
|
||||
return d >= -2 && d <= 2;
|
||||
}
|
||||
|
||||
|
||||
static void deliverFare(StGameT *game, StPassengerT *p) {
|
||||
|
|
@ -117,6 +129,18 @@ static uint8_t rngPad(uint8_t n) {
|
|||
}
|
||||
|
||||
|
||||
// One +/-2 px walk stride ($6D66 rightward, $6D68 leftward), with
|
||||
// the stride cel alternation on waitPhase's low bit.
|
||||
static void walkStride(StPassengerT *p) {
|
||||
if (p->targetX > p->x) {
|
||||
p->x = (int16_t)(p->x + 2);
|
||||
} else if (p->targetX < p->x) {
|
||||
p->x = (int16_t)(p->x - 2);
|
||||
}
|
||||
p->waitPhase ^= 1u;
|
||||
}
|
||||
|
||||
|
||||
static void spawnPassenger(StGameT *game) {
|
||||
StPassengerT *p = &game->passengers[0];
|
||||
uint8_t padIdx;
|
||||
|
|
@ -128,51 +152,17 @@ static void spawnPassenger(StGameT *game) {
|
|||
|
||||
p->active = true;
|
||||
p->onboard = false;
|
||||
p->walkPhase = 0u;
|
||||
p->walkDir = 0u;
|
||||
p->phase = ST_PASS_BEAM_IN;
|
||||
p->waitPhase = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void stPassengerReset(StGameT *game) {
|
||||
uint8_t i;
|
||||
|
||||
gWalkTick = 0u;
|
||||
gWaveTicks = 0u;
|
||||
gDeliveredThisScreen = 0u;
|
||||
gSpawnCountdown = ST_SPAWN_CAP_FRAMES;
|
||||
// Deterministic per-screen seed: varies the pad sequence level to
|
||||
|
|
@ -206,26 +196,113 @@ void stPassengerTick(StGameT *game) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!p->onboard) {
|
||||
// Walk in place while waiting.
|
||||
if (++gWalkTick >= ST_WALK_TICKS_PER_CEL) {
|
||||
gWalkTick = 0u;
|
||||
walkPassenger(game, p);
|
||||
// One shared step clock (the C64's 3-game-tick cadence, $715E == 3)
|
||||
// drives every phase: beams, wave, and walk strides. Called once
|
||||
// per fixed game tick, so this is a plain tick count.
|
||||
{
|
||||
bool stepDue = (++gWaveTicks >= ST_WAVE_STEP_TICKS);
|
||||
if (stepDue) {
|
||||
gWaveTicks = 0u;
|
||||
}
|
||||
|
||||
switch (p->phase) {
|
||||
case ST_PASS_BEAM_IN:
|
||||
// Materialize at standX: cels $CB..$C7 (5 steps).
|
||||
if (stepDue && ++p->waitPhase >= 5u) {
|
||||
p->phase = ST_PASS_WAIT;
|
||||
p->waitPhase = 0u;
|
||||
}
|
||||
break;
|
||||
|
||||
case ST_PASS_WAIT:
|
||||
// Stand still and wave through the $66D6 cycle.
|
||||
if (stepDue) {
|
||||
p->waitPhase = (uint8_t)((p->waitPhase + 1u) & 3u);
|
||||
}
|
||||
// Board when the taxi has landed on their pad.
|
||||
if (t->landed && t->onPad == p->currentPad) {
|
||||
// $6811 captures the taxi X ONCE at landing; the
|
||||
// walk aims there even if the cab lifts off.
|
||||
p->targetX = (int16_t)(t->x >> ST_SUBPIXEL_SHIFT);
|
||||
p->phase = ST_PASS_WALK_TO_CAB;
|
||||
p->waitPhase = 0u;
|
||||
}
|
||||
break;
|
||||
|
||||
case ST_PASS_WALK_TO_CAB:
|
||||
if (stepDue) {
|
||||
walkStride(p);
|
||||
if (arrivedAtTarget(p)) {
|
||||
if (t->landed && t->onPad == p->currentPad) {
|
||||
p->phase = ST_PASS_BOARD_OUT;
|
||||
p->waitPhase = 0u;
|
||||
} else {
|
||||
// Cab left early: walk home and wait again.
|
||||
// (Not byte-traced -- the C64 cab cannot
|
||||
// normally leave mid-walk; this just keeps
|
||||
// the state machine sane if ours does.)
|
||||
p->targetX = (int16_t)game->level.pads[p->currentPad].standX;
|
||||
p->phase = ST_PASS_WALK_TO_PAD;
|
||||
p->waitPhase = 0u;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ST_PASS_BOARD_OUT:
|
||||
// Shrink into the cab: cels $C7..$CB (5 steps).
|
||||
if (stepDue && ++p->waitPhase >= 5u) {
|
||||
p->onboard = true;
|
||||
p->phase = ST_PASS_RIDING;
|
||||
p->waitPhase = 0u;
|
||||
pickDestination(game, p);
|
||||
stAudioSfxPickup();
|
||||
}
|
||||
} 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 &&
|
||||
break;
|
||||
|
||||
case ST_PASS_RIDING:
|
||||
// Deliver by landing on the destination pad. (UP-PLEASE
|
||||
// fares deliver on the transporter fly-up instead; see
|
||||
// stPassengerTransporterExit.)
|
||||
if (p->destPad != ST_DEST_TRANSPORTER &&
|
||||
t->landed &&
|
||||
t->onPad < game->level.padCount &&
|
||||
t->onPad == p->destPad) {
|
||||
p->onboard = false;
|
||||
p->phase = ST_PASS_DROP_IN;
|
||||
p->waitPhase = 0u;
|
||||
p->x = (int16_t)(t->x >> ST_SUBPIXEL_SHIFT);
|
||||
p->targetX = (int16_t)game->level.pads[p->destPad].standX;
|
||||
p->currentPad = p->destPad;
|
||||
p->y = (int16_t)((game->level.pads[p->destPad].tileY -
|
||||
ST_PASSENGER_H_TILES) * ST_TILE_PIXELS);
|
||||
}
|
||||
break;
|
||||
|
||||
case ST_PASS_DROP_IN:
|
||||
// Materialize beside the cab: cels $CB..$C7.
|
||||
if (stepDue && ++p->waitPhase >= 5u) {
|
||||
p->phase = ST_PASS_WALK_TO_PAD;
|
||||
p->waitPhase = 0u;
|
||||
}
|
||||
break;
|
||||
|
||||
case ST_PASS_WALK_TO_PAD:
|
||||
if (stepDue) {
|
||||
walkStride(p);
|
||||
if (arrivedAtTarget(p)) {
|
||||
p->phase = ST_PASS_DROP_OUT;
|
||||
p->waitPhase = 0u;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ST_PASS_DROP_OUT:
|
||||
// Shrink out at standX -- fare complete.
|
||||
if (stepDue && ++p->waitPhase >= 5u) {
|
||||
deliverFare(game, p);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
640
stRender.c
640
stRender.c
|
|
@ -33,9 +33,10 @@
|
|||
// DOS 8.3 filename limit: "tilebank0.tbk" is 9.3, fopen fails under
|
||||
// DOSBox strict 8.3. Shortened to "tbank%u.tbk" (6.3) so all four
|
||||
// targets can use the same filenames without per-platform aliasing.
|
||||
#define ST_TILE_BANK_PATH_FMT "DATA/tiles/tbank%u.tbk"
|
||||
#define ST_SPRITE_SHEET_PATH "DATA/sprites/sprites.spr"
|
||||
#define ST_FONT_PATH "DATA/font.tbk"
|
||||
#define ST_TILE_BANK_PATH_FMT "tiles/tbank%u.tbk"
|
||||
#define ST_SPRITE_SHEET_PATH "sprites/sprites.spr"
|
||||
#define ST_SPRITE_SHEET_SPC "sprites/sprites.spc"
|
||||
#define ST_FONT_PATH "font.tbk"
|
||||
|
||||
#define ST_TILE_BANK_MAX 256u
|
||||
|
||||
|
|
@ -52,29 +53,33 @@
|
|||
// cels), row 2 is the flame (8 cels). Cells lay out left-to-right
|
||||
// top-to-bottom in the .spr blob's cellCount = 27.
|
||||
#define ST_SPRITE_SHEET_COLS 9
|
||||
#define ST_SPRITE_SHEET_CELS 27
|
||||
#define ST_SPRITE_SHEET_CELS 55 /* 11 cols x 5 rows: taxi, passenger, flame, warp, death */
|
||||
#define ST_SPRITE_TAXI_FIRST 0
|
||||
#define ST_SPRITE_PASS_FIRST 9
|
||||
#define ST_SPRITE_FLAME_FIRST 18
|
||||
#define ST_SPRITE_PASS_FIRST 11
|
||||
#define ST_SPRITE_FLAME_FIRST 22
|
||||
// Taxi sprite: 24x24 px = 3 tiles wide x 3 tiles tall. The port's
|
||||
// sprite asset (genPlaceholderArt.py) authors 4 cels:
|
||||
// cel 0: idle cab (no thrust)
|
||||
// cel 1: thrust frame A
|
||||
// cel 2: thrust frame B
|
||||
// cel 3: thrust frame C
|
||||
// Index: thrusting ? (1 + thrustFrame/ST_THRUST_CEL_TICKS) : 0. The
|
||||
// C64's $619B selects between LEFT-facing ($DC) and RIGHT-facing
|
||||
// ($C0) cab bases with a 1-bit flicker on each, but the port's asset
|
||||
// doesn't carry left/right variants -- left/right facing isn't
|
||||
// visually reflected until the sheet adds them.
|
||||
// sprite asset (extractSprites.py, real C64 art from raw.bin) lays
|
||||
// out sheet row 0 in the live sprite-pointer order:
|
||||
// cel 0 = $C1 (right, gear DOWN) cel 1 = $C0 (right, gear up)
|
||||
// cel 2 = $DC (left, gear up) cel 3 = $DD (left, gear DOWN)
|
||||
// $619B selects the base pair by held direction (LEFT -> $DC pair,
|
||||
// RIGHT -> $C0 pair, otherwise keep the last facing) and preserves
|
||||
// bit 0 = the LANDING GEAR state (fire-toggled, $63DD). The in-
|
||||
// flight body NEVER animates -- the cel is a static function of
|
||||
// (facing, gearDown):
|
||||
// cel = facingLeft ? (2 + gearDown) : (1 - gearDown)
|
||||
// (The $6A95 EOR that an earlier port version modeled as a flicker
|
||||
// is the crash-debris animation, $CC<->$CD during the fall.)
|
||||
#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
|
||||
// Transporter shrink-warp cels (sheet row 3): the SEVEN drawn cels
|
||||
// $E2..$E8 of the $5CB9 chain, identity-mapped to warp steps 0..6.
|
||||
// $5CDA ends the warp when the pointer REACHES $E9 and hides the
|
||||
// sprite ($718E = 0) -- $E9 is never displayed, so step 7 draws
|
||||
// nothing at all.
|
||||
#define ST_SPRITE_WARP_FIRST 33
|
||||
#define ST_WARP_CEL_COUNT 7
|
||||
|
||||
// Flame placeholder fallback (only used if real sprite-2 cel is
|
||||
// missing): a small bright rectangle below the cab while thrusting.
|
||||
|
|
@ -88,10 +93,16 @@
|
|||
// Real flame sprite cels (24x24, from raw.bin sprite ptrs in $6DB0
|
||||
// table). Indexed by direction-mask -> cel via kFlameCelByDirMask
|
||||
// below. $6D6A's parity bit ($716C) flickers the flame off every
|
||||
// other frame.
|
||||
// other GAME TICK (2 video frames, so a 12.5Hz PAL strobe that fuses
|
||||
// on a CRT). The port reproduces the LOOK with a wall-clock virtual
|
||||
// strobe (flameStrobeOn): at high frame rates it yields the authentic
|
||||
// shimmer; a rendered frame long enough to span a whole strobe phase
|
||||
// (low-fps IIgs) shows the flame continuously instead of a 7.5Hz
|
||||
// blink that reads as "no flame".
|
||||
#define ST_FLAME_CEL_W_PX 24
|
||||
#define ST_FLAME_CEL_H_PX 24
|
||||
#define ST_FLAME_CEL_COUNT 8
|
||||
#define ST_FLAME_STROBE_MS 40u /* one $716C phase = 1 PAL game tick */
|
||||
|
||||
// Passenger: 24x24 (C64 hardware sprite size is 24x21; we pad 3 rows
|
||||
// transparent at the bottom in the asset extraction to keep a square
|
||||
|
|
@ -101,11 +112,28 @@
|
|||
// $67A6 incrementing gSpr1PtrShadow from $C6 toward $CC.
|
||||
#define ST_PASSENGER_W_PX 24
|
||||
#define ST_PASSENGER_H_PX 24
|
||||
#define ST_PASSENGER_CEL_COUNT 9
|
||||
#define ST_PASSENGER_CEL_WALK0 0
|
||||
#define ST_PASSENGER_CEL_WALK1 1
|
||||
#define ST_PASSENGER_CEL_COUNT 11
|
||||
#define ST_PASSENGER_CEL_WALK0 0 /* $C4 walk LEFT a */
|
||||
#define ST_PASSENGER_CEL_WALK1 1 /* $C5 walk LEFT b */
|
||||
#define ST_PASSENGER_CEL_BOARD0 2 /* maps to C64 ptr $C6 */
|
||||
#define ST_PASSENGER_CEL_SPARKLE 8 /* C64 ptr $D9 -- 3rd sparkle cel */
|
||||
#define ST_PASSENGER_CEL_WALKR0 9 /* $C2 walk RIGHT a */
|
||||
#define ST_PASSENGER_CEL_WALKR1 10 /* $C3 walk RIGHT b */
|
||||
|
||||
// Crash-death cels (sheet row 4): $CC/$CD debris alternation during
|
||||
// the fall, then the $CE..$D1 ground-impact walk, then hidden.
|
||||
#define ST_DEATH_CEL_COUNT 6
|
||||
#define ST_SPRITE_DEATH_FIRST 44
|
||||
|
||||
// Startup progress bar: 2 load steps (font, sprite bank) + the 36
|
||||
// per-cel compiles (4 taxi + 11 passenger + 8 flame + 7 warp +
|
||||
// 6 death).
|
||||
#define ST_LOAD_STEPS_TOTAL 38
|
||||
#define ST_LOAD_BAR_X 60
|
||||
#define ST_LOAD_BAR_Y 110
|
||||
#define ST_LOAD_BAR_H 6
|
||||
#define ST_LOAD_BAR_COLOR 1u /* white fill */
|
||||
#define ST_LOAD_BAR_FRAME_COLOR 6u /* blue frame, cab color */
|
||||
|
||||
// Library-wide worst case (16-px-group window on planar ports).
|
||||
#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_TAXI_W_PX / 8, ST_TAXI_H_PX / 8)
|
||||
|
|
@ -118,6 +146,7 @@ typedef struct {
|
|||
bool bankLoaded;
|
||||
jlSpriteT *taxiCels[ST_TAXI_CEL_COUNT];
|
||||
jlSpriteT *warpCels[ST_WARP_CEL_COUNT];
|
||||
jlSpriteT *deathCels[ST_DEATH_CEL_COUNT];
|
||||
jlSpriteT *flameCels[ST_FLAME_CEL_COUNT];
|
||||
jlSpriteBackupT flameBackup;
|
||||
uint8_t flameBackupMem[ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2)));
|
||||
|
|
@ -132,6 +161,10 @@ typedef struct {
|
|||
uint8_t passengerBackupMem[ST_MAX_PASSENGERS][ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2)));
|
||||
bool taxiHasBackup;
|
||||
bool passengerHasBackup[ST_MAX_PASSENGERS];
|
||||
uint32_t flamePrevMs; // flameStrobeOn frame-span tracker
|
||||
uint32_t deathMs; // crash-anim step anchor
|
||||
uint8_t deathStep; // impact-walk position (0..4)
|
||||
bool deathActive; // crash anim state latched
|
||||
// Tilemap repaint gating: the static playfield art only needs to
|
||||
// be blitted to the stage once per scene change, not every frame.
|
||||
// Per-frame full repaint blows the per-frame budget on emulated
|
||||
|
|
@ -145,8 +178,11 @@ static StRenderStateT gRender;
|
|||
|
||||
|
||||
static bool loadTileBank(uint8_t bankId);
|
||||
static bool loadSpriteSheet(void);
|
||||
static bool loadSpriteSheet(jlSurfaceT *stage);
|
||||
static bool loadFontSheet(jlSurfaceT *stage);
|
||||
static void loadProgress(jlSurfaceT *stage, uint8_t step);
|
||||
static bool flameStrobeOn(void);
|
||||
static int16_t passCelForPtr(uint8_t ptr);
|
||||
static void buildAsciiMap(void);
|
||||
static void destroySprites(void);
|
||||
|
||||
|
|
@ -172,26 +208,11 @@ static void destroySprites(void);
|
|||
// parks all hardware sprite slots at (col $AA, row $8C) so they don't
|
||||
// flash garbage when the title is first shown.
|
||||
|
||||
// Recorded (input_mask, frame_duration) pairs at C64 $0902. Played by
|
||||
// $48F2 once the title intro reaches the demo phase.
|
||||
static const uint8_t kTitleDemoScript[256] = {
|
||||
0x80, 0x17, 0x88, 0x0A, 0x80, 0x0A, 0x84, 0x04, 0x85, 0x03, 0x81, 0x03, 0x80, 0x0A, 0x88, 0x04,
|
||||
0x80, 0x01, 0x81, 0x0E, 0x80, 0x0D, 0x82, 0x02, 0x80, 0x0A, 0x81, 0x09, 0x85, 0x07, 0x84, 0x05,
|
||||
0x80, 0x02, 0x88, 0x05, 0x8A, 0x04, 0x82, 0x03, 0x80, 0x05, 0x85, 0x10, 0x81, 0x08, 0x89, 0x02,
|
||||
0x88, 0x0E, 0x8A, 0x03, 0x88, 0x06, 0x81, 0x07, 0x80, 0x12, 0x82, 0x05, 0x80, 0x04, 0x81, 0x11,
|
||||
0x80, 0x04, 0x82, 0x04, 0x86, 0x09, 0x84, 0x02, 0x95, 0x07, 0x85, 0x04, 0x81, 0x01, 0x80, 0x43,
|
||||
0x81, 0x0A, 0x84, 0x05, 0x86, 0x03, 0x82, 0x06, 0x80, 0x07, 0x81, 0x09, 0x80, 0x04, 0x88, 0x0B,
|
||||
0x80, 0x09, 0x84, 0x04, 0x80, 0x09, 0x81, 0x01, 0x89, 0x04, 0x80, 0x08, 0x85, 0x04, 0x81, 0x01,
|
||||
0x80, 0x05, 0x81, 0x15, 0x80, 0x0C, 0x82, 0x02, 0x86, 0x07, 0x80, 0x05, 0x81, 0x09, 0x80, 0x13,
|
||||
0x84, 0x01, 0x85, 0x03, 0x80, 0x06, 0x88, 0x0B, 0x80, 0x16, 0x81, 0x0A, 0x80, 0x01, 0x88, 0x04,
|
||||
0x80, 0x0C, 0x81, 0x03, 0x85, 0x02, 0x84, 0x03, 0x85, 0x0C, 0x81, 0x01, 0x80, 0x0D, 0x82, 0x07,
|
||||
0x80, 0x06, 0x81, 0x02, 0x89, 0x0C, 0x88, 0x02, 0x80, 0x05, 0x82, 0x04, 0x80, 0x0A, 0x81, 0x04,
|
||||
0x85, 0x01, 0x84, 0x04, 0x80, 0x0D, 0x81, 0x05, 0x89, 0x0E, 0x81, 0x01, 0x80, 0x11, 0x84, 0x17,
|
||||
0x80, 0x05, 0x81, 0x04, 0x80, 0x19, 0x81, 0x03, 0x89, 0x01, 0x88, 0x0E, 0x80, 0x07, 0x84, 0x05,
|
||||
0x80, 0x0C, 0x81, 0x0D, 0x80, 0x19, 0x88, 0x01, 0x80, 0x02, 0x88, 0x04, 0x80, 0x1B, 0x88, 0x0A,
|
||||
0x89, 0x0B, 0x81, 0x14, 0x80, 0x02, 0x82, 0x03, 0x80, 0x05, 0x84, 0x03, 0x85, 0x09, 0x84, 0x05,
|
||||
0x85, 0x05, 0x84, 0x02, 0x86, 0x08, 0x84, 0x01, 0x80, 0x01, 0x81, 0x03, 0x89, 0x12, 0x81, 0x08,
|
||||
};
|
||||
// (The fabricated title "demo physics" stage and its truncated copy
|
||||
// of demo stream 01 were deleted 2026-07-21: the real C64 never flies
|
||||
// the cab around the title after takeoff -- it enters the ATTRACT
|
||||
// DEMO, now a proper game state (ST_STATE_DEMO) driven by the full
|
||||
// recorded input streams in stDemoStreams.h.)
|
||||
|
||||
// C64 reference values straight from the asm at $4A03 dispatch +
|
||||
// titleSpriteSetup ($4525) + sprite-init tables ($4994/$49AC).
|
||||
|
|
@ -199,7 +220,7 @@ static const uint8_t kTitleDemoScript[256] = {
|
|||
#define ST_TITLE_STAGE_WALK 3u // $4A24
|
||||
#define ST_TITLE_STAGE_HANDOFF 4u // $4A45 -> $67A6
|
||||
#define ST_TITLE_STAGE_LIFTOFF 5u // $4A48
|
||||
#define ST_TITLE_STAGE_DEMO 6u // intro over; demo physics
|
||||
#define ST_TITLE_STAGE_DONE 6u // intro over; attract demo takes over
|
||||
#define ST_TITLE_SPARKLE_FRAMES 0x5Au // $47CC LDA #$5A STA $473F
|
||||
#define ST_TITLE_WALK_TICK_RELOAD 3u // $715E observed in raw.bin
|
||||
// Walk target = gPadHoverXCol ($715F) = $28 set at $47D8.
|
||||
|
|
@ -220,7 +241,16 @@ static const uint8_t kTitleDemoScript[256] = {
|
|||
|
||||
// $66D6 sparkle cel table -- cycled by $66B7 during stage 2.
|
||||
// Each entry shown for $715E (= 3) frames; one full cycle = 12 frames.
|
||||
static const uint8_t kTitleSparkleCels[4] = { 0xC6, 0xC7, 0xD9, 0xC7 };
|
||||
// The $66D6 wave/sparkle cycle {$C6,$C7,$D9,$C7}: the title sparkle
|
||||
// AND the waiting passenger's in-place wave both step through it (the
|
||||
// gameplay passenger never moves -- he stands at standX and waves).
|
||||
static const uint8_t kPassengerWaveCels[4] = { 0xC6, 0xC7, 0xD9, 0xC7 };
|
||||
|
||||
// Crash impact walk cadence ($6B2A: reload starts at 2 ticks and
|
||||
// INCs each step -- 2,3,4,5 ticks for cels $CE..$D1 at the NTSC
|
||||
// 33 ms game tick), then the sprite hides.
|
||||
static const uint16_t kDeathStepMs[4] = { 66u, 100u, 133u, 166u };
|
||||
#define ST_DEATH_TUMBLE_MS 66u /* $CC/$CD swap every 2 game ticks */
|
||||
|
||||
// Map direction-mask (CIA1 PortA bits after EOR #$FF: bit0=UP bit1=DOWN
|
||||
// bit2=LEFT bit3=RIGHT) to a flameCels[] index, mirroring the C64
|
||||
|
|
@ -267,22 +297,16 @@ typedef struct {
|
|||
bool cabVisible;
|
||||
bool flameVisible; // sprite 2 from $6D6A flameSpriteUpdate
|
||||
uint8_t flameDirMask; // mirrors $716A passed into $6DB0,X
|
||||
uint8_t flameParity; // mirrors $716C; flickers flame off when 0
|
||||
// Demo (post-intro stage 6) playback state -- script from $0902.
|
||||
uint16_t scriptIdx;
|
||||
uint8_t scriptTimer;
|
||||
int32_t demoX; // ST_SUBPIXEL fixed sub-pixel x
|
||||
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 logoFlipDiv; // mirrors $48A6 (mod-4 tick divider)
|
||||
uint8_t logoFlipIdx; // mirrors $48A5 (0..5 ping-pong index)
|
||||
uint8_t logoColorIdx; // mirrors $48A4 (0..7 color cycle index)
|
||||
bool logoNeedsPaint; // flip/color changed (or tilemap repainted)
|
||||
} StTitleStateT;
|
||||
|
||||
// The single animated logo character. Its bitmap is flip-booked through
|
||||
|
|
@ -355,14 +379,45 @@ void stRenderInit(jlSurfaceT *stage) {
|
|||
jlPaletteSet(stage, 0u, kDefaultPalette);
|
||||
jlScbSetRange(stage, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u);
|
||||
|
||||
// Tile bank is loaded on demand in stRenderLevel (per-level).
|
||||
if (!loadSpriteSheet()) {
|
||||
jlLogF("stRender: ! sprite sheet load failed (%s)", ST_SPRITE_SHEET_PATH);
|
||||
}
|
||||
// Startup feedback: the IIgs spends several seconds streaming the
|
||||
// banks and compiling sprite cels to their asm routines, all of
|
||||
// which used to happen against a black screen. Show a growing bar
|
||||
// immediately; once the font is in, add the LOADING caption. The
|
||||
// font loads FIRST because text needs its glyph surface.
|
||||
jlSurfaceClear(stage, 0u);
|
||||
loadProgress(stage, 0u);
|
||||
if (!loadFontSheet(stage)) {
|
||||
jlLogF("stRender: ! font load failed (%s)", ST_FONT_PATH);
|
||||
}
|
||||
buildAsciiMap();
|
||||
loadProgress(stage, 1u);
|
||||
|
||||
// Tile bank is loaded on demand in stRenderLevel (per-level).
|
||||
if (!loadSpriteSheet(stage)) {
|
||||
jlLogF("stRender: ! sprite sheet load failed (%s)", ST_SPRITE_SHEET_PATH);
|
||||
}
|
||||
titleReset();
|
||||
}
|
||||
|
||||
|
||||
bool stRenderTitleIntroDone(void) {
|
||||
return gTitle.stage == ST_TITLE_STAGE_DONE;
|
||||
}
|
||||
|
||||
|
||||
// Advance the title intro by ONE game tick. The main loop calls this
|
||||
// on the fixed 30 Hz accumulator (not once per rendered frame) so the
|
||||
// sparkle / walk / takeoff sequence and the logo flip run at the C64's
|
||||
// rate on every port -- identical pre-game timing everywhere.
|
||||
void stRenderTitleAdvance(void) {
|
||||
titleTick();
|
||||
}
|
||||
|
||||
|
||||
// Restart the title intro (sparkle -> walk -> takeoff). Called on
|
||||
// every return to the title so the attract cycle replays -- the C64
|
||||
// re-enters the intro the same way after a demo exits.
|
||||
void stRenderTitleReset(void) {
|
||||
titleReset();
|
||||
}
|
||||
|
||||
|
|
@ -478,14 +533,12 @@ static void titleReset(void) {
|
|||
gTitle.cabVisible = true;
|
||||
gTitle.passengerVisible = true;
|
||||
gTitle.flameVisible = false;
|
||||
// Demo state: initial script pos / pair-0 duration preload.
|
||||
gTitle.scriptIdx = 0u;
|
||||
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.
|
||||
// Force the first tick to be an animation step (3 -> 0 on first ++)
|
||||
// AND force a logo paint on the first render frame so the flip
|
||||
// frame + color appear immediately (the tilemap was just repainted
|
||||
// by stRenderLevelChanged, erasing any prior logo cells).
|
||||
gTitle.logoFlipDiv = 3u;
|
||||
gTitle.logoNeedsPaint = true;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -498,7 +551,32 @@ static void titleReset(void) {
|
|||
// 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) {
|
||||
// Flip/color advance ($4827 divider + $484D/$4853 index+color). Runs
|
||||
// once per GAME TICK (called from titleTick), so the flip-book steps
|
||||
// every 4 game ticks -- the C64's $48A6 mod-4 gate at the 30 Hz main-
|
||||
// loop rate, frame-rate-independent. Sets logoNeedsPaint when it steps;
|
||||
// the actual re-paste is deferred to titlePaintLogo in the render pass.
|
||||
static void titleAdvanceLogo(void) {
|
||||
gTitle.logoFlipDiv = (uint8_t)((gTitle.logoFlipDiv + 1u) & 3u);
|
||||
if (gTitle.logoFlipDiv != 0u) {
|
||||
return;
|
||||
}
|
||||
gTitle.logoFlipIdx++;
|
||||
if (gTitle.logoFlipIdx == 6u) {
|
||||
gTitle.logoFlipIdx = 0u;
|
||||
}
|
||||
if (gTitle.logoFlipIdx == 4u) {
|
||||
gTitle.logoColorIdx = (uint8_t)((gTitle.logoColorIdx + 1u) & 7u);
|
||||
}
|
||||
gTitle.logoNeedsPaint = true;
|
||||
}
|
||||
|
||||
|
||||
// Re-paste every $84 logo cell with the current flip-frame tile + color
|
||||
// ($489C palette codes = the port's indices). Render-pass only, and only
|
||||
// when the flip advanced or the tilemap was just repainted -- between
|
||||
// steps the logo cells sit static on the tilemap.
|
||||
static void titlePaintLogo(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
|
||||
|
|
@ -509,14 +587,11 @@ static void titleAnimateLogo(jlSurfaceT *stage, const StLevelT *level) {
|
|||
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) {
|
||||
if (!gTitle.logoNeedsPaint) {
|
||||
return;
|
||||
}
|
||||
gTitle.logoNeedsPaint = false;
|
||||
|
||||
// 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;
|
||||
|
|
@ -530,16 +605,6 @@ static void titleAnimateLogo(jlSurfaceT *stage, const StLevelT *level) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// $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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -577,6 +642,11 @@ static void titleStepPassenger(void) {
|
|||
// $4A03 dispatcher. One iteration per host frame, exactly as the C64
|
||||
// runs $4A03 from $47E4 each iteration of the title intro loop.
|
||||
static void titleTick(void) {
|
||||
// Logo flip-book + color cycle advance once per game tick (its own
|
||||
// mod-4 divider gates the actual step), independent of the intro
|
||||
// stage machine below.
|
||||
titleAdvanceLogo();
|
||||
|
||||
switch (gTitle.stage) {
|
||||
case ST_TITLE_STAGE_SPARKLE:
|
||||
// $4A17: JSR $66B7 / DEC $473F / BEQ -> INC $7163.
|
||||
|
|
@ -600,7 +670,7 @@ static void titleTick(void) {
|
|||
if (gTitle.decayTimer == 0u) {
|
||||
gTitle.decayTimer = gTitle.decayReload;
|
||||
gTitle.sparkleIdx = (uint8_t)((gTitle.sparkleIdx + 1u) & 0x03u);
|
||||
gTitle.passengerPtr = kTitleSparkleCels[gTitle.sparkleIdx];
|
||||
gTitle.passengerPtr = kPassengerWaveCels[gTitle.sparkleIdx];
|
||||
}
|
||||
if (gTitle.introFrameCount > 0u) {
|
||||
gTitle.introFrameCount--;
|
||||
|
|
@ -652,90 +722,23 @@ static void titleTick(void) {
|
|||
gTitle.cabPtr = 0xC0;
|
||||
gTitle.flameDirMask = 0x01; // UP only
|
||||
gTitle.flameVisible = true;
|
||||
gTitle.flameParity ^= 1u; // $6D8B-$6D90 toggle on every call
|
||||
if (gTitle.cabSy < (int16_t)ST_TITLE_CAB_TAKEOFF_SY) {
|
||||
// $4A64-$4A6C: INC gDeathStage; silence voice 3.
|
||||
gTitle.cabVisible = false;
|
||||
gTitle.flameVisible = false;
|
||||
gTitle.stage = ST_TITLE_STAGE_DEMO;
|
||||
// Demo continues from wherever the cab is -- the C64 does
|
||||
// NOT reset the cab position. Physics + gravity bring it
|
||||
// back into view from the top.
|
||||
gTitle.demoX = (int32_t)gTitle.cabSx * ST_SUBPIXEL;
|
||||
gTitle.demoY = (int32_t)gTitle.cabSy * ST_SUBPIXEL;
|
||||
gTitle.demoVx = 0;
|
||||
gTitle.demoVy = (int16_t)(-2 * ST_SUBPIXEL); // takeoff momentum
|
||||
// $4A64 INCs gDeathStage out of the intro: on the C64 the
|
||||
// machine now enters the rolling attract demo. The port
|
||||
// signals spacetaxi.c via stRenderTitleIntroDone().
|
||||
gTitle.stage = ST_TITLE_STAGE_DONE;
|
||||
}
|
||||
break;
|
||||
|
||||
case ST_TITLE_STAGE_DEMO:
|
||||
case ST_TITLE_STAGE_DONE:
|
||||
default:
|
||||
{
|
||||
// Demo phase = physicsTick with $48F2 sourcing input from
|
||||
// kTitleDemoScript. $48F2 advance pattern at $4903-$4923:
|
||||
// DEC $4740; BNE return; -- still on current pair
|
||||
// LDY #$03; LDA ($3F),Y; -- new timer from NEXT pair
|
||||
// STA $4740;
|
||||
// $3F += 2; -- advance to next pair
|
||||
uint8_t mask;
|
||||
int8_t dx;
|
||||
int8_t dy;
|
||||
int32_t maxX = (int32_t)(SURFACE_WIDTH - ST_TAXI_W_PX) * ST_SUBPIXEL;
|
||||
int32_t maxY = (int32_t)(SURFACE_HEIGHT - ST_TAXI_H_PX) * ST_SUBPIXEL;
|
||||
if (gTitle.scriptTimer > 0u) {
|
||||
gTitle.scriptTimer--;
|
||||
}
|
||||
if (gTitle.scriptTimer == 0u) {
|
||||
gTitle.scriptIdx = (uint16_t)((gTitle.scriptIdx + 2u) & 0xFFu);
|
||||
gTitle.scriptTimer =
|
||||
kTitleDemoScript[(gTitle.scriptIdx + 1u) & 0xFFu];
|
||||
if (gTitle.scriptTimer == 0u) {
|
||||
gTitle.scriptTimer = 1u;
|
||||
}
|
||||
}
|
||||
mask = kTitleDemoScript[gTitle.scriptIdx & 0xFFu];
|
||||
// CIA1 PortA bits (after EOR #$FF at $6043):
|
||||
// bit0 = UP, bit1 = DOWN, bit2 = LEFT, bit3 = RIGHT, bit4 = FIRE.
|
||||
dx = (int8_t)(((mask & 0x08u) ? 1 : 0) - ((mask & 0x04u) ? 1 : 0));
|
||||
dy = (int8_t)(((mask & 0x02u) ? 1 : 0) - ((mask & 0x01u) ? 1 : 0));
|
||||
// Level-A defaults for the demo: xAccel/yAccel = 14, yGrav = 1.
|
||||
gTitle.demoVx = (int16_t)(gTitle.demoVx + dx * 14);
|
||||
gTitle.demoVy = (int16_t)(gTitle.demoVy + dy * 14 + 1);
|
||||
if (gTitle.demoVx > 512) gTitle.demoVx = 512;
|
||||
if (gTitle.demoVx < -512) gTitle.demoVx = -512;
|
||||
if (gTitle.demoVy > 512) gTitle.demoVy = 512;
|
||||
if (gTitle.demoVy < -512) gTitle.demoVy = -512;
|
||||
gTitle.demoX += gTitle.demoVx;
|
||||
gTitle.demoY += gTitle.demoVy;
|
||||
if (gTitle.demoX < 0) { gTitle.demoX = 0; gTitle.demoVx = 0; }
|
||||
if (gTitle.demoX > maxX) { gTitle.demoX = maxX; gTitle.demoVx = 0; }
|
||||
// Y can go negative (cab off-screen above) -- let it. Only
|
||||
// clamp at the bottom.
|
||||
if (gTitle.demoY > maxY) { gTitle.demoY = maxY; gTitle.demoVy = 0; }
|
||||
gTitle.cabSx = (int16_t)(gTitle.demoX >> ST_SUBPIXEL_SHIFT);
|
||||
gTitle.cabSy = (int16_t)(gTitle.demoY >> ST_SUBPIXEL_SHIFT);
|
||||
// Only re-show the cab once it's back inside visible Y.
|
||||
if (gTitle.cabSy >= (int16_t)ST_SPRITE_Y_OFFSET) {
|
||||
gTitle.cabVisible = true;
|
||||
}
|
||||
// Flame: per $6D6A, on whenever any direction bit is set in the
|
||||
// mask. Build the mask from dx/dy the same way the C64 reads
|
||||
// CIA1 PortA: bit0=UP bit1=DOWN bit2=LEFT bit3=RIGHT.
|
||||
{
|
||||
uint8_t mask = 0u;
|
||||
if (dy < 0) mask |= 0x01u; // UP
|
||||
if (dy > 0) mask |= 0x02u; // DOWN
|
||||
if (dx < 0) mask |= 0x04u; // LEFT
|
||||
if (dx > 0) mask |= 0x08u; // RIGHT
|
||||
gTitle.flameDirMask = mask;
|
||||
gTitle.flameVisible = (mask != 0u);
|
||||
if (gTitle.flameVisible) {
|
||||
gTitle.flameParity ^= 1u;
|
||||
}
|
||||
}
|
||||
// Intro finished -- the title art sits static (logo cycle
|
||||
// continues); spacetaxi.c switches to ST_STATE_DEMO.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -778,10 +781,12 @@ 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);
|
||||
// Paint the "SPACE TAXI" logo (char $84 cells) at its current
|
||||
// flip frame + color over the freshly-painted tilemap, BEFORE
|
||||
// the sprites save-under and draw so they overlay it. The flip
|
||||
// ADVANCE happens on the game-tick clock (titleAdvanceLogo);
|
||||
// this only re-pastes when it changed or the tilemap repainted.
|
||||
titlePaintLogo(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
|
||||
|
|
@ -790,15 +795,15 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) {
|
|||
// text -- only "BY JOHN F. BUTCHER" credits and the
|
||||
// UP=hiscore / DOWN=instructions / FIRE=begin joystick line.
|
||||
// Options-menu scene is a separate state to be built later.
|
||||
// Run the C64 title intro state machine (sparkle -> passenger
|
||||
// walk -> taxi takeoff -> scripted demo) and draw whichever
|
||||
// sprites are visible this frame.
|
||||
titleTick();
|
||||
// The intro state machine (sparkle -> passenger walk -> taxi
|
||||
// takeoff) is advanced on the fixed game-tick clock in the main
|
||||
// loop (stRenderTitleAdvance), not here -- this pass only DRAWS
|
||||
// whichever sprites the current intro state has visible.
|
||||
// sprite-X -> visible col = sprite-X - 24 (C64 border offset).
|
||||
// sprite-Y -> visible row = sprite-Y - 50 (top border).
|
||||
// Draw lowest priority first: flame (sprite 2), then passenger
|
||||
// (sprite 1), then cab (sprite 0) on top.
|
||||
if (gTitle.flameVisible && (gTitle.flameParity & 1u) &&
|
||||
if (gTitle.flameVisible && flameStrobeOn() &&
|
||||
gTitle.flameDirMask < 16u) {
|
||||
int8_t cel = kFlameCelByDirMask[gTitle.flameDirMask];
|
||||
if (cel >= 0 && gRender.flameCels[cel] != NULL) {
|
||||
|
|
@ -812,15 +817,7 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) {
|
|||
}
|
||||
}
|
||||
if (gTitle.passengerVisible) {
|
||||
// Map live C64 sprite ptr -> sprite-sheet cel index.
|
||||
// $C4..$CB -> cels 0..7 (walk-LEFT cels then boarding)
|
||||
// $D9 -> cel 8 (3rd sparkle cel from $66D6)
|
||||
int16_t cel;
|
||||
if (gTitle.passengerPtr == 0xD9) {
|
||||
cel = ST_PASSENGER_CEL_SPARKLE;
|
||||
} else {
|
||||
cel = (int16_t)gTitle.passengerPtr - 0xC4;
|
||||
}
|
||||
int16_t cel = passCelForPtr(gTitle.passengerPtr);
|
||||
if (cel < 0) {
|
||||
cel = 0;
|
||||
}
|
||||
|
|
@ -931,12 +928,19 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) {
|
|||
}
|
||||
|
||||
// Restore prev-frame sprite backups FIRST so the static tilemap
|
||||
// shows through where the taxi/passenger used to be. Then we'll
|
||||
// save+draw at the new positions below.
|
||||
// shows through where the taxi/passenger/flame used to be. Then
|
||||
// we'll save+draw at the new positions below. LIFO order: the cab
|
||||
// was saved AFTER the flame (its backup holds flame pixels), so
|
||||
// the cab restores first and the flame restore then cleans the
|
||||
// re-deposited flame pixels.
|
||||
if (gRender.taxiHasBackup) {
|
||||
jlSpriteRestoreUnder(stage, &gRender.taxiBackup);
|
||||
gRender.taxiHasBackup = false;
|
||||
}
|
||||
if (gRender.flameHasBackup) {
|
||||
jlSpriteRestoreUnder(stage, &gRender.flameBackup);
|
||||
gRender.flameHasBackup = false;
|
||||
}
|
||||
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
|
||||
if (gRender.passengerHasBackup[i]) {
|
||||
jlSpriteRestoreUnder(stage, &gRender.passengerBackup[i]);
|
||||
|
|
@ -952,43 +956,82 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) {
|
|||
px = (int16_t)(game->taxi.x >> ST_SUBPIXEL_SHIFT);
|
||||
py = (int16_t)(game->taxi.y >> ST_SUBPIXEL_SHIFT);
|
||||
|
||||
// Cel = idle (0) when not thrusting, otherwise cycle through the
|
||||
// three authored flame frames (1, 2, 3). Falls back to cel 0 if
|
||||
// a thrust cel isn't authored. During the crash anim ($6B24/$6B4C
|
||||
// in the C64) the original walks sprite-0 through death cels
|
||||
// $CC..$D1 at a slowing rate; the port doesn't have those cels
|
||||
// so the cab keeps its normal sprite as it falls -- the falling
|
||||
// motion + crash SFX + voice-1 scream are the cue.
|
||||
if (game->taxi.thrusting) {
|
||||
taxiCel = (uint8_t)(1u +
|
||||
(game->taxi.thrustFrame / ST_THRUST_CEL_TICKS));
|
||||
if (taxiCel >= ST_TAXI_CEL_COUNT) {
|
||||
taxiCel = 0u;
|
||||
}
|
||||
// $619B cel select: facing picks the $C0/$C1 (right) or $DC/$DD
|
||||
// (left) pair; bit 0 is the LANDING GEAR state. The body is a
|
||||
// STATIC cel -- no flicker of any kind. Sheet order
|
||||
// [$C1,$C0,$DC,$DD]:
|
||||
// right: ptr = $C0|gear -> cel 1 - gear
|
||||
// left: ptr = $DC|gear -> cel 2 + gear
|
||||
// (The crash anim uses the dedicated death cels below.)
|
||||
if (game->taxi.facing == ST_DIR_LEFT) {
|
||||
taxiCel = (uint8_t)(game->taxi.gearDown ? 3u : 2u);
|
||||
} else {
|
||||
taxiCel = 0u;
|
||||
taxiCel = (uint8_t)(game->taxi.gearDown ? 0u : 1u);
|
||||
}
|
||||
if (gRender.taxiCels[taxiCel] == NULL) {
|
||||
taxiCel = 0u;
|
||||
}
|
||||
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);
|
||||
// Engine flame: C64 sprite 2 ($6D6A) at (cab_x - 2, cab_y),
|
||||
// cel indexed by the $716A direction mask through the $6DB0
|
||||
// table, drawn on alternate frames only (the parity flicker),
|
||||
// hidden when no input is held. Drawn BEFORE the cab so the cab
|
||||
// (sprite 0) keeps VIC priority on top.
|
||||
if (game->taxi.warpFrame == 0u && game->taxi.thrusting &&
|
||||
!game->taxi.landed && flameStrobeOn() &&
|
||||
game->taxi.dirMask < 16u) {
|
||||
int8_t fcel = kFlameCelByDirMask[game->taxi.dirMask];
|
||||
if (fcel >= 0 && gRender.flameCels[fcel] != NULL) {
|
||||
jlSpriteSaveAndDraw(stage, gRender.flameCels[fcel],
|
||||
(int16_t)(px - 2), py,
|
||||
&gRender.flameBackup);
|
||||
gRender.flameHasBackup = true;
|
||||
}
|
||||
if (gRender.warpCels[wcel] != NULL) {
|
||||
jlSpriteSaveAndDraw(stage, gRender.warpCels[wcel], px, py,
|
||||
}
|
||||
if (game->taxi.crashTicks > 0u) {
|
||||
// Crash death: phase 1 tumbles the $CC/$CD debris pair while
|
||||
// falling; the floor hit ($6ACC row $DA) starts the $CE..$D1
|
||||
// impact walk at the slowing $6B2A cadence, then the wreck
|
||||
// hides until respawn.
|
||||
uint32_t nowMs = jlMillisElapsed();
|
||||
int16_t dcel = -1;
|
||||
if (!gRender.deathActive) {
|
||||
gRender.deathActive = true;
|
||||
gRender.deathMs = nowMs;
|
||||
gRender.deathStep = 0u;
|
||||
}
|
||||
if (!game->taxi.crashImpacted) {
|
||||
dcel = (int16_t)((nowMs / ST_DEATH_TUMBLE_MS) & 1u);
|
||||
gRender.deathMs = nowMs; // re-anchor for the walk
|
||||
gRender.deathStep = 0u;
|
||||
} else {
|
||||
if (gRender.deathStep < 4u &&
|
||||
nowMs - gRender.deathMs >= kDeathStepMs[gRender.deathStep]) {
|
||||
gRender.deathMs = nowMs;
|
||||
gRender.deathStep++;
|
||||
}
|
||||
if (gRender.deathStep < 4u) {
|
||||
dcel = (int16_t)(2u + gRender.deathStep);
|
||||
}
|
||||
}
|
||||
if (dcel >= 0 && gRender.deathCels[dcel] != NULL) {
|
||||
jlSpriteSaveAndDraw(stage, gRender.deathCels[dcel], px, py,
|
||||
&gRender.taxiBackup);
|
||||
gRender.taxiHasBackup = true;
|
||||
}
|
||||
} else if (game->taxi.warpFrame > 0u) {
|
||||
// Transporter shrink-warp: identity map onto the SEVEN drawn
|
||||
// chain cels $E2..$E8; step 7 (pointer reaching $E9) hides
|
||||
// the sprite entirely, exactly like $5CDA/$718E.
|
||||
uint8_t step = stEngineWarpStep(game); /* 0..7 */
|
||||
if (step < ST_WARP_CEL_COUNT && gRender.warpCels[step] != NULL) {
|
||||
jlSpriteSaveAndDraw(stage, gRender.warpCels[step], 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.
|
||||
gRender.deathActive = false;
|
||||
jlSpriteSaveAndDraw(stage, gRender.taxiCels[taxiCel], px, py,
|
||||
&gRender.taxiBackup);
|
||||
gRender.taxiHasBackup = true;
|
||||
|
|
@ -1000,12 +1043,6 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) {
|
|||
jlFillRect(stage, px, py, ST_TAXI_W_PX, ST_TAXI_H_PX,
|
||||
game->level.sprite0Color);
|
||||
gRender.tilemapDirty = true;
|
||||
// Flame placeholder (only when the cab sprite isn't authored,
|
||||
// since cels 1..3 of the authored asset already include the
|
||||
// flame baked in). C64 sprite 2 ($6D6A, writes $7177/$7199)
|
||||
// is positioned at
|
||||
// (taxi_col - 2, taxi_row), cel-indexed by $716A direction;
|
||||
// we use sprite1Color ($D028) so per-level palette is honored.
|
||||
if (game->taxi.thrusting) {
|
||||
jlFillRect(stage,
|
||||
(int16_t)(px + ST_FLAME_OFFSET_X_PX),
|
||||
|
|
@ -1020,10 +1057,38 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) {
|
|||
for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
|
||||
const StPassengerT *p = &game->passengers[i];
|
||||
jlSpriteT *cel;
|
||||
if (!p->active || p->onboard) {
|
||||
continue; // onboard passengers ride invisibly inside the cab
|
||||
int16_t pcel;
|
||||
if (!p->active || p->phase == ST_PASS_RIDING) {
|
||||
continue; // riding passengers are invisible in the cab
|
||||
}
|
||||
cel = gRender.passengerCels[p->walkPhase % ST_PASSENGER_CEL_COUNT];
|
||||
// Cel by lifecycle phase (see StPassPhaseE): beams shrink or
|
||||
// grow through $C7..$CB, the wait wave cycles $66D6, walks
|
||||
// alternate the direction's stride pair.
|
||||
switch (p->phase) {
|
||||
case ST_PASS_BEAM_IN:
|
||||
case ST_PASS_DROP_IN:
|
||||
pcel = (int16_t)(7 - (p->waitPhase > 4u ? 4u : p->waitPhase));
|
||||
break;
|
||||
case ST_PASS_BOARD_OUT:
|
||||
case ST_PASS_DROP_OUT:
|
||||
pcel = (int16_t)(3 + (p->waitPhase > 4u ? 4u : p->waitPhase));
|
||||
break;
|
||||
case ST_PASS_WALK_TO_CAB:
|
||||
case ST_PASS_WALK_TO_PAD:
|
||||
if (p->targetX < p->x) {
|
||||
pcel = (int16_t)((p->waitPhase & 1u) ? ST_PASSENGER_CEL_WALK1
|
||||
: ST_PASSENGER_CEL_WALK0);
|
||||
} else {
|
||||
pcel = (int16_t)((p->waitPhase & 1u) ? ST_PASSENGER_CEL_WALKR1
|
||||
: ST_PASSENGER_CEL_WALKR0);
|
||||
}
|
||||
break;
|
||||
case ST_PASS_WAIT:
|
||||
default:
|
||||
pcel = passCelForPtr(kPassengerWaveCels[p->waitPhase & 3u]);
|
||||
break;
|
||||
}
|
||||
cel = gRender.passengerCels[pcel];
|
||||
if (cel == NULL) {
|
||||
jlFillRect(stage, p->x, p->y,
|
||||
ST_PASSENGER_W_PX, ST_PASSENGER_H_PX, 8u);
|
||||
|
|
@ -1035,6 +1100,13 @@ void stRenderFrame(jlSurfaceT *stage, const StGameT *game) {
|
|||
gRender.passengerHasBackup[i] = true;
|
||||
}
|
||||
|
||||
if (game->state == ST_STATE_DEMO) {
|
||||
// Attract banners ($45A7-$4614 text): drawn every frame over
|
||||
// the live demo, before the present.
|
||||
stRenderDrawText(stage, 7u, 3u, "DEMO, USE JOYSTICK TO EXIT");
|
||||
stRenderDrawText(stage, 3u, 24u, "THIS IS 1 OF 25 DIFFERENT SCREENS!");
|
||||
}
|
||||
|
||||
if (game->state == ST_STATE_LEVEL_DONE) {
|
||||
int16_t midY = (int16_t)(10 * ST_TILE_PIXELS);
|
||||
jlFillRect(stage, 0, midY, SURFACE_WIDTH,
|
||||
|
|
@ -1082,37 +1154,99 @@ static bool loadTileBank(uint8_t bankId) {
|
|||
}
|
||||
|
||||
|
||||
static bool loadSpriteSheet(void) {
|
||||
static bool loadSpriteSheet(jlSurfaceT *stage) {
|
||||
jlSpriteT *cels[ST_SPRITE_SHEET_CELS];
|
||||
uint16_t count;
|
||||
uint16_t i;
|
||||
uint8_t step;
|
||||
bool precompiled;
|
||||
|
||||
// .spr blob carries all 27 cels (9 cols x 3 rows of 3x3-tile
|
||||
// .spr blob carries all 36 cels (9 cols x 4 rows of 3x3-tile
|
||||
// chunky 4bpp blobs) in PNG reading order: row 0 taxi, row 1
|
||||
// passenger, row 2 flame. The padding slots between named
|
||||
// sprites are loaded but never referenced; they leak ~1.7 KB
|
||||
// at startup which is well below the cost of writing a
|
||||
// passenger, row 2 flame, row 3 warp. The padding slots between
|
||||
// named sprites are loaded but never referenced; they leak a few
|
||||
// KB at startup which is well below the cost of writing a
|
||||
// free-loop here (extra code in _ROOT eats the IIgs cluster
|
||||
// budget more than the heap fragments hurt).
|
||||
memset(cels, 0, sizeof(cels));
|
||||
// Prefer the pre-compiled bank: its cels arrive already compiled (native
|
||||
// routines dropped straight into the codegen arena), so the jlSpriteCompile
|
||||
// loop below is a no-op fast path. Fall back to the portable .spr + runtime
|
||||
// JIT if no .spc is staged for this build.
|
||||
count = jlSpriteBankLoadPrecompiled(ST_SPRITE_SHEET_SPC, cels, ST_SPRITE_SHEET_CELS, NULL);
|
||||
precompiled = (count != 0u);
|
||||
if (count == 0u) {
|
||||
count = jlSpriteBankLoad(ST_SPRITE_SHEET_PATH, cels, ST_SPRITE_SHEET_CELS, NULL);
|
||||
}
|
||||
if (count == 0u) {
|
||||
return false;
|
||||
}
|
||||
jlLogF("stRender: sprite sheet loaded (%u cels)", (unsigned)count);
|
||||
jlLogF("stRender: sprite sheet loaded (%u cels, %s)", (unsigned)count,
|
||||
precompiled ? "PRECOMPILED" : "jit");
|
||||
loadProgress(stage, 2u);
|
||||
|
||||
// Compile every drawn cel to its per-shift asm routine. Without
|
||||
// this, jlSprite{Draw,SaveUnder,SaveAndDraw} fall to the per-pixel
|
||||
// interpreter (sp->slot stays NULL) -- catastrophic on the IIgs
|
||||
// 65816: the 3 title sprites alone cost ~200 ms/frame interpreted
|
||||
// (~3.7 fps). Compiling routes them to the fast path (UBER's
|
||||
// benchmarks prove codegen works on every port). jlSpriteCompile is
|
||||
// a no-op fallback if the arena is exhausted (cel stays interpreted).
|
||||
{
|
||||
uint16_t compiled = 0u;
|
||||
|
||||
// Assign all cels first, then compile title-critical ones
|
||||
// (taxi, passenger, flame) before the gameplay-only warp cels so
|
||||
// a tight arena still fast-paths the title.
|
||||
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_DEATH_CEL_COUNT; i++) {
|
||||
gRender.deathCels[i] = cels[ST_SPRITE_DEATH_FIRST + i];
|
||||
}
|
||||
for (i = 0u; i < ST_PASSENGER_CEL_COUNT; i++) {
|
||||
gRender.passengerCels[i] = cels[ST_SPRITE_PASS_FIRST + i];
|
||||
}
|
||||
for (i = 0u; i < ST_FLAME_CEL_COUNT; i++) {
|
||||
gRender.flameCels[i] = cels[ST_SPRITE_FLAME_FIRST + i];
|
||||
}
|
||||
step = 3u;
|
||||
for (i = 0u; i < ST_TAXI_CEL_COUNT; i++) {
|
||||
compiled = (uint16_t)(compiled + (jlSpriteCompile(gRender.taxiCels[i]) ? 1u : 0u));
|
||||
loadProgress(stage, step++);
|
||||
}
|
||||
for (i = 0u; i < ST_PASSENGER_CEL_COUNT; i++) {
|
||||
compiled = (uint16_t)(compiled + (jlSpriteCompile(gRender.passengerCels[i]) ? 1u : 0u));
|
||||
loadProgress(stage, step++);
|
||||
}
|
||||
for (i = 0u; i < ST_FLAME_CEL_COUNT; i++) {
|
||||
compiled = (uint16_t)(compiled + (jlSpriteCompile(gRender.flameCels[i]) ? 1u : 0u));
|
||||
loadProgress(stage, step++);
|
||||
}
|
||||
for (i = 0u; i < ST_WARP_CEL_COUNT; i++) {
|
||||
compiled = (uint16_t)(compiled + (jlSpriteCompile(gRender.warpCels[i]) ? 1u : 0u));
|
||||
loadProgress(stage, step++);
|
||||
}
|
||||
for (i = 0u; i < ST_DEATH_CEL_COUNT; i++) {
|
||||
compiled = (uint16_t)(compiled + (jlSpriteCompile(gRender.deathCels[i]) ? 1u : 0u));
|
||||
loadProgress(stage, step++);
|
||||
}
|
||||
jlLogF("stRender: sprite cels compiled: %u/%u", (unsigned)compiled,
|
||||
(unsigned)(ST_TAXI_CEL_COUNT + ST_PASSENGER_CEL_COUNT + ST_FLAME_CEL_COUNT + ST_WARP_CEL_COUNT + ST_DEATH_CEL_COUNT));
|
||||
}
|
||||
|
||||
// First-run cache: having just JIT-compiled the bank, write it out as a
|
||||
// .spc so the next launch loads the compiled routines and skips the JIT
|
||||
// (ship the small .spr, cache the big per-platform compiled bank). Best-
|
||||
// effort -- a read-only/write-protected disk just means we JIT again.
|
||||
if (!precompiled) {
|
||||
if (jlSpriteBankSavePrecompiled(ST_SPRITE_SHEET_SPC, cels, count, NULL)) {
|
||||
jlLogF("stRender: cached compiled bank -> %s", ST_SPRITE_SHEET_SPC);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1125,6 +1259,12 @@ static void destroySprites(void) {
|
|||
gRender.taxiCels[i] = NULL;
|
||||
}
|
||||
}
|
||||
for (i = 0u; i < ST_DEATH_CEL_COUNT; i++) {
|
||||
if (gRender.deathCels[i] != NULL) {
|
||||
jlSpriteDestroy(gRender.deathCels[i]);
|
||||
gRender.deathCels[i] = NULL;
|
||||
}
|
||||
}
|
||||
for (i = 0u; i < ST_WARP_CEL_COUNT; i++) {
|
||||
if (gRender.warpCels[i] != NULL) {
|
||||
jlSpriteDestroy(gRender.warpCels[i]);
|
||||
|
|
@ -1172,6 +1312,70 @@ static bool loadFontSheet(jlSurfaceT *stage) {
|
|||
}
|
||||
|
||||
|
||||
// The C64 flame strobe ($716C: one game tick on, one off) mapped to
|
||||
// wall-clock so every port shows the intended LOOK: at >=50fps this
|
||||
// is the authentic 12.5Hz shimmer; when a rendered frame spans a
|
||||
// whole strobe phase (low fps) the flame draws every frame -- the
|
||||
// CRT-fusion appearance instead of an unreadable slow blink.
|
||||
static bool flameStrobeOn(void) {
|
||||
uint32_t nowMs = jlMillisElapsed();
|
||||
bool phase = ((nowMs / ST_FLAME_STROBE_MS) & 1u) != 0u;
|
||||
bool spans = (nowMs - gRender.flamePrevMs) >= ST_FLAME_STROBE_MS;
|
||||
|
||||
gRender.flamePrevMs = nowMs;
|
||||
return phase || spans;
|
||||
}
|
||||
|
||||
|
||||
// Map a live C64 sprite-1 pointer value onto the sheet's passenger
|
||||
// row (extractSprites.py pass_ptrs order):
|
||||
// $C4..$CB -> cels 0..7 (walk cels, boarding shrink)
|
||||
// $D9 -> cel 8 (the wave/sparkle cel)
|
||||
// Single source of truth for the title path and the gameplay wave.
|
||||
static int16_t passCelForPtr(uint8_t ptr) {
|
||||
if (ptr == 0xD9u) {
|
||||
return ST_PASSENGER_CEL_SPARKLE;
|
||||
}
|
||||
if (ptr == 0xC2u) {
|
||||
return ST_PASSENGER_CEL_WALKR0;
|
||||
}
|
||||
if (ptr == 0xC3u) {
|
||||
return ST_PASSENGER_CEL_WALKR1;
|
||||
}
|
||||
return (int16_t)ptr - 0xC4;
|
||||
}
|
||||
|
||||
|
||||
// Startup progress: outline drawn on step 0 so the screen shows life
|
||||
// the moment stRenderInit runs, bar grows per completed step, LOADING
|
||||
// caption appears once the font glyphs exist. Cheap enough to call
|
||||
// per sprite compile even on the IIgs (present base ~3 ms).
|
||||
static void loadProgress(jlSurfaceT *stage, uint8_t step) {
|
||||
int16_t w;
|
||||
|
||||
if (step > (uint8_t)ST_LOAD_STEPS_TOTAL) {
|
||||
step = (uint8_t)ST_LOAD_STEPS_TOTAL;
|
||||
}
|
||||
if (step == 0u) {
|
||||
jlFillRect(stage, ST_LOAD_BAR_X - 2, ST_LOAD_BAR_Y - 2,
|
||||
(uint16_t)(SURFACE_WIDTH - 2 * ST_LOAD_BAR_X + 4),
|
||||
ST_LOAD_BAR_H + 4, ST_LOAD_BAR_FRAME_COLOR);
|
||||
jlFillRect(stage, ST_LOAD_BAR_X - 1, ST_LOAD_BAR_Y - 1,
|
||||
(uint16_t)(SURFACE_WIDTH - 2 * ST_LOAD_BAR_X + 2),
|
||||
ST_LOAD_BAR_H + 2, 0u);
|
||||
}
|
||||
w = (int16_t)(((int32_t)step * (SURFACE_WIDTH - 2 * ST_LOAD_BAR_X)) / ST_LOAD_STEPS_TOTAL);
|
||||
if (w > 0) {
|
||||
jlFillRect(stage, ST_LOAD_BAR_X, ST_LOAD_BAR_Y, (uint16_t)w,
|
||||
ST_LOAD_BAR_H, ST_LOAD_BAR_COLOR);
|
||||
}
|
||||
if (gRender.fontReady) {
|
||||
stRenderDrawText(stage, 16u, 11u, "LOADING");
|
||||
}
|
||||
jlStagePresent();
|
||||
}
|
||||
|
||||
|
||||
// Build the ASCII -> (blockX | blockY << 8) lookup table used by
|
||||
// jlDrawText. The font sheet was authored with each ASCII glyph at
|
||||
// cell (ascii % 40, ascii / 40) (see assets/genPlaceholderArt.py),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue