| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- const { Controller } = require('jsnes')
- const NES = require('./nes')
- const { cartesianToPolar:c2p, cartesianToPolar:p2c } = require('./util')
- const smbRom = require('../Super Mario Bros. (World).nes')
- const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
- const sign = (b, n) => n >> (b - 1) ? n - (1 << b) : n
- class SMB extends NES {
- constructor() {
- super(smbRom)
- // Starting level hack:
- // this.hack(0x9087, 0x07, 0xFF)
- // this.hack(0x9089, 0x5F, 0xA0)
- // this.hack(0x908A, 0x07, 0x03)
- // Infinite clock hack:
- this.hack(0x0787, 0x01) // Stop clock
- // Invincible hack:
- this.hack(0x079F, 0x10) // Invincible
- this.genie('ANTOSA')
- this.genie('SXIOPO')
- // this.genie('PAKOPPAA')
- // setInterval(async () => {
- // this.nes.buttonDown(1, Controller.BUTTON_SELECT)
- // await sleep(200)
- // this.nes.buttonUp(1, Controller.BUTTON_SELECT)
- // }, 1000)
- }
- gameState() {
- const mem = this.nes.cpu.mem
- const stageAMemory = mem.slice(0x500,0x500 + 16*13)
- const stageBMemory = mem.slice(0x500 + 16*13, 0x500 + 16*13*2)
- const blocks = []
- const levelX = (mem[0x006d] * 256 + mem[0x0086])
- const levelY = mem[0x03b8]
- const screenX = mem[0x03ad]
- const setBlock = (x, y, v) => {
- if (v) {
- /*
- this.context.rect(x * 16 + 0.5 - this.gameState.levelX % 512 + xo, y * 16 + 0.5 - data.y, 16, 16)
- */
- const block = {x: (x * 16 - levelX % 512), y: y * 16 - levelY, v}
- if (block.x > 256) block.x -= 512
- if (block.x < -256) block.x += 512
- block.hitbox = {
- x1: block.x,
- y1: block.y,
- x2: block.x + 16,
- y2: block.y + 16
- }
- blocks.push(block)
- }
- }
- for (let i = 0; i < 16 * 13; i++) {
- const x = i % 16
- const y = Math.floor(i / 16)
- const a = stageAMemory[i]
- const b = stageBMemory[i]
- setBlock(x, y, a)
- setBlock(x + 16, y, b)
- }
- const hitbox = a => {
- const hb = {
- x1: mem[a] - screenX,
- y1: mem[a + 1] - levelY - 32,
- x2: mem[a + 2] - screenX,
- y2: mem[a + 3] - levelY - 32,
- // x: (mem[a] + mem[a + 2]) / 2,
- // y: (mem[a + 1] + mem[a + 3]) / 2,
- // w: mem[a + 2] - mem[a],
- // h: mem[a + 3] - mem[a + 1]
- }
- if (hb.y2 < hb.y1) hb.y1 -= 256
- return hb
- }
- const int16 = a => mem[a + 25] * 256 + mem[a]
- const state = {
- test: mem[0x2000],
- screenX: (mem[0x006d] * 256 + mem[0x0086]) - mem[0x03ad],
- player: {
- hitbox: hitbox(0x04ac),
- x: mem[0x006d] * 256 + mem[0x0086],
- y: mem[0x00b5] * 256 + mem[0x00CE],
- x2: mem[0x0086 - 1],
- y2: mem[0x00CE + (0x0086 - 0x006d)],
- facing: sign(2, mem[0x0033]),
- speedX: sign(8, mem[0x0057]),
- speedY: sign(8, mem[0x009f]),
- screenY: mem[0x03b8],
- screenX: mem[0x03ad],
- },
- enemies: Array(5).fill().map((_, i) => ({
- idx: i,
- hitbox: hitbox(0x04b0 + 4 * i),
- type: mem[0x0016 + i],
- facing: sign(2, mem[0x0046 + i]),
- speed: sign(8, mem[0x0058 + i]),
- x: mem[0x0058 + i] * 256 + mem[0x006e + i],
- y: mem[0x00b6 + i] * 256 + mem[0x00cf + i],
- screenY: mem[0x03b9 + i],
- screenX: mem[0x03ae + i]
- }))
- }
- //TODO: Convert enemy x,y to polar coordinates r,d, then slice the pie,
- // filter enemies by slice.start > r > slice.end, then the value for that
- // slice is the filtered enemies.map(d).reduce(min)
- const summary = {
- levelX: (mem[0x006d] * 256 + mem[0x0086]),
- x: state.player.screenX,
- y: state.player.screenY,
- hitbox: state.player.hitbox,
- // TODO: Assign solids, breakables, magics
- solids: blocks.filter(({v}) => blockTypes[v][0]),
- breakables: blocks.filter(({v}) => blockTypes[v][1]),
- magics: blocks.filter(({v}) => blockTypes[v][2]),
- enemies: state.enemies.map(enemy => enemy.hitbox.x2 === 0 ? null : ({
- x: enemy.hitbox.x1 - state.player.hitbox.x1,
- y: enemy.hitbox.y1 - state.player.hitbox.y1,
- hitbox: enemy.hitbox
- })),
- blocks
- }
- const vision = (list) => {
- const slices = 16
- const visibility = 128
- const arc = (Math.PI * 2) / slices
- const views = new Array(slices).fill(0)
- const check = (coords) => {
- const p = c2p(coords)
- const i = Math.floor((p.t + Math.PI) / arc)
- views[i] = Math.min(Math.max(views[i], 1 - p.r / visibility), 1)
- }
- list.forEach(entity => {
- if (entity) {
- check(entity)
- if (entity.hitbox) {
- check({x: entity.hitbox.x1, y: entity.hitbox.y1})
- check({x: entity.hitbox.x2, y: entity.hitbox.y1})
- check({x: entity.hitbox.x1, y: entity.hitbox.y2})
- check({x: entity.hitbox.x2, y: entity.hitbox.y2})
- }
- }
- })
- return views
- }
- summary.enemyVision = vision(summary.enemies)
- summary.solidVision = vision(summary.solids)
- summary.breakableVision = vision(summary.breakables)
- summary.magicVision = vision(summary.magics)
- // TODO: populate solidVision, breakableVision, magicVision
-
- Object.assign(summary, {
- slices: 16,
- visibility: 256
- })
- return summary
- return {
- player: {
- hitbox: hitbox(0x04ac),
- facing: mem[0x0033],
- speedX: mem[0x0057],
- speedY: mem[0x009f],
- levelX: mem[0x006d],
- levelY: mem[0x00b5] * mem[0x00ce],
- screenY: mem[0x03b8],
- screenX: mem[0x03ad]
- },
- enemies: Array(5).fill().map((_, i) => ({
- idx: i,
- hitbox: hitbox(0x04b0 + (4 * i)),
- type: mem[0x0016 + i],
- facing: mem[0x0046 + i],
- speed: mem[0x0058 + i],
- levelX: mem[0x006e + i],
- levelY: mem[0x00b6 + i] * mem[0x00cf + i],
- screenY: mem[0x03b9 + i],
- screenX: mem[0x03ae + i]
- })),
- levelLayoutAddress: mem[0x00e7],
- level: mem[0x0760]
- }
- }
- }
- const blockTypes = Object.assign(Array(256).fill([0,0,0]), {
- // Solid
- // | Breakable
- // | | Magic
- // | | |
- 0x10: [1, 0, 1], // Secret Pipe Rim Left
- 0x11: [1, 0, 1], // Secret Pipe Rim Right
- 0x12: [1, 0, 0], // Pipe Rim Left
- 0x13: [1, 0, 0], // Pipe Rim Right
- 0x14: [1, 0, 0], // Pipe Shaft Left
- 0x15: [1, 0, 0], // Pipe Shaft Right
- 0x16: [1, 0, 0], // Plant Platform Left
- 0x17: [1, 0, 0], // Plant Platform middle
- 0x18: [1, 0, 0], // Plant platform right
- 0x19: [1, 0, 0], // Mushroom Left
- 0x1a: [1, 0, 0], // Mushroom middle
- 0x1b: [1, 0, 0], // Mushroom right
- 0x1c: [1, 0, 1], // Pipe Rim Top
- 0x1d: [1, 0, 1], // Pipe Shaft Top
- 0x1e: [1, 0, 0], // Pipe Joint Top
- 0x1f: [1, 0, 0], // Pipe Rim Bottom
- 0x20: [1, 0, 0], // Pipe Shaft Bottom
- 0x21: [1, 0, 0], // Pipe Joint Bottom
- 0x23: [1, 0, 1], // Block being bonked
- 0x24: [0, 0, 1], // Flag pole top
- 0x25: [0, 0, 1], // Flag pole
- 0x26: [0, 0, 1], // Vine
- 0x51: [1, 1, 1], // Red? Bricks at rest
- 0x52: [1, 1, 1], // Bricks at rest
- 0x52: [1, 0, 0], // End of level
- 0x54: [1, 0, 0], // Ground Block
- 0x56: [1, 1, 1], // Bricks hiding vine
- 0x57: [1, 1, 1], // Bricks hiding star
- 0x58: [1, 1, 1], // Red? Bricks hiding coins
- 0x5a: [1, 1, 1], // Bricks hiding mushroom or flower
- 0x5b: [1, 1, 1], // Bricks hiding vine
- 0x5d: [1, 1, 1], // Bricks hiding coins
- 0x5f: [0, 1, 1], // hidden coin (KAIZO!!)
- 0x60: [0, 1, 1], // Hidden 1-up
- 0x61: [1, 0, 0], // Diamond Block
- 0x62: [1, 0, 0], // Castle block
- 0x63: [1, 0, 0], // Bridge
- 0x64: [1, 0, 0], // Bullet Bill barrell
- 0x65: [1, 0, 0], // Bullet Bill Base
- 0x66: [1, 0, 0], // Bullet Bill Shaft
- 0x67: [1, 0, 1], // Spring top
- 0x68: [1, 0, 1], // Spring bottom
- 0x88: [1, 0, 0], // Cloud
- 0x89: [1, 0, 0], // bowser bridge
- 0xc0: [1, 0, 1], // Question block with coin
- 0xc1: [1, 0, 1], // Question block with mushroom
- 0xc2: [0, 0, 1], // Coin
- 0xc4: [1, 0, 0], // Spent block
- 0xc5: [0, 0, 1], // Axe
- })
- module.exports = SMB
|