// Platformer — an end-to-end side-scrolling demo game that
// exercises nearly every subsystem of the NEScript compiler in
// one program: custom CHR tiles, a full 32×30 background nametable
// with per-region attribute palettes, a multi-tile metasprite
// player with gravity/jump physics, wrap-around horizontal
// scrolling, moving enemies with stomp-or-die contact logic, a
// sprite-based status bar at the top of the viewport (coin icon +
// two-digit score on the left, heart icon + lives counter on the
// right) that rides along through the death screen, cross-state
// life tracking that cycles GameOver → Playing while hearts last
// and bounces back to Title when the last heart is spent,
// collectible coins, user-declared SFX envelopes, a user-declared
// music track, and a title → playing → game-over state machine
// driven by both controller input and an autopilot so the jsnes
// golden harness (which runs headless with no simulated buttons)
// still captures the full gameplay loop inside the first ~6
// seconds of demo.
//
// Controls (when played by a human):
//     D-pad left/right  — walk
//     A                 — jump
//     Start             — on title screen: begin; after game over: retry
//
// Build:  cargo run --release -- build examples/platformer.ne
// Output: examples/platformer.nes
//
// All CHR tile art, nametable bytes, and attribute bytes in this
// file are generated by `cargo run --bin gen_platformer_tiles`.
// Regenerate them from `scripts/gen_platformer_tiles.rs` if you
// want to tweak the level.

game "Platformer" {
    mapper: NROM
    // Horizontal arrangement means nametable 1 mirrors nametable 0
    // horizontally, so our single loaded nametable tiles endlessly
    // as the camera scrolls right.
    mirroring: horizontal
}

// ── Palette ──────────────────────────────────────────────────
// Authored in grouped form: one shared `universal:` colour fills
// every sub-palette's index-0 slot automatically. That single
// declaration is enough to dodge the notorious `$3F10/$3F14/$3F18/
// $3F1C` PPU mirror trap — when a program writes 8 distinct
// "index 0" bytes sequentially, the last write clobbers the
// shared background colour and the screen turns the wrong colour
// (or all-black). Using one shared universal everywhere is what
// every shipped NES game does; the parser handles it for us.
//
// `0x22` is the lavender-blue NES master palette slot Super Mario
// Bros uses for its iconic sky. The colour names map to the
// curated set documented in `docs/language-guide.md`.
palette Main {
    universal: 0x22                         // sky blue (NES $22)

    // Background sub-palettes
    bg0: [white,        gray,       black]   // sky / clouds
    bg1: [red,          orange,     dk_red]  // bricks, Q-blocks
    bg2: [bright_green, dk_orange,  brown]   // grass, hills, bushes, dirt
    bg3: [red,          orange,     white]   // HUD chrome (matches sp0)

    // Sprite sub-palette 0 — every `draw` uses this one slot
    // because the OAM attribute byte is always 0 in v0.1.
    //   1 = red cap, 2 = orange skin, 3 = white highlight.
    sp0: [red,          orange,     white]
    sp1: [brown,        dk_orange,  orange]    // reserved
    sp2: [neon_green,   bright_green, white]   // reserved
    sp3: [yellow,       olive,      cream]     // reserved
}

