/// SUPER BREAD PAN // SUPERPANSKATOL' // Based on Garydos'es Pong example import nes_joy segment(chrrom) const array graphics @ $0000 = file("tileset.chr") // ============================================================================ // STRUCTS // ============================================================================ // sprite layout in oam struct Sprite { byte y, byte tile, byte attrs, byte x } // abstraction for 16x16 animated monstrosities // ============================================================================ // VARIABLES // ============================================================================ byte i byte score1 array oam_buffer [256] @$200 // sprite buffer word framecounter Entity sam @$204 // player character Entity baby @$250 // baby character volatile Gamestate gamestate // the current Gamestate // ============================================================================ // CONSTANTS // ============================================================================ const array fg_pallete = [ $22,$1C,$15,$14, $0F,$18,$28,$0F, // sam $0F,$28,$29,$0F, // enemies $03,$07,$05,$03 // angry owo ] enum Gamestate { STATETITLE, STATEPLAYING, STATEGAMEOVER } const array scorebackground = "P1 Score- " ascii const array gameover_msg = "G A M E O V E R" ascii const array title_msg = "Press Start" ascii // ------------------------------------- // LOCATIONS // ------------------------------------- // vram locations declared as constants for readability/convenience // *note that these do not correlate to CPU ram locations* const word ppu_pallete_ram = $3F00 const word ppu_nametable_ram = $2000 const word ppu_nametable_0_attr_ram = $23C0 // ------------------------------------- // LEVEL // ------------------------------------- const byte RIGHTWALL = $F4 const byte TOPWALL = $18 const byte BOTTOMWALL = $B0 const byte LEFTWALL = $04 // ------------------------------------- // ATTRIBUTES // ------------------------------------- // i don't actually remember if the super crate box guy is named sam or not // or if he even has a name? const byte SAM_ATTR = %00000001 const byte SAM_ATTR_HFLIP = %01000001 const byte SAM_ATTR_VFLIP = %10000001 const byte SAM_ATTR_HVFLIP = %11000001 const byte BAD_ATTR = %00000010 const byte BAD_ATTR_HFLIP = %01000010 const byte BAD_ATTR_VFLIP = %10000010 const byte BAD_ATTR_HVFLIP = %11000010 const byte MAD_ATTR = %00000011 const byte MAD_ATTR_HFLIP = %01000011 const byte MAD_ATTR_VFLIP = %10000011 const byte MAD_ATTR_HVFLIP = %11000011 // ------------------------------------- // PHYSICS? // ------------------------------------- const byte SAM_WALKSPEED = 2 const byte BABY_WALKSPEED = SAM_WALKSPEED / 2 const byte FALLSPEED = 4 // ------------------------------------- // ANIMATIONS // ------------------------------------- // all Entity (16x16)'s animations are assumed by update_entity() to consist of // 5 frames. An animation array just contains 20 locations in CHR, four per // frame, in this totally logical order: // top-left, bottom-left, top-right, bottom-right const array SAM_IDLE = [ $01, $11, $02, $12, $03, $13, $04, $14, $05, $15, $06, $16, $07, $17, $08, $18, $09, $19, $0A, $1A ] const array SAM_WALK = [ $21, $31, $22, $32, $23, $33, $24, $34, $25, $35, $26, $36, $27, $37, $28, $38, $29, $39, $2A, $3A ] const array BABY_WALK = [ $00, $C0, $00, $00, $00, $C1, $00, $C2, $00, $C3, $00, $00, $00, $C4, $00, $00, $00, $C5, $00, $00 ] // ============================================================================ // CORE // ============================================================================ // quite important uwu void main() { sam.dircount = 0 gamestate = STATETITLE title_init() while(true){} // all work is done in nmi // thnx nmi <3 } // run at each non-maskable interrupt // generated during each vertical blanking interval. cool void nmi() { // push all sprite info to the ppu (picture processing unit) // through dma (direct memory access) transfer ppu_oam_dma_write( oam_buffer.addr.hi ) main_game_logic() } // run at each interrupt request // do I know when these are called on the nes? absolutely not // at reset? maybe? void irq() { } // pretty self-explanatory inline void main_game_logic() { // use a return dispatch here // to use different logic for each screen/gamestate return [gamestate] { STATETITLE @ title_logic STATEPLAYING @ ingame_logic STATEGAMEOVER @ gameover_logic } } // uwu owo uwu owo uwu owo inline void init_graphics() { init_sprites() load_fg_pallete( fg_pallete ) } // cleans up oam (object attribute memory) // each sprite gets 4 bytes in oam: // 1: ypos, 2: tile index, 3: attr table, 4: xpos void init_sprites() { byte i for i,0,to,255 { if (i & %00000011) == 0 { //each sprite takes up 4 bytes, and we want to edit //the y position of each sprite (0th byte) //so we use the %00000011 mask to write every 4th byte //(every 0th sprite byte) oam_buffer[i] = $ef // move the sprite off screen } else { oam_buffer[i] = 0 } } } // ============================================================================ // TITLE-SCREEN // ============================================================================ void title_init() { byte i //for now, turn off the screen and nmi ppu_ctrl = 0 ppu_mask = 0 //initialize the sprites and palletes init_graphics() //write a full screen of background data //write the title screen message read_ppu_status() // read PPU status to reset the high/low latch ppu_set_addr(ppu_nametable_ram+$018B) // point the PPU to the message's start for i,0,until,$0B { ppu_write_data(title_msg[i]) } //write the border //top border read_ppu_status() // read PPU status to reset the high/low latch ppu_set_addr(ppu_nametable_ram+$0020) for i,0,until,$20 { ppu_write_data($01) } //bottom border read_ppu_status() // read PPU status to reset the high/low latch ppu_set_addr(ppu_nametable_ram+$0380) for i,0,until,$20 { ppu_write_data($01) } //set ppu address increment to 32 so we can draw the left and right borders //(allows us to draw to the nametable in vertical strips rather than horizontal) ppu_ctrl = %00000100 //left border read_ppu_status() // read PPU status to reset the high/low latch ppu_set_addr(ppu_nametable_ram) for i,0,until,$20 { ppu_write_data($01) } //right border read_ppu_status() // read PPU status to reset the high/low latch ppu_set_addr(ppu_nametable_ram+$1F) for i,0,until,$20 { ppu_write_data($01) } framecounter = 0 ppu_set_scroll(0,0) ppu_wait_vblank() //wait for next vblank before re-enabling NMI //so that we don't get messed up scroll registers //re-enable the screen and nmi ppu_ctrl = %10010000 // enable NMI, sprites from Pattern Table 0, background from Pattern Table 1 ppu_mask = %00011110 // enable sprites, enable background, no clipping on left side } void title_logic() { read_joy1() if input_start != 0 { gamestate = STATEPLAYING ingame_init() return } framecounter += 1 } // ============================================================================ // IN-GAME // ============================================================================ void ingame_init() { //for now, turn off the screen and nmi ppu_ctrl = 0 ppu_mask = 0 //write a full screen of data init_map1_c() // draw_score_text_background() ppu_set_scroll(0,0) ppu_wait_vblank() //wait for next vblank before re-enabling NMI //so that we don't get messed up scroll registers //re-enable the screen and nmi ppu_ctrl = %10010000 // enable NMI, sprites from Pattern Table 0, background from Pattern Table 1 ppu_mask = %00011110 // enable sprites, enable background, no clipping on left side sam.top0.y = 40 sam.top0.x = 50 sam.walkspeed = SAM_WALKSPEED baby.top0.y = 40 baby.top0.x = 100 baby.walkspeed = BABY_WALKSPEED } void ingame_logic() { // draw_score() //update scroll last because writes to vram also //overwrite the scroll register ppu_set_scroll(0,0) // tell the ppu there is no background scrolling ingame_input() update_ingame_sprites() if score1 >= 15 { //Someone's reached 15 points, the game is over, //so set the state to game over and reset the //framecounter gamestate = STATEGAMEOVER //move all the sprites off screen //in preperation for the gameover screen sam.top0.x = $ef gameover_init() } } void ingame_input ( ) { // Player 1 controls read_joy1() sam.movement = 0 // left if ( input_dx < 0 ) { sam.direction = 0 sam.movement = 1 } // right else if ( input_dx > 0 ) { sam.direction = 1 sam.movement = 1 } // a button if ( input_btn == 1 && sam.jump == 0 ) { sam.jump = 1 sam.dircount = 0 } } void update_ingame_sprites ( ) { // player. sam is the player. update_entity( pointer.Entity( sam.addr ), SAM_IDLE, SAM_WALK ) update_entity( pointer.Entity( baby.addr ), BABY_WALK, BABY_WALK ) } // ============================================================================ // GAME-OVER // ============================================================================ void gameover_init() { //for now, turn off nmi and sprites ppu_ctrl = 0 ppu_mask = 0 draw_score() //draw the final score draw_gameover() //draw the game over message framecounter = 0 ppu_set_scroll(0,0) // tell the ppu there is no background scrolling ppu_wait_vblank() //wait for next vblank before re-enabling NMI //so that we don't get messed up scroll registers //re-enable the screen and nmi ppu_ctrl = %10010000 // enable NMI, sprites from Pattern Table 0, background from Pattern Table 1 ppu_mask = %00011110 // enable sprites, enable background, no clipping on left side } void gameover_logic() { if framecounter >= 240{ //3 seconds have passed, //reset the game simulate_reset() } framecounter += 1 } void draw_gameover() { byte i //draw the static game over message read_ppu_status() // read PPU status to reset the high/low latch ppu_set_addr(ppu_nametable_ram+$0107) // point the PPU to the message's start for i,0,until,$12 { ppu_write_data(gameover_msg[i]) } //draw the win message read_ppu_status() // read PPU status to reset the high/low latch ppu_set_addr(ppu_nametable_ram+$01AC) // point the PPU to the message's start } // ============================================================================ // MISC GRAPHICS // ============================================================================ // ------------------------------------- // BACKGROUND // ------------------------------------- inline void blank_bg() { word xx read_ppu_status() // read PPU status to reset the high/low latch ppu_set_addr(ppu_nametable_ram) // point the PPU to palette ram for xx,0,until,$0300 { ppu_write_data($00) } } // ------------------------------------- // PALLETE MANAGEMENT // ------------------------------------- // loads colours into the section of memory dedicated to background (sprite) colours. // useful resource: https://wiki.nesdev.com/w/index.php/PPU_palettes // (edits $3F00 to $3F0F) void load_bg_pallete ( pointer pallete ) { byte i ppu_set_addr( ppu_pallete_ram ) for i,0,to,$F { ppu_write_data( pallete[i] ) } } // loads colours into the section of memory dedicated to foreground (sprite) colours. // unrelated, but heck do I feel like I'm spelling pallete wrong. doesn't it have 'double-t' vibes? // like palette? or maybe pallette? hmm. // (edits $3F10 to $3F1F) void load_fg_pallete ( pointer pallete ) { byte i ppu_set_addr( ppu_pallete_ram + $10 ) for i,0,to,$F { ppu_write_data( pallete[i] ) } } // load pallete selection for bg tiles to ppu // useful resource: https://wiki.nesdev.com/w/index.php/PPU_palettes macro void load_attr_table( pointer attributes ) { byte i read_ppu_status() // read PPU status to reset the high/low latch ppu_set_addr(ppu_nametable_0_attr_ram) // point the PPU to nametable 0's attribute table for i,0,to,64 { ppu_write_data(attributes[i]) } } macro void draw_score_text_background() { byte i read_ppu_status() // read PPU status to reset the high/low latch ppu_set_addr(ppu_nametable_ram+$20) // point the PPU to score text's start for i,0,until,$1C { ppu_write_data(scorebackground[i]) } } inline void draw_score() { byte digit01 byte digit10 read_ppu_status() // read PPU status to reset the high/low latch //display player1's score digit01 = score1 %% 10 //get the ones digit digit10 = score1 / 10 //get the tens digit digit10 %%= 10 ppu_set_addr(ppu_nametable_ram+$29) // point the PPU to player1's score number if digit10 > 0 { ppu_write_data(digit10 + '0') } ppu_write_data(digit01 + '0') } // ============================================================================ // ENTITIES // ============================================================================ struct Entity { Sprite top0, // left side, for the record Sprite bottom0, Sprite top1, // right side, please kill me Sprite bottom1, // all this can be crammed into a byte (if merge dircount w frame) `o` // note to self: do that byte movement, // 0-3; idle, walk, jump/fall byte jump, // 0/1 byte direction, // 0/1 byte dircount, // counter for time-limited movement; i.e., jumping byte frame, // internal count used for animation; max 5 byte walkspeed } void update_entity ( pointer.Entity ent, pointer idle, pointer walk ) { entity_physics( ent ) if ( ent[0].bottom0.tile > $39 ) { entity_destiny ( ent ) } entity_sprite( ent, ent[0].top0.x, ent[0].top0.y, idle, walk ) } // sets the current frame of ent's animation and determines which animation to use void entity_sprite ( pointer.Entity ent, byte xx, byte yy, pointer idle, pointer walk ) { if ( ent[0].direction == 0 ) { if ( ent[0].bottom0.tile < $3A ) { // if player ent[0].top0.attrs = SAM_ATTR_HFLIP ent[0].bottom0.attrs = SAM_ATTR_HFLIP ent[0].top1.attrs = SAM_ATTR_HFLIP ent[0].bottom1.attrs = SAM_ATTR_HFLIP } else { ent[0].top0.attrs = BAD_ATTR_HFLIP ent[0].bottom0.attrs = BAD_ATTR_HFLIP ent[0].top1.attrs = BAD_ATTR_HFLIP ent[0].bottom1.attrs = BAD_ATTR_HFLIP } ent[0].top1.x = xx - 8 ent[0].bottom1.x = xx - 8 } else { if ( ent[0].bottom0.tile < $3A ) { // if player ent[0].top0.attrs = SAM_ATTR ent[0].bottom0.attrs = SAM_ATTR ent[0].top1.attrs = SAM_ATTR ent[0].bottom1.attrs = SAM_ATTR } else { ent[0].top0.attrs = BAD_ATTR ent[0].bottom0.attrs = BAD_ATTR ent[0].top1.attrs = BAD_ATTR ent[0].bottom1.attrs = BAD_ATTR } ent[0].top1.x = xx + 8 ent[0].bottom1.x = xx + 8 } ent[0].top0.x = xx ent[0].bottom0.x = xx ent[0].top0.y = yy ent[0].bottom0.y = yy + 8 ent[0].top1.y = yy ent[0].bottom1.y = yy + 8 byte ani ani = 0 if ( ent[0].frame < 4 ) { ani = 0 } else if ( ent[0].frame < 8 ) { ani = 4 } else if (ent[0].frame < 12 ) { ani = 8 } else if (ent[0].frame < 16 ) { ani = 12 } else if (ent[0].frame < 20 ) { ani = 16 } else { ent[0].frame = 0 ani = 0 } if ( ent[0].jump == 1 ) { ent[0].top0.tile = idle[0] ent[0].bottom0.tile = idle[1] ent[0].top1.tile = idle[2] ent[0].bottom1.tile = idle[3] } else if ( ent[0].movement == 1 ) { ent[0].top0.tile = walk[ani + 0] ent[0].bottom0.tile = walk[ani + 1] ent[0].top1.tile = walk[ani + 2] ent[0].bottom1.tile = walk[ani + 3] } else { ent[0].top0.tile = idle[ani + 0] ent[0].bottom0.tile = idle[ani + 1] ent[0].top1.tile = idle[ani + 2] ent[0].bottom1.tile = idle[ani + 3] } ent[0].frame += 1 } // ------------------------------------- // "PHYSICS" // ------------------------------------- void entity_physics ( pointer.Entity ent ) { // if player if ( ent[0].bottom0.tile < $3A ) { entity_jump_physics( ent ) } if ( ent[0].bottom0.y < BOTTOMWALL - 10 && ent[0].jump == 0 ) { ent[0].top0.y += FALLSPEED } if ( ent[0].movement == 1 ) { if ( ent[0].direction == 0 ) { ent[0].top0.x -= ent[0].walkspeed } else if ( ent[0].direction == 1) { ent[0].top0.x += ent[0].walkspeed } } } // frankly, this is just terrible. reeeee !!!! // please replace this, please replace this, please replace this // oh gods almightly please please please void entity_jump_physics ( pointer.Entity ent ) { if ( ent[0].bottom0.y < BOTTOMWALL - 10 && ent[0].jump == 1 && ent[0].dircount == 0 ) { // if not on ground, don't start jumping ent[0].jump = 0 } else if ( ent[0].jump == 1 ) { // a meager attempt at replicating scb's jump arc // a few deficiencies: // * a bit too short // * "float" is a bit too short // it might work out gameplay-wise, though, so idk we'll see if ( ent[0].dircount < 3) { ent[0].top0.y -= 6 } else if ( ent[0].dircount == 3 ) { ent[0].top0.y -= 2 } else if ( ent[0].dircount == 4 ) { ent[0].top0.y -= 6 } else if ( ent[0].dircount < 9 ) { ent[0].top0.y -= 3 } else if ( 9 <= ent[0].dircount <= 10 ) { ent[0].top0.y -= 1 } else if ( ent[0].dircount == 13 ) { ent[0].top0.y += 2 } else if ( 13 < ent[0].dircount < 20 ) { ent[0].top0.y += 2 } else if ( 20 < ent[0].dircount ) { ent[0].dircount = -1 ent[0].jump = 0 } ent[0].dircount += 1 } } // enemy pathfinding, blah blah. // determines an npc's destiny void entity_destiny ( pointer.Entity ent ) { ent[0].movement = 1 } // ============================================================================ // MAPS // ============================================================================ // load a map onto the ppu void init_map ( pointer.word locations, pointer location_tiles, pointer.word ranges, pointer range_tiles, pointer pallete, pointer attrs ) { byte i word j byte last_range last_range = 0 read_ppu_status() // read PPU status to reset the high/low latch load_bg_pallete( pallete ) load_attr_table( attrs ) blank_bg() read_ppu_status() i = 0 while ( locations[i] != nullchar ) { ppu_set_addr(ppu_nametable_ram + locations[i]) ppu_write_data(location_tiles[i]) i += 1 } i = 0 while ( ranges[i] != nullchar ) { if last_range != 0 { ppu_set_addr(ppu_nametable_ram + ranges[i-1]) for j,ranges[i-1],to,ranges[i] { ppu_write_data(range_tiles[i/2]) } last_range = 0 } else { last_range = 1 } i += 1 } } void init_map1_c () { init_map( pointer.word( MAP1_C_LOCATIONS ), MAP1_C_LOCATION_TILES, pointer.word( MAP1_C_RANGES ), MAP1_C_RANGE_TILES, MAP1_C_PALLETE, MAP1_C_ATTRS ) } // ------------------------------------- // MAP DATA // ------------------------------------- // map1: construction site // not the most efficient way to store this data. // _RANGES is for horizontal lines, left to right. // _LOCATIONS should only be used for one-off, random blocks. right now it's used // for manually typing in vertical lines, gross. const array(word) MAP1_C_LOCATIONS = [ // border pillars (far left and right) $22,$3D, $42,$5D, $62,$7D, $82,$9D, $A2,$BD, $C2,$DD, $E2,$FD, $102,$11D, $122,$13D, $142,$15D, $162,$17D, $182,$19D, $1A2,$1BD, $1C2,$1DD, $1E2,$1FD, $202,$21D, $222,$23D, $242,$25D, $262,$27D, $282,$29D, $2A2,$2BD, $2C2,$2DD, $2E2,$2FD, // lift up top platform $A,$2A,$4A,$6A,$8A,$AA,$CA,$EA, $15,$35,$55,$75,$95,$B5,$D5,$F5, // lift up bottom platform $26A,$28A,$2AA,$2CA,$2EA,$30A, $275,$295,$2B5,$2D5,$2F5,$315, nullchar ] const array(byte) MAP1_C_LOCATION_TILES = [ $81,$81, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $81,$81, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $82,$82, $81,$81, $85,$85,$85,$85,$85,$85,$85,$89, $86,$86,$86,$86,$86,$86,$86,$8A, $87,$85,$85,$85,$85,$85, $88,$86,$86,$86,$86,$86 ] const array(word) MAP1_C_RANGES = [ $340,$3A0, // sandstone subterrain $300,$308, $317,$31F, // brick floor (top) $320,$32E, $331,$33F, // brick floor (second) $23,$2E, $31,$3C, // ceiling $109,$116, // top platform $1A3,$1A8, $1B7,$1BC, // middle platforms $249,$256, // bottom platform nullchar ] const array(byte) MAP1_C_RANGE_TILES = [ $84, $83,$83,$83,$83, $80,$80, $80, $80,$80, $80 ] const array(byte) MAP1_C_PALLETE = [ $21, // bg color $29,$1A,$0F, $00, // construction-blocks $06,$07,$16, $00, // construction-bricks $06,$07,$00, $01, // sandstone $2B,$2B,$1A ] const word MAP1_C_SPAWN = $2F const word MAP1_C_FIRE = $34E // palette selections for the ppu tiles // also not the most efficient way to store this data. (... clearly) !!!! // %DownRight DownLeft TopRight TopLeft const array(byte) MAP1_C_ATTRS = [ %01010101, %10101010, %10101010, %10101010, %10101010, %10101010, %10101010, %01010101, %01010101, %10101010, %10101010, %10101010, %10101010, %10101010, %10101010, %01010101, %01010101, %10101010, %10101010, %10101010, %10101010, %10101010, %10101010, %01010101, %01010101, %10101010, %10101010, %10101010, %10101010, %10101010, %10101010, %01010101, %01010101, %10101010, %10101010, %10101010, %10101010, %10101010, %10101010, %01010101, %01010101, %10101010, %10101010, %10101010, %10101010, %10101010, %10101010, %01010101, %11111010, %11111010, %11111010, %11111010, %11111010, %11111010, %11111010, %11111010, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111 ] // ============================================================================ // UTIL // ============================================================================ inline asm void ppu_wait_vblank() { vblankwait: BIT $2002 ! BPL vblankwait ? RTS }