// 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 in the level blob and re-expresses each program here, // naming the tables it reads at the addresses the original used, so // nothing has to be transcribed by hand. Only the bytes a hook WRITES // are held in the port's own typed scratch (StHookScratchT). #include #include "stSim.h" #include "stHookTables.h" // Keeping the two per-tick dispatch halves out of line is what lets the // 65816 build use the default register allocator. Inlined back together // they are one twelve-way switch whose live values exceed what greedy // allocation can place on a single-accumulator machine, and the build // then falls back to the basic allocator for this entire file. The call // costs one JSL per game tick. #if defined(__GNUC__) || defined(__clang__) #define ST_NOINLINE __attribute__((noinline)) #else #define ST_NOINLINE #endif // A level's read-only tables, straight out of the shipped level blob. // Nothing writes these, so there is no working copy and no second source // of truth: the .dat is the only place the values live. Every byte a // hook WRITES lives in the typed per-level scratch (StHookScratchT), so // the 616-byte C64-addressed working copy is gone. #define HKT(sim, addr) ((sim)->level->hookData[(addr) - ST_HOOK_BASE]) #define HKTP(sim, addr) (&(sim)->level->hookData[(addr) - ST_HOOK_BASE]) // ---- level H "PUZZLER" ($7E15/$7E4C/$7E53..$7E56 scratch) ---- // Five parallel 11-entry segment tables sit back to back in the blob, // then the pad and switch segment lists (four ids each, $80 = end). #define H_SPR_COL(sim, k) HKT(sim, 0x7DCF + (k)) #define H_SPR_MSB(sim, k) HKT(sim, 0x7DD7 + (k)) #define H_SPR_ROW(sim, k) HKT(sim, 0x7DDF + (k)) #define H_SEG_COL(sim, id) HKT(sim, 0x7DE7 + (id)) #define H_SEG_ROW(sim, id) HKT(sim, 0x7DF2 + (id)) #define H_SEG_SENSE(sim, id) HKT(sim, 0x7DFD + (id)) #define H_SEG_GLYPH(sim, id) HKT(sim, 0x7E08 + (id)) #define H_PAD_SEGS(sim, i) HKT(sim, 0x7E24 + (i)) #define H_SFX_CHIME(sim) HKTP(sim, 0x7ED0) // These two index PAST the table they name and must see live scratch, so // they go through the byte-addressed view below -- see hookHByte. #define H_SEG_FLAG(sim, id) hookHByte(sim, (uint16_t)(0x7E15u + (id))) #define H_SWITCH_SEGS(sim, i) hookHByte(sim, (uint16_t)(0x7E38u + (i))) // ---- level G "TELEPORTS" ($7DD1..$7DD3/$7E00/$7F02 scratch) ---- #define G_CEL(sim, ph) HKT(sim, ST_HK_G_CEL + (ph)) #define G_ORB_COL(sim, x) HKT(sim, ST_HK_G_ORB_COL + (x)) #define G_ORB_MSB(sim, x) HKT(sim, ST_HK_G_ORB_MSB + (x)) #define G_ORB_ROW(sim, x) HKT(sim, ST_HK_G_ORB_ROW + (x)) #define G_SPOT_COL(sim, i) HKT(sim, ST_HK_G_SPOT_COL + (i)) #define G_SPOT_MSB(sim, i) HKT(sim, ST_HK_G_SPOT_MSB + (i)) #define G_SPOT_ROW(sim, i) HKT(sim, ST_HK_G_SPOT_ROW + (i)) #define G_SFX_HOP(sim) HKTP(sim, ST_HK_G_SFX_HOP) // ---- level I "CROSSFIRE" ($7DB8/$7DC0/$7DC8/$7DD0 scratch) ---- #define I_CEL_RISE(sim, ph) HKT(sim, ST_HK_I_CEL_RISE + (ph)) #define I_START_COL(sim, d) HKT(sim, 0x7DDD + (d)) #define I_DX(sim, d) HKT(sim, 0x7DE1 + (d)) #define I_CEL_BURST(sim, ph) HKT(sim, ST_HK_I_CEL_BURST + (ph)) #define I_COLOR(sim, i) HKT(sim, ST_HK_I_COLOR + (i)) #define I_SFX_FIRE(sim) HKTP(sim, 0x7E82) // ---- level Q "INTERFERENCE" ($7E83 scratch) ---- #define Q_SPR_COL(sim, x) HKT(sim, 0x7DD2 + (x)) #define Q_SPR_MSB(sim, x) HKT(sim, 0x7DDA + (x)) #define Q_SPR_ROW(sim, x) HKT(sim, 0x7DE2 + (x)) #define Q_INIT_PHASE(sim, x) HKT(sim, 0x7DEA + (x)) #define Q_CEL(sim, ph) HKT(sim, ST_HK_Q_CEL + (ph)) #define Q_SCRAMBLE(sim, i) HKT(sim, 0x7E7F + (i)) // ---- level T "FAST BREAK" ($7D9C scratch) ---- #define T_SFX_BOUNCE(sim) HKTP(sim, 0x7E56) #define T_SFX_OPEN(sim) HKTP(sim, 0x7E5F) #define T_SFX_SHUT(sim) HKTP(sim, 0x7E68) // ---- level U "REBOUND" ($7F4F/$7F57/$7F5F/$7F6D/$7F6E scratch) ---- #define U_ENTRY(sim, d) HKT(sim, 0x7F3B + (d)) #define U_ENTRY_MSB(sim, d) HKT(sim, 0x7F3F + (d)) #define U_DX(sim, d) HKT(sim, 0x7F43 + (d)) #define U_DY(sim, d) HKT(sim, 0x7F47 + (d)) #define U_CEL(sim, ph) HKT(sim, ST_HK_U_CEL + (ph)) #define U_LIFE(sim, d) HKT(sim, 0x7F69 + (d)) #define U_SFX_BOUNCE(sim) HKTP(sim, 0x7F76) // ---- level W "LASERS" ($7DAB/$7DAC/$7DB5/$7DE5 scratch) ---- #define W_BEAM_GLYPH(sim, i) HKT(sim, 0x7DBD + (i)) #define W_TOP_ROW(sim, i) HKT(sim, 0x7DC5 + (i)) #define W_COL(sim, i) HKT(sim, 0x7DCD + (i)) #define W_START_ROW(sim, i) HKT(sim, 0x7DD5 + (i)) #define W_ROW_STEP(sim, i) HKT(sim, 0x7DDD + (i)) #define W_FADE_COLOR(sim, ph) HKT(sim, 0x7DE6 + (ph)) #define W_END_ROW(sim, i) HKT(sim, 0x7DE9 + (i)) #define W_SFX_HUM(sim) HKTP(sim, 0x7DA2) // ---- level X "MOVING PADS" ($7D9C..$7D9E scratch) ---- #define X_GLYPH(sim, col) HKT(sim, 0x7D9F + (col)) #define X_PAD_SLOT(sim, k) HKTP(sim, 0x7FB8 + (k) * 8u) // ---- level E "BEANSTALK" ($7D98..$7D9B scratch) ---- #define E_SFX_GROW(sim) HKTP(sim, 0x7EA0) // ---- level J "SHOOTING STARS" ($7ED3/$7EDB scratch) ---- #define J_SFX_BURST(sim) HKTP(sim, 0x7DF8) // ---- level P "BLIZZARD" ($7F0E/$7F16/$7F1F scratch) ---- #define P_SFX_WIND(sim) HKTP(sim, 0x7E3C) // ---- level S "THE SWITCH" ---- #define S_DIR_MAP(sim, d) HKT(sim, 0x7DAC + (d)) // Levels J and P run the same falling-hazard program over hardware // sprites 3..7 with their own tables: the per-sprite X drift, the // per-sprite spawn column base and the burst (J) or melt (P) cel run. typedef struct { uint16_t dxTable; // blob address, indexed by sprite uint16_t startTable; // blob address, indexed by sprite uint16_t celTable; // blob address, indexed by phase 1..phaseEnd-1 uint8_t phaseEnd; // the phase that ends the burst bool avoidMiddle; // J rerolls a spawn over the middle band } StHookFallT; static const StHookFallT kFallJ = { 0x7EB9u, 0x7EC1u, ST_HK_J_CEL_BURST, ST_HK_J_BURST_PHASES, true }; static const StHookFallT kFallP = { 0x7EF9u, 0x7F01u, ST_HK_P_CEL_BURST, ST_HK_P_BURST_PHASES, false }; // Each level H switch drives four wall segments ($7E24 stride, $7E38 // list), animated over four steps. #define ST_HOOK_H_SEGMENTS 4u #define ST_HOOK_H_STEPS 4u // Level U: hardware sprites 3..7 are the drifting orbs, $80 is the first // of their three cels, and a bounce holds off for twelve ticks ($7E0E). #define ST_HOOK_U_COOLDOWN 0x0Cu // Level E: pad 1 alone at first; while the fare wants UP a leaf cell // sprouts every $57 ticks (the count starts at $1E, or $46 on the // second screen), four to a side from columns 20/19, and the fifth // step hangs the next pair of pads at the leaf ends, opens them and // moves the growth four rows up, until pad 9 is out. #define ST_HOOK_E_GROW_TICKS 0x57u #define ST_HOOK_E_TICK_START 0x1Eu #define ST_HOOK_E_TICK_SECOND 0x46u #define ST_HOOK_E_LEAVES 5u #define ST_HOOK_E_LAST_PAD 9u #define ST_HOOK_E_FIRST_ROW 0x13u #define ST_HOOK_E_ROW_STEP 4u #define ST_HOOK_E_STALK_COL 20u #define ST_HOOK_E_PAD_COL_RIGHT 25u #define ST_HOOK_E_PAD_COL_LEFT 15u #define ST_HOOK_E_CHAR_LEAF 0xD2u #define ST_HOOK_E_CHAR_TIP_R 0xD7u #define ST_HOOK_E_CHAR_TIP_L 0xD5u #define ST_HOOK_E_COLOR 5u // Levels J and P: hardware sprites 3..7 are the falling hazards, cels // $80/$81 falling and $82 the burst. An idle sprite spawns on rng($3C) // < 3, on row $17; a scenery hit counts from row $38 down and drops // the sprite four rows onto it. J rerolls a spawn column in $91..$CC. #define ST_HOOK_FALL_SPRITES 0xF8u // $D01F bits 3..7 #define ST_HOOK_FALL_SPAWN_ODDS 0x3Cu #define ST_HOOK_FALL_SPAWN_HIT 3u #define ST_HOOK_FALL_TOP_ROW 0x17u #define ST_HOOK_FALL_HIT_ROW 0x38u #define ST_HOOK_FALL_HIT_DROP 4u #define ST_HOOK_FALL_BAND_LO 0x91u #define ST_HOOK_FALL_BAND_HI 0xCDu // Level G: the five orbs (sprites 3..7) cycle a cel every fourth tick; // the cab touching one hides it for $14 ticks under a falling tone that // starts at $5A and drops 4 a tick, then lands it on one of five spots: // the fare's destination pad half the time, else a random spot other // than the last one used. #define ST_HOOK_G_HOP_TICKS 0x14u #define ST_HOOK_G_SWEEP_START 0x5Au #define ST_HOOK_G_SWEEP_STEP 4u #define ST_HOOK_G_SPOTS 5u #define ST_HOOK_G_PAD_SPOTS 6u // a destination of 6+ (UP) lands on spot 5 // Level L: hardware sprite 3 is the hole at ($AC, $82), cels $80..$82. // Gravity pulls the cab toward it: $10 (Y) and $0C (X) less an eighth // of the distance, X measured in halved sprite units so it fits a // byte. Touching it returns verdict $FF: a kill with no falling wreck. #define ST_HOOK_L_HOLE 3u #define ST_HOOK_L_HOLE_COL 0xACu #define ST_HOOK_L_HOLE_ROW 0x82u #define ST_HOOK_L_HOLE_X_HALF 0x5Bu #define ST_HOOK_L_PULL_Y 0x10u #define ST_HOOK_L_PULL_X 0x0Cu #define ST_HOOK_L_VERDICT 0xFFu // Level P: the wind turns on rng($78) < 3 per tick, flipping the three // windsock cells (glyph ^ 6) and the sign of the X gravity. #define ST_HOOK_P_WIND_ODDS 0x78u #define ST_HOOK_P_WIND_HIT 3u #define ST_HOOK_P_SOCK_FLIP 0x06u // Level S: FIRE passes, the four direction bits go through a swap table. #define ST_HOOK_S_FIRE 0x10u #define ST_HOOK_S_DIRS 0x0Fu 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 uint8_t hookHByte(const StSimT *sim, uint16_t addr); 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); static void hookKPerTick(StSimT *sim); static void hookOPerTick(StSimT *sim); ST_NOINLINE static void hookPerTickHigh(StSimT *sim); ST_NOINLINE static void hookPerTickLow(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 hookGHop(StSimT *sim); static void hookGPerTick(StSimT *sim); static void hookGPrelude0(StSimT *sim); static void hookUBounce(StSimT *sim); static void hookUOrbs(StSimT *sim); static void hookUPerTick(StSimT *sim); static void hookUPrelude0(StSimT *sim); static void hookEPerTick(StSimT *sim); static void hookEPrelude0(StSimT *sim); static void hookEPut(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch); static void hookETips(StSimT *sim); static void hookFallBgHits(StSimT *sim, uint8_t *state, uint8_t *phase, const uint8_t *sfx); static void hookFallTick(StSimT *sim, uint8_t *state, uint8_t *phase, const StHookFallT *d, uint8_t *parity); static void hookJPerTick(StSimT *sim); static void hookJPrelude0(StSimT *sim); static void hookLPerTick(StSimT *sim); static void hookLPrelude0(StSimT *sim); static uint16_t hookLPull(uint8_t base, uint8_t dist); static void hookPFlipSock(StSimT *sim, uint16_t cell); static void hookPPerTick(StSimT *sim); static void hookPPrelude0(StSimT *sim); static uint8_t hookSInput(StSimT *sim, uint8_t input); // $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's byte-addressed view of its own data block. // // Two of its reads index past the end of the table they name and land on // bytes the level itself writes, so H cannot simply split into "const // tables" plus "named scratch" the way the other levels do: // // * the pad lists ($7E24) end with $FF, and the landing check indexes // the segment flags with that terminator unguarded -- $7E15 + $FF is // $7F14, 380 bytes into the blob, holding $30. Non-zero, so the // "no segment here" case reports a match, with switch id $FF. // * `active` is therefore 6 on every landing, so the switch-segment // read is always $7E38 + 20 = $7E4C -- which is where the id was just // stored. The level writes a value and reads it back as table data. // ($7E4D..$7E4F ship as $FF, so the $80 guard skips them and only the // stored id is ever animated. That also bounds every write to ids // 0..10, so the flags array is never indexed out of range.) // // Scratch where the level has written, the shipped blob everywhere else. static uint8_t hookHByte(const StSimT *sim, uint16_t addr) { const StHookHT *h = &sim->hookScratch.h; if (addr >= 0x7E15u && addr < 0x7E15u + ST_HOOK_H_SEGS) { return h->segFlag[addr - 0x7E15u]; } switch (addr) { case 0x7E4Cu: return h->switchId; case 0x7E53u: return h->tick; case 0x7E54u: return h->active; case 0x7E55u: return h->step; case 0x7E56u: return h->tone; default: break; } return HKT(sim, addr); } // 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 (sim->hookScratch.h.active == 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 = H_PAD_SEGS(sim, x + n); if (H_SEG_FLAG(sim, y) != 0u) { sim->hookScratch.h.switchId = y; sim->hookScratch.h.active = 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; } sim->hookScratch.h.active = y; setSpriteColors(sim, 0x0Au); sim->spr[2u + y].color = 0x05u; } // $7EB2: start the animation. sim->hookScratch.h.tick = 0u; sim->hookScratch.h.step = 0xFFu; sim->hookScratch.h.tone = 0x12u; stAudioSfx(H_SFX_CHIME(sim)); } // $7ED9 sim->hookScratch.h.tone++; stAudioVoice2(sim->hookScratch.h.tone, sim->hookScratch.h.tone, 0xFFu); sim->hookScratch.h.tick++; if ((sim->hookScratch.h.tick & 7u) != 0u) { return; } sim->hookScratch.h.step++; { uint8_t idx = (uint8_t)((sim->hookScratch.h.active - 1u) * ST_HOOK_H_SEGMENTS); bool last = (sim->hookScratch.h.step == ST_HOOK_H_STEPS); uint8_t n; if (last) { // $7F7C: finished -- flip the segment flags. sim->hookScratch.h.active = 0u; setSpriteColors(sim, 0x01u); } for (n = 0u; n < ST_HOOK_H_SEGMENTS; n++) { uint8_t id = H_SWITCH_SEGS(sim, idx + n); if ((id & 0x80u) != 0u) { continue; } if (last) { sim->hookScratch.h.segFlag[id] ^= 1u; } else { hookHSegment(sim, id); } } } } // 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 < ST_HOOK_H_SEGS; k++) { sim->hookScratch.h.segFlag[k] = 0u; } sim->hookScratch.h.active = 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 = H_SPR_COL(sim, k); sim->spr[k].msb = H_SPR_MSB(sim, k); sim->spr[k].row = H_SPR_ROW(sim, 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; if ((H_SEG_FLAG(sim, id) ^ H_SEG_SENSE(sim, id)) == 0u) { step = sim->hookScratch.h.step; } else { step = (uint8_t)(3u - sim->hookScratch.h.step); } if (H_SEG_GLYPH(sim, id) == 0x9Bu) { row = (uint8_t)(step + H_SEG_ROW(sim, id)); col = H_SEG_COL(sim, id); } else { col = (uint8_t)(step + H_SEG_COL(sim, id)); row = H_SEG_ROW(sim, id); } ch = (H_SEG_FLAG(sim, id) != 0u) ? H_SEG_GLYPH(sim, 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 (sim->hookScratch.t.gateShut != 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(T_SFX_BOUNCE(sim)); return; } if (sim->spr[0].row >= 0x3Fu) { return; } sim->velY = 0; sim->velX = 0; hookTGate(sim, 0x75u); sim->hookScratch.t.gateShut++; stAudioSfx(T_SFX_SHUT(sim)); } // Level T prelude 1 ($7DC4): centre open, sides barred. static void hookTPrelude1(StSimT *sim) { hookTGate(sim, ST_CHAR_SPACE); sim->hookScratch.t.gateShut = 0u; stAudioSfx(T_SFX_OPEN(sim)); } // 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++) { stSimGlyphMut(sim, (uint8_t)(0x92u + (k >> 3)))[k & 7u] = 0u; } stSimMarkChar(sim, 0x92u); stSimMarkChar(sim, 0x93u); for (i = 0u; i < 8u; i++) { uint8_t state = sim->hookScratch.w.state[i]; uint8_t col = W_COL(sim, i); if (state == 0u) { if (stSimRng(sim, 12u) >= 3u) { continue; } sim->hookScratch.w.state[i] = 1u; sim->hookScratch.w.row[i] = W_START_ROW(sim, i); stSimPutChar(sim, col, sim->hookScratch.w.row[i], W_BEAM_GLYPH(sim, i)); stSimPutColor(sim, col, sim->hookScratch.w.row[i], 2u); continue; } if (state == 1u) { stSimPutChar(sim, col, sim->hookScratch.w.row[i], 0x91u); if (sim->hookScratch.w.row[i] == W_END_ROW(sim, i)) { sim->hookScratch.w.state[i] = 2u; sim->hookScratch.w.row[i] = 0u; state = 2u; } else { sim->hookScratch.w.row[i] = (uint8_t)(sim->hookScratch.w.row[i] + W_ROW_STEP(sim, i)); stSimPutChar(sim, col, sim->hookScratch.w.row[i], W_BEAM_GLYPH(sim, i)); stSimPutColor(sim, col, sim->hookScratch.w.row[i], 2u); continue; } } if (state == 2u) { uint8_t phase; sim->hookScratch.w.row[i]++; phase = sim->hookScratch.w.row[i]; if (phase == 3u) { sim->hookScratch.w.state[i] = 0u; stSimPutChar(sim, col, W_TOP_ROW(sim, i), ST_CHAR_SPACE); fillDownChar(sim, col, W_TOP_ROW(sim, i), ST_CHAR_SPACE, 7u); } else { uint8_t color = W_FADE_COLOR(sim, phase); stSimPutColor(sim, col, W_TOP_ROW(sim, i), color); fillDownColor(sim, col, W_TOP_ROW(sim, i), color, 7u); } continue; } sim->hookScratch.w.state[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 (sim->hookScratch.w.started != 0u) { stAudioSfx(W_SFX_HUM(sim)); sim->hookScratch.w.started = 0u; } c = (uint8_t)(stSimRng(sim, 0x6Eu) + 6u); stAudioVoice2(c, c, 0x81u); sim->hookScratch.w.colorPhase = (uint8_t)((sim->hookScratch.w.colorPhase + 1u) & 7u); c = sim->hookScratch.w.colorPhase; 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. stSimGlyphMut(sim, (uint8_t)(0x91u + ((7u + c) >> 3)))[(7u + c) & 7u] = 0x3Cu; stSimGlyphMut(sim, 0x93u)[8u - c] = 0x3Cu; stSimMarkChar(sim, 0x92u); stSimMarkChar(sim, 0x93u); 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 = sim->hookScratch.x.dir; uint8_t off = 0u; uint8_t phase = 0u; stSimSpriteAddX(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; } stSimSpriteAddX(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; sim->hookScratch.x.tick++; if (sim->hookScratch.x.tick != 0x10u) { return; } sim->hookScratch.x.tick = 0u; col = sim->hookScratch.x.col; if ((sim->hookScratch.x.dir & 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, X_GLYPH(sim, col)); stSimPutChar(sim, col, 10u, X_GLYPH(sim, col)); stSimPutChar(sim, col, 15u, X_GLYPH(sim, col)); stSimPutChar(sim, col, 20u, X_GLYPH(sim, 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, X_GLYPH(sim, col + 11u)); stSimPutChar(sim, (uint8_t)(col + 11u), 10u, X_GLYPH(sim, col + 11u)); stSimPutChar(sim, (uint8_t)(col + 11u), 15u, X_GLYPH(sim, col + 11u)); stSimPutChar(sim, (uint8_t)(col + 11u), 20u, X_GLYPH(sim, 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); sim->hookScratch.x.col++; if (sim->hookScratch.x.col == 0x15u) { sim->hookScratch.x.dir = 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, X_GLYPH(sim, col + 6u)); stSimPutChar(sim, (uint8_t)(col + 6u), 10u, X_GLYPH(sim, col + 6u)); stSimPutChar(sim, (uint8_t)(col + 6u), 15u, X_GLYPH(sim, col + 6u)); stSimPutChar(sim, (uint8_t)(col + 6u), 20u, X_GLYPH(sim, 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, X_GLYPH(sim, col + 17u)); stSimPutChar(sim, (uint8_t)(col + 17u), 10u, X_GLYPH(sim, col + 17u)); stSimPutChar(sim, (uint8_t)(col + 17u), 15u, X_GLYPH(sim, col + 17u)); stSimPutChar(sim, (uint8_t)(col + 17u), 20u, X_GLYPH(sim, 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); sim->hookScratch.x.col--; if (sim->hookScratch.x.col == 1u) { sim->hookScratch.x.dir = 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; sim->hookScratch.x.dir = 0xF8u; sim->hookScratch.x.col = 0x0Bu; sim->hookScratch.x.tick = 0u; for (k = 0u; k < 8u; k++) { const uint8_t *src = X_PAD_SLOT(sim, k); 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). // 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; sim->hookScratch.k.parity ^= 1u; if ((sim->hookScratch.k.parity & 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) { stSimRotateGlyph(sim, ST_CHAR_ELECTROID); // $7E6E bitScroll of the barrier glyph sim->hookScratch.o.tick = (uint8_t)((sim->hookScratch.o.tick + 1u) & 7u); if ((sim->hookScratch.o.tick & 3u) != 0u) { return; } hookOScrollRow(sim, 0xC9u); // $04C9 row 5 col 1 hookOScrollRow(sim, 0x209u); // $0609 row 12 col 1 if (sim->hookScratch.o.tick != 4u) { return; } hookOScrollRow(sim, 0x169u); // $0569 row 9 col 1 hookOScrollRow(sim, 0x2A9u); // $06A9 row 17 col 1 } // 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, the 12 // rows 6..17 each scroll one column, alternating direction in bands of // two (bit 1 of the row index): bands with the bit set go left, the // others 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--) { sim->hookScratch.q.phase[x] = Q_INIT_PHASE(sim, x); sim->spr[x].ptr = Q_CEL(sim, sim->hookScratch.q.phase[x]); sim->spr[x].color = 0x06u; sim->spr[x].col = Q_SPR_COL(sim, x); sim->spr[x].msb = Q_SPR_MSB(sim, x); sim->spr[x].row = Q_SPR_ROW(sim, x); sim->spr[x].enable = 1u; } } static void hookQPerTick(StSimT *sim) { int8_t x; for (x = 7; x >= 3; x--) { sim->hookScratch.q.phase[x]++; if (sim->hookScratch.q.phase[x] == 0x0Cu) { sim->hookScratch.q.phase[x] = 0u; } sim->spr[x].ptr = Q_CEL(sim, sim->hookScratch.q.phase[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 Q_SCRAMBLE(sim, (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--) { sim->hookScratch.i.state[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 = sim->hookScratch.i.state[x]; if (state == 0u) { uint8_t y; if (stSimRng(sim, 0x75u) >= 3u) { continue; } y = (uint8_t)(stSimRng(sim, 4u) - 1u); // 0..3 direction sim->hookScratch.i.dx[x] = I_DX(sim, y); // dx sim->spr[x].col = I_START_COL(sim, y); // start col sim->spr[x].msb = 0u; sim->spr[x].row = 0xD1u; sim->hookScratch.i.dy[x] = (uint8_t)(stSimRng(sim, 2u) + 0xFDu); // dy = rng(2)-3 sim->hookScratch.i.phase[x] = 0u; // cel phase sim->hookScratch.i.state[x] = 1u; sim->spr[x].enable = 1u; sim->spr[x].ptr = 0x84u; stAudioSfx(I_SFX_FIRE(sim)); sim->spr[x].color = I_COLOR(sim, (uint8_t)(stSimRng(sim, 3u) - 1u)); } else if (state == 1u) { uint8_t ph = (uint8_t)(sim->hookScratch.i.phase[x] + 1u); sim->hookScratch.i.phase[x] = ph; if (ph == 5u) { sim->hookScratch.i.phase[x] = 0u; sim->spr[x].ptr = 0x85u; sim->hookScratch.i.state[x] = 2u; } else { if (ph >= 3u) { stSimSpriteAddX(sim, x, sim->hookScratch.i.dx[x]); sim->spr[x].row = (uint8_t)(sim->spr[x].row + sim->hookScratch.i.dy[x]); } sim->spr[x].ptr = I_CEL_RISE(sim, ph); } } else { uint8_t ph = (uint8_t)((sim->hookScratch.i.phase[x] + 1u) & 7u); sim->hookScratch.i.phase[x] = ph; sim->spr[x].ptr = I_CEL_BURST(sim, ph); stSimSpriteAddX(sim, x, sim->hookScratch.i.dx[x]); sim->spr[x].row = (uint8_t)(sim->spr[x].row + sim->hookScratch.i.dy[x]); if (sim->spr[x].row < 0x25u) { sim->spr[x].enable = 0u; sim->hookScratch.i.state[x] = 0u; } } } } // Level G "TELEPORTS" ($7DA0 prelude0, $7D9A prelude1, $7E01 per-tick2, // $7DF2 verdict): five orbs on sprites 3..7 sit where the blob puts them // and cycle their cel every fourth tick. The cab touching one starts a // hop: it vanishes for $14 ticks with the stick dead and a falling tone, // then reappears on a landing spot (hookGHop). Touching an orb is never // a kill -- the verdict is 0 unless the scenery was hit, and a scenery // hit also puts the cab sprite back. static void hookGPrelude0(StSimT *sim) { uint8_t x; for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { sim->spr[x].color = ST_HK_G_ORB_COLOR; sim->spr[x].ptr = HKT(sim, ST_HK_G_CEL); sim->spr[x].col = G_ORB_COL(sim, x); sim->spr[x].msb = G_ORB_MSB(sim, x); sim->spr[x].row = G_ORB_ROW(sim, x); sim->spr[x].enable = 1u; } sim->hookScratch.g.celPhase = 0u; sim->hookScratch.g.hop = 0u; } // $7E4A -- land: rng(2) odd with a fare aboard sends the cab to its // destination pad's spot (UP and beyond = spot 5); otherwise a random // spot, stepped down if it repeats the last one (wrapping 1 -> 5). static void hookGHop(StSimT *sim) { StHookGT *g = &sim->hookScratch.g; uint8_t spot; uint8_t i; if ((stSimRng(sim, 2u) & 1u) != 0u && sim->activeSpriteIdx != 0u) { spot = sim->activeSpriteIdx; if (spot >= ST_HOOK_G_PAD_SPOTS) { spot = ST_HOOK_G_SPOTS; } } else { spot = stSimRng(sim, ST_HOOK_G_SPOTS); if (spot == g->lastSpot) { spot--; if (spot == 0u) { spot = ST_HOOK_G_SPOTS; } } } g->lastSpot = spot; i = (uint8_t)(spot - 1u); sim->spr[0].col = G_SPOT_COL(sim, i); sim->posXcol = sim->spr[0].col; sim->spr[0].msb = G_SPOT_MSB(sim, i); sim->posXmsb = sim->spr[0].msb; sim->spr[0].row = G_SPOT_ROW(sim, i); sim->posYrow = sim->spr[0].row; sim->velX = 0; sim->velY = 0; } static void hookGPerTick(StSimT *sim) { StHookGT *g = &sim->hookScratch.g; uint8_t x; g->tick++; if ((g->tick & 3u) == 0u) { uint8_t cel; g->celPhase++; if (g->celPhase == ST_HK_G_CEL_PHASES) { g->celPhase = 0u; } cel = G_CEL(sim, g->celPhase); for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { sim->spr[x].ptr = cel; } } if (g->hop != 0u) { // $7ECB: the tone falls, the stick is dead, and at zero the cab // is back and lands. g->sweep = (uint8_t)(g->sweep - ST_HOOK_G_SWEEP_STEP); stAudioVoice1Freq(g->sweep); sim->dirMask = 0u; g->hop--; if (g->hop == 0u) { sim->spr[0].enable = 1u; hookGHop(sim); } return; } if ((sim->spriteSpriteColl & ST_HOOK_FALL_SPRITES) == 0u) { return; } // $7E3D: an orb took the cab. sim->spr[0].enable = 0u; g->hop = ST_HOOK_G_HOP_TICKS; stAudioSfx(G_SFX_HOP(sim)); g->sweep = ST_HOOK_G_SWEEP_START; sim->velX = 0; sim->velY = 0; } // Level U "REBOUND" ($7D9B prelude0, $7DAD per-tick2). Five orbs drift // in from the four edges on their own cel walk; touching one bounces the // cab by negating its velocity. The orb cels are the level's own sprite // blocks $80..$82, and every table below is read from the shipped hook // blob at the address the original indexes. // // $7D9B -- clear the orb states and colour the five hazard sprites. static void hookUPrelude0(StSimT *sim) { uint8_t x; for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { sim->hookScratch.u.state[x] = 0u; sim->spr[x].color = 0x07u; } } // $7DDD -- a cab/orb contact bounces the cab, then holds off for twelve // ticks. Which axes flip is a 1-in-3 roll: 1 negates both, 2 negates // neither and 3 negates Y alone -- the original has no X-only case. static void hookUBounce(StSimT *sim) { uint8_t coll; uint8_t hits = 0u; uint8_t b; uint8_t pick; if (sim->hookScratch.u.cooldown != 0u) { sim->hookScratch.u.cooldown--; return; } if ((sim->spriteSpriteColl & 1u) == 0u) { return; } // $7DF8: count the orb sprites in the latch (bits 7..3). coll = sim->spriteSpriteColl; for (b = (uint8_t)(ST_HW_SPRITES - 1u); b >= ST_HOOK_FIRST_SPRITE; b--) { if ((coll & 0x80u) != 0u) { hits++; } coll = (uint8_t)(coll << 1); } if (hits == 0u) { return; } sim->hookScratch.u.cooldown = ST_HOOK_U_COOLDOWN; stAudioSfx(U_SFX_BOUNCE(sim)); pick = stSimRng(sim, 3u); if (pick == 1u) { sim->velX = (int16_t)(-sim->velX); } if (pick == 2u) { return; } sim->velY = (int16_t)(-sim->velY); } // $7E60 -- the orbs themselves: an idle one appears on a 2-in-30 roll // from one of the four edges (0 up from the floor, 1 down from the // ceiling, 2 in from the left, 3 in from the right), an active one // drifts by its direction's step, walks its cel and expires. static void hookUOrbs(StSimT *sim) { uint8_t x; for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { uint8_t state = sim->hookScratch.u.state[x]; uint8_t dir; if (state == 0u) { // $7E84: nothing here yet -- maybe start one. if (stSimRng(sim, 0x1Eu) >= 3u) { continue; } dir = (uint8_t)(stSimRng(sim, 4u) - 1u); sim->hookScratch.u.dir[x] = dir; sim->hookScratch.u.life[x] = U_LIFE(sim, dir); if ((dir & 2u) == 0u) { // $7EA8: fixed row, random column. uint16_t col = (uint16_t)(((uint16_t)stSimRng(sim, 0x90u) + 0x11u) << 1); sim->spr[x].row = U_ENTRY(sim, dir); sim->spr[x].col = (uint8_t)col; sim->spr[x].msb = (uint8_t)(col >> 8); } else { // $7ECF: fixed column, random row. sim->spr[x].col = U_ENTRY(sim, dir); sim->spr[x].msb = U_ENTRY_MSB(sim, dir); sim->spr[x].row = (uint8_t)((uint8_t)(stSimRng(sim, 0x4Fu) + 0x1Du) << 1); } sim->hookScratch.u.state[x] = 1u; sim->spr[x].enable = 1u; sim->spr[x].ptr = ST_HK_FALL_CEL; sim->hookScratch.u.celPhase[x] = 0u; continue; } if (state != 1u) { sim->hookScratch.u.state[x] = 0u; continue; } // $7F02: drift until the lifetime runs out. sim->hookScratch.u.life[x]--; if (sim->hookScratch.u.life[x] == 0u) { sim->spr[x].enable = 0u; sim->hookScratch.u.state[x] = 0u; continue; } dir = sim->hookScratch.u.dir[x]; stSimSpriteAddX(sim, x, U_DX(sim, dir)); sim->spr[x].row = (uint8_t)(sim->spr[x].row + U_DY(sim, dir)); sim->hookScratch.u.celPhase[x] = (uint8_t)((sim->hookScratch.u.celPhase[x] + 1u) & 3u); sim->spr[x].ptr = U_CEL(sim, sim->hookScratch.u.celPhase[x]); } } static void hookUPerTick(StSimT *sim) { // $7DAD: the original opens with a ceiling reflect that only runs // while a crash is in progress and only above row $11. The wreck // sequence moves sprite 0 downward to $DA and never back up, so this // cannot fire on the real machine either; it is kept as shipped. if (sim->collisionPhase != 0u && sim->spr[0].row < 0x11u) { sim->velY = (int16_t)(-sim->velY); sim->posYrow = (uint8_t)(sim->posYrow + 2u); } hookUBounce(sim); hookUOrbs(sim); } // --------------------------------------------------------------------------- // Dispatch // --------------------------------------------------------------------------- // $62F0 -- a converted level's scratch starts from the bytes the shipped // blob holds at the addresses the original used, so the first tick sees // exactly what the C64's did. // Level E "BEANSTALK" ($7DA0 prelude0, $7DCA per-tick2): the screen // opens with pad 1 only. While the fare aboard wants UP the stalk grows: // every $57 ticks a leaf cell sprouts right along row `row` and left // along row `row`-2, the green tips one cell beyond; the fifth step // hangs pads N and N+1 at the leaf ends instead, opens them (pad count // +2) and moves the growth four rows up, until pad 9 is out. $7DA0 runs // from the respawn prelude, so a lost cab restarts the count from pad 1 // while the leaves already drawn stay. static void hookEPrelude0(StSimT *sim) { StHookET *e = &sim->hookScratch.e; sim->specialPad = 1u; sim->padCount = 1u; e->seg = 0u; e->padSlot = 2u; e->row = ST_HOOK_E_FIRST_ROW; e->tick = ST_HOOK_E_TICK_START; if (sim->levelState == 1u) { e->tick = ST_HOOK_E_TICK_SECOND; } } // $7E89 -- one green cell. static void hookEPut(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch) { stSimPutChar(sim, col, row, ch); stSimPutColor(sim, col, row, ST_HOOK_E_COLOR); } // $7E39 -- the tips, one cell beyond this step's leaves. static void hookETips(StSimT *sim) { const StHookET *e = &sim->hookScratch.e; hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL + 1u + e->seg), e->row, ST_HOOK_E_CHAR_TIP_R); hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL - 1u - e->seg), (uint8_t)(e->row - 2u), ST_HOOK_E_CHAR_TIP_L); } static void hookEPerTick(StSimT *sim) { StHookET *e = &sim->hookScratch.e; if (sim->specialPad == ST_HOOK_E_LAST_PAD || sim->activeSpriteIdx != ST_FARE_DEST_UP) { return; } e->tick++; if (e->tick != ST_HOOK_E_GROW_TICKS) { return; } e->tick = 0u; e->seg++; if (e->seg != ST_HOOK_E_LEAVES) { hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL + e->seg), e->row, ST_HOOK_E_CHAR_LEAF); hookEPut(sim, (uint8_t)(ST_HOOK_E_STALK_COL - e->seg), (uint8_t)(e->row - 2u), ST_HOOK_E_CHAR_LEAF); hookETips(sim); return; } // $7E15: the pads hang from the leaf ends, then the stalk climbs. hookEPut(sim, ST_HOOK_E_PAD_COL_RIGHT, e->row, (uint8_t)(ST_CHAR_DIGIT_BASE + e->padSlot)); hookEPut(sim, ST_HOOK_E_PAD_COL_LEFT, (uint8_t)(e->row - 2u), (uint8_t)(ST_CHAR_DIGIT_BASE + e->padSlot + 1u)); hookETips(sim); sim->specialPad = (uint8_t)(sim->specialPad + 2u); sim->padCount = (uint8_t)(sim->padCount + 2u); e->seg = 0u; e->row = (uint8_t)(e->row - ST_HOOK_E_ROW_STEP); e->padSlot = (uint8_t)(e->padSlot + 2u); stAudioSfx(E_SFX_GROW(sim)); } // $7DAF (J) / $7DFE (P) -- a hazard that touched the scenery below row // $38 bursts: cel $82, phase 0, dropped four rows onto what it hit. // Sprites 7 down to 3, every one that hit. static void hookFallBgHits(StSimT *sim, uint8_t *state, uint8_t *phase, const uint8_t *sfx) { uint8_t x; for (x = (uint8_t)(ST_HW_SPRITES - 1u); x >= ST_HOOK_FIRST_SPRITE; x--) { if ((sim->spriteBgColl & kStBit[x]) == 0u || sim->spr[x].row < ST_HOOK_FALL_HIT_ROW || state[x] == 2u) { continue; } state[x] = 2u; sim->spr[x].ptr = ST_HK_FALL_CEL_BURST; phase[x] = 0u; sim->spr[x].row = (uint8_t)(sim->spr[x].row + ST_HOOK_FALL_HIT_DROP); if (sfx != 0) { stAudioSfx(sfx); } } } // $7E01 (J) / $7E45 (P) -- the per-sprite machine, sprites 3..7. Idle: // spawn on rng($3C) < 3 at rng($FF) + the sprite's column base (J // rerolls one over the middle band), cel $80 on row $17. Falling: one // row per tick, cel flipping $80/$81 and drifting sideways on the odd // cel. Bursting: walk the cel run (P only every other count of a // shared parity) and go idle at its end. static void hookFallTick(StSimT *sim, uint8_t *state, uint8_t *phase, const StHookFallT *d, uint8_t *parity) { uint8_t x; for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { if (state[x] == 0u) { uint16_t col; if (stSimRng(sim, ST_HOOK_FALL_SPAWN_ODDS) >= ST_HOOK_FALL_SPAWN_HIT) { continue; } do { col = (uint16_t)(stSimRng(sim, 0xFFu) + HKT(sim, d->startTable + x)); } while (d->avoidMiddle && col >= ST_HOOK_FALL_BAND_LO && col < ST_HOOK_FALL_BAND_HI); sim->spr[x].col = (uint8_t)col; sim->spr[x].msb = (uint8_t)(col >> 8); sim->spr[x].ptr = ST_HK_FALL_CEL; sim->spr[x].row = ST_HOOK_FALL_TOP_ROW; sim->spr[x].enable = 1u; state[x] = 1u; } else if (state[x] == 1u) { sim->spr[x].ptr ^= 1u; if ((sim->spr[x].ptr & 1u) != 0u) { stSimSpriteAddX(sim, x, HKT(sim, d->dxTable + x)); } sim->spr[x].row++; } else if (state[x] == 2u) { if (parity != 0) { (*parity)++; if ((*parity & 1u) != 0u) { continue; } } phase[x]++; if (phase[x] == d->phaseEnd) { state[x] = 0u; sim->spr[x].enable = 0u; } else { sim->spr[x].ptr = HKT(sim, d->celTable + phase[x]); } } else { state[x] = 0u; // $7E1C: anything else resets } } } // Level J "SHOOTING STARS" ($7D9D prelude0, $7DAF per-tick2): stars fall // from the top and burst on the scenery (hookFall*). The hook reads the // stars' own $D01F bits, so the prelude opts them into the collision // test. static void hookJPrelude0(StSimT *sim) { uint8_t x; for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { sim->hookScratch.j.state[x] = 0u; sim->spr[x].color = ST_HK_J_COLOR; } sim->bgCollSprites = ST_HOOK_FALL_SPRITES; } static void hookJPerTick(StSimT *sim) { StHookJT *j = &sim->hookScratch.j; hookFallBgHits(sim, j->state, j->phase, J_SFX_BURST(sim)); hookFallTick(sim, j->state, j->phase, &kFallJ, 0); } // Level L "BLACK HOLE" ($7D9F prelude0, $7DBC per-tick2, $7D9B verdict): // hardware sprite 3 spins through cels $80..$82 at ($AC, $82) and pulls // the cab in -- each tick the gravity templates become the pull toward // it, weaker with distance. static void hookLPrelude0(StSimT *sim) { StSpriteT *hole = &sim->spr[ST_HOOK_L_HOLE]; hole->color = ST_HK_L_COLOR; hole->msb = 0u; hole->col = ST_HOOK_L_HOLE_COL; hole->row = ST_HOOK_L_HOLE_ROW; hole->ptr = ST_HK_L_CEL_FIRST; hole->enable = 1u; } // $7DCB / $7E01 -- base less |dist|/8 ($7E42), signed by the direction. static uint16_t hookLPull(uint8_t base, uint8_t dist) { uint8_t mag = dist; uint16_t pull; if ((mag & 0x80u) != 0u) { mag = (uint8_t)(0u - mag); } pull = (uint8_t)(base - (uint8_t)(mag >> 3)); if ((dist & 0x80u) != 0u) { pull = (uint16_t)(0u - pull); } return pull; } static void hookLPerTick(StSimT *sim) { StSpriteT *hole = &sim->spr[ST_HOOK_L_HOLE]; uint8_t dy; uint8_t dx; hole->ptr++; if (hole->ptr == ST_HK_L_CEL_END) { hole->ptr = ST_HK_L_CEL_FIRST; } dy = (uint8_t)(ST_HOOK_L_HOLE_ROW - sim->spr[0].row); sim->gravTemplateY = hookLPull(ST_HOOK_L_PULL_Y, dy); dx = (uint8_t)(ST_HOOK_L_HOLE_X_HALF - (uint8_t)(stSimSpriteX(sim, 0u) >> 1)); sim->gravTemplateX = hookLPull(ST_HOOK_L_PULL_X, dx); } // Level P "BLIZZARD" ($7DA8 prelude0, $7D9C prelude1, $7DBA per-tick2): // snow falls and melts on the scenery (hookFall*), and now and then the // wind turns: the windsocks flip and the X gravity changes sign. static void hookPPrelude0(StSimT *sim) { uint8_t x; for (x = ST_HOOK_FIRST_SPRITE; x < ST_HW_SPRITES; x++) { sim->hookScratch.p.state[x] = 0u; sim->spr[x].color = ST_HK_P_COLOR; } sim->bgCollSprites = ST_HOOK_FALL_SPRITES; } static void hookPFlipSock(StSimT *sim, uint16_t cell) { stSimPutChar(sim, (uint8_t)(cell % ST_SCREEN_COLS), (uint8_t)(cell / ST_SCREEN_COLS), (uint8_t)(sim->screen[cell] ^ ST_HOOK_P_SOCK_FLIP)); } static void hookPPerTick(StSimT *sim) { StHookPT *p = &sim->hookScratch.p; if (stSimRng(sim, ST_HOOK_P_WIND_ODDS) < ST_HOOK_P_WIND_HIT) { stAudioSfx(P_SFX_WIND(sim)); sim->gravTemplateX = (uint16_t)(0u - sim->gravTemplateX); hookPFlipSock(sim, ST_CELL(7, 6)); // $051E hookPFlipSock(sim, ST_CELL(9, 21)); // $057D hookPFlipSock(sim, ST_CELL(15, 31)); // $0677 } hookFallBgHits(sim, p->state, p->phase, 0); hookFallTick(sim, p->state, p->phase, &kFallP, &p->parity); } // Level S "THE SWITCH" ($7D9D input filter): the stick's direction bits // come back swapped through the level's table; FIRE passes. static uint8_t hookSInput(StSimT *sim, uint8_t input) { return (uint8_t)((input & ST_HOOK_S_FIRE) | S_DIR_MAP(sim, input & ST_HOOK_S_DIRS)); } void stHookSceneLoad(StSimT *sim) { uint8_t k; switch (sim->hookLevel) { case ST_LEVEL_G: sim->hookScratch.g.lastSpot = HKT(sim, 0x7DD1); sim->hookScratch.g.celPhase = HKT(sim, 0x7DD2); sim->hookScratch.g.hop = HKT(sim, 0x7DD3); sim->hookScratch.g.tick = HKT(sim, 0x7E00); sim->hookScratch.g.sweep = HKT(sim, 0x7F02); break; case ST_LEVEL_H: sim->hookScratch.h.switchId = HKT(sim, 0x7E4C); sim->hookScratch.h.tick = HKT(sim, 0x7E53); sim->hookScratch.h.active = HKT(sim, 0x7E54); sim->hookScratch.h.step = HKT(sim, 0x7E55); sim->hookScratch.h.tone = HKT(sim, 0x7E56); for (k = 0u; k < ST_HOOK_H_SEGS; k++) { sim->hookScratch.h.segFlag[k] = HKT(sim, 0x7E15 + k); } break; case ST_LEVEL_I: for (k = 0u; k < ST_HW_SPRITES; k++) { sim->hookScratch.i.state[k] = HKT(sim, 0x7DB8 + k); sim->hookScratch.i.phase[k] = HKT(sim, 0x7DC0 + k); sim->hookScratch.i.dx[k] = HKT(sim, 0x7DC8 + k); sim->hookScratch.i.dy[k] = HKT(sim, 0x7DD0 + k); } break; case ST_LEVEL_K: sim->hookScratch.k.parity = HKT(sim, 0x7DBD); break; case ST_LEVEL_O: sim->hookScratch.o.tick = HKT(sim, 0x7DBD); break; case ST_LEVEL_P: sim->hookScratch.p.parity = HKT(sim, 0x7F1F); break; case ST_LEVEL_Q: for (k = 0u; k < ST_HW_SPRITES; k++) { sim->hookScratch.q.phase[k] = HKT(sim, 0x7E83 + k); } break; case ST_LEVEL_T: sim->hookScratch.t.gateShut = HKT(sim, 0x7D9C); break; case ST_LEVEL_U: sim->hookScratch.u.cooldown = HKT(sim, 0x7F6D); for (k = 0u; k < ST_HW_SPRITES; k++) { sim->hookScratch.u.celPhase[k] = HKT(sim, 0x7F4F + k); sim->hookScratch.u.state[k] = HKT(sim, 0x7F57 + k); sim->hookScratch.u.dir[k] = HKT(sim, 0x7F5F + k); sim->hookScratch.u.life[k] = HKT(sim, 0x7F6E + k); } break; case ST_LEVEL_W: sim->hookScratch.w.colorPhase = HKT(sim, 0x7DAB); sim->hookScratch.w.started = HKT(sim, 0x7DE5); for (k = 0u; k < ST_HOOK_W_LASERS; k++) { sim->hookScratch.w.state[k] = HKT(sim, 0x7DAC + k); sim->hookScratch.w.row[k] = HKT(sim, 0x7DB5 + k); } break; case ST_LEVEL_X: sim->hookScratch.x.col = HKT(sim, 0x7D9C); sim->hookScratch.x.dir = HKT(sim, 0x7D9D); sim->hookScratch.x.tick = HKT(sim, 0x7D9E); break; default: break; } } // $7D75 -- 0 means sprite contact (passenger, level sprites) is safe. uint8_t stHookHitVerdict(StSimT *sim) { switch (sim->hookLevel) { case ST_LEVEL_G: // $7DF2: only the scenery kills; that also unhides the cab. if ((sim->spriteBgColl & 1u) != 0u) { sim->spr[0].enable = 1u; return 1u; } return 0u; case ST_LEVEL_H: return 0u; case ST_LEVEL_L: return ST_HOOK_L_VERDICT; default: return 1u; } } // $7D72 -- the input filter. uint8_t stHookInput(StSimT *sim, uint8_t input) { switch (sim->hookLevel) { case ST_LEVEL_Q: return hookQInput(sim, input); case ST_LEVEL_S: return hookSInput(sim, input); default: break; } return input; } // $7D6C -- before the sprite flush. Split in two: one switch over all // sixteen levels left the 65816's single accumulator with more live // values than greedy register allocation can place, and the build then // falls back to the basic allocator for this whole file. ST_NOINLINE static void hookPerTickLow(StSimT *sim) { switch (sim->hookLevel) { case ST_LEVEL_E: hookEPerTick(sim); break; case ST_LEVEL_G: hookGPerTick(sim); break; case ST_LEVEL_H: hookHPerTick(sim); break; case ST_LEVEL_I: hookIPerTick(sim); break; case ST_LEVEL_J: hookJPerTick(sim); break; case ST_LEVEL_K: hookKPerTick(sim); break; case ST_LEVEL_L: hookLPerTick(sim); break; default: break; } } ST_NOINLINE static void hookPerTickHigh(StSimT *sim) { switch (sim->hookLevel) { case ST_LEVEL_O: hookOPerTick(sim); break; case ST_LEVEL_P: hookPPerTick(sim); break; case ST_LEVEL_Q: hookQPerTick(sim); break; case ST_LEVEL_R: hookRPerTick(sim); break; case ST_LEVEL_T: hookTPerTick(sim); break; case ST_LEVEL_U: hookUPerTick(sim); break; case ST_LEVEL_V: hookVPerTick(sim); break; case ST_LEVEL_W: hookWPerTick(sim); break; case ST_LEVEL_X: hookXPerTick(sim); break; default: break; } } void stHookPerTick2(StSimT *sim) { if (sim->hookLevel <= ST_LEVEL_L) { hookPerTickLow(sim); return; } hookPerTickHigh(sim); } // $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; case ST_LEVEL_U: hookUPrelude0(sim); break; case ST_LEVEL_Q: hookQPrelude(sim); break; case ST_LEVEL_I: hookIPrelude(sim); break; case ST_LEVEL_W: memset(sim->hookScratch.w.state, 0, sizeof(sim->hookScratch.w.state)); break; case ST_LEVEL_E: hookEPrelude0(sim); break; case ST_LEVEL_G: hookGPrelude0(sim); break; case ST_LEVEL_J: hookJPrelude0(sim); break; case ST_LEVEL_L: hookLPrelude0(sim); break; case ST_LEVEL_P: hookPPrelude0(sim); 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: sim->hookScratch.w.started = 1u; break; case ST_LEVEL_R: hookRMaze(sim, ST_CHAR_BLANK); break; case ST_LEVEL_P: stAudioSfx(P_SFX_WIND(sim)); break; case ST_LEVEL_G: sim->hookScratch.g.lastSpot = ST_HOOK_G_SPOTS; // $7D9A break; default: break; } }