// ── Tileset ──────────────────────────────────────────────────
// One big sprite declaration holds every custom CHR tile used by
// both sprites and the background nametable. `draw Tileset ...
// frame: N` overrides the OAM tile-index byte with `N`, so the
// same declaration drives the player, enemies, coins, and every
// background tile after tile 0. Tile 0 is reserved by the linker
// for the built-in smiley — the background leaves it unreferenced.
//
// All 15 tiles are authored as 8×8 ASCII pixel art and stacked
// vertically (1 tile wide × 15 tiles tall) so the parser splits
// them into consecutive CHR tile indices in reading order. The
// art uses the `.abc` vocabulary (`. = 0`, `a = 1`, `b = 2`,
// `c = 3`); regenerate with `cargo run --bin gen_platformer_tiles`.
sprite Tileset {
    pixels: [
        // tile 1: Player head L
        "..aaaaaa",
        ".aaaaaaa",
        ".aaabbbb",
        ".aabbbbc",
        "aabbcccc",
        "aabbccbc",
        "aabbbbbb",
        "aabbbbbb",
        // tile 2: Player head R
        "aaaaaa..",
        "aaaaaaa.",
        "bbbbaaa.",
        "cbbbbbaa",
        "cccccbbb",
        "bcbbccbb",
        "bbbbbbbb",
        "bbbbbbbb",
        // tile 3: Player body L
        ".aaaaaaa",
        "aaacaaaa",
        "aaaccaaa",
        "aaaaaaaa",
        ".bbbbbbb",
        ".bbbbbbb",
        ".bb..bbb",
        "aaa..bbb",
        // tile 4: Player body R
        "aaaaaaa.",
        "aaaacaaa",
        "aaaccaaa",
        "aaaaaaaa",
        "bbbbbbb.",
        "bbbbbbb.",
        "bbb..bb.",
        "bbb..aaa",
        // tile 5: Enemy
        "..aaaa..",
        ".aaaaaa.",
        ".aacbaa.",
        "aacccaaa",
        "abbccbba",
        ".aaaaaa.",
        "a.aaaa.a",
        "a..aa..a",
        // tile 6: Coin
        "...bb...",
        "..bbbb..",
        ".bbccbb.",
        ".bcbbcb.",
        ".bcbbcb.",
        ".bbccbb.",
        "..bbbb..",
        "...bb...",
        // tile 7: Grass top
        "aaaaaaaa",
        "a.aaa.aa",
        "aaaaaaaa",
        "accacaca",
        "ccccaccc",
        "cccccccc",
        "cbccccbc",
        "cccccccc",
        // tile 8: Dirt
        "cccccccc",
        "cbccccbc",
        "cccccccc",
        "ccccbccc",
        "cccccccc",
        "cbccccbc",
        "ccccbccc",
        "cccccccc",
        // tile 9: Brick
        "cccccccc",
        "caaaaaca",
        "caaaaaca",
        "caaaaaca",
        "cccccccc",
        "aaacaaaa",
        "aaacaaaa",
        "aaacaaaa",
        // tile 10: Cloud L
        "........",
        "...aaa..",
        "..aaaaa.",
        ".aaaaaaa",
        ".aaaaaaa",
        ".bbaaaaa",
        "..bbbbba",
        ".....bbb",
        // tile 11: Cloud R
        "........",
        ".aaa....",
        "aaaaa...",
        "aaaaaaa.",
        "aaaaaaa.",
        "aaaaabb.",
        "abbbbb..",
        "bbb.....",
        // tile 12: Hill
        "........",
        "....aa..",
        "...aaaa.",
        "..aaaaaa",
        "..abaaaa",
        ".abbabaa",
        ".aaababa",
        "aabbabba",
        // tile 13: Bush
        "........",
        "...aa...",
        "..aaaa..",
        ".aaaaac.",
        "aaaaaaca",
        "aaaaacca",
        "aaaaacca",
        "acacacac",
        // tile 14: Q Block
        "cccccccc",
        "caaaaaac",
        "cabbbbac",
        "cabccbac",
        "cabcbbac",
        "cabbbbac",
        "caaaaaac",
        "cccccccc",
        // tile 15: Sky (blank)
        "........",
        "........",
        "........",
        "........",
        "........",
        "........",
        "........",
        "........",
        // tile 16: Digit 0
        "........",
        "..cccc..",
        "..c..c..",
        "..c..c..",
        "..c..c..",
        "..c..c..",
        "..cccc..",
        "........",
        // tile 17: Digit 1
        "........",
        "...cc...",
        "..ccc...",
        "...cc...",
        "...cc...",
        "...cc...",
        "..cccc..",
        "........",
        // tile 18: Digit 2
        "........",
        "..cccc..",
        ".....c..",
        "....cc..",
        "...cc...",
        "..cc....",
        "..cccc..",
        "........",
        // tile 19: Digit 3
        "........",
        "..cccc..",
        ".....c..",
        "...ccc..",
        ".....c..",
        ".....c..",
        "..cccc..",
        "........",
        // tile 20: Digit 4
        "........",
        "..c..c..",
        "..c..c..",
        "..cccc..",
        ".....c..",
        ".....c..",
        ".....c..",
        "........",
        // tile 21: Digit 5
        "........",
        "..cccc..",
        "..c.....",
        "..cccc..",
        ".....c..",
        "..c..c..",
        "..cccc..",
        "........",
        // tile 22: Digit 6
        "........",
        "..cccc..",
        "..c.....",
        "..cccc..",
        "..c..c..",
        "..c..c..",
        "..cccc..",
        "........",
        // tile 23: Digit 7
        "........",
        "..cccc..",
        ".....c..",
        "....c...",
        "...c....",
        "..c.....",
        "..c.....",
        "........",
        // tile 24: Digit 8
        "........",
        "..cccc..",
        "..c..c..",
        "..cccc..",
        "..c..c..",
        "..c..c..",
        "..cccc..",
        "........",
        // tile 25: Digit 9
        "........",
        "..cccc..",
        "..c..c..",
        "..cccc..",
        ".....c..",
        ".....c..",
        "..cccc..",
        "........",
        // tile 26: Heart
        "........",
        ".aa..aa.",
        "aaaaaaaa",
        "aaaaaaaa",
        ".aaaaaa.",
        "..aaaa..",
        "...aa...",
        "........",
        // tile 27: Sprite 0 anchor — one opaque pixel at
        //          row 7 col 3. Paired with the BG anchor tile
        //          at (NT col 31, row 2) below; the single
        //          opaque dot at screen (251, 16) fires the
        //          sprite-0 hit one scanline *below* the HUD
        //          row, keeping the HUD glyphs pixel-stable.
        "........",
        "........",
        "........",
        "........",
        "........",
        "........",
        "........",
        "...c....",
        // tile 28: BG anchor — one opaque pixel at row 0 col 3.
        //          Pre-painted at NT (col 31, row 2) so it
        //          overlaps sprite 0's opaque pixel at screen
        //          (251, 16). Column 31 sits inside jsnes's
        //          right-edge overscan so the dot is invisible.
        "...c....",
        "........",
        "........",
        "........",
        "........",
        "........",
        "........",
        "........"
    ]
}

