1778 lines
63 KiB
C
1778 lines
63 KiB
C
// Space Taxi -- per-level hook programs.
|
|
//
|
|
// Every level's data blob ends with six trampolines ($7D66..$7D77)
|
|
// and a small program at $7D98..$7FFF that the main loop calls at
|
|
// fixed points: two prelude hooks, two per-tick hooks (before and
|
|
// after the sprite flush), an input filter and the sprite-contact
|
|
// verdict. Those programs are the level gimmicks -- the puzzle
|
|
// switches, lasers, moving pads, trap doors. The port keeps the
|
|
// original bytes 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 <string.h>
|
|
|
|
#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;
|
|
}
|
|
}
|