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