// Space Taxi -- per-level hook programs. // // Every level's data blob ends with six trampolines ($7D66..$7D77) and // a small program from $7D98 (on to $80CA for level Y) 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 F "TAXI PONG" ($7DD1/$7DD2, $7DD5..$7DDA scratch) ---- #define F_GRAVITY(sim) ((uint16_t)(HKT(sim, 0x7DD3) | ((uint16_t)HKT(sim, 0x7DD4) << 8))) #define F_SFX_BOUNCE(sim) HKTP(sim, 0x7EAA) // ---- 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 K "MAGNETS" ($7DBD scratch) ---- #define K_POLE_COL(sim, k) HKT(sim, 0x7DBE + (k)) // ---- level O "ELECTROIDS" ($7DBD scratch) ---- #define O_GAP_COL(sim, k) HKT(sim, 0x7E66 + (k)) #define O_GAP_ROW(sim, k) HKT(sim, 0x7E6A + (k)) // ---- level V "SHIFT-O-RAMA" ($7DE2 scratch) ---- #define V_SFX_SHIFT(sim) HKTP(sim, 0x7D9D) // ---- 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)) #define Q_SFX_BUZZ(sim) HKTP(sim, 0x7E76) // ---- 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 Y "MYSTERY SCREEN" (StHookYT scratch; $7168 too) ---- #define Y_WALK_TURN_COL(sim, d) HKT(sim, 0x7EC3 + (d)) #define Y_STEP(sim, d) HKT(sim, 0x7EC5 + (d)) #define Y_TURN_DIR(sim, d) HKT(sim, 0x7EC7 + (d)) #define Y_BLOCK_TURN_COL(sim, d) HKT(sim, 0x7F67 + (d)) #define Y_BLOCK_OFFSET(sim, i) HKT(sim, 0x7FB8 + (i)) #define Y_BLOCK_COLOR(sim, i) HKT(sim, 0x7FC1 + (i)) #define Y_BLOCK_CHAR(sim, i) HKT(sim, 0x7FCB + (i)) #define Y_SFX_STEP(sim) HKTP(sim, 0x808E) #define Y_SFX_SHOT(sim) HKTP(sim, 0x8097) #define Y_SFX_PARK(sim) HKTP(sim, 0x80A0) #define Y_SFX_HUM(sim) HKTP(sim, 0x80A9) #define Y_HUM_PITCH(sim, i) HKT(sim, 0x80B2 + (i)) #define Y_SFX_WALK(sim) HKTP(sim, 0x80C2) // ---- 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 }; // Levels Q and Y sound their buzz on voice 2 (SFX voice index 1) only // while its release timer ($7217) has run out. Q's buzz pitch is // rng(100) + 100 ($7E5C, the carry clear out of the RNG). #define ST_HOOK_BUZZ_VOICE 1u #define ST_HOOK_Q_BUZZ_SPAN 0x64u // $D01C with every sprite multicolor (levels I and Q). #define ST_HOOK_ALL_MULTICOLOR 0xFFu // A hit verdict ($7D75) with bit 7 set skips the falling wreck ($6A72 // tests $721D bit 7): levels K and L kill outright with LDA #$FF. #define ST_HOOK_KILL_NO_WRECK 0xFFu // 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; unless 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 is a kill with no falling wreck ($7D9B LDA #$FF). #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 // Level K: six magnet poles on row 4. Touching a sprite kills outright // ($7DC4 LDA #$FF), like level L's hole. #define ST_HOOK_K_POLES 6u #define ST_HOOK_K_POLE_ROW 4u // Level O: the prelude ($7D9C) cuts an eight-cell gap of spaces into // each of the four barrier rows ($401B puts the first cell, $4021 the // seven to its right), from the column and row tables at $7E66/$7E6A. // Every tick ($7DBE) the barrier glyph rolls a pixel right ($7E6E); on // every fourth tick rows 5 and 13 scroll a cell left, and on every // eighth rows 9 and 17 as well (hookRowScrollLeft). #define ST_HOOK_O_GAPS 4u #define ST_HOOK_O_GAP_CELLS 8u #define ST_HOOK_O_TICK_MASK 7u #define ST_HOOK_O_FAST_MASK 3u #define ST_HOOK_O_SLOW_TICK 4u #define ST_HOOK_O_ROW_FAST_A 5u // $04C9 #define ST_HOOK_O_ROW_FAST_B 13u // $0609 #define ST_HOOK_O_ROW_SLOW_A 9u // $0569 #define ST_HOOK_O_ROW_SLOW_B 17u // $06A9 // Level R: a gate cell holds the bar cells' glyph EOR $46 ($7DA5). #define ST_HOOK_R_GATE_FLIP 0x46u // The rows levels O and V scroll: cells 1..38, the borders stay. #define ST_HOOK_ROW_FIRST_COL 1u #define ST_HOOK_ROW_LAST_COL 0x26u // Level V: rows 6..17 shift every sixteenth tick ($7DAB AND #$0F on the // $7DE2 count), bit 1 of the row picking the direction. #define ST_HOOK_V_RATE_MASK 0x0Fu #define ST_HOOK_V_FIRST_ROW 6u #define ST_HOOK_V_END_ROW 0x12u #define ST_HOOK_V_LEFT_BAND 2u // Level F: the ball steps 3 pixels; it drops from row $6E, bounces at // row $6D or from row $AA, and turns where its X low byte is 0 or $6E. #define ST_HOOK_F_SPEED 3u #define ST_HOOK_F_DROP_Y 0x6E00u #define ST_HOOK_F_CEILING_ROW 0x6Du #define ST_HOOK_F_FLOOR_ROW 0xAAu #define ST_HOOK_F_WALL_LO 0x6Eu // Level Y (see hookYPerTick). #define ST_HOOK_Y_SAUCER_ROW 0xA5u #define ST_HOOK_Y_SAUCER_STEP 2u #define ST_HOOK_Y_SAUCER_END_COL 0x58u // with the msb set: X $158 #define ST_HOOK_Y_LAUNCH_ODDS 0xC8u #define ST_HOOK_Y_LAUNCH_HIT 3u #define ST_HOOK_Y_HUM_STEPS_MASK 0x0Fu #define ST_HOOK_Y_WALKER_ROW 0x82u #define ST_HOOK_Y_WALKER_COL 0xE6u #define ST_HOOK_Y_WALK_CYCLE_MASK 0x3Fu #define ST_HOOK_Y_AIM_GAP 6u #define ST_HOOK_Y_SHOT_DELAY 0x1Eu #define ST_HOOK_Y_SHOT_ROW 0x7Bu #define ST_HOOK_Y_SHOT_TOP_ROW 0x2Bu #define ST_HOOK_Y_SHOT_STEP 2u #define ST_HOOK_Y_LIGHT_ROW 19u // $06F8 #define ST_HOOK_Y_LIGHT_CELLS 0x78u #define ST_HOOK_Y_LIGHT_STEP_MASK 3u #define ST_HOOK_Y_LIGHT_START 3u #define ST_HOOK_Y_LIGHT_COLOR 1u #define ST_HOOK_Y_BLOCK_ROW 9u #define ST_HOOK_Y_BLOCK_COL 5u #define ST_HOOK_Y_BLOCK_CELLS 9u #define ST_HOOK_Y_BLOCK_TICKS 0x0Fu #define ST_HOOK_Y_BLOCK_STEP_FRAMES 2u // frames 0/1 step left, 2/3 right #define ST_HOOK_Y_BLOCK_START 2u // the frame $7DF4 draws #define ST_HOOK_Y_BLOCK_BLANK 4u // the all-space frame // An SFX program's control byte (the SID waveform/gate register). #define ST_HOOK_SFX_CTRL_AT 4u // 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 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 hookFPerTick(StSimT *sim); static void hookFPrelude0(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 hookGHop(StSimT *sim); static void hookGPerTick(StSimT *sim); static void hookGPrelude0(StSimT *sim); 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 hookIPerTick(StSimT *sim); static void hookIPrelude(StSimT *sim); static void hookJPerTick(StSimT *sim); static void hookJPrelude0(StSimT *sim); static void hookKPerTick(StSimT *sim); static void hookLPerTick(StSimT *sim); static void hookLPrelude0(StSimT *sim); static uint16_t hookLPull(uint8_t base, uint8_t dist); static void hookOPerTick(StSimT *sim); static void hookOPrelude0(StSimT *sim); static void hookPFlipSock(StSimT *sim, uint16_t cell); static void hookPPerTick(StSimT *sim); static void hookPPrelude0(StSimT *sim); ST_NOINLINE static void hookPerTickHigh(StSimT *sim); ST_NOINLINE static void hookPerTickLow(StSimT *sim); static uint8_t hookQInput(StSimT *sim, uint8_t input); static void hookQPerTick(StSimT *sim); static void hookQPrelude(StSimT *sim); static void hookRMaze(StSimT *sim, uint8_t ch); static void hookRPerTick(StSimT *sim); static void hookRowScrollLeft(StSimT *sim, uint8_t row); static void hookRowScrollRight(StSimT *sim, uint8_t row); static uint8_t hookSInput(StSimT *sim, uint8_t input); static void hookTGate(StSimT *sim, uint8_t ch); static void hookTPerTick(StSimT *sim); static void hookTPrelude1(StSimT *sim); static void hookUBounce(StSimT *sim); static void hookUOrbs(StSimT *sim); static void hookUPerTick(StSimT *sim); static void hookUPrelude0(StSimT *sim); static void hookVPerTick(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 hookYBlockDraw(StSimT *sim, uint8_t frame); static uint8_t hookYHalfX(const StSpriteT *spr); static void hookYHum(StSimT *sim); static void hookYLightAndBlock(StSimT *sim); static void hookYPerTick(StSimT *sim); static void hookYPrelude0(StSimT *sim); static void hookYPrelude1(StSimT *sim); static void hookYShot(StSimT *sim); static void setSpriteColors(StSimT *sim, uint8_t color); // $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 color 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 E "BEANSTALK" ($7DA0 prelude0, $7DCA per-tick2): the screen // opens with pad 1 only. Unless the fare wants UP ($7DD5: CMP #$0B, RTS // on equal) 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. 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); stSimSfx(sim, E_SFX_GROW(sim)); } 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 hookFPerTick(StSimT *sim) { StHookFT *f = &sim->hookScratch.f; StSpriteT *ball = &sim->spr[ST_HK_F_SPRITE]; uint8_t xLo; f->x = (uint16_t)(f->x + f->vx); f->vy = (uint16_t)(f->vy + F_GRAVITY(sim)); f->y = (uint16_t)(f->y + f->vy); ball->col = (uint8_t)f->x; ball->msb = (uint8_t)(f->x >> 8); ball->row = (uint8_t)(f->y >> 8); if (ball->row == ST_HOOK_F_CEILING_ROW) { f->vy = (uint16_t)(0u - f->vy); } else if (ball->row >= ST_HOOK_F_FLOOR_ROW) { stSimSfx(sim, F_SFX_BOUNCE(sim)); f->vy = (uint16_t)(0u - f->vy); } xLo = (uint8_t)f->x; if (xLo == 0u || xLo == ST_HOOK_F_WALL_LO) { f->vx = (uint16_t)(0u - f->vx); f->y = ST_HOOK_F_DROP_Y; f->vy = 0u; } } // Level F "TAXI PONG" ($7D9C prelude0, $7DDB per-tick2): a ball // (sprite 3) crosses the screen three pixels a tick in 16-bit X and falls // under the blob's own gravity ($7DD3) in 8.8 Y. It bounces off the floor // (any row from $AA, with a click) and off an invisible ceiling at row // $6D; whenever its X low byte reaches 0 or $6E -- the left wall at 0 and // the right one at $16E -- it turns round and drops again from row $6E. static void hookFPrelude0(StSimT *sim) { StHookFT *f = &sim->hookScratch.f; StSpriteT *ball = &sim->spr[ST_HK_F_SPRITE]; ball->enable = 1u; ball->color = ST_HK_F_COLOR; ball->col = 0u; ball->msb = 0u; ball->row = 0u; ball->ptr = ST_HK_F_CEL; f->x = 0u; f->vx = ST_HOOK_F_SPEED; f->y = ST_HOOK_F_DROP_Y; f->vy = 0u; } // $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) { stSimSfx(sim, 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 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. // // $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; stSimSfx(sim, G_SFX_HOP(sim)); g->sweep = ST_HOOK_G_SWEEP_START; sim->velX = 0; sim->velY = 0; } 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; } // 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; stSimSfx(sim, 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 I "CROSSFIRE" ($7D9C prelude0, $7DEF per-tick2, $7DB1 prelude1): // bullets (hw 3..7) fly up from the floor, cel-walk, then burst. 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; } // $7E21 draws the climb (rng(2) - 3) before $7E2F draws the // direction: the demo RNG is a sequence, so the order counts. sim->hookScratch.i.dy[x] = (uint8_t)(stSimRng(sim, 2u) + 0xFDu); 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.phase[x] = 0u; // cel phase sim->hookScratch.i.state[x] = 1u; sim->spr[x].enable = 1u; sim->spr[x].ptr = 0x84u; stSimSfx(sim, 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; } } } } 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; } } // 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 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); } 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; } // Level K "MAGNETS" per-tick ($7D9D, the hook after the sprite flush): // the level is globally anti-gravity (its yGrav template is -7); the // hook only animates the six magnet poles on row 4 ($04A0 + the // columns at $7DBE) by flipping bit 0 of their glyphs every other tick. static void hookKPerTick(StSimT *sim) { uint8_t k; sim->hookScratch.k.parity ^= 1u; if ((sim->hookScratch.k.parity & 1u) != 0u) { return; } for (k = 0u; k < ST_HOOK_K_POLES; k++) { uint8_t col = K_POLE_COL(sim, k); stSimPutChar(sim, col, ST_HOOK_K_POLE_ROW, (uint8_t)(sim->screen[ST_CELL(ST_HOOK_K_POLE_ROW, col)] ^ 1u)); } } // 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 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); } 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; } // Level O per-tick ($7DBE, the hook after the sprite flush): the // barrier glyph rolls and the rows scroll left (see ST_HOOK_O_GAPS). static void hookOPerTick(StSimT *sim) { stSimRotateGlyph(sim, ST_CHAR_ELECTROID); sim->hookScratch.o.tick = (uint8_t)((sim->hookScratch.o.tick + 1u) & ST_HOOK_O_TICK_MASK); if ((sim->hookScratch.o.tick & ST_HOOK_O_FAST_MASK) != 0u) { return; } hookRowScrollLeft(sim, ST_HOOK_O_ROW_FAST_A); hookRowScrollLeft(sim, ST_HOOK_O_ROW_FAST_B); if (sim->hookScratch.o.tick != ST_HOOK_O_SLOW_TICK) { return; } hookRowScrollLeft(sim, ST_HOOK_O_ROW_SLOW_A); hookRowScrollLeft(sim, ST_HOOK_O_ROW_SLOW_B); } // Level O "ELECTROIDS" prelude ($7D9C): the gaps in the barriers. static void hookOPrelude0(StSimT *sim) { uint8_t k; for (k = 0u; k < ST_HOOK_O_GAPS; k++) { uint8_t col = O_GAP_COL(sim, k); uint8_t row = O_GAP_ROW(sim, k); uint8_t c; for (c = 0u; c < ST_HOOK_O_GAP_CELLS; c++) { stSimPutChar(sim, (uint8_t)(col + c), row, ST_CHAR_SPACE); } } } // 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 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) { stSimSfx(sim, 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); } 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; } // $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 hookPerTickHigh(StSimT *sim) { switch (sim->hookLevel) { case ST_LEVEL_P: hookPPerTick(sim); break; case ST_LEVEL_Q: hookQPerTick(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; case ST_LEVEL_Y: hookYPerTick(sim); break; default: break; } } ST_NOINLINE static void hookPerTickLow(StSimT *sim) { switch (sim->hookLevel) { case ST_LEVEL_E: hookEPerTick(sim); break; case ST_LEVEL_F: hookFPerTick(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_L: hookLPerTick(sim); break; default: break; } } // Level Q "INTERFERENCE" ($7DA1 prelude0, $7D9B prelude1, $7DFF // per-tick2, $7E2E input): five interference sprites (hw 3..7) sit at // fixed spots flickering through cels $80..$8B every other tick, every // sprite multicolor; while the cab is airborne mid-screen (rows $5A.. // $B3) the joystick is replaced by one random direction on 3 ticks in // 10 (rng(10) >= 8), with a buzz. static uint8_t hookQInput(StSimT *sim, uint8_t input) { uint8_t scrambled; 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; } scrambled = Q_SCRAMBLE(sim, (uint8_t)(stSimRng(sim, 4u) - 1u)); // $7E55: the static buzzes too, unless voice 2 is still sounding. if (sim->sidRelTimer[ST_HOOK_BUZZ_VOICE] == 0u) { uint8_t buzz[ST_SFX_PROGRAM_BYTES]; memcpy(buzz, Q_SFX_BUZZ(sim), sizeof(buzz)); buzz[0] = (uint8_t)(stSimRng(sim, ST_HOOK_Q_BUZZ_SPAN) + ST_HOOK_Q_BUZZ_SPAN); buzz[1] = buzz[0]; stSimSfx(sim, buzz); } return scrambled; } static void hookQPerTick(StSimT *sim) { int8_t x; sim->hookScratch.q.parity = (uint8_t)((sim->hookScratch.q.parity + 1u) & 1u); if (sim->hookScratch.q.parity != 0u) { return; } 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 void hookQPrelude(StSimT *sim) { int8_t x; 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; } } // Level R "TAXI MAZE" wall program ($7D9C): put `ch` in the vertical // bar cells ($043E/$0466/$048E) and its complement (ch^$46) in the // twelve gate cells ($0439/$0461/$0489, $04A1..$04A4, $0709..$070D); // 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[] = { 0x3Eu, 0x66u, 0x8Eu }; static const uint16_t kGate[] = { 0x39u, 0x61u, 0x89u, 0xA1u, 0xA2u, 0xA3u, 0xA4u, 0x309u, 0x30Au, 0x30Bu, 0x30Cu, 0x30Du }; uint8_t k; uint8_t alt = (uint8_t)(ch ^ ST_HOOK_R_GATE_FLIP); for (k = 0u; k < (uint8_t)(sizeof(kBar) / sizeof(kBar[0])); k++) { stSimPutChar(sim, (uint8_t)(kBar[k] % ST_SCREEN_COLS), (uint8_t)(kBar[k] / ST_SCREEN_COLS), ch); } for (k = 0u; k < (uint8_t)(sizeof(kGate) / sizeof(kGate[0])); k++) { stSimPutChar(sim, (uint8_t)(kGate[k] % ST_SCREEN_COLS), (uint8_t)(kGate[k] / ST_SCREEN_COLS), alt); } } // Level R per-tick ($7DD2, the hook after the sprite flush): 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]); } // A row's cells 1..38 rotate one cell, screen and color together, the // end cell wrapping round; the border columns 0 and 39 stay. Level O's // barriers ($7DD1..$7E62) and V's left bands ($7E0E) go left, V's other // bands ($7E35) right. static void hookRowScrollLeft(StSimT *sim, uint8_t row) { uint16_t base = ST_CELL(row, 0u); uint8_t chSave = sim->screen[base + ST_HOOK_ROW_FIRST_COL]; uint8_t colSave = sim->color[base + ST_HOOK_ROW_FIRST_COL]; uint8_t col; for (col = ST_HOOK_ROW_FIRST_COL; col < ST_HOOK_ROW_LAST_COL; col++) { stSimPutChar(sim, col, row, sim->screen[base + col + 1u]); stSimPutColor(sim, col, row, sim->color[base + col + 1u]); } stSimPutChar(sim, ST_HOOK_ROW_LAST_COL, row, chSave); stSimPutColor(sim, ST_HOOK_ROW_LAST_COL, row, colSave); } static void hookRowScrollRight(StSimT *sim, uint8_t row) { uint16_t base = ST_CELL(row, 0u); uint8_t chSave = sim->screen[base + ST_HOOK_ROW_LAST_COL]; uint8_t colSave = sim->color[base + ST_HOOK_ROW_LAST_COL]; uint8_t col; for (col = ST_HOOK_ROW_LAST_COL; col > ST_HOOK_ROW_FIRST_COL; col--) { stSimPutChar(sim, col, row, sim->screen[base + col - 1u]); stSimPutColor(sim, col, row, sim->color[base + col - 1u]); } stSimPutChar(sim, ST_HOOK_ROW_FIRST_COL, row, chSave); stSimPutColor(sim, ST_HOOK_ROW_FIRST_COL, row, colSave); } // 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)); } // Level T "FAST BREAK" $7D9D -- the barrier on row 3: `ch` into the // center 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 center 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); stSimSfx(sim, 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++; stSimSfx(sim, T_SFX_SHUT(sim)); } // Level T prelude 1 ($7DC4): center open, sides barred. static void hookTPrelude1(StSimT *sim) { hookTGate(sim, ST_CHAR_SPACE); sim->hookScratch.t.gateShut = 0u; stSimSfx(sim, T_SFX_OPEN(sim)); } // 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. // // $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; stSimSfx(sim, 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); } // $7D9B -- clear the orb states and color 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; } } // Level V "SHIFT-O-RAMA" per-tick ($7DA6): every sixteenth tick a // shift sounds and rows 6..17 each move a cell, in bands of two by bit // 1 of the row: set to the left ($7E0E), clear to the right ($7E35). static void hookVPerTick(StSimT *sim) { uint8_t row; sim->hookScratch.v.tick++; if ((sim->hookScratch.v.tick & ST_HOOK_V_RATE_MASK) != 0u) { return; } stSimSfx(sim, V_SFX_SHIFT(sim)); for (row = ST_HOOK_V_FIRST_ROW; row < ST_HOOK_V_END_ROW; row++) { if ((row & ST_HOOK_V_LEFT_BAND) != 0u) { hookRowScrollLeft(sim, row); } else { hookRowScrollRight(sim, row); } } } // 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 colors 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) { stSimSfx(sim, 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 + color) 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); } } // Level Y "MYSTERY SCREEN" ($7D9B prelude0, $7E02 prelude1, $7E21 // per-tick2). A saucer (sprite 3) waits off the left edge until it is // launched (rng($C8) < 3) and crosses two pixels a tick to X $158 under a // hum that restarts whenever voice 2 falls silent. A walker (sprite 4) // patrols row $82 between X $0C8 and $13C a pixel every fourth tick, // showing its other cel and sounding once every 128 ticks; whenever the // cab is above row $82 and within six half-pixels of it, it fires after // $1E ticks: a shot (sprite 5) rising from row $7B two rows a tick to row // $2B, where it bursts. A light blinks along rows 19..21, a cell every // four ticks, and a 3x3 block creature on row 9 steps between columns 2 // and $0F every fifteen ticks. ($7EC3..$7EC8 holds the two directions' // turning columns, steps and the direction each turns into, for the // walker and the block alike.) static void hookYBlockDraw(StSimT *sim, uint8_t frame) { const StHookYT *y = &sim->hookScratch.y; uint8_t i; for (i = 0u; i < ST_HOOK_Y_BLOCK_CELLS; i++) { uint8_t off = Y_BLOCK_OFFSET(sim, i); uint16_t cell = (uint16_t)(ST_CELL(ST_HOOK_Y_BLOCK_ROW, y->blockCol) + off); uint8_t col = (uint8_t)(cell % ST_SCREEN_COLS); uint8_t row = (uint8_t)(cell / ST_SCREEN_COLS); stSimPutColor(sim, col, row, Y_BLOCK_COLOR(sim, i)); stSimPutChar(sim, col, row, Y_BLOCK_CHAR(sim, (uint8_t)(frame * ST_HOOK_Y_BLOCK_CELLS + i))); } } // $8009 / $8013 -- a sprite's 9-bit X halved into a byte (LSR the msb, // ROR the column), so the walker can aim with one subtraction. static uint8_t hookYHalfX(const StSpriteT *spr) { return (uint8_t)((((uint16_t)(spr->msb & 1u) << 8) | spr->col) >> 1); } // $7E10 -- the saucer's hum starts over. static void hookYHum(StSimT *sim) { stSimSfx(sim, Y_SFX_HUM(sim)); sim->hookScratch.y.humStep = 0u; } // $7ECD..$7F63 -- the blinking light and, every fifteenth tick, a step // of the block creature. static void hookYLightAndBlock(StSimT *sim) { StHookYT *y = &sim->hookScratch.y; uint8_t col; uint8_t row; y->lightTick++; if ((y->lightTick & ST_HOOK_Y_LIGHT_STEP_MASK) == 0u) { y->lightCell++; if (y->lightCell == ST_HOOK_Y_LIGHT_CELLS) { y->lightCell = 0u; } } col = (uint8_t)(y->lightCell % ST_SCREEN_COLS); row = (uint8_t)(ST_HOOK_Y_LIGHT_ROW + y->lightCell / ST_SCREEN_COLS); if ((y->lightTick & 1u) != 0u) { stSimPutChar(sim, col, row, y->lightSaveChar); stSimPutColor(sim, col, row, y->lightSaveColor); } else { y->lightSaveChar = sim->screen[ST_CELL(row, col)]; y->lightSaveColor = sim->color[ST_CELL(row, col)]; stSimPutColor(sim, col, row, ST_HOOK_Y_LIGHT_COLOR); stSimPutChar(sim, col, row, ST_CHAR_BLANK); } y->blockTimer--; if (y->blockTimer != 0u) { return; } y->blockTimer = ST_HOOK_Y_BLOCK_TICKS; if (Y_BLOCK_TURN_COL(sim, y->blockDir) == y->blockCol) { y->blockDir = Y_TURN_DIR(sim, y->blockDir); } hookYBlockDraw(sim, ST_HOOK_Y_BLOCK_BLANK); y->blockCol = (uint8_t)(y->blockCol + Y_STEP(sim, y->blockDir)); y->blockFrame++; hookYBlockDraw(sim, (uint8_t)(y->blockDir * ST_HOOK_Y_BLOCK_STEP_FRAMES + (y->blockFrame & 1u))); stSimSfx(sim, Y_SFX_STEP(sim)); } static void hookYPerTick(StSimT *sim) { StHookYT *y = &sim->hookScratch.y; StSpriteT *saucer = &sim->spr[ST_HK_Y_SAUCER]; StSpriteT *walker = &sim->spr[ST_HK_Y_WALKER]; bool moving = (saucer->msb | saucer->col) != 0u; if (!moving && stSimRng(sim, ST_HOOK_Y_LAUNCH_ODDS) < ST_HOOK_Y_LAUNCH_HIT) { hookYHum(sim); moving = true; } if (moving) { uint8_t pitch; stSimSpriteAddX(sim, ST_HK_Y_SAUCER, ST_HOOK_Y_SAUCER_STEP); y->humStep++; pitch = Y_HUM_PITCH(sim, y->humStep & ST_HOOK_Y_HUM_STEPS_MASK); stAudioVoice2(pitch, pitch, Y_SFX_HUM(sim)[ST_HOOK_SFX_CTRL_AT]); if (sim->sidRelTimer[ST_HOOK_BUZZ_VOICE] == 0u) { hookYHum(sim); } if (saucer->msb != 0u && saucer->col >= ST_HOOK_Y_SAUCER_END_COL) { saucer->msb = 0u; saucer->col = 0u; stSimSfx(sim, Y_SFX_PARK(sim)); } } y->walkerParity++; if ((y->walkerParity & 1u) != 0u) { y->walkerCount++; if ((y->walkerCount & ST_HOOK_Y_WALK_CYCLE_MASK) == 0u) { walker->ptr = ST_HK_Y_CEL_WALK_ALT; stSimSfx(sim, Y_SFX_WALK(sim)); } else if ((y->walkerCount & 1u) == 0u) { walker->ptr = ST_HK_Y_CEL_WALK; } if ((y->walkerCount & 1u) == 0u) { if (Y_WALK_TURN_COL(sim, y->walkerDir) == walker->col) { y->walkerDir = Y_TURN_DIR(sim, y->walkerDir); } stSimSpriteAddX(sim, ST_HK_Y_WALKER, Y_STEP(sim, y->walkerDir)); } } hookYLightAndBlock(sim); hookYShot(sim); } static void hookYPrelude0(StSimT *sim) { StHookYT *y = &sim->hookScratch.y; StSpriteT *saucer = &sim->spr[ST_HK_Y_SAUCER]; StSpriteT *walker = &sim->spr[ST_HK_Y_WALKER]; StSpriteT *shot = &sim->spr[ST_HK_Y_SHOT]; saucer->col = 0u; saucer->msb = 0u; y->shotLive = 0u; y->shotDelay = 0u; saucer->color = ST_HK_Y_COLOR_SAUCER; walker->color = ST_HK_Y_COLOR_WALKER; shot->color = ST_HK_Y_COLOR_WALKER; saucer->row = ST_HOOK_Y_SAUCER_ROW; saucer->enable = 1u; walker->enable = 1u; saucer->ptr = ST_HK_Y_CEL_SAUCER; walker->ptr = ST_HK_Y_CEL_WALK; walker->row = ST_HOOK_Y_WALKER_ROW; walker->msb = 0u; y->walkerDir = 0u; y->walkerCount = 0u; walker->col = ST_HOOK_Y_WALKER_COL; y->blockTimer = 1u; y->blockDir = 1u; y->blockCol = ST_HOOK_Y_BLOCK_COL; hookYBlockDraw(sim, ST_HOOK_Y_BLOCK_START); y->lightCell = 0u; y->lightTick = ST_HOOK_Y_LIGHT_START; } // $7E02 -- every sprite up to the saucer is multicolor, and a saucer // already in flight hums again. static void hookYPrelude1(StSimT *sim) { sim->multiColorMask = ST_HK_Y_MULTICOLOR; if ((sim->spr[ST_HK_Y_SAUCER].col | sim->spr[ST_HK_Y_SAUCER].msb) != 0u) { hookYHum(sim); } } // $7FF8..$808D -- the walker fires when the cab is above it and close; // the shot rises, then bursts and is gone. static void hookYShot(StSimT *sim) { StHookYT *y = &sim->hookScratch.y; StSpriteT *walker = &sim->spr[ST_HK_Y_WALKER]; StSpriteT *shot = &sim->spr[ST_HK_Y_SHOT]; if (sim->spr[0].row < ST_HOOK_Y_WALKER_ROW && y->shotLive == 0u && y->shotDelay == 0u) { uint8_t gap = (uint8_t)(hookYHalfX(walker) - hookYHalfX(&sim->spr[0])); if ((int8_t)gap < 0) { gap = (uint8_t)(0u - gap); } if (gap < ST_HOOK_Y_AIM_GAP) { y->shotDelay = ST_HOOK_Y_SHOT_DELAY; shot->row = ST_HOOK_Y_SHOT_ROW; shot->ptr = ST_HK_Y_CEL_SHOT; } } if (y->shotDelay != 0u) { y->shotDelay--; if (y->shotDelay == 0u) { y->shotLive = 1u; shot->enable = 1u; shot->msb = walker->msb; shot->col = walker->col; stSimSfx(sim, Y_SFX_SHOT(sim)); } } if (y->shotLive == 0u) { return; } if (shot->row != ST_HOOK_Y_SHOT_TOP_ROW) { shot->row = (uint8_t)(shot->row - ST_HOOK_Y_SHOT_STEP); return; } shot->ptr++; if (shot->ptr == ST_HK_Y_CEL_SHOT_END) { // $807B falls into the rise at $8083 with A = 0. y->shotLive = 0u; shot->enable = 0u; shot->row = (uint8_t)(0u - ST_HOOK_Y_SHOT_STEP); } } // Sprites 3..7 share one color 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; } } // --------------------------------------------------------------------------- // Dispatch // --------------------------------------------------------------------------- // $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: case ST_LEVEL_U: // $7E57 / $7DDA: LDA #$00 -- touching a hook sprite is safe. return 0u; case ST_LEVEL_K: case ST_LEVEL_L: return ST_HOOK_KILL_NO_WRECK; 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; } void stHookPerTick2(StSimT *sim) { if (sim->hookLevel <= ST_LEVEL_L) { hookPerTickLow(sim); return; } hookPerTickHigh(sim); } // $7D6F -- after the sprite flush. void stHookPerTick3(StSimT *sim) { switch (sim->hookLevel) { case ST_LEVEL_K: hookKPerTick(sim); break; case ST_LEVEL_O: hookOPerTick(sim); break; case ST_LEVEL_R: hookRPerTick(sim); break; default: break; } } // $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_F: hookFPrelude0(sim); break; case ST_LEVEL_O: hookOPrelude0(sim); break; case ST_LEVEL_P: hookPPrelude0(sim); break; case ST_LEVEL_Y: hookYPrelude0(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: stSimSfx(sim, P_SFX_WIND(sim)); break; case ST_LEVEL_G: sim->hookScratch.g.lastSpot = ST_HOOK_G_SPOTS; // $7D9A break; case ST_LEVEL_I: case ST_LEVEL_Q: sim->multiColorMask = ST_HOOK_ALL_MULTICOLOR; // $7DB1 / $7D9B: $D01C = $FF break; case ST_LEVEL_Y: hookYPrelude1(sim); break; default: break; } } // $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. 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); } sim->hookScratch.q.parity = HKT(sim, 0x7DFE); 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_V: sim->hookScratch.v.tick = HKT(sim, 0x7DE2); 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; case ST_LEVEL_Y: sim->hookScratch.y.humStep = HKT(sim, 0x7ECC); sim->hookScratch.y.walkerParity = HKT(sim, 0x7ECB); sim->hookScratch.y.lightSaveChar = HKT(sim, 0x7F11); sim->hookScratch.y.lightSaveColor = HKT(sim, 0x7F12); sim->hookScratch.y.blockFrame = HKT(sim, 0x7F6A); break; default: break; } }