commit 143a93f9f4d2e0f1d5a53d061a15b5e51b54c76d Author: Evert Prants Date: Wed Jan 8 22:07:58 2020 +0200 beginnings of another project that will never be finished, yay diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..aeda47e --- /dev/null +++ b/.babelrc @@ -0,0 +1,15 @@ +{ + "presets": ["@babel/preset-env"], + "plugins": [ + [ + "@babel/plugin-transform-runtime", + { + "corejs": false, + "helpers": true, + "regenerator": true, + "useESModules": false + } + ] + ] +} + diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a0a4de8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.js] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99d9404 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/node_modules/ +/dist/ +/package-lock.json +/**/dna.txt +*.db +*.sqlite3 diff --git a/assets/ground.png b/assets/ground.png new file mode 100644 index 0000000..1c93153 Binary files /dev/null and b/assets/ground.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..d63dc8a --- /dev/null +++ b/index.html @@ -0,0 +1,10 @@ + + + + + + tilegame + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..0bc06c3 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "tilegame", + "version": "0.0.0", + "description": "Tile game", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "serve": "node .", + "build": "webpack -p", + "watch": "webpack -w --mode=development" + }, + "keywords": [], + "private": true, + "author": "Evert \"Diamond\" Prants ", + "license": "MIT", + "devDependencies": { + "@babel/core": "^7.7.7", + "@babel/plugin-transform-runtime": "^7.7.6", + "@babel/preset-env": "^7.7.7", + "@babel/runtime": "^7.7.7", + "babel-loader": "^8.0.4", + "html-webpack-plugin": "^3.2.0", + "seedrandom": "^3.0.5", + "standard": "^12.0.1", + "webpack": "^4.41.5", + "webpack-command": "^0.4.2" + } +} diff --git a/src/canvas.js b/src/canvas.js new file mode 100644 index 0000000..ddfcaf4 --- /dev/null +++ b/src/canvas.js @@ -0,0 +1,41 @@ +const canvas = document.createElement('canvas') +const ctx = canvas.getContext('2d') + +// Resize the canvas when window is resized +canvas.resizeWorkspace = function () { + canvas.width = window.innerWidth + canvas.height = window.innerHeight +} + +window.addEventListener('resize', canvas.resizeWorkspace, false) + +// Add elements to the document +document.body.appendChild(canvas) + +canvas.resizeWorkspace() + +ctx.imageSmoothingEnabled = false + +class ResourceCacheFactory { + constructor () { + this.canvas = document.createElement('canvas') + this.ctx = this.canvas.getContext('2d') + this.ctx.imageSmoothingEnabled = false + } + + prepare (width, height) { + if (width === this.canvas.width && height === this.canvas.height) { + this.ctx.clearRect(0, 0, width, height) + } + this.canvas.width = width + this.canvas.height = height + } + + capture () { + let img = new window.Image() + img.src = this.canvas.toDataURL() + return img + } +} + +export { canvas, ctx, ResourceCacheFactory } diff --git a/src/debug.js b/src/debug.js new file mode 100644 index 0000000..01a0bdc --- /dev/null +++ b/src/debug.js @@ -0,0 +1,49 @@ +import { ctx } from './canvas' + +class Debugging { + draw (vp, world, fps) { + let p = vp.chunkIn(world.chunkSize * world.tileSize) + ctx.fillStyle = '#fff' + ctx.fillText(fps + ' fps', 4, 16) + ctx.fillText('cam-in-chunk (x: ' + p.x + '; y: ' + p.y + ')', 4, 32) + ctx.fillText('loaded ' + world.chunks.length, 4, 48) + ctx.fillText('drawn ' + world._lastDrawCount, 4, 64) + ctx.fillText('updates ' + world._lastUpdateCount, 4, 80) + } + + createSliders (obj, args, fn) { + let overlay = document.createElement('div') + overlay.style = 'color:#fff;position:absolute;top:15px;right:15px;background-color:hsla(0,0%,47%,0.5);padding:10px;display:flex;flex-direction:column;' + + for (let a in args) { + let min = args[a][0] + let max = args[a][1] + let step = args[a][2] + let div = document.createElement('div') + div.style = 'display:flex;flex-direction:row;' + let name = document.createElement('span') + let value = document.createElement('value') + let slider = document.createElement('input') + slider.style = 'flex-grow:1;' + slider.type = 'range' + slider.min = min + slider.max = max + slider.step = step + slider.value = obj[a] + name.innerHTML = a + value.innerHTML = obj[a] + slider.addEventListener('input', function (e) { + obj[a] = parseFloat(slider.value) + value.innerHTML = slider.value + fn(a, slider.value) + }) + div.appendChild(name) + div.appendChild(slider) + div.appendChild(value) + overlay.appendChild(div) + } + document.body.appendChild(overlay) + } +} + +export default new Debugging() diff --git a/src/heightmap.js b/src/heightmap.js new file mode 100644 index 0000000..997b9f6 --- /dev/null +++ b/src/heightmap.js @@ -0,0 +1,90 @@ +import seedrandom from 'seedrandom' + +class PerlinNoise { + constructor (rand = Math.random) { + let perm = new Int8Array(257) + + for (let i = 0; i < 256; i++) { + perm[i] = i & 1 ? 1 : -1 + } + + for (let i = 0; i < 256; i++) { + let j = rand() * 4294967296 & 255 + + var _ref = [perm[j], perm[i]] + perm[i] = _ref[0] + perm[j] = _ref[1] + } + perm[256] = perm[0] + + function noise1d (x) { + let x0 = x | 0 + let x1 = x - x0 + let xi = x0 & 255 + let fx = (3 - 2 * x1) * x1 * x1 + let a = x1 * perm[xi] + let b = (x1 - 1) * perm[xi + 1] + + return a + fx * (b - a) + } + + function noise (x) { + let sum = 0 + + sum += (1 + noise1d(x)) * 0.25 + sum += (1 + noise1d(x * 2)) * 0.125 + sum += (1 + noise1d(x * 4)) * 0.0625 + sum += (1 + noise1d(x * 8)) * 0.03125 + + return sum + } + + this.noise = noise + } +} + +class HeightMap { + // amplitude - Controls the amount the height changes. The higher, the taller the hills. + // persistence - Controls details, value in [0,1]. Higher increases grain, lower increases smoothness. + // octaves - Number of noise layers + // period - Distance above which we start to see similarities. The higher, the longer "hills" will be on a terrain. + // lacunarity - Controls period change across octaves. 2 is usually a good value to address all detail levels. + constructor (offsetX, offsetY, size, seed, amplitude = 30, persistence = 0.5, octaves = 5, period = 80, lacunarity = 2) { + this.ix = offsetX + this.iy = offsetY + this.seed = seed + this.size = size + + this.noise = new PerlinNoise(seedrandom(seed)) + + this.amplitude = amplitude + this.period = period + this.lacunarity = lacunarity + this.octaves = octaves + this.persistence = persistence + } + + getNoise (zx) { + let x = ((this.size * this.ix) + zx) / this.period + + let amp = 1.0 + let max = 1.0 + let sum = this.noise.noise(x) + + let i = 0 + while (++i < this.octaves) { + x *= this.lacunarity + amp *= this.persistence + max += amp + sum += this.noise.noise(x) * amp + } + + return sum / max + } + + getHeight (x) { + return this.getNoise(x) * this.amplitude + } +} + +export { HeightMap } diff --git a/src/image.js b/src/image.js new file mode 100644 index 0000000..76a46bc --- /dev/null +++ b/src/image.js @@ -0,0 +1,33 @@ +import Resource from './resource' +import { ctx } from './canvas' + +let imageCache = {} + +class Image { + constructor (file, img) { + this.file = file + this.img = img + } + + static async load (file) { + if (imageCache[file]) return imageCache[file] + let img = await Resource.loadImage(file) + let imgCl = new Image(file, img) + imageCache[file] = imgCl + return imgCl + } + + get width () { + return this.img.width + } + + get height () { + return this.img.height + } + + draw (x, y, w, h) { + ctx.drawImage(this.img, x, y, w, h) + } +} + +export { Image } diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..48ec239 --- /dev/null +++ b/src/index.js @@ -0,0 +1,131 @@ +/* global requestAnimationFrame */ +import { canvas, ctx } from './canvas' +import { TileMap, World } from './tiles' +import { HeightMap } from './heightmap' +import Debug from './debug' +import Input from './input' +import Viewport from './viewport' +import RES from './resource' + +let playing = false +let frameTime = 0 +let frameCount = 0 +let fps = 0 + +let vp = new Viewport(0, 0) + +let height = new HeightMap(0, 0, 16, 0) +let map = new TileMap('assets/ground.png', 32) + +const chunkSize = 32 +const tileSize = 12 + +// Define dirt tiles +map.define({ + 'DIRT_CORNER_TOP_LEFT': 0, + 'DIRT_TOP': 1, + 'DIRT_CORNER_TOP_RIGHT': 2, + 'DIRT_INNER_BOTTOM_RIGHT': 3, + 'DIRT_INNER_BOTTOM_LEFT': 4, + 'DIRT_LEFT': 32, + 'DIRT': 33, + 'DIRT_RIGHT': 34, + 'DIRT_INNER_TOP_RIGHT': 35, + 'DIRT_INNER_TOP_LEFT': 36, + 'DIRT_CORNER_BOTTOM_LEFT': 64, + 'DIRT_BOTTOM': 65, + 'DIRT_CORNER_BOTTOM_RIGHT': 66 +}) + +// Define grass tiles +map.define({ + 'GRASS_CORNER_TOP_LEFT': 5, + 'GRASS_TOP': 6, + 'GRASS_CORNER_TOP_RIGHT': 7, + 'GRASS_INNER_BOTTOM_RIGHT': 8, + 'GRASS_INNER_BOTTOM_LEFT': 9, + 'GRASS_LEFT': 37, + 'GRASS_MID': 38, + 'GRASS_RIGHT': 39, + 'GRASS_INNER_TOP_RIGHT': 40, + 'GRASS_INNER_TOP_LEFT': 41, + 'GRASS_CORNER_BOTTOM_LEFT': 69, + 'GRASS_BOTTOM': 70, + 'GRASS_CORNER_BOTTOM_RIGHT': 71 +}) + +map.define({ + 'AIR': -1, + 'STONE': 10 +}) + +let test = new World(height, { GROUND: map }, chunkSize, tileSize) + +function update (dt) { + test.update(dt, vp) + if (Input.isDown('w')) { + vp.y -= 5 + } else if (Input.isDown('s')) { + vp.y += 5 + } + + if (Input.isDown('a')) { + vp.x -= 5 + } else if (Input.isDown('d')) { + vp.x += 5 + } +} + +function draw () { + test.draw(vp) + Debug.draw(vp, test, fps) +} + +function step () { + draw() + + let ts = window.performance.now() + let timeDiff = ts - frameTime // time difference in milliseconds + frameCount++ + + if (timeDiff > 0) { + fps = Math.floor(frameCount / timeDiff * 1000) + frameCount = 0 + frameTime = ts + } + + update(timeDiff / 1000) +} + +function gameLoop () { + playing && requestAnimationFrame(gameLoop) + ctx.fillStyle = '#111' + ctx.fillRect(0, 0, canvas.width, canvas.height) + + step() + + Input.update() +} + +function start () { + Debug.createSliders(height, { + amplitude: [0, 100, 1], + persistence: [0.1, 5, 0.1], + octaves: [1, 16, 1], + period: [1, 100, 1], + lacunarity: [1, 5, 1] + }, function (key, val) { + test.chunks = [] + }) + playing = true + gameLoop() +} + +async function loadAll () { + let images = ['assets/ground.png'] + for (let i in images) { + await RES.loadImage(images[i]) + } +} + +loadAll().then(start) diff --git a/src/input.js b/src/input.js new file mode 100644 index 0000000..ecc59de --- /dev/null +++ b/src/input.js @@ -0,0 +1,223 @@ +import { canvas } from './canvas' + +const specialKeyMap = { + 'backspace': 8, + 'tab': 9, + 'enter': 13, + 'shift': 16, + 'ctrl': 17, + 'alt': 18, + 'pausebreak': 19, + 'capslock': 20, + 'escape': 27, + 'pgup': 33, + 'pgdown': 34, + 'end': 35, + 'home': 36, + 'left': 37, + 'up': 38, + 'right': 39, + 'down': 40, + 'insert': 45, + 'delete': 46, + 'left-window': 91, + 'right-window': 92, + 'select': 93, + 'numpad0': 96, + 'numpad1': 97, + 'numpad2': 98, + 'numpad3': 99, + 'numpad4': 100, + 'numpad5': 101, + 'numpad6': 102, + 'numpad7': 103, + 'numpad8': 104, + 'numpad9': 105, + 'multiply': 106, + 'add': 107, + 'subtract': 109, + 'decimal': 110, + 'divide': 111, + 'f1': 112, + 'f2': 113, + 'f3': 114, + 'f4': 115, + 'f5': 116, + 'f6': 117, + 'f7': 118, + 'f8': 119, + 'f9': 120, + 'f10': 121, + 'f11': 122, + 'f12': 123, + 'numlock': 144, + 'scrolllock': 145, + 'semi-colon': 186, + 'equals': 187, + 'comma': 188, + 'dash': 189, + 'period': 190, + 'fwdslash': 191, + 'grave': 192, + 'open-bracket': 219, + 'bkslash': 220, + 'close-braket': 221, + 'single-quote': 222 +} + +class Input { + constructor (canvas) { + this.keyList = {} + this.previousKeyList = {} + this.canvas = canvas + + this.mouse = { + pos: { x: 0, y: 0 }, + frame: { x: 0, y: 0 }, + previous: { x: 0, y: 0 } + } + + window.addEventListener('keydown', (e) => this.keyDown(e), false) + window.addEventListener('keyup', (e) => this.keyUp(e), false) + + canvas.addEventListener('mousemove', (e) => { + let x + let y + + if (e.changedTouches) { + let touch = e.changedTouches[0] + if (touch) { + e.pageX = touch.pageX + e.pageY = touch.pageY + } + } + + if (e.pageX || e.pageY) { + x = e.pageX + y = e.pageY + } else { + x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft + y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop + } + + x -= canvas.offsetLeft + y -= canvas.offsetTop + + this.mouse.frame.x = x + this.mouse.frame.y = y + }, false) + + canvas.addEventListener('mousedown', (e) => { + e.preventDefault() + this.mouse['btn' + e.button] = true + }) + + canvas.addEventListener('mouseup', (e) => { + e.preventDefault() + this.mouse['btn' + e.button] = false + }) + + canvas.addEventListener('contextmenu', (e) => { + e.preventDefault() + }) + } + + get mousePos () { + return this.mouse.pos + } + + get mouseMoved () { + return (this.mouse.pos.x !== this.mouse.previous.x || + this.mouse.pos.y !== this.mouse.previous.y) + } + + get mouseOffset () { + return { + x: this.mouse.previous.x - this.mouse.pos.x, + y: this.mouse.previous.y - this.mouse.pos.y + } + } + + toggleKey (keyCode, on) { + // Find key in special key list + let key = null + for (let k in specialKeyMap) { + let val = specialKeyMap[k] + if (keyCode === val) { + key = k + break + } + } + + // Use fromCharCode + if (!key) { + key = String.fromCharCode(keyCode).toLowerCase() + } + + this.keyList[key] = (on === true) + } + + keyDown (e) { + let keycode + + if (window.event) { + keycode = window.event.keyCode + } else if (e) { + keycode = e.which + } + + this.toggleKey(keycode, true) + } + + keyUp (e) { + let keycode + + if (window.event) { + keycode = window.event.keyCode + } else if (e) { + keycode = e.which + } + + this.toggleKey(keycode, false) + } + + down (key) { + return this.keyList[key] != null ? this.keyList[key] : false + } + + downLast (key) { + return this.previousKeyList[key] != null ? this.previousKeyList[key] : false + } + + isDown (key) { + return this.down(key) && this.downLast(key) + } + + isUp (key) { + return !this.isDown(key) + } + + isPressed (key) { + return this.down(key) === true && this.downLast(key) === false + } + + update () { + this.previousKeyList = {} + for (let k in this.keyList) { + if (this.keyList[k] === true) { + this.previousKeyList[k] = true + } + } + + // Mouse positions in the previous frame + this.mouse.previous.x = this.mouse.pos.x + this.mouse.previous.y = this.mouse.pos.y + + // Mouse positions in the current frame + // Convert to OpenGL coordinate system + this.mouse.pos.x = this.mouse.frame.x / this.canvas.width * 2 - 1 + this.mouse.pos.y = this.mouse.frame.y / this.canvas.height * 2 - 1 + } +} + +export default new Input(canvas) diff --git a/src/player.js b/src/player.js new file mode 100644 index 0000000..4157654 --- /dev/null +++ b/src/player.js @@ -0,0 +1,189 @@ +import { ctx } from './canvas' +import { GameObject } from './level' +import { deg2rad, rad2vec, intersectRect } from './utils' +import RES from './resource' + +const FULL_ROTATION = -85 +const FULL_ROTATION_EDGE = -FULL_ROTATION + +class Hook extends GameObject { + constructor (player, x, y, w, h, len) { + super(x, y, w, h) + this.player = player + + // Return position + this.rx = x + this.ry = y + + // Hook rotation + // Hook rotation direction + this.r = 0 + this.rd = 1 + + // Distance from center + // Moving direction + this.d = 0 + this.md = -1 + + // Travel length + this.len = len + + // Attached object + this.obj = null + } + + draw () { + if (this.md !== -1) { + // Draw line + ctx.beginPath() + ctx.moveTo(ctx.oX + this.rx, ctx.oY + this.ry) + ctx.lineTo(ctx.oX + this.x, ctx.oY + this.y) + ctx.stroke() + ctx.closePath() + } + + let hookr = deg2rad(360 - this.r) + ctx.save() + ctx.translate(ctx.oX + this.x, ctx.oY + this.y) + ctx.rotate(hookr) + ctx.drawImage(RES.loadImage('static/hook_open.png', true), -this.w / 2, -this.h / 2, this.w, this.h) + ctx.restore() + } + + clear () { + this.obj = null + this.d = 0 + this.md = -1 + this.x = this.rx + this.y = this.ry + } + + update (level) { + if (this.d === 0 && this.r < FULL_ROTATION_EDGE && this.rd === 1) { + this.r += 1 + } + + if (this.d === 0 && this.r > FULL_ROTATION && this.rd === 0) { + this.r -= 1 + } + + if (this.r >= FULL_ROTATION_EDGE && this.rd === 1) { + this.r = FULL_ROTATION_EDGE + this.rd = 0 + } + + if (this.r <= FULL_ROTATION && this.rd === 0) { + this.r = FULL_ROTATION + this.rd = 1 + } + + if (ctx.mouse.down['btn0'] && this.d === 0 && !this.obj) { + this.d = 0 + this.md = 1 + } + + if (this.md > -1) { + if (this.d > this.len && this.md === 1) { + this.md = 0 + } + + if (this.d <= 2 && this.md === 0) { + this.d = 0 + this.md = -1 + this.x = this.rx + this.y = this.ry + + // Score + if (this.obj) { + this.player.score(this.obj) + this.obj.destroy() + this.obj = null + } + return + } + + let dir = rad2vec(deg2rad(90 - this.r)) + dir.x *= this.d + dir.y *= this.d + + this.x = this.rx + dir.x + this.y = this.ry + dir.y + + if (this.obj) { + this.obj.x = this.x - this.obj.w / 2 + this.obj.y = this.y + } + + // Detect intersection + if (this.md === 1) { + if (!this.obj) { + let firstIntersect + for (let i in level.objects) { + let obj = level.objects[i] + if (obj.physical && intersectRect(obj, this)) { + firstIntersect = obj + break + } + } + + if (firstIntersect) { + if (firstIntersect.explode) { + let obj = firstIntersect.explode(level) + this.obj = obj + this.md = 0 + return + } + + this.obj = firstIntersect + this.md = 0 + return + } + } + + if (this.player.superStrength) { + this.d += 10 + } else { + this.d += 5 + } + } else { + if (this.player.superStrength) { + this.d -= 10 + } else { + this.d -= this.obj ? 5 * (1 - this.obj.weight) : 10 + } + } + } + } +} + +export class Player extends GameObject { + constructor (x, y, mh) { + super(x, y, 60, 55, '#4d4b4f') + this.x = x + this.y = y + + this.hook = new Hook(this, this.x + this.w / 2, this.y + this.h, 20, 20, mh) + this.hook.r = FULL_ROTATION + + this.superStrength = false + } + + score (object) { + console.log('Scored', object) + } + + draw () { + // Draw player + super.draw() + + // Draw hook + this.hook.draw() + + // + } + + update (level) { + if (!level) return + this.hook.update(level) + } +} diff --git a/src/resource.js b/src/resource.js new file mode 100644 index 0000000..eb1bbe9 --- /dev/null +++ b/src/resource.js @@ -0,0 +1,103 @@ +/* global XMLHttpRequest, Image */ + +let imgCache = {} + +function powerOfTwo (n) { + return n && (n & (n - 1)) === 0 +} + +function GET (url, istext) { + return new Promise((resolve, reject) => { + var xmlHttp = new XMLHttpRequest() + + xmlHttp.onreadystatechange = function () { + if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { + resolve(xmlHttp.responseText) + } else if (xmlHttp.readyState === 4 && xmlHttp.status >= 400) { + let err = new Error(xmlHttp.status) + err.request = xmlHttp + reject(err) + } + } + + xmlHttp.open('GET', url, true) + istext && (xmlHttp.responseType = 'text') + xmlHttp.send(null) + }) +} + +function smartGET (data) { + if (typeof data === 'string') { + data = { + url: data, + type: 'text' + } + } + + if (!data.type) data.type = 'text' + let istext = (data.type !== 'image' && data.type !== 'file') + + let url = data.url + if (!url) throw new Error('URL is required!') + + if (data.type === 'json') { + return new Promise((resolve, reject) => { + GET(url).then((dtext) => { + try { + let jsonp = JSON.parse(dtext) + return resolve(jsonp) + } catch (e) { + reject(e) + } + }, reject) + }) + } + + return GET(data.url, istext) +} + +function loadImage (url, onlyReturn = false, nowarn = false) { + if (onlyReturn && imgCache[url]) { + return imgCache[url] + } + + // Ensure we don't load a texture multiple times + if (imgCache[url]) return new Promise((resolve, reject) => resolve(imgCache[url])) + + return new Promise((resolve, reject) => { + let img = new Image() + + img.onload = function () { + // Friendly warnings + if ((!powerOfTwo(img.width) || !powerOfTwo(img.height)) && !nowarn) { + console.warn(`warn: image ${url} does not have dimensions that are powers of two. 16x16 to 128x128 recommended.`) + } + + if ((img.width / img.height) !== 1 && !nowarn) { + console.warn(`warn: image ${url} does not have an aspect ratio of 1 to 1.`) + } + + imgCache[url] = img + resolve(img) + } + + img.onerror = function (e) { + reject(e) + } + + img.src = url + }) +} + +function imageToSampler (img) { + let canvas = document.createElement('canvas') + let ctx = canvas.getContext('2d') + canvas.width = img.width + canvas.height = img.height + ctx.drawImage(img, 0, 0, img.width, img.height) + return function (x, y) { + return ctx.getImageData(x, y, 1, 1).data + } +} + +export default { GET: smartGET, imageToSampler, loadImage } diff --git a/src/tiles.js b/src/tiles.js new file mode 100644 index 0000000..51890dc --- /dev/null +++ b/src/tiles.js @@ -0,0 +1,192 @@ +import { ctx, ResourceCacheFactory } from './canvas' +import Resource from './resource' + +const cacheFactory = new ResourceCacheFactory() + +class TileMap { + constructor (image, rows) { + this._src = image + this.rows = rows + this.defs = {} + } + + get height () { + return this.image.height + } + + get width () { + return this.image.width + } + + get tile () { + return this.width / this.rows + } + + get image () { + return Resource.loadImage(this._src, true) + } + + tileAt (i) { + return { x: (i % this.rows) * this.tile, y: Math.floor(i / this.rows) * this.tile } + } + + define (tile, index) { + if (typeof tile === 'object') { + this.defs = Object.assign(this.defs, tile) + return + } + this.defs[tile] = index + } + + indexOf (tile) { + return this.defs[tile] || null + } + + positionOf (tile) { + return this.tileAt(this.indexOf(tile)) + } +} + +class TileChunk { + constructor (ix, iy, size = 16, tileSize = 16) { + this.x = ix + this.y = iy + this.size = size + this.tile = tileSize + this.tiles = [] + this.dirty = true + this.img = null + this._updated = false + } + + generateMap (tileMap, heightMap) { + for (let i = 0; i < this.size * this.size; i++) { + let tileCoords = this.toXY(i) + let tileAbs = this.toAbs(tileCoords) + let y = Math.ceil(heightMap.getHeight(tileAbs.x) * 5 / 2) - 4 + if (tileAbs.y < y) { + this.tiles.push(-1) + continue + } + if (tileAbs.y === y) { + this.tiles.push(tileMap.indexOf('GRASS_TOP')) + continue + } + if (tileAbs.y < y + 10) { + this.tiles.push(tileMap.indexOf('DIRT')) + continue + } + this.tiles.push(tileMap.indexOf('STONE')) + } + } + + tileAt (i) { + return this.tiles[i] + } + + tileAtXY (x, y) { + return this.tileAt(x + this.size * y) + } + + toXY (i) { + return { x: i % this.size, y: Math.floor(i / this.size) } + } + + toAbs (x, y) { + if (typeof x === 'object') { + y = x.y + x = x.x + } + return { x: this.x * this.size + x, y: this.y * this.size + y } + } + + get absPos () { + return { x: this.x * this.size * this.tile, y: this.y * this.size * this.tile } + } + + draw (view, map) { + // Create a cached image of the chunk + if (this.dirty || !this.img) { + cacheFactory.prepare(this.size * this.tile, this.size * this.tile) + for (let i in this.tiles) { + let tilei = this.tiles[i] + if (tilei === -1) continue + let coords = this.toXY(parseInt(i)) + let tileCoords = map.tileAt(tilei) + cacheFactory.ctx.drawImage(map.image, tileCoords.x, tileCoords.y, map.tile, map.tile, + coords.x * this.tile, coords.y * this.tile, this.tile, this.tile) + } + this.img = cacheFactory.capture() + this.dirty = false + this._updated = true + return + } + // Draw the cached image + let p = this.absPos + ctx.drawImage(this.img, p.x - view.x, p.y - view.y) + } + + update (dt) { + + } +} + +class World { + constructor (heightMap, tileMaps, chunkSize = 16, tileSize = 16) { + this.heightMap = heightMap + this.chunkSize = chunkSize + this.tileSize = tileSize + this.tileMaps = tileMaps + this.chunks = [] + + // Debug info + this._lastDrawCount = 0 + this._lastUpdateCount = 0 + } + + getChunk (x, y) { + for (let i in this.chunks) { + let chunk = this.chunks[i] + if (chunk && chunk.x === x && chunk.y === y) return chunk + } + return null + } + + update (dt, vp) { + let posPoint = vp.chunkIn(this.chunkSize * this.tileSize) + for (let x = posPoint.x - 4; x < posPoint.x + 5; x++) { + for (let y = posPoint.y - 4; y < posPoint.y + 5; y++) { + if (x < 0 || y < 0) continue + let exists = this.getChunk(x, y) + if (!exists) { + let n = new TileChunk(x, y, this.chunkSize, this.tileSize) + n.generateMap(this.tileMaps.GROUND, this.heightMap) + this.chunks.push(n) + break + } + } + } + + for (let i in this.chunks) { + this.chunks[i].update(dt) + } + } + + draw (vp) { + this._lastDrawCount = 0 + this._lastUpdateCount = 0 + for (let i in this.chunks) { + let chunk = this.chunks[i] + let absPos = chunk.absPos + let chunkSize = chunk.size * this.tileSize + if (absPos.x > vp.x + vp.width || absPos.x + chunkSize < vp.x || + absPos.y > vp.y + vp.height || absPos.y + chunkSize < vp.y) continue + chunk._updated = false + chunk.draw(vp, this.tileMaps.GROUND) + if (chunk._updated) this._lastUpdateCount++ + this._lastDrawCount++ + } + } +} + +export { TileMap, TileChunk, World } diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..0c082f0 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,29 @@ + +// Utility function + +export function randomi (min, max) { + return Math.floor(Math.random() * (max - min) + min) +} + +export function deg2rad (deg) { + return deg * Math.PI / 180 +} + +export function rad2vec (r) { + return { x: Math.cos(r), y: Math.sin(r) } +} + +export function intersectRect (r1, r2) { + return !(r2.x > r1.w + r1.x || + r2.w + r2.x < r1.x || + r2.y > r1.h + r1.y || + r2.h + r2.y < r1.y) +} + +export function distanceTo (o1, o2) { + return Math.sqrt(Math.pow(o2.x - o1.x, 2) + Math.pow(o2.y - o1.y, 2)) +} + +export function mobile () { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) +} diff --git a/src/viewport.js b/src/viewport.js new file mode 100644 index 0000000..566a040 --- /dev/null +++ b/src/viewport.js @@ -0,0 +1,23 @@ +import { canvas } from './canvas' + +class Viewport { + constructor (x, y, scale = 1) { + this.x = x + this.y = y + this.scale = scale + } + + get width () { + return canvas.width + } + + get height () { + return canvas.height + } + + chunkIn (size) { + return { x: Math.floor((this.x + this.width / 2) / size), y: Math.floor((this.y + this.height / 2) / size) } + } +} + +export default Viewport diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..7e4fab0 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,30 @@ +const path = require('path') +const HtmlWebpackPlugin = require('html-webpack-plugin') + +module.exports = (env) => { + return { + entry: './src/index.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'app.js' + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /(node_modules)/, + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env'] + } + } + } + ] + }, + devtool: env.mode === 'development' ? 'inline-source-map' : '', + plugins: [ + new HtmlWebpackPlugin({ template: 'index.html' }) + ] + } +}