// Tile index constants — must match `scripts/gen_platformer_tiles.rs`.
const TILE_PLAYER_HL: u8 = 1
const TILE_PLAYER_HR: u8 = 2
const TILE_PLAYER_BL: u8 = 3
const TILE_PLAYER_BR: u8 = 4
const TILE_ENEMY: u8 = 5
const TILE_COIN: u8 = 6
// HUD glyphs. The digit tiles are contiguous so
// `TILE_DIGIT_0 + n` picks digit `n`. Digit + heart tiles are
// shared between sprite rendering (v0.1 sprites) and background
// rendering (the HUD row's `nt_set`s) via the matching `bg3`
// sub-palette.
const TILE_DIGIT_0: u8 = 16
const TILE_HEART: u8 = 26
// Sprite-0 hit pair. `TILE_SPRITE0_ANCHOR` has one opaque pixel
// at row 7, col 3; `TILE_BG_ANCHOR` has its one opaque pixel at
// row 0, col 3. Drawing the sprite at OAM (248, 8) and pre-
// painting the bg tile at NT (col 31, row 2) puts both opaque
// pixels at screen (251, 16) — the first scanline of the
// playfield, one scanline *below* the HUD row. The PPU's
// sprite-0 hit fires there and `sprite_0_split` flips the
// horizontal scroll before scanline 17 renders, so rows 8-15
// stay pixel-stable regardless of `camera_x`.
const TILE_SPRITE0_ANCHOR: u8 = 27
const TILE_BG_ANCHOR: u8 = 28

