mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 17:06:04 +00:00
506 lines
22 KiB
Text
506 lines
22 KiB
Text
|
|
// 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, collectible coins, a user-declared
|
|||
|
|
// SFX envelope, 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 an
|
|||
|
|
// interesting frame-180 composition.
|
|||
|
|
//
|
|||
|
|
// 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 ──────────────────────────────────────────────────
|
|||
|
|
// The background palette drives the sky / hill / ground look.
|
|||
|
|
// Each sub-palette reserves index 0 for the shared background
|
|||
|
|
// colour (sky blue $22), plus three working colours.
|
|||
|
|
// bg 0 (sky region): sky blue, white, light gray, black
|
|||
|
|
// bg 1 (air/blocks row): sky blue, red, peach, dark red
|
|||
|
|
// bg 2 (ground row): sky blue, lime green, dark green, dark brown
|
|||
|
|
// bg 3 (unused reserve): sky blue, yellow, orange, gold
|
|||
|
|
// The sprite sub-palette 0 (used by every `draw` statement — the
|
|||
|
|
// OAM attribute byte is always 0 in the current NEScript
|
|||
|
|
// runtime) carries the hero's red/peach/white body colours.
|
|||
|
|
// NOTE: the first byte of every sub-palette must match. The PPU
|
|||
|
|
// mirrors $3F10/$3F14/$3F18/$3F1C onto $3F00/$3F04/$3F08/$3F0C, so
|
|||
|
|
// writing 8 distinct "index 0" bytes ends up clobbering the
|
|||
|
|
// background universal colour with the last write. The canonical
|
|||
|
|
// fix — and what every NES game does — is to use the universal
|
|||
|
|
// background colour ($22 sky blue here) in all 8 slots. For sprite
|
|||
|
|
// palettes, index 0 is always transparent regardless of the stored
|
|||
|
|
// value, so this costs nothing visually.
|
|||
|
|
palette Main {
|
|||
|
|
colors: [
|
|||
|
|
// bg palette 0 — sky / clouds
|
|||
|
|
// 0 = sky blue (universal bg)
|
|||
|
|
// 1 = white
|
|||
|
|
// 2 = light gray
|
|||
|
|
// 3 = black
|
|||
|
|
0x22, 0x30, 0x10, 0x0F,
|
|||
|
|
// bg palette 1 — bricks, Q-blocks
|
|||
|
|
// 1 = red, 2 = peach, 3 = dark red
|
|||
|
|
0x22, 0x16, 0x27, 0x06,
|
|||
|
|
// bg palette 2 — grass, hills, bushes, dirt
|
|||
|
|
// 1 = light green
|
|||
|
|
// 2 = light brown
|
|||
|
|
// 3 = dark brown
|
|||
|
|
0x22, 0x1A, 0x17, 0x07,
|
|||
|
|
// bg palette 3 — unused but the mirroring still writes here
|
|||
|
|
0x22, 0x28, 0x27, 0x17,
|
|||
|
|
// sprite palette 0 — hero / enemy / coin
|
|||
|
|
// (OAM attribute is always 0 in the current runtime so
|
|||
|
|
// every sprite uses this one sub-palette)
|
|||
|
|
// 1 = red cap, 2 = peach skin, 3 = white
|
|||
|
|
0x22, 0x16, 0x27, 0x30,
|
|||
|
|
// sprite palette 1 — reserved
|
|||
|
|
0x22, 0x07, 0x17, 0x27,
|
|||
|
|
// sprite palette 2 — reserved
|
|||
|
|
0x22, 0x2A, 0x1A, 0x30,
|
|||
|
|
// sprite palette 3 — reserved
|
|||
|
|
0x22, 0x28, 0x18, 0x38
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 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.
|
|||
|
|
sprite Tileset {
|
|||
|
|
chr: [
|
|||
|
|
// tile 1: Player head L
|
|||
|
|
0x3F, 0x7F, 0x70, 0x61, 0xCF, 0xCD, 0xC0, 0xC0, 0x00, 0x00, 0x0F, 0x1F, 0x3F, 0x3F, 0x3F, 0x3F,
|
|||
|
|
// tile 2: Player head R
|
|||
|
|
0xFC, 0xFE, 0x0E, 0x83, 0xF8, 0x4C, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF,
|
|||
|
|
// tile 3: Player body L
|
|||
|
|
0x7F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x10, 0x18, 0x00, 0x7F, 0x7F, 0x67, 0x07,
|
|||
|
|
// tile 4: Player body R
|
|||
|
|
0xFE, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x07, 0x00, 0x08, 0x18, 0x00, 0xFE, 0xFE, 0xE6, 0xE0,
|
|||
|
|
// tile 5: Enemy
|
|||
|
|
0x3C, 0x7E, 0x76, 0xFF, 0x99, 0x7E, 0xBD, 0x99, 0x00, 0x00, 0x18, 0x38, 0x7E, 0x00, 0x00, 0x00,
|
|||
|
|
// tile 6: Coin
|
|||
|
|
0x00, 0x00, 0x18, 0x24, 0x24, 0x18, 0x00, 0x00, 0x18, 0x3C, 0x7E, 0x7E, 0x7E, 0x7E, 0x3C, 0x18,
|
|||
|
|
// tile 7: Grass top
|
|||
|
|
0xFF, 0xBB, 0xFF, 0xFF, 0xFF, 0xFF, 0xBD, 0xFF, 0x00, 0x00, 0x00, 0x6A, 0xF7, 0xFF, 0xFF, 0xFF,
|
|||
|
|
// tile 8: Dirt
|
|||
|
|
0xFF, 0xBD, 0xFF, 0xF7, 0xFF, 0xBD, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|||
|
|
// tile 9: Brick
|
|||
|
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x82, 0x82, 0x82, 0xFF, 0x10, 0x10, 0x10,
|
|||
|
|
// tile 10: Cloud L
|
|||
|
|
0x00, 0x1C, 0x3E, 0x7F, 0x7F, 0x1F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x3E, 0x07,
|
|||
|
|
// tile 11: Cloud R
|
|||
|
|
0x00, 0x70, 0xF8, 0xFE, 0xFE, 0xF8, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x7C, 0xE0,
|
|||
|
|
// tile 12: Hill
|
|||
|
|
0x00, 0x0C, 0x1E, 0x3F, 0x2F, 0x4B, 0x75, 0xC9, 0x00, 0x00, 0x00, 0x00, 0x10, 0x34, 0x0A, 0x36,
|
|||
|
|
// tile 13: Bush
|
|||
|
|
0x00, 0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x02, 0x06, 0x06, 0x55,
|
|||
|
|
// tile 14: Q Block
|
|||
|
|
0xFF, 0xFF, 0xC3, 0xDB, 0xD3, 0xC3, 0xFF, 0xFF, 0xFF, 0x81, 0xBD, 0xBD, 0xBD, 0xBD, 0x81, 0xFF,
|
|||
|
|
// tile 15: Sky (blank)
|
|||
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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
|
|||
|
|
|
|||
|
|
// ── Background ──────────────────────────────────────────────
|
|||
|
|
// Full-screen 32×30 nametable showing a static level view that
|
|||
|
|
// horizontal scrolling pans across. The attribute table splits
|
|||
|
|
// the screen into three palette regions (sky/blocks/ground) so
|
|||
|
|
// the grass and hills pick up the right colours even without
|
|||
|
|
// per-tile attribute churn.
|
|||
|
|
background Level {
|
|||
|
|
tiles: [
|
|||
|
|
// rows 0-4: clean sky
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
// row 5: clouds at x=4..5 and x=20..21
|
|||
|
|
15, 15, 15, 15, 10, 11, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 10, 11, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
// row 6: cloud at x=12..13
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 10, 11, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
// rows 7-14: more clean sky
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
// row 15: brick/Q-block platform left, brick platform right
|
|||
|
|
15, 15, 15, 15, 15, 15, 9, 9, 14, 9, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 9, 9, 9, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
// rows 16-19: sky
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
// row 20: hills (3 on left, 4 on right)
|
|||
|
|
15, 15, 12, 12, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 12, 12, 12, 12, 15, 15, 15, 15, 15, 15,
|
|||
|
|
// row 21: bushes above the grass line
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 15, 15, 15,
|
|||
|
|
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 15, 15, 15,
|
|||
|
|
// row 22: grass top (full width)
|
|||
|
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
|||
|
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
|||
|
|
// rows 23-29: dirt with a few buried bricks for texture
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 8,
|
|||
|
|
8, 8, 8, 8, 8, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|||
|
|
8, 8, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 8, 8, 8, 8,
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 9, 8, 8, 8, 8, 8, 8, 8,
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|||
|
|
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8
|
|||
|
|
]
|
|||
|
|
attributes: [
|
|||
|
|
// 8×8 attribute grid, one byte per 32×32-px metatile group.
|
|||
|
|
// Each byte packs 4 identical 2-bit quadrants selecting a
|
|||
|
|
// sub-palette (0..3):
|
|||
|
|
// 0x00 = sub-palette 0 (sky / clouds)
|
|||
|
|
// 0x55 = sub-palette 1 (bricks / Q-blocks)
|
|||
|
|
// 0xAA = sub-palette 2 (grass / hills / dirt)
|
|||
|
|
//
|
|||
|
|
// Row mapping: ay 0-2 (nt rows 0-11) = sky, ay 3 (nt rows
|
|||
|
|
// 12-15) = brick row, ay 4 (nt rows 16-19) = more sky, ay
|
|||
|
|
// 5-7 (nt rows 20-29) = ground.
|
|||
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|||
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|||
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|||
|
|
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
|||
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|||
|
|
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
|||
|
|
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
|||
|
|
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Audio ────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
// Custom boingy jump sound — descending pitch arc on pulse 1.
|
|||
|
|
sfx Boing {
|
|||
|
|
duty: 2
|
|||
|
|
pitch: [0x30, 0x38, 0x40, 0x48, 0x50, 0x58, 0x60]
|
|||
|
|
volume: [12, 11, 10, 9, 7, 5, 2]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Custom coin ding — short rising blip.
|
|||
|
|
sfx Ding {
|
|||
|
|
duty: 2
|
|||
|
|
pitch: [0x20, 0x20, 0x20, 0x20]
|
|||
|
|
volume: [14, 12, 8, 3]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Custom looping underworld theme — playful four-bar loop on
|
|||
|
|
// pulse 2 that plays while the player is alive.
|
|||
|
|
music Theme {
|
|||
|
|
duty: 2
|
|||
|
|
volume: 9
|
|||
|
|
repeat: true
|
|||
|
|
notes: [
|
|||
|
|
37, 10, // C4
|
|||
|
|
41, 10, // E4
|
|||
|
|
44, 10, // G4
|
|||
|
|
49, 10, // C5
|
|||
|
|
44, 10, // G4
|
|||
|
|
41, 10, // E4
|
|||
|
|
39, 15, // D4
|
|||
|
|
0, 5, // rest
|
|||
|
|
36, 10, // B3
|
|||
|
|
41, 10, // E4
|
|||
|
|
44, 10, // G4
|
|||
|
|
49, 20, // C5
|
|||
|
|
0, 10 // rest
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 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
|
|||
|
|
|
|||
|
|
// ── Game state ──────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
// Player physics
|
|||
|
|
var player_y: u8 = 160
|
|||
|
|
var on_ground: u8 = 1
|
|||
|
|
var rise_count: u8 = 0 // frames of upward motion remaining
|
|||
|
|
var fall_vy: u8 = 0 // gravity accumulator
|
|||
|
|
|
|||
|
|
// World/camera
|
|||
|
|
var camera_x: u8 = 0
|
|||
|
|
var frame_tick: u8 = 0 // free-running frame counter
|
|||
|
|
var jump_timer: u8 = 0 // autopilot: jump every N frames
|
|||
|
|
var anim_tick: u8 = 0 // visual animation phase
|
|||
|
|
|
|||
|
|
// ── 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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── States ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
state Title {
|
|||
|
|
var blink: u8 = 0
|
|||
|
|
|
|||
|
|
on enter {
|
|||
|
|
blink = 0
|
|||
|
|
frame_tick = 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 {
|
|||
|
|
on enter {
|
|||
|
|
player_y = GROUND_Y
|
|||
|
|
on_ground = 1
|
|||
|
|
rise_count = 0
|
|||
|
|
fall_vy = 0
|
|||
|
|
camera_x = 0
|
|||
|
|
frame_tick = 0
|
|||
|
|
jump_timer = 0
|
|||
|
|
anim_tick = 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Autopilot ────────────────────────────────────────
|
|||
|
|
// The headless golden harness never presses buttons, so
|
|||
|
|
// we advance the camera and periodically hop all by
|
|||
|
|
// ourselves. A human holding right still accelerates
|
|||
|
|
// forward — the two effects compose.
|
|||
|
|
camera_x += 1
|
|||
|
|
jump_timer += 1
|
|||
|
|
if jump_timer >= 40 {
|
|||
|
|
jump_timer = 0
|
|||
|
|
try_jump()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Physics ──────────────────────────────────────────
|
|||
|
|
step_physics()
|
|||
|
|
|
|||
|
|
// ── Collisions ───────────────────────────────────────
|
|||
|
|
// Coin pickup when the player's screen X is within ±8 of
|
|||
|
|
// a coin's screen position and the coin's Y range overlaps
|
|||
|
|
// the player. Uses u8 subtraction so "screen X" of a
|
|||
|
|
// world entity is just (world_x - camera_x).
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
// 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 stomp: if we're above an enemy horizontally *and*
|
|||
|
|
// we're mid-fall (fall_vy > 0), count the stomp and
|
|||
|
|
// bounce. Otherwise just ignore — the enemy slides past.
|
|||
|
|
if e1_sx >= 72 and e1_sx < 96 {
|
|||
|
|
if fall_vy > 0 {
|
|||
|
|
if player_y + 16 >= ENEMY_Y {
|
|||
|
|
play hit
|
|||
|
|
rise_count = 6
|
|||
|
|
fall_vy = 0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Drawing ──────────────────────────────────────────
|
|||
|
|
draw_player()
|
|||
|
|
|
|||
|
|
// Enemies — walking on the grass line. anim_tick drives
|
|||
|
|
// a small vertical bob so they visibly move on the golden.
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
// Coins — alternate two vertical positions to fake a
|
|||
|
|
// spin via position change (OAM attr flip isn't wired up
|
|||
|
|
// in the current runtime).
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
// ── PPU scroll latch ─────────────────────────────────
|
|||
|
|
// Write the scroll at the very end of the frame so it's
|
|||
|
|
// the last $2005 pair before the implicit wait_frame /
|
|||
|
|
// NMI handshake, minimizing mid-frame artifacts.
|
|||
|
|
scroll(camera_x, 0)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
start Title
|