joeylib2/examples/spacetaxi/stHooks.c

1001 lines
33 KiB
C

// Space Taxi -- per-level hook programs.
//
// Every level's data blob ends with six trampolines ($7D66..$7D77)
// and a small program at $7D98..$7FFF that the main loop calls at
// fixed points: two prelude hooks, two per-tick hooks (before and
// after the sprite flush), an input filter and the sprite-contact
// verdict. Those programs are the level gimmicks -- the puzzle
// switches, lasers, moving pads, trap doors. The port keeps the
// original bytes (sim->hookRam, for the tables the code indexes) and
// re-expresses each program here, reading its tables at the same
// addresses so nothing has to be transcribed by hand.
#include "stSim.h"
// Hook RAM accessors by C64 address.
#define HK(sim, addr) ((sim)->hookRam[(addr) - ST_HOOK_BASE])
#define HKP(sim, addr) (&(sim)->hookRam[(addr) - ST_HOOK_BASE])
// Level indices (0 = A).
#define ST_LEVEL_E 4u
#define ST_LEVEL_G 6u
#define ST_LEVEL_H 7u
#define ST_LEVEL_I 8u
#define ST_LEVEL_J 9u
#define ST_LEVEL_K 10u
#define ST_LEVEL_L 11u
#define ST_LEVEL_O 14u
#define ST_LEVEL_P 15u
#define ST_LEVEL_Q 16u
#define ST_LEVEL_R 17u
#define ST_LEVEL_T 19u
#define ST_LEVEL_U 20u
#define ST_LEVEL_V 21u
#define ST_LEVEL_W 22u
#define ST_LEVEL_X 23u
static void addToSpriteCol(StSimT *sim, uint8_t idx, uint8_t delta);
static void fillDownChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch, uint8_t rows);
static void fillDownColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color, uint8_t rows);
static void hookHPerTick(StSimT *sim);
static void hookHPrelude0(StSimT *sim);
static void hookHSegment(StSimT *sim, uint8_t id);
static void hookTGate(StSimT *sim, uint8_t ch);
static void hookTPerTick(StSimT *sim);
static void hookTPrelude1(StSimT *sim);
static void hookWLaserUpdate(StSimT *sim);
static void hookWPerTick(StSimT *sim);
static void hookXMovePads(StSimT *sim);
static void hookXPerTick(StSimT *sim);
static void hookXPrelude0(StSimT *sim);
static void hookXShiftRow(StSimT *sim, uint16_t rowCell, uint8_t from, uint8_t to, int8_t dir);
static void setSpriteColors(StSimT *sim, uint8_t color);
#if !defined(__W65816__)
static void hookKPerTick(StSimT *sim);
static void hookOPerTick(StSimT *sim);
static void hookOTramp0(StSimT *sim);
static void hookRMaze(StSimT *sim, uint8_t ch);
static void hookRPerTick(StSimT *sim);
static void hookVPerTick(StSimT *sim);
static void hookVScrollLeft(StSimT *sim, uint8_t row);
static void hookVScrollRight(StSimT *sim, uint8_t row);
static void hookQPrelude(StSimT *sim);
static void hookQPerTick(StSimT *sim);
static uint8_t hookQInput(StSimT *sim, uint8_t input);
static void hookIPrelude(StSimT *sim);
static void hookIPerTick(StSimT *sim);
static void hookGPerTick(StSimT *sim);
static void hookUPerTick(StSimT *sim);
#endif
// $411B -- add a signed byte to a sprite's X (carry into the msb).
static void addToSpriteCol(StSimT *sim, uint8_t idx, uint8_t delta) {
uint16_t x = (uint16_t)(((uint16_t)sim->spr[idx].msb << 8) | sim->spr[idx].col);
x = (uint16_t)(x + (uint16_t)(int16_t)(int8_t)delta);
sim->spr[idx].col = (uint8_t)x;
sim->spr[idx].msb = (uint8_t)(x >> 8);
}
// $41AD after $401B -- the same character into the `rows` cells below.
static void fillDownChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch, uint8_t rows) {
uint8_t k;
for (k = 1u; k <= rows; k++) {
if ((uint8_t)(row + k) < ST_SCREEN_ROWS) {
stSimPutChar(sim, col, (uint8_t)(row + k), ch);
}
}
}
// $41AD after $401E -- the same colour into the `rows` cells below.
static void fillDownColor(StSimT *sim, uint8_t col, uint8_t row, uint8_t color, uint8_t rows) {
uint8_t k;
for (k = 1u; k <= rows; k++) {
if ((uint8_t)(row + k) < ST_SCREEN_ROWS) {
stSimPutColor(sim, col, (uint8_t)(row + k), color);
}
}
}
// Level H "PUZZLER" per-tick ($7E5A): touching a switch sprite, or
// landing on a pad whose list holds an active segment, animates the
// switch's four wall segments over four 8-tick steps with a rising
// tone, then toggles their flags.
static void hookHPerTick(StSimT *sim) {
if (HK(sim, 0x7E54) == 0u) {
if (sim->collisionPhase != 0u) {
return;
}
if (sim->activePad != 0u) {
uint8_t x = (uint8_t)((sim->activePad - 1u) * 4u);
uint8_t n;
bool found = false;
for (n = 0u; n < 4u; n++) {
uint8_t y = HK(sim, 0x7E24 + x + n);
if (HK(sim, 0x7E15 + y) != 0u) {
HK(sim, 0x7E4C) = y;
HK(sim, 0x7E54) = 6u;
found = true;
break;
}
}
if (!found) {
return;
}
} else {
uint8_t a = sim->spriteSpriteColl;
uint8_t y = 5u;
while (y != 0u) {
bool hit = (a & 0x80u) != 0u;
a = (uint8_t)(a << 1);
if (hit) {
break;
}
y--;
}
if (y == 0u) {
return;
}
HK(sim, 0x7E54) = y;
setSpriteColors(sim, 0x0Au);
sim->spr[2u + y].color = 0x05u;
}
// $7EB2: start the animation.
HK(sim, 0x7E53) = 0u;
HK(sim, 0x7E55) = 0xFFu;
HK(sim, 0x7E56) = 0x12u;
stAudioSfx(HKP(sim, 0x7ED0));
}
// $7ED9
HK(sim, 0x7E56)++;
stAudioVoice2(HK(sim, 0x7E56), HK(sim, 0x7E56), 0xFFu);
HK(sim, 0x7E53)++;
if ((HK(sim, 0x7E53) & 7u) != 0u) {
return;
}
HK(sim, 0x7E55)++;
HK(sim, 0x7E52) = (uint8_t)((HK(sim, 0x7E54) - 1u) * 4u);
HK(sim, 0x7E51) = 4u;
if (HK(sim, 0x7E55) == 4u) {
// $7F7C: finished -- flip the segment flags.
HK(sim, 0x7E54) = 0u;
setSpriteColors(sim, 0x01u);
while (HK(sim, 0x7E51) != 0u) {
uint8_t id = HK(sim, 0x7E38 + HK(sim, 0x7E52));
if ((id & 0x80u) == 0u) {
HK(sim, 0x7E15 + id) ^= 1u;
}
HK(sim, 0x7E52)++;
HK(sim, 0x7E51)--;
}
return;
}
while (HK(sim, 0x7E51) != 0u) {
uint8_t id = HK(sim, 0x7E38 + HK(sim, 0x7E52));
if ((id & 0x80u) == 0u) {
hookHSegment(sim, id);
}
HK(sim, 0x7E52)++;
HK(sim, 0x7E51)--;
}
}
// Level H prelude 0 ($7D9B): clear the segment flags, park the five
// switch sprites (3..7) from the position tables.
static void hookHPrelude0(StSimT *sim) {
uint8_t k;
for (k = 0u; k <= 10u; k++) {
HK(sim, 0x7E15 + k) = 0u;
}
HK(sim, 0x7E54) = 0u;
for (k = 7u; k >= 3u; k--) {
sim->spr[k].color = 1u;
sim->spr[k].enable = 1u;
sim->spr[k].ptr = 0x80u;
sim->spr[k].col = HK(sim, 0x7DCF + k);
sim->spr[k].msb = HK(sim, 0x7DD7 + k);
sim->spr[k].row = HK(sim, 0x7DDF + k);
}
}
// $7F22 -- draw (or erase) one cell of segment `id` for the current
// animation step, growing from whichever end the tables say.
static void hookHSegment(StSimT *sim, uint8_t id) {
uint8_t step;
uint8_t col;
uint8_t row;
uint8_t ch;
HK(sim, 0x7E50) = id;
if ((HK(sim, 0x7E15 + id) ^ HK(sim, 0x7DFD + id)) == 0u) {
step = HK(sim, 0x7E55);
} else {
step = (uint8_t)(3u - HK(sim, 0x7E55));
}
if (HK(sim, 0x7E08 + id) == 0x9Bu) {
row = (uint8_t)(step + HK(sim, 0x7DF2 + id));
col = HK(sim, 0x7DE7 + id);
} else {
col = (uint8_t)(step + HK(sim, 0x7DE7 + id));
row = HK(sim, 0x7DF2 + id);
}
HK(sim, 0x7E13) = col;
HK(sim, 0x7E14) = row;
ch = (HK(sim, 0x7E15 + id) != 0u) ? HK(sim, 0x7E08 + id) : ST_CHAR_SPACE;
stSimPutChar(sim, col, row, ch);
}
// Level T "FAST BREAK" $7D9D -- the barrier on row 3: `ch` into the
// centre opening (cols 18..21), its complement into the side gaps.
static void hookTGate(StSimT *sim, uint8_t ch) {
uint8_t k;
for (k = 0u; k < 4u; k++) {
stSimPutChar(sim, (uint8_t)(18u + k), 3u, ch);
}
ch ^= 0x55u;
for (k = 0u; k < 4u; k++) {
stSimPutChar(sim, (uint8_t)(1u + k), 3u, ch);
stSimPutChar(sim, (uint8_t)(35u + k), 3u, ch);
}
}
// Level T per-tick ($7DDA): a slow cab up on the right bounces off
// the ceiling; a fast climb into the top rows slams the centre gate
// shut and opens the sides until the cab drops back down.
static void hookTPerTick(StSimT *sim) {
if (HK(sim, 0x7D9C) != 0u) {
if (sim->spr[0].row >= 0x5Fu) {
hookTPrelude1(sim);
}
return;
}
if (sim->spr[0].row >= 0x4Fu) {
return;
}
if ((uint8_t)((uint16_t)sim->velY >> 8) >= 0xFCu) {
if (sim->spr[0].msb != 0u) {
return;
}
if (sim->spr[0].col < 0x96u) {
return;
}
sim->velY = (int16_t)(-sim->velY);
stAudioSfx(HKP(sim, 0x7E56));
return;
}
if (sim->spr[0].row >= 0x3Fu) {
return;
}
sim->velY = 0;
sim->velX = 0;
hookTGate(sim, 0x75u);
HK(sim, 0x7D9C)++;
stAudioSfx(HKP(sim, 0x7E68));
}
// Level T prelude 1 ($7DC4): centre open, sides barred.
static void hookTPrelude1(StSimT *sim) {
hookTGate(sim, ST_CHAR_SPACE);
HK(sim, 0x7D9C) = 0u;
stAudioSfx(HKP(sim, 0x7E5F));
}
// Level W "LASERS" $7E46 -- clear the beam glyphs, then step each of
// the eight lasers: idle ones fire on a 2-in-12 roll, extending ones
// grow a cell per pass until their end row, retracting ones fade
// through two colours and vanish.
static void hookWLaserUpdate(StSimT *sim) {
uint8_t i;
uint8_t k;
for (k = 0u; k < 16u; k++) {
sim->charset[0x92u + (k >> 3)][k & 7u] = 0u;
}
sim->charDirty[0x92u] = 1u;
sim->charDirty[0x93u] = 1u;
for (i = 0u; i < 8u; i++) {
uint8_t state = HK(sim, 0x7DAC + i);
uint8_t col = HK(sim, 0x7DCD + i);
HK(sim, 0x7DB4) = i;
if (state == 0u) {
if (stSimRng(sim, 12u) >= 3u) {
continue;
}
HK(sim, 0x7DAC + i) = 1u;
HK(sim, 0x7DB5 + i) = HK(sim, 0x7DD5 + i);
stSimPutChar(sim, col, HK(sim, 0x7DB5 + i), HK(sim, 0x7DBD + i));
stSimPutColor(sim, col, HK(sim, 0x7DB5 + i), 2u);
continue;
}
if (state == 1u) {
stSimPutChar(sim, col, HK(sim, 0x7DB5 + i), 0x91u);
if (HK(sim, 0x7DB5 + i) == HK(sim, 0x7DE9 + i)) {
HK(sim, 0x7DAC + i) = 2u;
HK(sim, 0x7DB5 + i) = 0u;
state = 2u;
} else {
HK(sim, 0x7DB5 + i) = (uint8_t)(HK(sim, 0x7DB5 + i) + HK(sim, 0x7DDD + i));
stSimPutChar(sim, col, HK(sim, 0x7DB5 + i), HK(sim, 0x7DBD + i));
stSimPutColor(sim, col, HK(sim, 0x7DB5 + i), 2u);
continue;
}
}
if (state == 2u) {
uint8_t phase;
HK(sim, 0x7DB5 + i)++;
phase = HK(sim, 0x7DB5 + i);
if (phase == 3u) {
HK(sim, 0x7DAC + i) = 0u;
stSimPutChar(sim, col, HK(sim, 0x7DC5 + i), ST_CHAR_SPACE);
fillDownChar(sim, col, HK(sim, 0x7DC5 + i), ST_CHAR_SPACE, 7u);
} else {
uint8_t color = HK(sim, 0x7DE6 + phase);
stSimPutColor(sim, col, HK(sim, 0x7DC5 + i), color);
fillDownColor(sim, col, HK(sim, 0x7DC5 + i), color, 7u);
}
continue;
}
HK(sim, 0x7DAC + i) = 0u;
}
}
// Level W per-tick ($7DF1): a random-pitched hum on voice 2, the beam
// glyph animation on the odd ticks and the laser step on every 8th.
static void hookWPerTick(StSimT *sim) {
for (;;) {
uint8_t c;
if (HK(sim, 0x7DE5) != 0u) {
stAudioSfx(HKP(sim, 0x7DA2));
HK(sim, 0x7DE5) = 0u;
}
c = (uint8_t)(stSimRng(sim, 0x6Eu) + 6u);
stAudioVoice2(c, c, 0x81u);
HK(sim, 0x7DAB) = (uint8_t)((HK(sim, 0x7DAB) + 1u) & 7u);
c = HK(sim, 0x7DAB);
if (c == 0u) {
hookWLaserUpdate(sim);
return;
}
// $2C8F + c (char $91 row 7 for c = 0, else char $92 rows 0..6)
// and $2C98 + (8 - c) (char $93 rows 7..1) get a beam bar.
sim->charset[0x91u + ((7u + c) >> 3)][(7u + c) & 7u] = 0x3Cu;
sim->charset[0x93u][8u - c] = 0x3Cu;
sim->charDirty[0x92u] = 1u;
sim->charDirty[0x93u] = 1u;
if ((c & 1u) == 0u) {
return;
}
}
}
// Level X "ON THE MOVE" $7F53 -- shift every pad's X bounds and stand
// column, the waiting passenger, and a parked cab, by the step.
static void hookXMovePads(StSimT *sim) {
uint8_t dir = HK(sim, 0x7D9D);
uint8_t off = 0u;
uint8_t phase = 0u;
addToSpriteCol(sim, 1u, dir);
while (off < 0x48u) {
uint8_t pad = (uint8_t)(off >> 3);
uint8_t field = (uint8_t)(off & 7u);
uint8_t *hi;
uint8_t *lo;
uint16_t v;
if (field == 0u) {
hi = &sim->pads[pad].x1Hi;
lo = &sim->pads[pad].x1Lo;
} else if (field == 2u) {
hi = &sim->pads[pad].x2Hi;
lo = &sim->pads[pad].x2Lo;
} else {
hi = &sim->pads[pad].passMsb;
lo = &sim->pads[pad].passCol;
}
v = (uint16_t)(((uint16_t)*hi << 8) | *lo);
v = (uint16_t)(v + (uint16_t)(int16_t)(int8_t)dir);
*lo = (uint8_t)v;
*hi = (uint8_t)(v >> 8);
// Offsets step +2, +3, +3 repeating: x1, x2, stand X per pad.
if (phase == 0u) {
off = (uint8_t)(off + 2u);
} else {
off = (uint8_t)(off + 3u);
}
phase = (uint8_t)((phase + 1u) % 3u);
}
if (sim->activePad == 0u) {
return;
}
addToSpriteCol(sim, 0u, dir);
sim->posXmsb = sim->spr[0].msb;
sim->hoverXFrac = sim->spr[0].msb;
sim->posXcol = sim->spr[0].col;
sim->hoverXCol = sim->spr[0].col;
}
// Level X per-tick ($7DE1): every 16 ticks slide the two pad columns
// one cell, bouncing between columns 1 and 21.
static void hookXPerTick(StSimT *sim) {
uint8_t col;
HK(sim, 0x7D9E)++;
if (HK(sim, 0x7D9E) != 0x10u) {
return;
}
HK(sim, 0x7D9E) = 0u;
col = HK(sim, 0x7D9C);
if ((HK(sim, 0x7D9D) & 0x80u) == 0u) {
// $7DF9: rightward.
hookXShiftRow(sim, ST_CELL(5, 0), (uint8_t)(col + 17u), col, 1);
hookXShiftRow(sim, ST_CELL(10, 0), (uint8_t)(col + 17u), col, 1);
hookXShiftRow(sim, ST_CELL(15, 0), (uint8_t)(col + 17u), col, 1);
hookXShiftRow(sim, ST_CELL(20, 0), (uint8_t)(col + 17u), col, 1);
stSimPutChar(sim, col, 5u, HK(sim, 0x7D9F + col));
stSimPutChar(sim, col, 10u, HK(sim, 0x7D9F + col));
stSimPutChar(sim, col, 15u, HK(sim, 0x7D9F + col));
stSimPutChar(sim, col, 20u, HK(sim, 0x7D9F + col));
stSimPutColor(sim, col, 5u, 0x0Cu);
stSimPutColor(sim, col, 10u, 0x0Cu);
stSimPutColor(sim, col, 15u, 0x0Cu);
stSimPutColor(sim, col, 20u, 0x0Cu);
// $7E56: the row-14 cell of the right group moves with it.
stSimPutChar(sim, (uint8_t)(col + 12u), 14u, sim->screen[ST_CELL(14, col + 11u)]);
stSimPutChar(sim, (uint8_t)(col + 11u), 14u, ST_CHAR_SPACE);
stSimPutColor(sim, (uint8_t)(col + 12u), 14u, 7u);
stSimPutChar(sim, (uint8_t)(col + 11u), 5u, HK(sim, 0x7D9F + col + 11u));
stSimPutChar(sim, (uint8_t)(col + 11u), 10u, HK(sim, 0x7D9F + col + 11u));
stSimPutChar(sim, (uint8_t)(col + 11u), 15u, HK(sim, 0x7D9F + col + 11u));
stSimPutChar(sim, (uint8_t)(col + 11u), 20u, HK(sim, 0x7D9F + col + 11u));
stSimPutColor(sim, (uint8_t)(col + 11u), 5u, 0x0Cu);
stSimPutColor(sim, (uint8_t)(col + 11u), 10u, 0x0Cu);
stSimPutColor(sim, (uint8_t)(col + 11u), 15u, 0x0Cu);
stSimPutColor(sim, (uint8_t)(col + 11u), 20u, 0x0Cu);
hookXMovePads(sim);
HK(sim, 0x7D9C)++;
if (HK(sim, 0x7D9C) == 0x15u) {
HK(sim, 0x7D9D) = 0xF8u;
}
return;
}
// $7E9E: leftward.
hookXShiftRow(sim, ST_CELL(5, 0), col, (uint8_t)(col + 17u), -1);
hookXShiftRow(sim, ST_CELL(10, 0), col, (uint8_t)(col + 17u), -1);
hookXShiftRow(sim, ST_CELL(15, 0), col, (uint8_t)(col + 17u), -1);
hookXShiftRow(sim, ST_CELL(20, 0), col, (uint8_t)(col + 17u), -1);
stSimPutChar(sim, (uint8_t)(col + 6u), 5u, HK(sim, 0x7D9F + col + 6u));
stSimPutChar(sim, (uint8_t)(col + 6u), 10u, HK(sim, 0x7D9F + col + 6u));
stSimPutChar(sim, (uint8_t)(col + 6u), 15u, HK(sim, 0x7D9F + col + 6u));
stSimPutChar(sim, (uint8_t)(col + 6u), 20u, HK(sim, 0x7D9F + col + 6u));
stSimPutColor(sim, (uint8_t)(col + 6u), 5u, 0x0Cu);
stSimPutColor(sim, (uint8_t)(col + 6u), 10u, 0x0Cu);
stSimPutColor(sim, (uint8_t)(col + 6u), 15u, 0x0Cu);
stSimPutColor(sim, (uint8_t)(col + 6u), 20u, 0x0Cu);
stSimPutChar(sim, (uint8_t)(col + 10u), 14u, sim->screen[ST_CELL(14, col + 11u)]);
stSimPutChar(sim, (uint8_t)(col + 11u), 14u, ST_CHAR_SPACE);
stSimPutColor(sim, (uint8_t)(col + 10u), 14u, 7u);
stSimPutChar(sim, (uint8_t)(col + 17u), 5u, HK(sim, 0x7D9F + col + 17u));
stSimPutChar(sim, (uint8_t)(col + 17u), 10u, HK(sim, 0x7D9F + col + 17u));
stSimPutChar(sim, (uint8_t)(col + 17u), 15u, HK(sim, 0x7D9F + col + 17u));
stSimPutChar(sim, (uint8_t)(col + 17u), 20u, HK(sim, 0x7D9F + col + 17u));
stSimPutColor(sim, (uint8_t)(col + 17u), 5u, 0x0Cu);
stSimPutColor(sim, (uint8_t)(col + 17u), 10u, 0x0Cu);
stSimPutColor(sim, (uint8_t)(col + 17u), 15u, 0x0Cu);
stSimPutColor(sim, (uint8_t)(col + 17u), 20u, 0x0Cu);
hookXMovePads(sim);
HK(sim, 0x7D9C)--;
if (HK(sim, 0x7D9C) == 1u) {
HK(sim, 0x7D9D) = 0x08u;
}
}
// Level X prelude 0 ($7DC6): reset the sweep and restore the pad table
// from the level's backup copy ($7FB8).
static void hookXPrelude0(StSimT *sim) {
uint8_t k;
HK(sim, 0x7D9D) = 0xF8u;
HK(sim, 0x7D9C) = 0x0Bu;
HK(sim, 0x7D9E) = 0u;
for (k = 0u; k < 8u; k++) {
const uint8_t *src = HKP(sim, 0x7FB8 + k * 8u);
sim->pads[k].x1Hi = src[0];
sim->pads[k].x1Lo = src[1];
sim->pads[k].x2Hi = src[2];
sim->pads[k].x2Lo = src[3];
sim->pads[k].row = src[4];
sim->pads[k].passMsb = src[5];
sim->pads[k].passCol = src[6];
sim->pads[k].unused = src[7];
}
}
// Copy cells of one screen row (char + colour) one column along, from
// column `from` to column `to` inclusive, in the direction that keeps
// the copy from overwriting its own source.
static void hookXShiftRow(StSimT *sim, uint16_t rowCell, uint8_t from, uint8_t to, int8_t dir) {
uint8_t x = from;
for (;;) {
uint16_t src = (uint16_t)(rowCell + x);
uint16_t dst = (uint16_t)(src + (uint16_t)(int16_t)dir);
stSimPutChar(sim, (uint8_t)(dst % ST_SCREEN_COLS), (uint8_t)(dst / ST_SCREEN_COLS), sim->screen[src]);
stSimPutColor(sim, (uint8_t)(dst % ST_SCREEN_COLS), (uint8_t)(dst / ST_SCREEN_COLS), sim->color[src]);
if (x == to) {
break;
}
x = (uint8_t)(x - (uint8_t)dir);
}
}
// Sprites 3..7 share one colour in the puzzle level.
static void setSpriteColors(StSimT *sim, uint8_t color) {
uint8_t k;
for (k = 3u; k < ST_HW_SPRITES; k++) {
sim->spr[k].color = color;
}
}
// Extra-level gimmicks (magnets, electroids, maze, shift-o-rama,
// interference, crossfire, teleports, rebound). Compiled out on the
// IIgs, whose bank-0 BSS/text budget is too tight for them; those
// levels play without their hazard there (the 4 attract-demo levels
// H/T/W/X keep their gimmicks everywhere).
#if !defined(__W65816__)
// Level K "MAGNETS" per-tick ($7D9D): the level is globally anti-gravity
// (its yGrav template is -7); the hook only animates the six magnet
// poles by flipping bit 0 of their glyphs every other tick.
static void hookKPerTick(StSimT *sim) {
static const uint8_t kPole[6] = { 0x04u, 0x0Bu, 0x10u, 0x17u, 0x1Cu, 0x23u };
uint8_t k;
HK(sim, 0x7DBD) ^= 1u;
if ((HK(sim, 0x7DBD) & 1u) != 0u) {
return;
}
for (k = 0u; k < 6u; k++) {
uint16_t cell = (uint16_t)(0xA0u + kPole[k]); // $04A0 + off
stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), (uint8_t)(sim->screen[cell] ^ 1u));
}
}
// Level O "ELECTROIDS" per-tick ($7DBE): four electric barriers (rows
// 5, 14, 22, 26 of screen RAM in cell terms) scroll left one column,
// the leftmost cell wrapping to the right, at a rate gated by a mod-8
// tick (rows 1-2 every tick, row 3 every 4th).
static void hookOScrollRow(StSimT *sim, uint16_t base) {
uint8_t chSave = sim->screen[base];
uint8_t colSave = sim->color[base];
uint8_t x;
uint16_t cell;
for (x = 1u; x < 0x26u; x++) {
cell = (uint16_t)(base + x);
stSimPutChar(sim, (uint8_t)((base + x - 1u) % ST_SCREEN_COLS), (uint8_t)((base + x - 1u) / ST_SCREEN_COLS), sim->screen[cell]);
stSimPutColor(sim, (uint8_t)((base + x - 1u) % ST_SCREEN_COLS), (uint8_t)((base + x - 1u) / ST_SCREEN_COLS), sim->color[cell]);
}
cell = (uint16_t)(base + 0x25u);
stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), chSave);
stSimPutColor(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), colSave);
}
static void hookOPerTick(StSimT *sim) {
hookOTramp0(sim); // $7E6E bitScroll of the barrier glyph
HK(sim, 0x7DBD) = (uint8_t)((HK(sim, 0x7DBD) + 1u) & 7u);
if ((HK(sim, 0x7DBD) & 3u) != 0u) {
return;
}
hookOScrollRow(sim, 0xC9u); // $04C9 row 5 col 1
hookOScrollRow(sim, 0x209u); // $0609 row 12 col 1
if (HK(sim, 0x7DBD) != 4u) {
return;
}
hookOScrollRow(sim, 0x169u); // $0569 row 9 col 1
hookOScrollRow(sim, 0x2A9u); // $06A9 row 17 col 1
}
// $63D0-style rotate of the two electroid glyphs ($2BE0 / char $6F).
static void hookOTramp0(StSimT *sim) {
uint8_t k;
for (k = 0u; k < 8u; k++) {
uint8_t b = sim->charset[0x6Fu][k];
sim->charset[0x6Fu][k] = (uint8_t)((b >> 1) | (b << 7));
}
sim->charDirty[0x6Fu] = 1u;
}
// Level R "TAXI MAZE" wall program ($7D9C): put `ch` in the vertical
// bar cells ($043E/$0466/$048E) and its complement (ch^$46) in the
// gate cells; called with $66 to close, with the gate's own char to
// flip. Toggling on pickup opens a path.
static void hookRMaze(StSimT *sim, uint8_t ch) {
static const uint16_t kBar[3] = { 0x3Eu, 0x66u, 0x8Eu };
static const uint16_t kGate[10] = { 0x39u, 0x61u, 0x89u, 0xA1u, 0xA2u, 0xA3u, 0xA4u, 0x309u, 0x30Au, 0x30Bu };
uint8_t k;
uint8_t alt = (uint8_t)(ch ^ 0x46u);
for (k = 0u; k < 3u; k++) {
stSimPutChar(sim, (uint8_t)(kBar[k] % ST_SCREEN_COLS), (uint8_t)(kBar[k] / ST_SCREEN_COLS), ch);
}
for (k = 0u; k < 10u; k++) {
stSimPutChar(sim, (uint8_t)(kGate[k] % ST_SCREEN_COLS), (uint8_t)(kGate[k] / ST_SCREEN_COLS), alt);
}
}
// Level R per-tick ($7DD2): when the passenger boards (stage 4) and the
// gate is open ($0439 == space) the maze flips.
static void hookRPerTick(StSimT *sim) {
if (sim->stage != ST_STAGE_BOARD) {
return;
}
if (sim->screen[0x39u] != ST_CHAR_SPACE) {
return;
}
hookRMaze(sim, sim->screen[0x39u]);
}
// Level V "SHIFT-O-RAMA" per-tick ($7D9D): every screen tick, 18 rows
// (0..17) each scroll one column, alternating direction by row (bit 1
// of the row index): even-band rows left, odd-band right, the edge
// cell wrapping around.
static void hookVScrollLeft(StSimT *sim, uint8_t row) {
uint16_t base = ST_CELL(row, 0);
uint8_t chSave = sim->screen[base];
uint8_t colSave = sim->color[base];
uint8_t x;
for (x = 1u; x < 0x27u; x++) {
stSimPutChar(sim, (uint8_t)(x - 1u), row, sim->screen[base + x]);
stSimPutColor(sim, (uint8_t)(x - 1u), row, sim->color[base + x]);
}
stSimPutChar(sim, 0x26u, row, chSave);
stSimPutColor(sim, 0x26u, row, colSave);
}
static void hookVScrollRight(StSimT *sim, uint8_t row) {
uint16_t base = ST_CELL(row, 0);
uint8_t chSave = sim->screen[base + 0x26u];
uint8_t colSave = sim->color[base + 0x26u];
uint8_t x;
for (x = 0x26u; x > 0u; x--) {
stSimPutChar(sim, x, row, sim->screen[base + x - 1u]);
stSimPutColor(sim, x, row, sim->color[base + x - 1u]);
}
stSimPutChar(sim, 0u, row, chSave);
stSimPutColor(sim, 0u, row, colSave);
}
static void hookVPerTick(StSimT *sim) {
uint8_t row;
for (row = 6u; row < 0x12u; row++) {
if ((row & 2u) != 0u) {
hookVScrollLeft(sim, row);
} else {
hookVScrollRight(sim, row);
}
}
}
// Level Q "INTERFERENCE" ($7DA1 prelude0, $7E0D per-tick2, $7E2E input):
// five interference sprites (hw 3..7) sit at fixed spots flickering
// through cels $80..$8B, and while the cab is mid-screen the joystick
// is scrambled 20% of ticks.
static void hookQPrelude(StSimT *sim) {
int8_t x;
sim->multiColorMask = 0xFFu;
for (x = 7; x >= 3; x--) {
HK(sim, 0x7E83 + (uint8_t)x) = HK(sim, 0x7DEA + (uint8_t)x);
sim->spr[x].ptr = HK(sim, 0x7DF2 + HK(sim, 0x7E83 + (uint8_t)x));
sim->spr[x].color = 0x06u;
sim->spr[x].col = HK(sim, 0x7DD2 + (uint8_t)x);
sim->spr[x].msb = HK(sim, 0x7DDA + (uint8_t)x);
sim->spr[x].row = HK(sim, 0x7DE2 + (uint8_t)x);
sim->spr[x].enable = 1u;
}
}
static void hookQPerTick(StSimT *sim) {
int8_t x;
for (x = 7; x >= 3; x--) {
HK(sim, 0x7E83 + (uint8_t)x)++;
if (HK(sim, 0x7E83 + (uint8_t)x) == 0x0Cu) {
HK(sim, 0x7E83 + (uint8_t)x) = 0u;
}
sim->spr[x].ptr = HK(sim, 0x7DF2 + HK(sim, 0x7E83 + (uint8_t)x));
}
}
static uint8_t hookQInput(StSimT *sim, uint8_t input) {
if (sim->activePad != 0u) {
return input;
}
if (sim->spr[0].row < 0x5Au || sim->spr[0].row >= 0xB4u) {
return input;
}
if (stSimRng(sim, 10u) < 8u) {
return input;
}
return HK(sim, 0x7E7F + (uint8_t)(stSimRng(sim, 4u) - 1u));
}
// Level I "CROSSFIRE" ($7D9C prelude0, $7DEF per-tick2, $7DB1 prelude1):
// bullets (hw 3..7) fly up from the floor, cel-walk, then burst.
static void hookIPrelude(StSimT *sim) {
int8_t x;
for (x = 7; x >= 3; x--) {
HK(sim, 0x7DB8 + (uint8_t)x) = 0u; // state
sim->spr[x].enable = 0u;
sim->spr[x].color = 0x02u;
}
}
static void hookIPerTick(StSimT *sim) {
int8_t s;
for (s = 3; s <= 7; s++) {
uint8_t x = (uint8_t)s;
uint8_t state = HK(sim, 0x7DB8 + x);
if (state == 0u) {
uint8_t y;
if (stSimRng(sim, 0x75u) >= 3u) {
continue;
}
y = (uint8_t)(stSimRng(sim, 4u) - 1u); // 0..3 direction
HK(sim, 0x7DC8 + x) = HK(sim, 0x7DE1 + y); // dx
sim->spr[x].col = HK(sim, 0x7DDD + y); // start col
sim->spr[x].msb = 0u;
sim->spr[x].row = 0xD1u;
HK(sim, 0x7DD0 + x) = (uint8_t)(stSimRng(sim, 2u) + 0xFDu); // dy = rng(2)-3
HK(sim, 0x7DC0 + x) = 0u; // cel phase
HK(sim, 0x7DB8 + x) = 1u;
sim->spr[x].enable = 1u;
sim->spr[x].ptr = 0x84u;
stAudioSfx(HKP(sim, 0x7E82));
sim->spr[x].color = HK(sim, 0x7E7F + (uint8_t)(stSimRng(sim, 3u) - 1u));
} else if (state == 1u) {
uint8_t ph = (uint8_t)(HK(sim, 0x7DC0 + x) + 1u);
HK(sim, 0x7DC0 + x) = ph;
if (ph == 5u) {
HK(sim, 0x7DC0 + x) = 0u;
sim->spr[x].ptr = 0x85u;
HK(sim, 0x7DB8 + x) = 2u;
} else {
if (ph >= 3u) {
addToSpriteCol(sim, x, HK(sim, 0x7DC8 + x));
sim->spr[x].row = (uint8_t)(sim->spr[x].row + HK(sim, 0x7DD0 + x));
}
sim->spr[x].ptr = HK(sim, 0x7DD8 + ph);
}
} else {
uint8_t ph = (uint8_t)((HK(sim, 0x7DC0 + x) + 1u) & 7u);
HK(sim, 0x7DC0 + x) = ph;
sim->spr[x].ptr = HK(sim, 0x7DE5 + ph);
addToSpriteCol(sim, x, HK(sim, 0x7DC8 + x));
sim->spr[x].row = (uint8_t)(sim->spr[x].row + HK(sim, 0x7DD0 + x));
if (sim->spr[x].row < 0x25u) {
sim->spr[x].enable = 0u;
HK(sim, 0x7DB8 + x) = 0u;
}
}
}
}
// Level G "TELEPORTS" ($7DA0 prelude0, $7DF2 per-tick2): orbs (hw 2..7)
// drift; touching the cab teleports it. Faithful reduction: place and
// drift the orb sprites (the teleport hop is a rare event we leave to
// the collision system, which crashes on contact like the C64's
// undelivered case -- see MECHANICS). Placement only for now.
static void hookGPerTick(StSimT *sim) {
// The orb colour cycles through $7DD4[phase] every 4 ticks.
HK(sim, 0x7E00)++;
if ((HK(sim, 0x7E00) & 3u) != 0u) {
return;
}
HK(sim, 0x7DD2)++;
if (HK(sim, 0x7DD2) == 6u) {
HK(sim, 0x7DD2) = 0u;
}
{
uint8_t c = HK(sim, 0x7DD4 + HK(sim, 0x7DD2));
uint8_t k;
for (k = 2u; k < ST_HW_SPRITES; k++) {
sim->spr[k].color = c;
}
}
}
// Level U "REBOUND" ($7DAD per-tick2): the ceiling and side walls
// reflect the cab's velocity; hazard sprites (hw 3..7) drift. The wall
// bounce is the gimmick and is handled here on the taxi velocity.
static void hookUPerTick(StSimT *sim) {
if (sim->collisionPhase != 0u) {
return;
}
// $7DB2: near the top -> reflect Y downward.
if (sim->spr[0].row < 0x11u) {
sim->velY = (int16_t)(-sim->velY);
sim->spr[0].row = (uint8_t)(sim->spr[0].row + 2u);
}
}
#endif /* !IIGS */
// ---------------------------------------------------------------------------
// Dispatch
// ---------------------------------------------------------------------------
// $7D75 -- 0 means sprite contact (passenger, level sprites) is safe.
uint8_t stHookHitVerdict(StSimT *sim) {
switch (sim->hookLevel) {
case ST_LEVEL_H:
return 0u;
default:
return 1u;
}
}
// $7D72 -- the input filter.
uint8_t stHookInput(StSimT *sim, uint8_t input) {
#if !defined(__W65816__)
switch (sim->hookLevel) {
case ST_LEVEL_Q:
return hookQInput(sim, input);
default:
break;
}
#else
(void)sim;
#endif
return input;
}
// $7D6C -- before the sprite flush.
void stHookPerTick2(StSimT *sim) {
switch (sim->hookLevel) {
case ST_LEVEL_H:
hookHPerTick(sim);
break;
case ST_LEVEL_T:
hookTPerTick(sim);
break;
case ST_LEVEL_W:
hookWPerTick(sim);
break;
case ST_LEVEL_X:
hookXPerTick(sim);
break;
#if !defined(__W65816__)
case ST_LEVEL_V:
hookVPerTick(sim);
break;
case ST_LEVEL_Q:
hookQPerTick(sim);
break;
case ST_LEVEL_I:
hookIPerTick(sim);
break;
case ST_LEVEL_G:
hookGPerTick(sim);
break;
case ST_LEVEL_U:
hookUPerTick(sim);
break;
case ST_LEVEL_K:
hookKPerTick(sim);
break;
case ST_LEVEL_O:
hookOPerTick(sim);
break;
case ST_LEVEL_R:
hookRPerTick(sim);
break;
#endif
default:
break;
}
}
// $7D6F -- after the sprite flush.
void stHookPerTick3(StSimT *sim) {
(void)sim;
}
// $7D66 -- first prelude hook.
void stHookPrelude0(StSimT *sim) {
switch (sim->hookLevel) {
case ST_LEVEL_H:
hookHPrelude0(sim);
break;
case ST_LEVEL_X:
hookXPrelude0(sim);
break;
#if !defined(__W65816__)
case ST_LEVEL_Q:
hookQPrelude(sim);
break;
case ST_LEVEL_I:
hookIPrelude(sim);
break;
#endif
case ST_LEVEL_W:
{
uint8_t k;
for (k = 1u; k <= 8u; k++) {
HK(sim, 0x7DAB + k) = 0u;
}
}
break;
default:
break;
}
}
// $7D69 -- second prelude hook.
void stHookPrelude1(StSimT *sim) {
switch (sim->hookLevel) {
case ST_LEVEL_T:
hookTPrelude1(sim);
break;
case ST_LEVEL_W:
HK(sim, 0x7DE5) = 1u;
break;
#if !defined(__W65816__)
case ST_LEVEL_R:
hookRMaze(sim, ST_CHAR_BLANK);
break;
#endif
default:
break;
}
}