// ── Background ──────────────────────────────────────────────
// The 32×30 nametable is authored as ASCII art with a `legend`
// block naming each tile by a single character. Horizontal
// scrolling pans this single nametable via `scroll()` so it only
// needs to look interesting at any X offset.
//
// `palette_map:` is the friendlier alternative to a 64-byte
// hand-packed attribute table: 16×15 metatile entries, one
// digit per 16×16 metatile, and the parser packs the 2-bit
// `(br<<6)|(bl<<4)|(tr<<2)|tl` quadrants automatically. The
// three palette regions are sky (`0`), brick row (`1`), and
// ground (`2`).
background Level {
    legend {
        ".": 15   // sky (blank)
        "<": 10   // cloud left half
        ">": 11   // cloud right half
        "#": 9    // brick
        "Q": 14   // question block
        "^": 12   // hill silhouette
        "*": 13   // bush
        "=": 7    // grass top
        "%": 8    // dirt
        // HUD chrome glyphs — row 0 only. Mapped to the sprite
        // CHR tiles we already declared so sprites and background
        // render identical glyphs under the matching bg3 palette.
        "o": 6    // coin icon (HUD score marker)
        "h": 26   // heart icon (HUD lives marker)
        "0": 16   // digit 0 (initial score tens/ones + fallback)
        "3": 19   // digit 3 (initial lives)
        "a": 28   // sprite-0 bg anchor (one opaque pixel at
                  // (251, 16), overlapping sprite 0's slot-0
                  // anchor; sits inside jsnes's overscan)
    }

    // Row 1 carries the status bar: coin + two score digits on the
    // left, heart + single lives digit on the right. Row 0 stays
    // sky because the jsnes golden harness (and many emulators)
    // mask out the top 8 scanlines as overscan — anything painted
    // there would never show in the committed PNG. Sprite 0
    // (painted over column 2's coin tile every frame) overlaps the
    // `o` glyph's opaque pixels so the sprite-0 hit flag fires on
    // a scanline inside the HUD row; `sprite_0_split(camera_x, 0)`
    // in `on frame` picks up that hit and swaps the horizontal
    // scroll from 0 to `camera_x` for the rest of the frame,
    // pinning the HUD while the playfield scrolls underneath. The
    // digits and heart glyph sit at fixed columns so shadow-compare
    // `nt_set` writes can tick the score / lives readouts without
    // repainting the whole row.
    map: [
        "................................",
        "..o.00..................h.3.....",
        "...............................a",
        "................................",
        "................................",
        "....<>..............<>..........",
        "............<>..................",
        "................................",
        "................................",
        "................................",
        "................................",
        "................................",
        "................................",
        "................................",
        "................................",
        "......##Q#........###...........",
        "................................",
        "................................",
        "................................",
        "................................",
        "..^^^.................^^^^......",
        "..........***..............**...",
        "================================",
        "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#%",
        "%%%%%#%%%%%%%%%%%%#%%%%%%%%%%%%%",
        "%%%%%%%%%%%#%%%%%%%%%%%%#%%%%%%%",
        "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
        "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
        "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
        "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
    ]

    palette_map: [
        "3333333333333333",   // metatile row 0 (NT rows 0-1)
                              //   → bg3 (HUD chrome: red / orange / white)
        "0000000000000000",   // metatile rows 1-5  → sky sub-palette
        "0000000000000000",
        "0000000000000000",
        "0000000000000000",
        "0000000000000000",
        "1111111111111111",   // metatile rows 6-7  → brick sub-palette
        "1111111111111111",
        "0000000000000000",   // metatile rows 8-9  → sky again
        "0000000000000000",
        "2222222222222222",   // metatile rows 10-15 → ground sub-palette
        "2222222222222222",   //   (rows 15 sits off-screen but the PPU
        "2222222222222222",   //    still reads its attribute byte, so
        "2222222222222222",   //    we emit it explicitly to match the
        "2222222222222222",   //    hand-packed attribute table the
        "2222222222222222"    //    old form was using.)
    ]
}

// ── Audio ────────────────────────────────────────────────────
//
// SFX: scalar `pitch:` matches the v1 driver's latch-once
// behaviour — pulse 1's period is sampled exactly once when the
// effect triggers and never updated, so a single byte is the
// natural form for the current pipeline. `envelope:` is the
// friendlier alias for `volume:`.

// Custom boingy jump sound — fixed-pitch arc on pulse 1.
sfx Boing {
    duty: 2
    pitch: 0x30
    envelope: [12, 11, 10, 9, 7, 5, 2]
}

// Custom coin ding — short rising blip.
sfx Ding {
    duty: 2
    pitch: 0x20
    envelope: [14, 12, 8, 3]
}

// Custom looping underworld theme — playful four-bar loop on
// pulse 2 that plays while the player is alive. Note names
// (C4 / E4 / etc.) plus a default `tempo:` so each note only
// has to spell its frame count when it deviates from the beat.
music Theme {
    duty: 2
    volume: 9
    repeat: true
    tempo: 10
    notes: [
        C4, E4, G4, C5, G4, E4,
        D4 15,
        rest 5,
        B3, E4, G4,
        C5 20,
        rest 10
    ]
}

