smb.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. const { Controller } = require('jsnes')
  2. const NES = require('./nes')
  3. const { cartesianToPolar:c2p, cartesianToPolar:p2c } = require('./util')
  4. const smbRom = require('../Super Mario Bros. (World).nes')
  5. const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
  6. const sign = (b, n) => n >> (b - 1) ? n - (1 << b) : n
  7. class SMB extends NES {
  8. constructor() {
  9. super(smbRom)
  10. // Starting level hack:
  11. // this.hack(0x9087, 0x07, 0xFF)
  12. // this.hack(0x9089, 0x5F, 0xA0)
  13. // this.hack(0x908A, 0x07, 0x03)
  14. // Infinite clock hack:
  15. this.hack(0x0787, 0x01) // Stop clock
  16. // Invincible hack:
  17. this.hack(0x079F, 0x10) // Invincible
  18. this.genie('ANTOSA')
  19. this.genie('SXIOPO')
  20. // this.genie('PAKOPPAA')
  21. // setInterval(async () => {
  22. // this.nes.buttonDown(1, Controller.BUTTON_SELECT)
  23. // await sleep(200)
  24. // this.nes.buttonUp(1, Controller.BUTTON_SELECT)
  25. // }, 1000)
  26. }
  27. gameState() {
  28. const mem = this.nes.cpu.mem
  29. const stageAMemory = mem.slice(0x500,0x500 + 16*13)
  30. const stageBMemory = mem.slice(0x500 + 16*13, 0x500 + 16*13*2)
  31. const blocks = []
  32. const levelX = (mem[0x006d] * 256 + mem[0x0086])
  33. const levelY = mem[0x03b8]
  34. const screenX = mem[0x03ad]
  35. const setBlock = (x, y, v) => {
  36. if (v) {
  37. /*
  38. this.context.rect(x * 16 + 0.5 - this.gameState.levelX % 512 + xo, y * 16 + 0.5 - data.y, 16, 16)
  39. */
  40. const block = {x: (x * 16 - levelX % 512), y: y * 16 - levelY, v}
  41. if (block.x > 256) block.x -= 512
  42. if (block.x < -256) block.x += 512
  43. block.hitbox = {
  44. x1: block.x,
  45. y1: block.y,
  46. x2: block.x + 16,
  47. y2: block.y + 16
  48. }
  49. blocks.push(block)
  50. }
  51. }
  52. for (let i = 0; i < 16 * 13; i++) {
  53. const x = i % 16
  54. const y = Math.floor(i / 16)
  55. const a = stageAMemory[i]
  56. const b = stageBMemory[i]
  57. setBlock(x, y, a)
  58. setBlock(x + 16, y, b)
  59. }
  60. const hitbox = a => {
  61. const hb = {
  62. x1: mem[a] - screenX,
  63. y1: mem[a + 1] - levelY - 32,
  64. x2: mem[a + 2] - screenX,
  65. y2: mem[a + 3] - levelY - 32,
  66. // x: (mem[a] + mem[a + 2]) / 2,
  67. // y: (mem[a + 1] + mem[a + 3]) / 2,
  68. // w: mem[a + 2] - mem[a],
  69. // h: mem[a + 3] - mem[a + 1]
  70. }
  71. if (hb.y2 < hb.y1) hb.y1 -= 256
  72. return hb
  73. }
  74. const int16 = a => mem[a + 25] * 256 + mem[a]
  75. const state = {
  76. test: mem[0x2000],
  77. screenX: (mem[0x006d] * 256 + mem[0x0086]) - mem[0x03ad],
  78. player: {
  79. hitbox: hitbox(0x04ac),
  80. x: mem[0x006d] * 256 + mem[0x0086],
  81. y: mem[0x00b5] * 256 + mem[0x00CE],
  82. x2: mem[0x0086 - 1],
  83. y2: mem[0x00CE + (0x0086 - 0x006d)],
  84. facing: sign(2, mem[0x0033]),
  85. speedX: sign(8, mem[0x0057]),
  86. speedY: sign(8, mem[0x009f]),
  87. screenY: mem[0x03b8],
  88. screenX: mem[0x03ad],
  89. },
  90. enemies: Array(5).fill().map((_, i) => ({
  91. idx: i,
  92. hitbox: hitbox(0x04b0 + 4 * i),
  93. type: mem[0x0016 + i],
  94. facing: sign(2, mem[0x0046 + i]),
  95. speed: sign(8, mem[0x0058 + i]),
  96. x: mem[0x0058 + i] * 256 + mem[0x006e + i],
  97. y: mem[0x00b6 + i] * 256 + mem[0x00cf + i],
  98. screenY: mem[0x03b9 + i],
  99. screenX: mem[0x03ae + i]
  100. }))
  101. }
  102. //TODO: Convert enemy x,y to polar coordinates r,d, then slice the pie,
  103. // filter enemies by slice.start > r > slice.end, then the value for that
  104. // slice is the filtered enemies.map(d).reduce(min)
  105. const summary = {
  106. levelX: (mem[0x006d] * 256 + mem[0x0086]),
  107. x: state.player.screenX,
  108. y: state.player.screenY,
  109. hitbox: state.player.hitbox,
  110. // TODO: Assign solids, breakables, magics
  111. solids: blocks.filter(({v}) => blockTypes[v][0]),
  112. breakables: blocks.filter(({v}) => blockTypes[v][1]),
  113. magics: blocks.filter(({v}) => blockTypes[v][2]),
  114. enemies: state.enemies.map(enemy => enemy.hitbox.x2 === 0 ? null : ({
  115. x: enemy.hitbox.x1 - state.player.hitbox.x1,
  116. y: enemy.hitbox.y1 - state.player.hitbox.y1,
  117. hitbox: enemy.hitbox
  118. })),
  119. blocks
  120. }
  121. const vision = (list) => {
  122. const slices = 16
  123. const visibility = 128
  124. const arc = (Math.PI * 2) / slices
  125. const views = new Array(slices).fill(0)
  126. const check = (coords) => {
  127. const p = c2p(coords)
  128. const i = Math.floor((p.t + Math.PI) / arc)
  129. views[i] = Math.min(Math.max(views[i], 1 - p.r / visibility), 1)
  130. }
  131. list.forEach(entity => {
  132. if (entity) {
  133. check(entity)
  134. if (entity.hitbox) {
  135. check({x: entity.hitbox.x1, y: entity.hitbox.y1})
  136. check({x: entity.hitbox.x2, y: entity.hitbox.y1})
  137. check({x: entity.hitbox.x1, y: entity.hitbox.y2})
  138. check({x: entity.hitbox.x2, y: entity.hitbox.y2})
  139. }
  140. }
  141. })
  142. return views
  143. }
  144. summary.enemyVision = vision(summary.enemies)
  145. summary.solidVision = vision(summary.solids)
  146. summary.breakableVision = vision(summary.breakables)
  147. summary.magicVision = vision(summary.magics)
  148. // TODO: populate solidVision, breakableVision, magicVision
  149. Object.assign(summary, {
  150. slices: 16,
  151. visibility: 256
  152. })
  153. return summary
  154. return {
  155. player: {
  156. hitbox: hitbox(0x04ac),
  157. facing: mem[0x0033],
  158. speedX: mem[0x0057],
  159. speedY: mem[0x009f],
  160. levelX: mem[0x006d],
  161. levelY: mem[0x00b5] * mem[0x00ce],
  162. screenY: mem[0x03b8],
  163. screenX: mem[0x03ad]
  164. },
  165. enemies: Array(5).fill().map((_, i) => ({
  166. idx: i,
  167. hitbox: hitbox(0x04b0 + (4 * i)),
  168. type: mem[0x0016 + i],
  169. facing: mem[0x0046 + i],
  170. speed: mem[0x0058 + i],
  171. levelX: mem[0x006e + i],
  172. levelY: mem[0x00b6 + i] * mem[0x00cf + i],
  173. screenY: mem[0x03b9 + i],
  174. screenX: mem[0x03ae + i]
  175. })),
  176. levelLayoutAddress: mem[0x00e7],
  177. level: mem[0x0760]
  178. }
  179. }
  180. }
  181. const blockTypes = Object.assign(Array(256).fill([0,0,0]), {
  182. // Solid
  183. // | Breakable
  184. // | | Magic
  185. // | | |
  186. 0x10: [1, 0, 1], // Secret Pipe Rim Left
  187. 0x11: [1, 0, 1], // Secret Pipe Rim Right
  188. 0x12: [1, 0, 0], // Pipe Rim Left
  189. 0x13: [1, 0, 0], // Pipe Rim Right
  190. 0x14: [1, 0, 0], // Pipe Shaft Left
  191. 0x15: [1, 0, 0], // Pipe Shaft Right
  192. 0x16: [1, 0, 0], // Plant Platform Left
  193. 0x17: [1, 0, 0], // Plant Platform middle
  194. 0x18: [1, 0, 0], // Plant platform right
  195. 0x19: [1, 0, 0], // Mushroom Left
  196. 0x1a: [1, 0, 0], // Mushroom middle
  197. 0x1b: [1, 0, 0], // Mushroom right
  198. 0x1c: [1, 0, 1], // Pipe Rim Top
  199. 0x1d: [1, 0, 1], // Pipe Shaft Top
  200. 0x1e: [1, 0, 0], // Pipe Joint Top
  201. 0x1f: [1, 0, 0], // Pipe Rim Bottom
  202. 0x20: [1, 0, 0], // Pipe Shaft Bottom
  203. 0x21: [1, 0, 0], // Pipe Joint Bottom
  204. 0x23: [1, 0, 1], // Block being bonked
  205. 0x24: [0, 0, 1], // Flag pole top
  206. 0x25: [0, 0, 1], // Flag pole
  207. 0x26: [0, 0, 1], // Vine
  208. 0x51: [1, 1, 1], // Red? Bricks at rest
  209. 0x52: [1, 1, 1], // Bricks at rest
  210. 0x52: [1, 0, 0], // End of level
  211. 0x54: [1, 0, 0], // Ground Block
  212. 0x56: [1, 1, 1], // Bricks hiding vine
  213. 0x57: [1, 1, 1], // Bricks hiding star
  214. 0x58: [1, 1, 1], // Red? Bricks hiding coins
  215. 0x5a: [1, 1, 1], // Bricks hiding mushroom or flower
  216. 0x5b: [1, 1, 1], // Bricks hiding vine
  217. 0x5d: [1, 1, 1], // Bricks hiding coins
  218. 0x5f: [0, 1, 1], // hidden coin (KAIZO!!)
  219. 0x60: [0, 1, 1], // Hidden 1-up
  220. 0x61: [1, 0, 0], // Diamond Block
  221. 0x62: [1, 0, 0], // Castle block
  222. 0x63: [1, 0, 0], // Bridge
  223. 0x64: [1, 0, 0], // Bullet Bill barrell
  224. 0x65: [1, 0, 0], // Bullet Bill Base
  225. 0x66: [1, 0, 0], // Bullet Bill Shaft
  226. 0x67: [1, 0, 1], // Spring top
  227. 0x68: [1, 0, 1], // Spring bottom
  228. 0x88: [1, 0, 0], // Cloud
  229. 0x89: [1, 0, 0], // bowser bridge
  230. 0xc0: [1, 0, 1], // Question block with coin
  231. 0xc1: [1, 0, 1], // Question block with mushroom
  232. 0xc2: [0, 0, 1], // Coin
  233. 0xc4: [1, 0, 0], // Spent block
  234. 0xc5: [0, 0, 1], // Axe
  235. })
  236. module.exports = SMB