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