// ── Gameplay constants ──────────────────────────────────────
const PLAYER_SCREEN_X: u8 = 80           // fixed camera-relative X
const GROUND_Y:        u8 = 160           // feet land here (y of head)
const JUMP_RISE:       u8 = 12            // upward frames after take-off
const JUMP_PX:         u8 = 3             // px moved each rise frame
const GRAVITY_CAP:     u8 = 4             // terminal fall speed
const ENEMY_Y:         u8 = 168           // enemies walk on the grass
const COIN_Y:          u8 = 144           // coins float a bit above

// World-space X positions of the moving entities. Wrap-around
// arithmetic (u8) makes the level feel like an endless loop.
const ENEMY1_WORLD_X:  u8 = 100
const ENEMY2_WORLD_X:  u8 = 200
const COIN1_WORLD_X:   u8 = 50
const COIN2_WORLD_X:   u8 = 150

// How many auto-jumps the autopilot will pre-queue per life. Two
// is exactly enough to stomp enemy 1 and enemy 2 once each; after
// that the autopilot stops assisting so the headless harness gets
// to see the player walk into the next enemy, die, and retry.
const AUTOPILOT_JUMPS: u8 = 2

// ── Game state ──────────────────────────────────────────────

// `frame_tick` is shared: Title reads it to auto-advance, Playing
// reads it for animation phasing. `stomp_count` bridges
// Playing → GameOver so the death screen can tally coins. The
// rest — player physics, camera, liveness, autopilot budget —
// are only meaningful while Playing is running, so they live on
// Playing's state block and overlay with the Title / GameOver
// locals (`blink`, `linger`) at the same bytes.

// Cross-state scratch
var frame_tick: u8 = 0     // free-running frame counter
var stomp_count: u8 = 0    // successful enemy stomps this life
var lives:       u8 = 3    // lives remaining; HUD heart readout

// HUD shadow-compare cache. The initial nametable already paints
// "coin 00" / "heart 3" into row 0 with the bg3 palette, so the
// shadow values here start matching the on-screen glyphs — every
// frame where `stomp_count == last_score` skips the score repaint
// entirely, and likewise for lives. 255 would also force a
// guaranteed-initial-paint fallback on the first frame if the map
// ever drifts from these defaults; we use the real starting values
// so the harness's frame-180 golden doesn't capture a transient
// paint flicker.
var last_score: u8 = 0
var last_lives: u8 = 3

// ── HUD helpers ─────────────────────────────────────────────
// Background-row HUD, pinned to the top of the viewport with a
// sprite-0 hit split instead of burning five OAM slots every
// frame. Two moving pieces:
//
//   1. The status glyphs — coin, two score digits, heart, lives
//      digit — live in NT row 0 and are repainted via `nt_set`
//      only when the backing state changes (`stomp_count`,
//      `lives`). The initial map already contains the starting
//      values so the first frame is already correct.
//
//   2. A single OAM sprite (slot 0) is drawn over the coin tile
//      every frame. The PPU sets the sprite-0 hit flag when the
//      sprite's opaque pixels overlap the coin tile's opaque
//      pixels — guaranteed to fire on some scanline inside the
//      HUD row — and a `sprite_0_split(camera_x, 0)` call picks
//      up that hit to switch the horizontal scroll from 0 to
//      `camera_x` for the rest of the frame. Row 0 stays pinned,
//      everything below it scrolls with the camera.
fun draw_hud() {
    // Sprite 0 — must be the FIRST draw of the frame so it lands
    // in OAM slot 0. The anchor tile's single opaque pixel sits
    // at row 7, col 3 of the tile; positioned at (248, 8) in OAM
    // it renders at screen (251, 16) — the first scanline of the
    // playfield, not the last scanline of the HUD. The map
    // pre-paints a dirt tile (%) at NT (col 31, row 2), whose
    // uniformly-opaque row 0 covers the same pixel, so the PPU's
    // sprite-0 hit fires at dot 251 of scanline 16. The
    // `sprite_0_split` write therefore affects the scanlines
    // *below* the HUD instead of the last HUD scanline itself,
    // which keeps rows 8-15 pixel-stable regardless of
    // `camera_x`. An earlier hit at scanline 15 (with the anchor
    // sitting on top of the visible coin in NT col 2) would
    // smear the HUD's bottom row of pixels across the split in
    // jsnes; moving the hit off the HUD row avoids the glitch
    // entirely. Column 31 / OAM x=248 are inside jsnes's
    // right-edge overscan, so the dirt anchor and sprite 0 are
    // both invisible in the committed PNG goldens.
    draw Tileset at: (248, 8) frame: TILE_SPRITE0_ANCHOR

    // Score tens / ones — shadow-compare so the VRAM ring only
    // gets an entry when `stomp_count` actually moved. The
    // comparison runs once per frame and most frames take the
    // no-op path (score only ticks on a stomp).
    if stomp_count != last_score {
        last_score = stomp_count
        nt_set(4, 1, TILE_DIGIT_0 + (stomp_count / 10))
        nt_set(5, 1, TILE_DIGIT_0 + (stomp_count % 10))
    }

    // Lives digit — same shadow-compare. Only fires on a life
    // transition (death in `GameOver.on_enter`, refill in
    // `Title.on_enter`).
    if lives != last_lives {
        last_lives = lives
        nt_set(26, 1, TILE_DIGIT_0 + lives)
    }
}

