beginnings of another project that will never be finished, yay
This commit is contained in:
commit
143a93f9f4
15
.babelrc
Normal file
15
.babelrc
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"presets": ["@babel/preset-env"],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@babel/plugin-transform-runtime",
|
||||||
|
{
|
||||||
|
"corejs": false,
|
||||||
|
"helpers": true,
|
||||||
|
"regenerator": true,
|
||||||
|
"useESModules": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@ -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
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/node_modules/
|
||||||
|
/dist/
|
||||||
|
/package-lock.json
|
||||||
|
/**/dna.txt
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
BIN
assets/ground.png
Normal file
BIN
assets/ground.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
10
index.html
Normal file
10
index.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<style type="text/css">*{margin:0;padding:0;}body{width:100%;height:100%;}body,html{overflow:hidden;}canvas{image-rendering: optimizeSpeed;image-rendering: -moz-crisp-edges;image-rendering: -webkit-optimize-contrast;image-rendering: -o-crisp-edges;image-rendering: pixelated;-ms-interpolation-mode: nearest-neighbor;}</style>
|
||||||
|
<title>tilegame</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
28
package.json
Normal file
28
package.json
Normal file
@ -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 <evert@lunasqu.ee>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
41
src/canvas.js
Normal file
41
src/canvas.js
Normal file
@ -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 }
|
49
src/debug.js
Normal file
49
src/debug.js
Normal file
@ -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()
|
90
src/heightmap.js
Normal file
90
src/heightmap.js
Normal file
@ -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 }
|
33
src/image.js
Normal file
33
src/image.js
Normal file
@ -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 }
|
131
src/index.js
Normal file
131
src/index.js
Normal file
@ -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)
|
223
src/input.js
Normal file
223
src/input.js
Normal file
@ -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)
|
189
src/player.js
Normal file
189
src/player.js
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
103
src/resource.js
Normal file
103
src/resource.js
Normal file
@ -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 }
|
192
src/tiles.js
Normal file
192
src/tiles.js
Normal file
@ -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 }
|
29
src/utils.js
Normal file
29
src/utils.js
Normal file
@ -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)
|
||||||
|
}
|
23
src/viewport.js
Normal file
23
src/viewport.js
Normal file
@ -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
|
30
webpack.config.js
Normal file
30
webpack.config.js
Normal file
@ -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' })
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user