|
|
@@ -4,51 +4,214 @@ const SCREEN_WIDTH = 256;
|
|
|
const SCREEN_HEIGHT = 240;
|
|
|
const BUFFER_FRAMES = 120;
|
|
|
const SAMPLE_REDUCTION = 4;
|
|
|
-
|
|
|
+const MAX_BUFFER = 0; 1200;
|
|
|
+const WORKING_MEMORY = 0x800;
|
|
|
+const { cartesianToPolar:c2p, polarToCartesian:p2c, findMemory } = require('../common/util')
|
|
|
+window.findMemory = findMemory
|
|
|
export default {
|
|
|
+ data: () => ({
|
|
|
+ gameState: {},
|
|
|
+ gameStateString: '',
|
|
|
+ memory: new Uint8Array(WORKING_MEMORY),
|
|
|
+ watch: [
|
|
|
+ 0x0086
|
|
|
+ ]
|
|
|
+ }),
|
|
|
+ beforeDestroy() {
|
|
|
+ console.log('beforeDestroy()')
|
|
|
+ this.stop = true
|
|
|
+ this.ws.close()
|
|
|
+ },
|
|
|
mounted() {
|
|
|
+ console.log('mounted()')
|
|
|
const buffer = new Fifo()
|
|
|
const url = new URL('game', location.href)
|
|
|
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
|
- this.ws = new WebSocket(url)
|
|
|
- this.ws.binaryType = 'arraybuffer'
|
|
|
- const canvas = this.$refs.canvas
|
|
|
- const context = canvas.getContext('2d')
|
|
|
- const imageData = context.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
|
- const buf = new ArrayBuffer(imageData.data.length);
|
|
|
+ const connect = () => {
|
|
|
+ this.ws = new WebSocket(url)
|
|
|
+ this.ws.binaryType = 'arraybuffer'
|
|
|
+ this.ws.addEventListener('message', msg => {
|
|
|
+ if (typeof msg.data === 'string') {
|
|
|
+ this.gameState = JSON.parse(msg.data)
|
|
|
+ this.gameStateString = JSON.stringify(this.gameState.enemyVision, null, 2)
|
|
|
+ .replace(/\{[^\{\}\[\]]*\}/g, x => x.replace(/\s+/g, ' '))
|
|
|
+ .replace(/\[[^\{\}\[\]]*\]/g, x => x.replace(/\s+/g, ' '))
|
|
|
+
|
|
|
+ } else {
|
|
|
+ //buffer.push(msg.data)
|
|
|
+ //if (buffer.length === MAX_BUFFER) buffer.shift()
|
|
|
+ // if (!timer) {
|
|
|
+ // timer = true
|
|
|
+ this.renderFrame(msg.data)
|
|
|
+ this.renderDebug(this.gameState)
|
|
|
+ //}
|
|
|
+ }
|
|
|
+ })
|
|
|
+ this.ws.addEventListener('close', () => {
|
|
|
+ if (!this.stop) setTimeout(connect, 1000)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ connect()
|
|
|
+ this.canvas = this.$refs.canvas
|
|
|
+ this.context = this.canvas.getContext('2d')
|
|
|
+ this.imageData = this.context.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
|
+ this.memoryCanvas = this.$refs.memoryCanvas
|
|
|
+ this.memoryContext = this.memoryCanvas.getContext('2d')
|
|
|
+ this.memoryImageData = this.memoryContext.getImageData(0, 0, this.memoryCanvas.getAttribute('width'), this.memoryCanvas.getAttribute('height'))
|
|
|
+ this.buf = new ArrayBuffer(this.imageData.data.length);
|
|
|
// Get the canvas buffer in 8bit and 32bit
|
|
|
- const buf8 = new Uint8ClampedArray(buf);
|
|
|
- const buf32 = new Uint32Array(buf);
|
|
|
+ this.buf8 = new Uint8ClampedArray(this.buf);
|
|
|
+ this.buf32 = new Uint32Array(this.buf);
|
|
|
+
|
|
|
+ this.timer = null
|
|
|
+
|
|
|
+ this.keyMap = {
|
|
|
+ a: /*BUTTON_B*/ 1,
|
|
|
+ s: /*BUTTON_A*/ 0,
|
|
|
+ '\\': /*BUTTON_SELECT*/ 2,
|
|
|
+ Enter: /*BUTTON_START*/ 3,
|
|
|
+ ArrowUp: /*BUTTON_UP*/ 4,
|
|
|
+ ArrowDown: /*BUTTON_DOWN*/ 5,
|
|
|
+ ArrowLeft: /*BUTTON_LEFT*/ 6,
|
|
|
+ ArrowRight: /*BUTTON_RIGHT*/ 7
|
|
|
+ }
|
|
|
+
|
|
|
+ this.key = (ev, pressed) => {
|
|
|
+ if (this.keyMap.hasOwnProperty(ev.key)) {
|
|
|
+ const button = this.keyMap[ev.key]
|
|
|
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
|
+ this.ws.send(JSON.stringify({
|
|
|
+ type: 'button',
|
|
|
+ button,
|
|
|
+ pressed
|
|
|
+ }))
|
|
|
+ } else {
|
|
|
+ console.warn('Not connected')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ document.addEventListener('keydown', (ev) => this.key(ev, true))
|
|
|
+ document.addEventListener('keyup', (ev) => this.key(ev, false))
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ renderDebug(data) {
|
|
|
+ window.data = data
|
|
|
+ this.context.save()
|
|
|
+ // this.context.translate(8, 8)
|
|
|
+ this.context.translate(data.x, data.y + 32)
|
|
|
+ // this.context.beginPath()
|
|
|
+ // this.context.fillStyle = '#080'
|
|
|
+ // //this.context.fillRect(data.hitbox.x1, data.hitbox.y1, data.hitbox.x2 - data.hitbox.x1, data.hitbox.y2 - data.hitbox.y1)
|
|
|
+ // this.context.arc(0, 0, 8, 0, 2 * Math.PI)
|
|
|
+ // this.context.fill()
|
|
|
+
|
|
|
+ // data.enemies.forEach(enemy => {
|
|
|
+ // if (enemy && enemy.hitbox.y2 > enemy.hitbox.y1) {
|
|
|
+ // this.context.beginPath()
|
|
|
+ // this.context.fillStyle = '#800'
|
|
|
+ // this.context.arc(enemy.x, enemy.y, 8, 0, 2 * Math.PI)
|
|
|
+ // //this.context.fillRect(enemy.hitbox.x1, enemy.hitbox.y1, enemy.hitbox.x2 - enemy.hitbox.x1, enemy.hitbox.y2 - enemy.hitbox.y1)
|
|
|
+ // this.context.fill()
|
|
|
+ // }
|
|
|
+ // })
|
|
|
|
|
|
- let timer
|
|
|
+ const renderVision = (collection, color) => {
|
|
|
+ const arc = (Math.PI * 2 / data.slices)
|
|
|
+ this.context.save()
|
|
|
+ this.context.translate((data.hitbox.x2 - data.hitbox.x1) / 2, (data.hitbox.y2 - data.hitbox.y1) / -2)
|
|
|
+ for (let i = 0; i < data.slices; i++) {
|
|
|
+ const r = collection[i]
|
|
|
+ const {x, y} = p2c({
|
|
|
+ r: (1 - r) * 30,
|
|
|
+ t: arc * i + arc / 2 - Math.PI
|
|
|
+ })
|
|
|
+ this.context.beginPath()
|
|
|
+ this.context.fillStyle = color
|
|
|
+ this.context.arc(x, y, 2, 0, 2 * Math.PI)
|
|
|
+ //this.context.fillRect(enemy.hitbox.x1, enemy.hitbox.y1, enemy.hitbox.x2 - enemy.hitbox.x1, enemy.hitbox.y2 - enemy.hitbox.y1)
|
|
|
+ this.context.fill()
|
|
|
|
|
|
- this.ws.addEventListener('message', msg => {
|
|
|
- buffer.push(msg.data)
|
|
|
- if (buffer.length > BUFFER_FRAMES && !timer) {
|
|
|
- timer = setInterval(renderFrame, (1000 / 60))
|
|
|
+ }
|
|
|
+ this.context.restore()
|
|
|
}
|
|
|
- })
|
|
|
+ renderVision(data.enemyVision, 'red')
|
|
|
+ renderVision(data.solidVision, 'blue')
|
|
|
+ renderVision(data.breakableVision, 'yellow')
|
|
|
+ renderVision(data.magicVision, 'green')
|
|
|
|
|
|
- const renderFrame = () => {
|
|
|
- const data = buffer.shift()
|
|
|
+ const renderHitboxes = (collection, color) => {
|
|
|
+ this.context.strokeStyle = color
|
|
|
+ this.context.beginPath()
|
|
|
+ collection.forEach(entity => {
|
|
|
+ if (entity) this.context.rect(entity.hitbox.x1, entity.hitbox.y1, entity.hitbox.x2 - entity.hitbox.x1, entity.hitbox.y2 - entity.hitbox.y1)
|
|
|
+ })
|
|
|
+ this.context.stroke()
|
|
|
+ }
|
|
|
+
|
|
|
+ renderHitboxes(this.gameState.blocks, 'blue')
|
|
|
+ renderHitboxes(data.enemies, 'red')
|
|
|
+ renderHitboxes([data], 'yellow')
|
|
|
+ // this.gameState.blocks.forEach(({x, y, v}) => {
|
|
|
+
|
|
|
+ // // v = v.toString(16).padStart(2)
|
|
|
+ // this.context.rect(x, y, 16, 16)
|
|
|
+// for (let xo = -512; xo <= 512; xo += 512) {
|
|
|
+ // const xo = 0
|
|
|
+ // this.context.rect(x * 16 + 0.5 - this.gameState.levelX % 512 + xo, y * 16 + 0.5 - data.y, 16, 16)
|
|
|
+ // this.context.fillStyle = 'black'
|
|
|
+ // this.context.fillText(v, x * 16 + 0.5 - this.gameState.levelX % 512 + xo + 1, y * 16 + 0.5 - data.y + 10)
|
|
|
+ // this.context.fillStyle = 'black'
|
|
|
+ // this.context.fillText(v, x * 16 + 0.5 - this.gameState.levelX % 512 + xo - 1, y * 16 + 0.5 - data.y + 7)
|
|
|
+ // this.context.fillStyle = 'black'
|
|
|
+ // this.context.fillText(v, x * 16 + 0.5 - this.gameState.levelX % 512 + xo + 1, y * 16 + 0.5 - data.y + 7)
|
|
|
+ // this.context.fillStyle = 'black'
|
|
|
+ // this.context.fillText(v, x * 16 + 0.5 - this.gameState.levelX % 512 + xo - 1, y * 16 + 0.5 - data.y + 10)
|
|
|
+ // this.context.fillStyle = '#aaaa00'
|
|
|
+ // this.context.fillText(v, x * 16 + 0.5 - this.gameState.levelX % 512 + xo, y * 16 + 0.5 - data.y + 9)
|
|
|
+// }
|
|
|
+ // })
|
|
|
+ // this.context.stroke();
|
|
|
+ // this.context.strokeStyle = null
|
|
|
+ this.context.restore()
|
|
|
+ },
|
|
|
+ renderFrame(data) {
|
|
|
+ //const data = buffer.shift()
|
|
|
if (data) {
|
|
|
const palette = new Uint32Array(data, 0, 256 * 4)
|
|
|
const pixels = new Uint8Array(data, 256 * 4, SCREEN_WIDTH * SCREEN_HEIGHT)
|
|
|
+ const memory = new Uint8Array(data, 256 * 4 + SCREEN_WIDTH * SCREEN_HEIGHT, WORKING_MEMORY)
|
|
|
+ Object.assign(this.memory, memory)
|
|
|
+ window.memory = this.memory
|
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
|
- buf32[i] = 0xff000000 | palette[pixels[i]]
|
|
|
+ this.buf32[i] = 0xff000000 | palette[pixels[i]]
|
|
|
+ }
|
|
|
+ this.imageData.data.set(this.buf8)
|
|
|
+ for (let i = 32 * 40; i < memory.length; i++) {
|
|
|
+ const m = memory[i]
|
|
|
+ for (let x = 0; x < 4; x++) {
|
|
|
+ this.memoryImageData.data[i * 4 + x - 32 * 40] = m
|
|
|
+ }
|
|
|
}
|
|
|
- imageData.data.set(buf8)
|
|
|
- context.putImageData(imageData, 0, 0)
|
|
|
- } else {
|
|
|
- clearInterval(timer)
|
|
|
- timer = null
|
|
|
+ this.context.putImageData(this.imageData, 0, 0)
|
|
|
+ this.memoryContext.putImageData(this.memoryImageData, 0, 0)
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
-
|
|
|
+
|
|
|
+ //const ms = Math.max(-0.8 * buffer.length + 160, 0)
|
|
|
+ //timer = setTimeout(renderFrame, ms)
|
|
|
+ //requestAnimationFrame(renderFrame)
|
|
|
+ /* https://www.calculator.net/slope-calculator.html?type=1&x11=0&y11=500&x12=100&y12=16&x=12&y=7
|
|
|
+ l = buffer.length
|
|
|
+ l = 0, sleep 500
|
|
|
+ l = 100, sleep 1000/60
|
|
|
+ l = 200, sleep more
|
|
|
+ */
|
|
|
+ }
|
|
|
},
|
|
|
destroyed() {
|
|
|
- this.ws.close()
|
|
|
+ console.log('destroyed()')
|
|
|
+ if (this.ws) this.ws.close()
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
@@ -61,13 +224,43 @@ export default {
|
|
|
<v-toolbar-title>Super Mario Bros</v-toolbar-title>
|
|
|
</v-toolbar>
|
|
|
<v-container>
|
|
|
- <canvas ref="canvas" width="256" height="240" />
|
|
|
-
|
|
|
+ <canvas style="zoom: 2;" ref="canvas" width="256" height="240" />
|
|
|
+ <canvas style="zoom: 4;" ref="memoryCanvas" width="16" height="128" />
|
|
|
+ <pre>{{gameStateString}}</pre>
|
|
|
+ <ul>
|
|
|
+ <!-- "0x0069", "0x0075", "0x0093", "0x0109", "0x0113", "0x0114", "0x0115", "0x0118", "0x0119", "0x0130", "0x0163", "0x0164", "0x0168", "0x0184", "0x0185", "0x0186", "0x0187", "0x0190", "0x0191", "0x0202", "0x0215", "0x0216", "0x0239", "0x0246", "0x0269", "0x0281", "0x0282", "0x0283", "0x0289", "0x0637", "0x0641", "0x0642", "0x0645", "0x0646", "0x0791", "0x0987", "0x0988", "0x0989", "0x0996", "0x0997", "0x1002", "0x1003", "0x1008", "0x1050", "0x1052", "0x1084", "0x1178", "0x1180", "0x1181", "0x1182", "0x1183", "0x1212", "0x1213", "0x1214", "0x1215", "0x1220", "0x1221", "0x1222", "0x1223", "0x1286", "0x1302", "0x1318", "0x1334", "0x1350", "0x1366", "0x1382", "0x1398", "0x1563", "0x1564", "0x1565", "0x1578", "0x1579", "0x1580", "0x1581", "0x1593", "0x1594", "0x1595", "0x1596", "0x1597", "0x1611", "0x1612", "0x1613", "0x1623", "0x1624", "0x1627", "0x1628", "0x1629", "0x1636", "0x1638", "0x1639", "0x1640", "0x1643", "0x1653", "0x1654", "0x1655", "0x1656", "0x1659", "0x1675", "0x1676", "0x1677" -->
|
|
|
+ <li> x, y: {{gameState.x}}, {{gameState.y}}</li>
|
|
|
+ <li v-for="(key, index) in watch" :key="index">
|
|
|
+ {{key.toString(16)}}: {{memory[key]}}
|
|
|
+ </li>
|
|
|
+ <li>0x006d (Level x): {{gameState.levelX}}</li>
|
|
|
+ <li>0x0491 (Enemy collision): {{memory[0x0491]}}</li>
|
|
|
+ <li>0x04AC: {{memory[0x04AC]}}</li>
|
|
|
+ <li>0x04AC: {{memory[0x04AC]}}</li>
|
|
|
+ <li>0x0750: {{memory[0x0750]}}</li>
|
|
|
+ </ul>
|
|
|
+ <!-- <div style="position: relative;">
|
|
|
+ <div v-for="block in gameState.blocks"
|
|
|
+ :key="gameState.blocks.indexOf(block)"
|
|
|
+ :style="{
|
|
|
+ display: 'block',
|
|
|
+ width: '4px',
|
|
|
+ height: '4px',
|
|
|
+ left: (block.x * 4) + 'px',
|
|
|
+ top: (block.y * 4) + 'px',
|
|
|
+ position: 'absolute',
|
|
|
+ backgroundColor: 'brown'
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div> -->
|
|
|
</v-container>
|
|
|
</v-content>
|
|
|
</v-app>
|
|
|
</template>
|
|
|
|
|
|
-<style>
|
|
|
-
|
|
|
+<style scoped>
|
|
|
+canvas {
|
|
|
+ image-rendering: pixelated;
|
|
|
+}
|
|
|
</style>
|