// ── Helper functions ────────────────────────────────────────

// Try to start a jump. Only valid when the player is on the
// ground (on_ground == 1); otherwise this is a no-op.
fun try_jump() {
    if on_ground == 1 {
        rise_count = JUMP_RISE
        on_ground = 0
        fall_vy = 0
        play Boing
    }
}

// Advance player physics one frame. Handles the jump-rise phase
// (upward movement while rise_count > 0) and the gravity fall
// phase (increment fall_vy each frame up to GRAVITY_CAP, clamp to
// GROUND_Y on landing).
fun step_physics() {
    if rise_count > 0 {
        // Clamp underflow so a too-high jump doesn't wrap into the
        // bottom of the screen on an 8-bit subtraction.
        if player_y >= JUMP_PX {
            player_y -= JUMP_PX
        } else {
            player_y = 0
        }
        rise_count -= 1
    } else {
        player_y += fall_vy
        if fall_vy < GRAVITY_CAP {
            fall_vy += 1
        }
        if player_y >= GROUND_Y {
            player_y = GROUND_Y
            on_ground = 1
            fall_vy = 0
        }
    }
}

// Draw the 2×2 hero metasprite at (PLAYER_SCREEN_X, player_y).
// Each `draw` statement writes a fresh OAM slot via the runtime
// cursor, so four calls produce four sprites occupying a 16×16
// block.
fun draw_player() {
    draw Tileset at: (PLAYER_SCREEN_X,     player_y    ) frame: TILE_PLAYER_HL
    draw Tileset at: (PLAYER_SCREEN_X + 8, player_y    ) frame: TILE_PLAYER_HR
    draw Tileset at: (PLAYER_SCREEN_X,     player_y + 8) frame: TILE_PLAYER_BL
    draw Tileset at: (PLAYER_SCREEN_X + 8, player_y + 8) frame: TILE_PLAYER_BR
}

// Resolve contact with an enemy whose screen X is `e_sx`. Mutates
// the player's physics / alive flag directly and returns 1 on a
// successful stomp, 0 on no contact or a fatal hit. The caller
// checks `alive` after invocation to decide whether to keep
// running the frame.
//
// The player's 16×16 hitbox is at ([80, 96), [player_y, player_y+16)).
// Each enemy's 8×8 hitbox is at ([e_sx, e_sx+8), [168, 176)). Those
// overlap horizontally when e_sx ∈ (72, 96) and overlap vertically
// when player_y ∈ (152, 176).
//
// Outcomes:
//   - No overlap                                  → no-op
//   - Overlap while rising (rise_count > 0)       → graceful
//     pass-through. rise_count > 0 only happens right after a
//     successful stomp bounce (try_jump() clears rise_count by
//     the end of the same frame) or on the stomp's own upward
//     leg, which we don't want to retrigger.
//   - Overlap while falling, feet near enemy head → STOMP: bounce
//     up, increment `stomp_count`, play Boing
//   - Any other overlap (walking, on ground)      → DEATH: set
//     `alive = 0` and play the builtin `hit` sfx
fun resolve_enemy_hit(e_sx: u8) -> u8 {
    if e_sx > 72 {
        if e_sx < 96 {
            if player_y > 152 {
                if player_y < 176 {
                    // Real contact. Stomp if falling; otherwise, if
                    // we're not in the post-bounce grace window,
                    // die.
                    if fall_vy > 0 {
                        rise_count = 6
                        fall_vy = 0
                        stomp_count += 1
                        play Boing
                        return 1
                    }
                    if rise_count == 0 {
                        alive = 0
                        play hit
                    }
                }
            }
        }
    }
    return 0
}

// ── States ──────────────────────────────────────────────────

state Title {
    var blink: u8 = 0

    on enter {
        blink = 0
        frame_tick = 0
        // Fresh run — restore the full life bar so the HUD reads 3
        // when Playing kicks in and the GameOver loop doesn't trap
        // the game in a zero-lives state forever.
        lives = 3
        stomp_count = 0
        start_music Theme
    }

    on frame {
        frame_tick += 1
        blink += 1
        if blink >= 60 {
            blink = 0
        }

        // Blink a "press start" marker at roughly 0.5 Hz. We use
        // the coin tile for visual continuity with the game world.
        if blink < 30 {
            draw Tileset at: (120, 120) frame: TILE_COIN
        }

        // Auto-advance so the jsnes golden harness (which never
        // presses start) still reaches Playing and captures gameplay
        // at frame 180. A human player can also press Start to skip
        // the wait early.
        if frame_tick >= 20 {
            transition Playing
        }
        if button.start {
            transition Playing
        }
    }
}

state Playing {
    // Physics, camera, liveness, and autopilot budget — all of
    // this is Playing-only. Declaring them inside the state block
    // lets the analyzer overlay them with Title.blink and
    // GameOver.linger; each variable's initializer re-runs on
    // entry, so the retry loop starts each life on the ground
    // with a fresh autopilot budget without any manual reset.
    var player_y:   u8 = GROUND_Y
    var on_ground:  u8 = 1
    var rise_count: u8 = 0
    var fall_vy:    u8 = 0
    var camera_x:   u8 = 0
    var anim_tick:  u8 = 0
    var alive:      u8 = 1
    var auto_jumps: u8 = 0

    on enter {
        frame_tick = 0
        stomp_count = 0
        start_music Theme
    }

    on frame {
        frame_tick += 1
        anim_tick += 1

        // ── Input ─────────────────────────────────────────────
        // Human controls — these work even while the autopilot
        // is running so you can stomp the demo at any time.
        if button.a {
            try_jump()
        }
        if button.right {
            camera_x += 1     // walk forward by scrolling right
        }
        if button.left {
            // Walk back (wraps the camera — the level is cyclic).
            camera_x -= 1
        }

        // Always advance camera one px per frame (baseline scroll).
        camera_x += 1

        // ── Compute screen positions of world entities ──────
        var e1_sx: u8 = ENEMY1_WORLD_X - camera_x
        var e2_sx: u8 = ENEMY2_WORLD_X - camera_x
        var c1_sx: u8 = COIN1_WORLD_X - camera_x
        var c2_sx: u8 = COIN2_WORLD_X - camera_x

        // ── Proximity autopilot ─────────────────────────────
        // Pre-jump when an enemy is 19 px ahead. The fall phase of
        // a JUMP_RISE=12, GRAVITY_CAP=4 jump lands the player's
        // feet at enemy head height ~20 frames later, by which
        // point the autopilot camera has scrolled the enemy under
        // the player. Limited to AUTOPILOT_JUMPS hops per life so
        // the third enemy encounter becomes a scripted death,
        // giving the golden harness a visible game-over sequence.
        if auto_jumps < AUTOPILOT_JUMPS {
            if on_ground == 1 {
                if e1_sx == 99 {
                    try_jump()
                    auto_jumps += 1
                }
                if e2_sx == 99 {
                    try_jump()
                    auto_jumps += 1
                }
            }
        }

        // ── Physics ──────────────────────────────────────────
        step_physics()

        // ── Coin pickups ─────────────────────────────────────
        // Coin collect: player occupies [80..96). A coin is "hit"
        // when its screen X is in [72..96) (8-px tolerance on each
        // side of the player's left edge).
        if c1_sx >= 72 and c1_sx < 96 {
            if player_y <= COIN_Y + 16 {
                play Ding
            }
        }
        if c2_sx >= 72 and c2_sx < 96 {
            if player_y <= COIN_Y + 16 {
                play Ding
            }
        }

        // ── Enemy collisions ────────────────────────────────
        // Both enemies get the full stomp-or-die check; the helper
        // mutates player state directly. If `alive` flips to 0
        // we fall through the rest of the frame (drawing is
        // guarded below) and transition to GameOver at the end.
        resolve_enemy_hit(e1_sx)
        resolve_enemy_hit(e2_sx)

        // ── Drawing ──────────────────────────────────────────
        // `draw_hud()` runs FIRST so its coin sprite lands in OAM
        // slot 0 — that's what makes the PPU's sprite-0 hit flag
        // fire on the coin-tile overlap inside the HUD row, which
        // we pick up with `sprite_0_split` at the end of the
        // frame. Every other `draw` has to come after.
        draw_hud()

        // Enemies and coins are always drawn so the scene stays
        // coherent even on the fatal-contact frame.
        var ey: u8 = ENEMY_Y
        if (anim_tick & 16) != 0 {
            ey = ENEMY_Y - 2
        }
        draw Tileset at: (e1_sx, ey)       frame: TILE_ENEMY
        draw Tileset at: (e2_sx, ENEMY_Y)  frame: TILE_ENEMY

        var cy: u8 = COIN_Y
        if (anim_tick & 8) != 0 {
            cy = COIN_Y - 1
        }
        draw Tileset at: (c1_sx, cy)       frame: TILE_COIN
        draw Tileset at: (c2_sx, COIN_Y)   frame: TILE_COIN

        // Player is only drawn while alive — on the dying frame
        // we show the scene without the hero on top of the enemy
        // that killed them, which reads as "player got got".
        if alive == 1 {
            draw_player()
        }

        // ── Sprite-0 scroll split ────────────────────────────
        // Explicitly re-latch scroll to (0, 0) before the hit-
        // wait so jsnes (whose PPU is scanline-quantised and
        // will happily apply the previous frame's mid-frame
        // `$2005` writes to the next frame's scanline 15) starts
        // every scanline below vblank from a known-zero scroll.
        // Then `sprite_0_split` busy-waits for the PPU's sprite-0
        // hit flag — fired at dot 251 of scanline 15 by the
        // overscan coin anchor — and latches `(camera_x, 0)` for
        // the remainder of the frame. Scanlines 0-15 render the
        // HUD pinned; scanlines 16+ render the playfield
        // scrolled.
        scroll(0, 0)
        sprite_0_split(camera_x, 0)

        // Fatal contact this frame → kick the state machine over
        // to GameOver on the next frame.
        if alive == 0 {
            transition GameOver
        }
    }
}

state GameOver {
    var linger: u8 = 0

    on enter {
        linger = 0
        // Burn a life on entry so the HUD's heart counter ticks
        // down visibly each death. Guard against underflow in case
        // something transitions here with lives already at zero.
        if lives > 0 {
            lives -= 1
        }
        stop_music
    }

    on frame {
        linger += 1

        // Sync the HUD row first (sprite 0 + shadow-compare
        // `nt_set` for the digits) so the tens/ones glyphs tick
        // down when `lives` decrements in `on_enter`. No
        // `sprite_0_split` call here — GameOver doesn't scroll,
        // so NMI's scroll-reset keeps the whole frame at
        // `(0, 0)` and the HUD row renders normally. The sprite-
        // 0 hit flag fires harmlessly and stays latched until the
        // next Playing frame clears it.
        draw_hud()

        // "GAME OVER" marker: a row of enemy tiles across the
        // middle of the screen. The death-screen backdrop is
        // whatever Playing last scrolled to — we don't clear it.
        draw Tileset at: (96,  112) frame: TILE_ENEMY
        draw Tileset at: (112, 112) frame: TILE_ENEMY
        draw Tileset at: (128, 112) frame: TILE_ENEMY
        draw Tileset at: (144, 112) frame: TILE_ENEMY

        // Auto-retry after ~1 s so the headless harness sees the
        // full loop. When the last life is spent, bounce back to
        // the Title screen instead (Title's `on enter` refills the
        // heart bar for the next run). A human can hit Start to
        // skip the wait.
        if linger >= 60 {
            if lives == 0 {
                transition Title
            } else {
                transition Playing
            }
        }
        if button.start {
            if lives == 0 {
                transition Title
            } else {
                transition Playing
            }
        }
    }
}

start Title
