commit 2351c9367767ec7896ccd1259960041aac26853a Author: Zutatensuppe Date: Sat Nov 7 11:35:29 2020 +0100 dirty initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d79a1d8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd1ffe6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/server/node_modules diff --git a/game/Bitmap.js b/game/Bitmap.js new file mode 100644 index 0000000..7bca65d --- /dev/null +++ b/game/Bitmap.js @@ -0,0 +1,78 @@ +import BoundingRectangle from './BoundingRectangle.js' + +export default class Bitmap { + constructor(width, height, rgba = null) { + this._w = width + this._h = height + this._com = 4 // number of components per pixel (RGBA) + this._boundingRect = new BoundingRectangle(0, this._w - 1, 0, this._h -1) + const len = this._w * this._h * this._com + this._data = new Uint8ClampedArray(len) + if (rgba) { + for (let i = 0; i < len; i+=4) { + this._data[i] = rgba[0] + this._data[i + 1] = rgba[1] + this._data[i + 2] = rgba[2] + this._data[i + 3] = rgba[3] + } + } + + // public + this.width = this._w + this.height = this._h + } + + toImage() { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + canvas.width = this._w; + canvas.height = this._h; + + const imgData = ctx.createImageData(canvas.width, canvas.height); + imgData.data.set(this._data); + ctx.putImageData(imgData, 0, 0); + + + return new Promise((resolve) => { + + const img = document.createElement('img') + img.onload = () => { + resolve(img) + } + img.src = canvas.toDataURL() + return img + }) + } + + getPix(x, y, out) { + if (x < 0 || y < 0 || x >= this._w || y >= this._h) { + return false; + } + x = Math.round(x) + y = Math.round(y) + const idx = (y * 4 * this._w) + (x * 4) + out[0] = this._data[idx] + out[1] = this._data[idx + 1] + out[2] = this._data[idx + 2] + out[3] = this._data[idx + 3] + return true + } + + putPix(x, y, rgba) { + if (x < 0 || y < 0 || x >= this._w || y >= this._h) { + return; + } + x = Math.round(x) + y = Math.round(y) + const idx = (y * this._com * this._w) + (x * this._com) + this._data[idx] = rgba[0] + this._data[idx + 1] = rgba[1] + this._data[idx + 2] = rgba[2] + this._data[idx + 3] = rgba[3] + } + + getBoundingRect() { + return this._boundingRect + } +} \ No newline at end of file diff --git a/game/BoundingRectangle.js b/game/BoundingRectangle.js new file mode 100644 index 0000000..8312034 --- /dev/null +++ b/game/BoundingRectangle.js @@ -0,0 +1,46 @@ +import Point from './Point.js' + +export default class BoundingRectangle { + constructor(x0, x1, y0, y1) { + this.x0 = x0 + this.x1 = x1 + this.y0 = y0 + this.y1 = y1 + this.width = (x1 - x0) + 1 + this.height = (y1 - y0) + 1 + } + + div(d) { + this.x0 /= d + this.x1 /= d + this.y0 /= d + this.y1 /= d + } + + move(x, y) { + this.x0 += x + this.x1 += x + this.y0 += y + this.y1 += y + } + + moved(x, y) { + return new BoundingRectangle( + this.x0 + x, + this.x1 + x, + this.y0 + y, + this.y1 + y + ) + } + + center() { + return new Point( + this.x0 + this.width / 2, + this.y0 + this.height / 2, + ) + } + + centerDistance(other) { + return this.center().distance(other.center()) + } +} \ No newline at end of file diff --git a/game/Camera.js b/game/Camera.js new file mode 100644 index 0000000..4a5627a --- /dev/null +++ b/game/Camera.js @@ -0,0 +1,77 @@ +import BoundingRectangle from "./BoundingRectangle.js" + +export default class Camera { + constructor(canvas) { + this.x = 0 + this.y = 0 + + // TODO: when canvas resizes, this should + // syncronize with the cam + this.width = canvas.width + this.height = canvas.height + + this.zoom = 1 + this.minZoom = .2 + this.maxZoom = 6 + this.zoomStep = .05 + } + + rect() { + // when no zoom is relevant: + return new BoundingRectangle( + this.x, + this.x + this.width - 1, + this.y, + this.y + this.height - 1 + ) + + // when zoom is relevant: + // TODO: check if still true + const w_final = this.width * this.zoom + const h_final = this.height * this.zoom + return new BoundingRectangle( + this.x + (this.width - w_final) / 2, + this.x + (this.width + w_final) / 2, + this.y + (this.height - h_final) / 2, + this.y + (this.height + h_final) / 2 + ) + } + + move(x, y) { + this.x += x / this.zoom + this.y += y / this.zoom + } + + zoomOut() { + const newzoom = Math.max(this.zoom - this.zoomStep, this.minZoom) + if (newzoom !== this.zoom) { + // centered zoom + this.x -= ((this.width / this.zoom) - (this.width / newzoom)) / 2 + this.y -= ((this.height / this.zoom) - (this.height / newzoom)) / 2 + + this.zoom = newzoom + return true + } + return false + } + + zoomIn() { + const newzoom = Math.min(this.zoom + this.zoomStep, this.maxZoom) + if (newzoom !== this.zoom) { + // centered zoom + this.x -= ((this.width / this.zoom) - (this.width / newzoom)) / 2 + this.y -= ((this.height / this.zoom) - (this.height / newzoom)) / 2 + + this.zoom = newzoom + return true + } + return false + } + + translateMouse(mouse) { + return { + x: (mouse.x / this.zoom) - this.x, + y: (mouse.y / this.zoom) - this.y, + } + } +} \ No newline at end of file diff --git a/game/CanvasAdapter.js b/game/CanvasAdapter.js new file mode 100644 index 0000000..ab1dde9 --- /dev/null +++ b/game/CanvasAdapter.js @@ -0,0 +1,58 @@ +import BoundingRectangle from './BoundingRectangle.js' + +export default class CanvasAdapter { + constructor(canvas) { + this._canvas = canvas + this._ctx = this._canvas.getContext('2d') + this._w = this._canvas.width + this._h = this._canvas.height + this._boundingRect = new BoundingRectangle(0, this._w - 1, 0, this._h - 1) + + this._imageData = this._ctx.createImageData(this._w, this._h) + this._data = this._imageData.data + + this.width = this._w + this.height = this._h + } + + clear() { + this._imageData = this._ctx.createImageData(this._w, this._h) + this._data = this._imageData.data + this.apply() + } + + getPix(x, y, out) { + if (x < 0 || y < 0 || x >= this._w || y >= this._h) { + return false; + } + x = Math.round(x) + y = Math.round(y) + const idx = (y * 4 * this._w) + (x * 4) + out[0] = this._data[idx] + out[1] = this._data[idx + 1] + out[2] = this._data[idx + 2] + out[3] = this._data[idx + 3] + return true + } + + putPix(x, y, rgba) { + if (x < 0 || y < 0 || x >= this._w || y >= this._h) { + return null; + } + x = Math.round(x) + y = Math.round(y) + const idx = (y * 4 * this._w) + (x * 4) + this._data[idx] = rgba[0] + this._data[idx + 1] = rgba[1] + this._data[idx + 2] = rgba[2] + this._data[idx + 3] = rgba[3] + } + + getBoundingRect() { + return this._boundingRect + } + + apply() { + this._ctx.putImageData(this._imageData, 0, 0) + } +} diff --git a/game/Color.js b/game/Color.js new file mode 100644 index 0000000..10484fb --- /dev/null +++ b/game/Color.js @@ -0,0 +1,22 @@ +export function tint(c, f) { + return [ + Math.max(0, Math.min(255, Math.round((255 - c[0]) * f))), + Math.max(0, Math.min(255, Math.round((255 - c[1]) * f))), + Math.max(0, Math.min(255, Math.round((255 - c[2]) * f))), + c[3] + ] +} + +export function shade(c, f) { + return [ + Math.max(0, Math.min(255, Math.round(c[0] * f))), + Math.max(0, Math.min(255, Math.round(c[1] * f))), + Math.max(0, Math.min(255, Math.round(c[2] * f))), + c[3] + ] +} + +export default { + tint, + shade +} diff --git a/game/EventAdapter.js b/game/EventAdapter.js new file mode 100644 index 0000000..d021662 --- /dev/null +++ b/game/EventAdapter.js @@ -0,0 +1,38 @@ +export default class EventAdapter { + constructor(canvas) { + this._mouseEvts = [] + canvas.addEventListener('mousedown', this._mouseDown.bind(this)) + canvas.addEventListener('mouseup', this._mouseUp.bind(this)) + canvas.addEventListener('mousemove', this._mouseMove.bind(this)) + canvas.addEventListener('wheel', this._wheel.bind(this)) + } + + consumeAll() { + if (this._mouseEvts.length === 0) { + return [] + } + const all = this._mouseEvts.slice() + this._mouseEvts = [] + return all + } + + _mouseDown(e) { + if (e.button === 0) { + this._mouseEvts.push({type: 'down', x: e.offsetX, y: e.offsetY}) + } + } + + _mouseUp(e) { + if (e.button === 0) { + this._mouseEvts.push({type: 'up', x: e.offsetX, y: e.offsetY}) + } + } + + _mouseMove(e) { + this._mouseEvts.push({type: 'move', x: e.offsetX, y: e.offsetY}) + } + + _wheel(e) { + this._mouseEvts.push({type: 'wheel', y: e.deltaY}) + } +} \ No newline at end of file diff --git a/game/Point.js b/game/Point.js new file mode 100644 index 0000000..fa855b1 --- /dev/null +++ b/game/Point.js @@ -0,0 +1,27 @@ +export default class Point { + constructor(x,y) { + this.x = x + this.y = y + } + move(x, y) { + this.x += x + this.y += y + } + add(other) { + return new Point( + this.x + other.x, + this.y + other.y + ) + } + sub(other) { + return new Point( + this.x - other.x, + this.y - other.y + ) + } + distance(other) { + const diffX = this.x - other.x + const diffY = this.y - other.y + return Math.sqrt(diffX * diffX + diffY * diffY) + } +} diff --git a/game/WsClient.js b/game/WsClient.js new file mode 100644 index 0000000..7739e3e --- /dev/null +++ b/game/WsClient.js @@ -0,0 +1,64 @@ +import WsWrapper from './WsWrapper.js' + +export default class WsClient extends WsWrapper { + constructor(addr, protocols) { + super(addr, protocols) + this._on = {} + this.onopen = (e) => { + this._dispatch('socket', 'open', e) + } + this.onmessage = (e) => { + this._dispatch('socket', 'message', e) + if (!!this._on['message']) { + const d = this._parseMessageData(e.data) + if (d.event) { + this._dispatch('message', d.event, d.data) + } + } + } + this.onclose = (e) => { + this._dispatch('socket', 'close', e) + } + } + + onSocket(tag, callback) { + this.addEventListener('socket', tag, callback) + } + + onMessage(tag, callback) { + this.addEventListener('message', tag, callback) + } + + addEventListener(type, tag, callback) { + const tags = Array.isArray(tag) ? tag : [tag] + this._on[type] = this._on[type] || {} + for (const t of tags) { + this._on[type][t] = this._on[type][t] || [] + this._on[type][t].push(callback) + } + } + + _parseMessageData(data) { + try { + const d = JSON.parse(data) + if (d.event) { + return {event: d.event, data: d.data || null} + } + } catch { + } + return {event: null, data: null} + } + + _dispatch(type, tag, ...args) { + const t = this._on[type] || {} + const callbacks = (t[tag] || []) + if (callbacks.length === 0) { + return + } + + console.log(`ws dispatch ${type} ${tag}`) + for (const callback of callbacks) { + callback(...args) + } + } +} diff --git a/game/WsWrapper.js b/game/WsWrapper.js new file mode 100644 index 0000000..ca5edb1 --- /dev/null +++ b/game/WsWrapper.js @@ -0,0 +1,55 @@ +/** + * Wrapper around ws that + * - buffers 'send' until a connection is available + * - automatically tries to reconnect on close + */ +export default class WsWrapper { + // actual ws handle + handle = null + + // timeout for automatic reconnect + reconnectTimeout = null + + // buffer for 'send' + sendBuffer = [] + + constructor(addr, protocols) { + this.addr = addr + this.protocols = protocols + + this.onopen = () => {} + this.onclose = () => {} + this.onmessage = () => {} + } + + send (txt) { + if (this.handle) { + this.handle.send(txt) + } else { + this.sendBuffer.push(txt) + } + } + + connect() { + let ws = new WebSocket(this.addr, this.protocols) + ws.onopen = (e) => { + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout) + } + this.handle = ws + // should have a queue worker + while (this.sendBuffer.length > 0) { + this.handle.send(this.sendBuffer.shift()) + } + this.onopen(e) + } + ws.onmessage = (e) => { + this.onmessage(e) + } + ws.onclose = (e) => { + this.handle = null + this.reconnectTimeout = setTimeout(() => { this.connect() }, 1000) + this.onclose(e) + } + } +} diff --git a/game/example-images/132-2048x1365.jpg b/game/example-images/132-2048x1365.jpg new file mode 100644 index 0000000..2093e06 Binary files /dev/null and b/game/example-images/132-2048x1365.jpg differ diff --git a/game/example-images/ima_86ec3fa.jpeg b/game/example-images/ima_86ec3fa.jpeg new file mode 100644 index 0000000..25f77b5 Binary files /dev/null and b/game/example-images/ima_86ec3fa.jpeg differ diff --git a/game/example-images/saechsische_schweiz.jpg b/game/example-images/saechsische_schweiz.jpg new file mode 100644 index 0000000..16fc0ca Binary files /dev/null and b/game/example-images/saechsische_schweiz.jpg differ diff --git a/game/gameloop.js b/game/gameloop.js new file mode 100644 index 0000000..857b9fc --- /dev/null +++ b/game/gameloop.js @@ -0,0 +1,31 @@ +export const run = options => { + const fps = options.fps || 60 + const slow = options.slow || 1 + const update = options.update + const render = options.render + const raf = window.requestAnimationFrame + const step = 1 / fps + const slowStep = slow * step + + let now + let dt = 0 + let last = window.performance.now() + + const frame = () => { + now = window.performance.now() + dt = dt + Math.min(1, (now - last) / 1000) // duration capped at 1.0 seconds + while (dt > slowStep) { + dt = dt - slowStep + update(step) + } + render(dt / slow) + last = now + raf(frame) + } + + raf(frame) +} + +export default { + run +} \ No newline at end of file diff --git a/game/index.html b/game/index.html new file mode 100644 index 0000000..add5b41 --- /dev/null +++ b/game/index.html @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/game/index.js b/game/index.js new file mode 100644 index 0000000..d907d41 --- /dev/null +++ b/game/index.js @@ -0,0 +1,1073 @@ +"use strict" +import CanvasAdapter from './CanvasAdapter.js' +import BoundingRectangle from './BoundingRectangle.js' +import Bitmap from './Bitmap.js' +import {run} from './gameloop.js' +import Camera from './Camera.js' +import Point from './Point.js' +import EventAdapter from './EventAdapter.js' +import { choice } from './util.js' + +import WsClient from './WsClient.js' + +const TILE_SIZE = 64 // cut size of each puzzle tile in the + // final resized version of the puzzle image +const TARGET_TILES = 1000 // desired number of tiles + // actual calculated number can be higher +const IMAGES = [ + './example-images/ima_86ec3fa.jpeg', + './example-images/saechsische_schweiz.jpg', + './example-images/132-2048x1365.jpg', +] +const IMAGE_URL = IMAGES[0] + +function createCanvas(width = 0, height = 0) { + const canvas = document.createElement('canvas') + canvas.width = width === 0 ? window.innerWidth : width + canvas.height = height === 0 ? window.innerHeight : height + return canvas +} + +function addCanvasToDom(canvas) { + document.body.append(canvas) + return canvas +} + +function fillBitmap (bitmap, rgba) { + const len = bitmap.width * bitmap.height * 4 + bitmap._data = new Uint8ClampedArray(len) + for (let i = 0; i < len; i+=4) { + bitmap._data[i] = rgba[0] + bitmap._data[i + 1] = rgba[1] + bitmap._data[i + 2] = rgba[2] + bitmap._data[i + 3] = rgba[3] + } +} + +function fillBitmapCapped(bitmap, rgba, rect_cap) { + if (!rect_cap) { + return fillBitmap(bitmap, rgba) + } + let startX = Math.floor(Math.max(rect_cap.x0)) + let startY = Math.floor(Math.max(rect_cap.y0)) + + let endX = Math.ceil(Math.min(rect_cap.x1)) + let endY = Math.ceil(Math.min(rect_cap.y1)) + + for (let x = startX; x < endX; x++) { + for (let y = startY; y < endY; y++) { + bitmap.putPix(x, y, rgba) + } + } +} + +function mapBitmapToBitmap (bitmap_src, rect_src, bitmap_dst, rect_dst) { + const tmp = new Uint8ClampedArray(4) + const w_f = (rect_src.width) / (rect_dst.width) + const h_f = (rect_src.height) / (rect_dst.height) + + let startX = Math.max(rect_dst.x0, Math.floor((-rect_src.x0 / w_f) + rect_dst.x0)) + let startY = Math.max(rect_dst.y0, Math.floor((-rect_src.y0 / h_f) + rect_dst.y0)) + + let endX = Math.min(rect_dst.x1, Math.ceil(((bitmap_src._w - rect_src.x0) / w_f) + rect_dst.x0)) + let endY = Math.min(rect_dst.y1, Math.ceil(((bitmap_src._h - rect_src.y0) / h_f) + rect_dst.y0)) + + for (let x = startX; x < endX; x++) { + for (let y = startY; y < endY; y++) { + const src_x = rect_src.x0 + Math.floor((x - rect_dst.x0) * w_f) + const src_y = rect_src.y0 + Math.floor((y - rect_dst.y0) * h_f) + if (bitmap_src.getPix(src_x, src_y, tmp)) { + if (tmp[3] === 255) { + bitmap_dst.putPix(x, y, tmp) + } + } + } + } +} + +function mapBitmapToBitmapCapped (bitmap_src, rect_src, bitmap_dst, rect_dst, rect_cap) { + if (!rect_cap) { + return mapBitmapToBitmap(bitmap_src, rect_src, bitmap_dst, rect_dst) + } + const tmp = new Uint8ClampedArray(4) + const w_f = (rect_src.width) / (rect_dst.width) + const h_f = (rect_src.height) / (rect_dst.height) + + let startX = Math.floor(Math.max(rect_cap.x0, rect_dst.x0, (-rect_src.x0 / w_f) + rect_dst.x0)) + let startY = Math.floor(Math.max(rect_cap.y0, rect_dst.y0, (-rect_src.y0 / h_f) + rect_dst.y0)) + + let endX = Math.ceil(Math.min(rect_cap.x1, rect_dst.x1, ((bitmap_src._w - rect_src.x0) / w_f) + rect_dst.x0)) + let endY = Math.ceil(Math.min(rect_cap.y1, rect_dst.y1, ((bitmap_src._h - rect_src.y0) / h_f) + rect_dst.y0)) + + for (let x = startX; x < endX; x++) { + for (let y = startY; y < endY; y++) { + const src_x = rect_src.x0 + Math.floor((x - rect_dst.x0) * w_f) + const src_y = rect_src.y0 + Math.floor((y - rect_dst.y0) * h_f) + if (bitmap_src.getPix(src_x, src_y, tmp)) { + if (tmp[3] === 255) { + bitmap_dst.putPix(x, y, tmp) + } + } + } + } +} + +function mapBitmapToAdapter (bitmap_src, rect_src, adapter_dst, rect_dst) { + const tmp = new Uint8ClampedArray(4) + const w_f = (rect_src.x1 - rect_src.x0) / (rect_dst.x1 - rect_dst.x0) + const h_f = (rect_src.y1 - rect_src.y0) / (rect_dst.y1 - rect_dst.y0) + + let startX = Math.max(rect_dst.x0, Math.floor((-rect_src.x0 / w_f) + rect_dst.x0)) + let startY = Math.max(rect_dst.y0, Math.floor((-rect_src.y0 / h_f) + rect_dst.y0)) + + let endX = Math.min(rect_dst.x1, Math.ceil(((bitmap_src._w - rect_src.x0) / w_f) + rect_dst.x0)) + let endY = Math.min(rect_dst.y1, Math.ceil(((bitmap_src._h - rect_src.y0) / h_f) + rect_dst.y0)) + + for (let x = startX; x < endX; x++) { + for (let y = startY; y < endY; y++) { + const src_x = rect_src.x0 + Math.floor((x - rect_dst.x0) * w_f) + const src_y = rect_src.y0 + Math.floor((y - rect_dst.y0) * h_f) + if (bitmap_src.getPix(src_x, src_y, tmp)) { + if (tmp[3] === 255) { + adapter_dst.putPix(x, y, tmp) + } + } + } + } + adapter_dst.apply() +} + +function copy(src) { + var arr = new Uint8ClampedArray(src.length) + arr.set(new Uint8ClampedArray(src)); + return arr +} + +function dataToBitmap(w, h, data) { + const bitmap = new Bitmap(w, h) + bitmap._data = copy(data) + return bitmap +} + +function imageToBitmap(img) { + const c = createCanvas(img.width, img.height) + const ctx = c.getContext('2d') + ctx.drawImage(img, 0, 0) + const data = ctx.getImageData(0, 0, c.width, c.height).data + + return dataToBitmap(c.width, c.height, data) +} + +function loadImageToBitmap(imagePath) { + return new Promise((resolve) => { + const img = new Image() + img.onload= () => { + resolve(imageToBitmap(img)) + } + img.src = imagePath + }) +} + +function pointInBounds(pt, rect) { + return pt.x >= rect.x0 && pt.x <= rect.x1 && pt.y >= rect.y0 && pt.y <= rect.y1 +} + +const tilesFit = (w, h, size) => { + return Math.floor(w / size) * Math.floor(h / size) +} + +const coordsByNum = (puzzleInfo) => { + const w_tiles = puzzleInfo.width / puzzleInfo.tileSize + const coords = new Array(puzzleInfo.tiles) + for (let i = 0; i < puzzleInfo.tiles; i++) { + const y = Math.floor(i / w_tiles) + const x = i % w_tiles + coords[i] = {x, y} + } + return coords +} + +const determinePuzzleInfo = (w, h, targetTiles) => { + let tileSize = 0 + let tiles = 0 + do { + tileSize++ + tiles = tilesFit(w, h, tileSize) + } while (tiles >= targetTiles) + tileSize-- + + tiles = tilesFit(w, h, tileSize) + let tiles_x = Math.round(w / tileSize) + let tiles_y = Math.round(h / tileSize) + tiles = tiles_x * tiles_y + + // then resize to final TILE_SIZE (which is always the same) + tileSize = TILE_SIZE + let width = tiles_x * tileSize + let height = tiles_y * tileSize + let coords = coordsByNum({width, height, tileSize, tiles}) + + var tileMarginWidth = tileSize * .5; + var tileDrawSize = Math.round(tileSize + tileMarginWidth*2) + + const info = { + width, + height, + tileSize, + tileMarginWidth, + tileDrawSize, + tiles, + tiles_x, + tiles_y, + coords, + } + return info +} + +const resizeBitmap = (bitmap, width, height) => { + const tmp = new Bitmap(width, height) + mapBitmapToBitmap( + bitmap, + bitmap.getBoundingRect(), + tmp, + tmp.getBoundingRect() + ) + return tmp +} + +function getSurroundingTilesByIdx(puzzle, idx) { + var _X = puzzle.info.coords[idx].x + var _Y = puzzle.info.coords[idx].y + + return [ + // top + _Y === 0 ? null : puzzle.tiles[idx - puzzle.info.tiles_x], + // right + (_X === puzzle.info.tiles_x - 1) ? null : puzzle.tiles[idx + 1], + // bottom + (_Y === puzzle.info.tiles_y - 1) ? null : puzzle.tiles[idx + puzzle.info.tiles_x], + // left + _X === 0 ? null : puzzle.tiles[idx - 1] + ] +} + +function determinePuzzleTileShapes (tiles, info) { + var tabs = [-1, 1] + for (let IDX in tiles) { + var _X = info.coords[IDX].x + var _Y = info.coords[IDX].y + + var topTab = _Y === 0 ? 0 : tiles[IDX - info.tiles_x].tabs.bottom * -1; + var rightTab = _X === info.tiles_x -1 ? 0 : choice(tabs) + var leftTab = _X === 0 ? 0 : tiles[IDX - 1].tabs.right * -1 + var bottomTab = _Y === info.tiles_y -1 ? 0 : choice(tabs) + + tiles[IDX].tabs = { + top: topTab, + right: rightTab, + left: leftTab, + bottom: bottomTab, + } + } + return tiles +} + +async function createPuzzleTileBitmaps (bitmap, tiles, info) { + let img = await bitmap.toImage() + var tileSize = info.tileSize + var tileMarginWidth = info.tileMarginWidth + var tileDrawSize = info.tileDrawSize + var tileRatio = tileSize / 100.0 + + var curvyCoords = [ + 0, 0, 40, 15, 37, 5, + 37, 5, 40, 0, 38, -5, + 38, -5, 20, -20, 50, -20, + 50, -20, 80, -20, 62, -5, + 62, -5, 60, 0, 63, 5, + 63, 5, 65, 15, 100, 0 + ]; + + const bitmaps = new Array(tiles.length) + + for (let tile of tiles) { + let c = createCanvas(tileDrawSize, tileDrawSize) + let ctx = c.getContext('2d') + ctx.clearRect(0, 0,tileDrawSize, tileDrawSize) + + var topTab = tile.tabs.top + var rightTab = tile.tabs.right + var leftTab = tile.tabs.left + var bottomTab = tile.tabs.bottom + + var topLeftEdge = new Point(tileMarginWidth, tileMarginWidth); + ctx.save(); + ctx.beginPath() + ctx.moveTo(topLeftEdge.x, topLeftEdge.y) + for (let i = 0; i < curvyCoords.length / 6; i++) { + var p1 = topLeftEdge.add(new Point( curvyCoords[i * 6 + 0] * tileRatio, topTab * curvyCoords[i * 6 + 1] * tileRatio) ); + var p2 = topLeftEdge.add(new Point( curvyCoords[i * 6 + 2] * tileRatio, topTab * curvyCoords[i * 6 + 3] * tileRatio) ); + var p3 = topLeftEdge.add(new Point( curvyCoords[i * 6 + 4] * tileRatio, topTab * curvyCoords[i * 6 + 5] * tileRatio) ); + ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + } + + //Right + var topRightEdge = topLeftEdge.add(new Point(tileSize, 0)); + for (var i = 0; i < curvyCoords.length / 6; i++) { + var p1 = topRightEdge.add(new Point(-rightTab * curvyCoords[i * 6 + 1] * tileRatio, curvyCoords[i * 6 + 0] * tileRatio)) + var p2 = topRightEdge.add(new Point(-rightTab * curvyCoords[i * 6 + 3] * tileRatio, curvyCoords[i * 6 + 2] * tileRatio)) + var p3 = topRightEdge.add(new Point(-rightTab * curvyCoords[i * 6 + 5] * tileRatio, curvyCoords[i * 6 + 4] * tileRatio)) + ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + } + //Bottom + var bottomRightEdge = topRightEdge.add(new Point(0, tileSize)) + for (var i = 0; i < curvyCoords.length / 6; i++) { + var p1 = bottomRightEdge.sub(new Point(curvyCoords[i * 6 + 0] * tileRatio, bottomTab * curvyCoords[i * 6 + 1] * tileRatio)) + var p2 = bottomRightEdge.sub(new Point(curvyCoords[i * 6 + 2] * tileRatio, bottomTab * curvyCoords[i * 6 + 3] * tileRatio)) + var p3 = bottomRightEdge.sub(new Point(curvyCoords[i * 6 + 4] * tileRatio, bottomTab * curvyCoords[i * 6 + 5] * tileRatio)) + ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + } + //Left + var bottomLeftEdge = bottomRightEdge.sub(new Point(tileSize, 0)); + for (var i = 0; i < curvyCoords.length / 6; i++) { + var p1 = bottomLeftEdge.sub(new Point(-leftTab * curvyCoords[i * 6 + 1] * tileRatio, curvyCoords[i * 6 + 0] * tileRatio)) + var p2 = bottomLeftEdge.sub(new Point(-leftTab * curvyCoords[i * 6 + 3] * tileRatio, curvyCoords[i * 6 + 2] * tileRatio)) + var p3 = bottomLeftEdge.sub(new Point(-leftTab * curvyCoords[i * 6 + 5] * tileRatio, curvyCoords[i * 6 + 4] * tileRatio)) + ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + } + + const srcRect = srcRectByIdx(info, tile.idx) + ctx.clip() + ctx.drawImage( + img, + srcRect.x0 - tileMarginWidth, + srcRect.y0 - tileMarginWidth, + tileDrawSize, + tileDrawSize, + 0, + 0, + tileDrawSize, + tileDrawSize, + ) + ctx.closePath() + ctx.restore(); + + + // ----------------------------------------------------------- + // ----------------------------------------------------------- + var topLeftEdge = new Point(tileMarginWidth, tileMarginWidth); + ctx.save() + ctx.beginPath() + ctx.moveTo(topLeftEdge.x, topLeftEdge.y) + for (let i = 0; i < curvyCoords.length / 6; i++) { + var p1 = topLeftEdge.add(new Point( curvyCoords[i * 6 + 0] * tileRatio, topTab * curvyCoords[i * 6 + 1] * tileRatio) ); + var p2 = topLeftEdge.add(new Point( curvyCoords[i * 6 + 2] * tileRatio, topTab * curvyCoords[i * 6 + 3] * tileRatio) ); + var p3 = topLeftEdge.add(new Point( curvyCoords[i * 6 + 4] * tileRatio, topTab * curvyCoords[i * 6 + 5] * tileRatio) ); + ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + } + + //Right + var topRightEdge = topLeftEdge.add(new Point(tileSize, 0)); + for (var i = 0; i < curvyCoords.length / 6; i++) { + var p1 = topRightEdge.add(new Point(-rightTab * curvyCoords[i * 6 + 1] * tileRatio, curvyCoords[i * 6 + 0] * tileRatio)) + var p2 = topRightEdge.add(new Point(-rightTab * curvyCoords[i * 6 + 3] * tileRatio, curvyCoords[i * 6 + 2] * tileRatio)) + var p3 = topRightEdge.add(new Point(-rightTab * curvyCoords[i * 6 + 5] * tileRatio, curvyCoords[i * 6 + 4] * tileRatio)) + ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + } + //Bottom + var bottomRightEdge = topRightEdge.add(new Point(0, tileSize)) + for (var i = 0; i < curvyCoords.length / 6; i++) { + var p1 = bottomRightEdge.sub(new Point(curvyCoords[i * 6 + 0] * tileRatio, bottomTab * curvyCoords[i * 6 + 1] * tileRatio)) + var p2 = bottomRightEdge.sub(new Point(curvyCoords[i * 6 + 2] * tileRatio, bottomTab * curvyCoords[i * 6 + 3] * tileRatio)) + var p3 = bottomRightEdge.sub(new Point(curvyCoords[i * 6 + 4] * tileRatio, bottomTab * curvyCoords[i * 6 + 5] * tileRatio)) + ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + } + //Left + var bottomLeftEdge = bottomRightEdge.sub(new Point(tileSize, 0)); + for (var i = 0; i < curvyCoords.length / 6; i++) { + var p1 = bottomLeftEdge.sub(new Point(-leftTab * curvyCoords[i * 6 + 1] * tileRatio, curvyCoords[i * 6 + 0] * tileRatio)) + var p2 = bottomLeftEdge.sub(new Point(-leftTab * curvyCoords[i * 6 + 3] * tileRatio, curvyCoords[i * 6 + 2] * tileRatio)) + var p3 = bottomLeftEdge.sub(new Point(-leftTab * curvyCoords[i * 6 + 5] * tileRatio, curvyCoords[i * 6 + 4] * tileRatio)) + ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + } + + ctx.lineWidth = 2 + ctx.stroke() + ctx.closePath() + ctx.restore() + // ----------------------------------------------------------- + // ----------------------------------------------------------- + + + const data = ctx.getImageData(0, 0, tileDrawSize, tileDrawSize).data + const bitmap = dataToBitmap(tileDrawSize, tileDrawSize, data) + + bitmaps[tile.idx] = bitmap + } + + return bitmaps +} + +function srcRectByIdx (puzzleInfo, idx) { + let c = puzzleInfo.coords[idx] + let cx = c.x * puzzleInfo.tileSize + let cy = c.y * puzzleInfo.tileSize + return new BoundingRectangle( + cx, + cx + puzzleInfo.tileSize, + cy, + cy + puzzleInfo.tileSize + ) +} + +function pointSub (a, b) { + return {x: a.x - b.x, y: a.y - b.y} +} + +function pointAdd (a, b) { + return {x: a.x + b.x, y: a.y + b.y} +} + +// Returns the index of the puzzle tile with the highest z index +// that is not finished yet and that matches the position +const unfinishedTileByPos = (puzzle, pos) => { + let maxZ = -1 + let tileIdx = -1 + for (let idx = 0; idx < puzzle.tiles.length; idx++) { + let tile = puzzle.tiles[idx] + if (tile.finished) { + continue + } + + // TODO: store collision boxes on the tiles + const collisionRect = new BoundingRectangle( + tile.pos.x, + tile.pos.x + puzzle.info.tileSize - 1, + tile.pos.y, + tile.pos.y + puzzle.info.tileSize - 1, + ) + if (pointInBounds(pos, collisionRect)) { + if (maxZ === -1 || tile.z > maxZ) { + maxZ = tile.z + tileIdx = idx + } + } + } + return tileIdx +} + +async function loadPuzzleBitmaps(puzzle) { + // load bitmap, to determine the original size of the image + let bitmpTmp = await loadImageToBitmap(puzzle.info.imageUrl) + + // creation of tile bitmaps + // then create the final puzzle bitmap + // NOTE: this can decrease OR increase in size! + const bitmap = resizeBitmap(bitmpTmp, puzzle.info.width, puzzle.info.height) + const bitmaps = await createPuzzleTileBitmaps(bitmap, puzzle.tiles, puzzle.info) + // tile bitmaps + return bitmaps +} + +async function loadPuzzle(targetTiles, imageUrl) { + // load bitmap, to determine the original size of the image + let bitmpTmp = await loadImageToBitmap(imageUrl) + + // determine puzzle information from the bitmap + let info = determinePuzzleInfo(bitmpTmp.width, bitmpTmp.height, targetTiles) + + let tiles = new Array(info.tiles) + for (let i = 0; i < tiles.length; i++) { + tiles[i] = { + idx: i, + } + } + tiles = await determinePuzzleTileShapes(tiles, info) + + // Complete puzzle object + const p = { + // tiles array + tiles: tiles.map(tile => { + return { + idx: tile.idx, // index of tile in the array + group: 0, // if grouped with other tiles + z: 0, // z index of the tile + finished: false, // if the tile is in its final position + tabs: tile.tabs, // tabs :) + // physical current position of the tile (x/y in pixels) + // this position is the initial position only and is the + // value that changes when moving a tile + // TODO: scatter the tiles on the table at the beginning + pos: { + x: info.coords[tile.idx].x * info.tileSize, + y: info.coords[tile.idx].y * info.tileSize, + }, + } + }), + // extra puzzle information + info: { + // information that was used to create the puzzle + targetTiles: targetTiles, + imageUrl: imageUrl, + + width: info.width, // actual puzzle width (same as bitmap.width) + height: info.height, // actual puzzle height (same as bitmap.height) + tileSize: info.tileSize, // width/height of each tile (without tabs) + tileDrawSize: info.tileDrawSize, // width/height of each tile (with tabs) + tileMarginWidth: info.tileMarginWidth, + // offset in x and y when drawing tiles, so that they appear to be at pos + tileDrawOffset: (info.tileDrawSize - info.tileSize) / -2, + // max distance between tile and destination that + // makes the tile snap to destination + snapDistance: info.tileSize / 2, + tiles: info.tiles, // the final number of tiles in the puzzle + tiles_x: info.tiles_x, // number of tiles each row + tiles_y: info.tiles_y, // number of tiles each col + coords: info.coords, // map of tile index to its coordinates + // ( index => {x, y} ) + // this is not the physical coordinate, but + // the tile_coordinate + // this can be used to determine where the + // final destination of a tile is + }, + } + return p +} + + +function uniqId() { + return Date.now().toString(36) + Math.random().toString(36).substring(2) +} + +function initme() { + return uniqId() + let ID = localStorage.getItem("ID") + if (!ID) { + ID = uniqId() + localStorage.setItem("ID", ID) + } + return ID +} + +function setupNetwork(me) { + const wsc = new WsClient('ws://localhost:1338/ws', me) + wsc.connect() + return wsc +} + +async function main () { + + // todo: maybe put in protocols, same as `me()` + let gameId = 'asdfbla' // uniqId() + let me = initme() + + let conn = setupNetwork(me + '|' + gameId) + conn.send(JSON.stringify({type: 'init'})) + conn.onSocket('message', async ({data}) => { + const d = JSON.parse(data) + let puzzle + if (d.type === 'init') { + if (d.puzzle) { + puzzle = d.puzzle + console.log('loaded from server') + } else { + // The game doesnt exist yet on the server, so load puzzle + // and then give the server some info about the puzzle + // Load puzzle and determine information about it + puzzle = await loadPuzzle(TARGET_TILES, IMAGE_URL) + conn.send(JSON.stringify({ + type: 'init_puzzle', + puzzle: puzzle, + })) + console.log('loaded from local config') + } + console.log(puzzle) + let bitmaps = await loadPuzzleBitmaps(puzzle) + startGame(puzzle, bitmaps, conn) + } else { + console.log(d) + } + }) + + const _STATE = { + m_x: 0, + m_y: 0, + m_d: false, + } + let _STATE_CHANGED = false + + // this must be fetched from server + + const startGame = (puzzle, bitmaps, conn) => { + // information for next render cycle + let rerenderTable = true + let rerenderTableRect = null + let rerender = true + let redrawMinX = null + let redrawMaxX = null + let redrawMinY = null + let redrawMaxY = null + const updateDrawMinMax = (pos, offset) => { + let x0 = pos.x - offset + let x1 = pos.x + offset + let y0 = pos.y - offset + let y1 = pos.y + offset + redrawMinX = redrawMinX === null ? x0 : Math.min(redrawMinX, x0) + redrawMaxX = redrawMaxX === null ? x1 : Math.max(redrawMaxX, x1) + redrawMinY = redrawMinY === null ? y0 : Math.min(redrawMinY, y0) + redrawMaxY = redrawMaxY === null ? y1 : Math.max(redrawMaxY, y1) + } + + conn.onSocket('message', ({data}) => { + const d = JSON.parse(data) + if (d.type === 'tile_changed' && d.origin !== me) { + updateDrawMinMax(puzzle.tiles[d.idx].pos, puzzle.info.tileDrawSize) + + puzzle.tiles[d.idx].pos = {x: d.pos.x, y: d.pos.y} + puzzle.tiles[d.idx].z = d.z + puzzle.tiles[d.idx].group = d.group + puzzle.tiles[d.idx].finished = d.finished + + updateDrawMinMax(puzzle.tiles[d.idx].pos, puzzle.info.tileDrawSize) + } + }) + + console.log(puzzle) + + + // Create a dom and attach adapters to it so we can work with it + const canvas = addCanvasToDom(createCanvas()) + const adapter = new CanvasAdapter(canvas) + const evts = new EventAdapter(canvas) + + // initialize some view data + // this global data will change according to input events + const cam = new Camera(canvas) + + // Information about the mouse + const EV_DATA = { + mouse_down_x: null, + mouse_down_y: null, + } + + // Information about what tile is the player currently grabbing + let grabbingTileIdx = -1 + + let maxZ = 0 + let maxGroup = 0 + + // The actual place for the puzzle. The tiles may + // not be moved around infinitely, just on the (invisible) + // puzzle table. however, the camera may move away from the table + const puzzleTableColor = [200, 0, 0, 255] + const puzzleTable = new Bitmap( + puzzle.info.width * 2, + puzzle.info.height * 2, + puzzleTableColor + ) + + // In the middle of the table, there is a board. this is to + // tell the player where to place the final puzzle + const boardColor = [0, 150, 0, 255] + const board = new Bitmap( + puzzle.info.width, + puzzle.info.height, + boardColor + ) + const boardPos = { + x: (puzzleTable.width - board.width) / 2, + y: (puzzleTable.height - board.height) / 2 + } // relative to table. + + + // Some helper functions for working with the grabbing and snapping + // --------------------------------------------------------------- + + // get all grouped tiles for a tile + function getGroupedTiles (tile) { + let grouped = [] + if (tile.group) { + for (let other of puzzle.tiles) { + if (other.group === tile.group) { + grouped.push(other) + } + } + } else { + grouped.push(tile) + } + return grouped + } + + // put both tiles (and their grouped tiles) in the same group + const groupTiles = (tile, other) => { + let targetGroup + let searchGroups = [] + if (tile.group) { + searchGroups.push(tile.group) + } + if (other.group) { + searchGroups.push(other.group) + } + if (tile.group) { + targetGroup = tile.group + } else if (other.group) { + targetGroup = other.group + } else { + maxGroup++ + targetGroup = maxGroup + } + tile.group = targetGroup + other.group = targetGroup + + if (searchGroups.length > 0) { + for (let tmp of puzzle.tiles) { + if (searchGroups.includes(tmp.group)) { + tmp.group = targetGroup + } + } + } + } + + // determine if two tiles are grouped together + const areGrouped = (t1, t2) => { + return t1.group && t1.group === t2.group + } + + // get the center position of a tile + const tileCenterPos = (tile) => { + return tileRectByTile(tile).center() + } + + // get the would-be visible bounding rect if a tile was + // in given position + const tileRectByPos = (pos) => { + return new BoundingRectangle( + pos.x, + pos.x + puzzle.info.tileSize, + pos.y, + pos.y + puzzle.info.tileSize + ) + } + + // get the current visible bounding rect for a tile + const tileRectByTile = (tile) => { + return tileRectByPos(tile.pos) + } + + const tilesSortedByZIndex = () => { + const sorted = puzzle.tiles.slice() + return sorted.sort((t1, t2) => t1.z - t2.z) + } + + const setGroupedZIndex = (tile, zIndex) => { + for(let t of getGroupedTiles(tile)) { + t.z = zIndex + + conn.send(JSON.stringify({ + type: 'change_tile', + idx: t.idx, + pos: t.pos, + z: t.z, + group: t.group, + finished: t.finished, + })) + } + } + + const moveGroupedTilesDiff = (tile, diffX, diffY) => { + for (let t of getGroupedTiles(tile)) { + t.pos = pointAdd(t.pos, {x: diffX, y: diffY}) + + conn.send(JSON.stringify({ + type: 'change_tile', + idx: t.idx, + pos: t.pos, + z: t.z, + finished: t.finished, + })) + // TODO: instead there could be a function to + // get min/max x/y of a group + updateDrawMinMax(tileCenterPos(t), puzzle.info.tileDrawSize) + } + } + const moveGroupedTiles = (tile, dst) => { + let diff = pointSub(tile.pos, dst) + moveGroupedTilesDiff(tile, -diff.x, -diff.y) + } + const finishGroupedTiles = (tile) => { + for (let t of getGroupedTiles(tile)) { + t.finished = true + t.z = 1 + conn.send(JSON.stringify({ + type: 'change_tile', + idx: t.idx, + pos: t.pos, + z: t.z, + finished: t.finished, + })) + } + } + // --------------------------------------------------------------- + + + + + + + + const onUpdate = () => { + let last_x = null + let last_y = null + // console.log(tp) + if (EV_DATA.mouse_down_x !== null) { + last_x = EV_DATA.mouse_down_x + } + if (EV_DATA.mouse_down_y !== null) { + last_y = EV_DATA.mouse_down_y + } + for (let mouse of evts.consumeAll()) { + if (mouse.type === 'move') { + const tp = cam.translateMouse(mouse) + _STATE.m_x = tp.x + _STATE.m_y = tp.y + _STATE_CHANGED = true + } + if (mouse.type === 'down') { + _STATE.m_d = true + _STATE_CHANGED = true + } else if (mouse.type === 'up') { + _STATE.m_d = false + _STATE_CHANGED = true + } + if (mouse.type === 'wheel') { + if (mouse.y < 0) { + if (cam.zoomIn()) { + rerender = true + } + } else { + if (cam.zoomOut()) { + rerender = true + } + } + } else if (mouse.type === 'down') { + EV_DATA.mouse_down_x = mouse.x + EV_DATA.mouse_down_y = mouse.y + if (last_x === null || last_y === null) { + last_x = mouse.x + last_y = mouse.y + } + + let tp = cam.translateMouse(mouse) + grabbingTileIdx = unfinishedTileByPos(puzzle, tp) + console.log(grabbingTileIdx) + if (grabbingTileIdx >= 0) { + maxZ++ + setGroupedZIndex(puzzle.tiles[grabbingTileIdx], maxZ) + } + console.log('down', tp) + + } else if (mouse.type === 'up') { + EV_DATA.mouse_down_x = null + EV_DATA.mouse_down_y = null + last_x = null + last_y === null + + if (grabbingTileIdx >= 0) { + // Check if the tile was dropped at the correct + // location + + let tile = puzzle.tiles[grabbingTileIdx] + let pt = pointSub(tile.pos, boardPos) + let dst = tileRectByPos(pt) + let srcRect = srcRectByIdx(puzzle.info, grabbingTileIdx) + if (srcRect.centerDistance(dst) < puzzle.info.snapDistance) { + // Snap the tile to the final destination + console.log('ok! !!!') + moveGroupedTiles(tile, new Point( + srcRect.x0 + boardPos.x, + srcRect.y0 + boardPos.y + )) + finishGroupedTiles(tile) + + let tp = cam.translateMouse(mouse) + updateDrawMinMax(tp, puzzle.info.tileDrawSize) + } else { + // Snap to other tiles + let other + let snapped = false + let off + let offs = [ + [0, 1], + [-1, 0], + [0, -1], + [1, 0], + ] + + const check = (t, off, other) => { + if (snapped || !other || other.finished || areGrouped(t, other)) { + return + } + let trec_ = tileRectByTile(t) + let otrec = tileRectByTile(other).moved( + off[0] * puzzle.info.tileSize, + off[1] * puzzle.info.tileSize + ) + if (trec_.centerDistance(otrec) < puzzle.info.snapDistance) { + console.log('yea top!') + moveGroupedTiles(t, {x: otrec.x0, y: otrec.y0}) + groupTiles(t, other) + setGroupedZIndex(t, t.z) + snapped = true + + updateDrawMinMax(tileCenterPos(t), puzzle.info.tileDrawSize) + } + } + + for (let t of getGroupedTiles(tile)) { + let others = getSurroundingTilesByIdx(puzzle, t.idx) + + // top + off = offs[0] + other = others[0] + check(t, off, other) + + // right + off = offs[1] + other = others[1] + check(t, off, other) + + // bottom + off = offs[2] + other = others[2] + check(t, off, other) + + // left + off = offs[3] + other = others[3] + check(t, off, other) + + if (snapped) { + break + } + } + } + } + grabbingTileIdx = -1 + console.log('up', cam.translateMouse(mouse)) + } else if ((EV_DATA.mouse_down_x !== null) && mouse.type === 'move') { + EV_DATA.mouse_down_x = mouse.x + EV_DATA.mouse_down_y = mouse.y + + if (last_x === null || last_y === null) { + last_x = mouse.x + last_y = mouse.y + } + + if (grabbingTileIdx >= 0) { + let tp = cam.translateMouse(mouse) + let tp_last = cam.translateMouse({x: last_x, y: last_y}) + const diffX = tp.x - tp_last.x + const diffY = tp.y - tp_last.y + + let t = puzzle.tiles[grabbingTileIdx] + moveGroupedTilesDiff(t, diffX, diffY) + + // todo: dont +- tileDrawSize, we can work with less + updateDrawMinMax(tp, puzzle.info.tileDrawSize) + updateDrawMinMax(tp_last, puzzle.info.tileDrawSize) + } else { + const diffX = Math.round(mouse.x - last_x) + const diffY = Math.round(mouse.y - last_y) + // move the cam + cam.move(diffX, diffY) + rerender = true + } + } + // console.log(mouse) + } + if (redrawMinX) { + rerenderTableRect = new BoundingRectangle( + redrawMinX, + redrawMaxX, + redrawMinY, + redrawMaxY + ) + rerenderTable = true + } + + if (_STATE_CHANGED) { + conn.send(JSON.stringify({ + type: 'state', + state: _STATE, + })) + _STATE_CHANGED = false + } + } + + const onRender = () => { + if (!rerenderTable && !rerender) { + return + } + + console.log('rendering') + + // draw the puzzle table + if (rerenderTable) { + fillBitmapCapped(puzzleTable, puzzleTableColor, rerenderTableRect) + + // draw the puzzle board on the table + mapBitmapToBitmapCapped(board, board.getBoundingRect(), puzzleTable, new BoundingRectangle( + boardPos.x, + boardPos.x + board.width - 1, + boardPos.y, + boardPos.y + board.height - 1, + ), rerenderTableRect) + + // draw all the tiles on the table + + for (let tile of tilesSortedByZIndex()) { + let rect = new BoundingRectangle( + puzzle.info.tileDrawOffset + tile.pos.x, + puzzle.info.tileDrawOffset + tile.pos.x + puzzle.info.tileDrawSize, + puzzle.info.tileDrawOffset + tile.pos.y, + puzzle.info.tileDrawOffset + tile.pos.y + puzzle.info.tileDrawSize, + ) + let bmp = bitmaps[tile.idx] + mapBitmapToBitmapCapped( + bmp, + bmp.getBoundingRect(), + puzzleTable, + rect, + rerenderTableRect + ) + } + } + + // finally draw the finished table onto the canvas + // only part of the table may be visible, depending on the + // camera + adapter.clear() + mapBitmapToAdapter(puzzleTable, new BoundingRectangle( + - cam.x, + - cam.x + (cam.width / cam.zoom), + - cam.y, + - cam.y + (cam.height / cam.zoom), + ), adapter, adapter.getBoundingRect()) + + rerenderTable = false + rerenderTableRect = null + rerender = false + redrawMinX = null + redrawMaxX = null + redrawMinY = null + redrawMaxY = null + } + + run({ + update: onUpdate, + render: onRender, + }) + } +} + +main() diff --git a/game/util.js b/game/util.js new file mode 100644 index 0000000..a6215c6 --- /dev/null +++ b/game/util.js @@ -0,0 +1,25 @@ + +// get a random int between min and max (inclusive) +export const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min + +// get one random item from the given array +export const choice = (array) => array[randomInt(0, array.length - 1)] + +// return a shuffled (shallow) copy of the given array +export const shuffle = (array) => { + let arr = array.slice() + for (let i = 0; i <= arr.length - 2; i++) + { + const j = randomInt(i, arr.length -1); + const tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } + return arr +} + +export default { + randomInt, + choice, + shuffle, +} \ No newline at end of file diff --git a/run b/run new file mode 100755 index 0000000..7c59cf3 --- /dev/null +++ b/run @@ -0,0 +1,18 @@ +#!/bin/sh + +RUN_BIN="$(realpath "$0")" +RUN_DIR=$(dirname "$RUN_BIN") + +export RUN_BIN +export RUN_DIR + +TASK="$1" +[ $# -gt 0 ] && shift + +# Map task to scripts here +if [ -f "$RUN_DIR/scripts/$TASK" ]; then + exec "$RUN_DIR/scripts/$TASK" "$@" +else + echo "Task not found: $TASK" >&2 + exit 2 +fi diff --git a/scripts/server b/scripts/server new file mode 100755 index 0000000..3bff09c --- /dev/null +++ b/scripts/server @@ -0,0 +1,5 @@ +#!/bin/sh + +cd "$RUN_DIR/server" + +nodemon index.js diff --git a/server/WebSocketServer.js b/server/WebSocketServer.js new file mode 100644 index 0000000..38afa2e --- /dev/null +++ b/server/WebSocketServer.js @@ -0,0 +1,92 @@ +import WebSocket from 'ws' + +/* +Example config + +config = { + hostname: 'localhost', + port: 1338, + connectstring: `ws://localhost:1338/ws`, +} +*/ + +class EvtBus { + constructor() { + this._on = {} + } + + on(type, callback) { + this._on[type] = this._on[type] || [] + this._on[type].push(callback) + } + + dispatch(type, ...args) { + (this._on[type] || []).forEach(cb => { + cb(...args) + }) + } +} + +class WebSocketServer { + constructor(config) { + this.config = config + this._websocketserver = null + this._interval = null + + this.evt = new EvtBus() + } + + on(type, callback) { + this.evt.on(type, callback) + } + + listen() { + this._websocketserver = new WebSocket.Server(this.config) + this._websocketserver.on('connection', (socket, request, client) => { + const pathname = new URL(this.config.connectstring).pathname + if (request.url.indexOf(pathname) !== 0) { + console.log('bad request url: ', request.url) + socket.close() + return + } + + socket.isAlive = true + socket.on('pong', function () { + this.isAlive = true; + }) + + socket.on('message', (data) => { + console.log(`ws| `, data) + this.evt.dispatch('message', {socket, data}) + }) + }) + + this._interval = setInterval(() => { + this._websocketserver.clients.forEach((socket) => { + if (socket.isAlive === false) { + return socket.terminate() + } + socket.isAlive = false + socket.ping(() => { }) + }) + }, 30000) + + this._websocketserver.on('close', () => { + clearInterval(this._interval) + }) + } + + notifyOne(data, socket) { + if (socket.isAlive) { + socket.send(JSON.stringify(data)) + } + } + + notifyAll(data) { + this._websocketserver.clients.forEach((socket) => { + this.notifyOne(data, socket) + }) + } +} + +export default WebSocketServer diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..8f754bf --- /dev/null +++ b/server/index.js @@ -0,0 +1,87 @@ +import WebSocketServer from './WebSocketServer.js' + +import express from 'express' + +const httpConfig = { + hostname: 'localhost', + port: 1337, +} +const port = httpConfig.port +const hostname = httpConfig.hostname +const app = express() +app.use('/', express.static('./../game/')) +app.listen(port, hostname, () => console.log(`server running on ${hostname}:${port}`)) + + +const players = { + +} +const games = {} + + +const wssConfig = { + hostname: 'localhost', + port: 1338, + connectstring: `ws://localhost:1338/ws`, +} +const wss = new WebSocketServer(wssConfig); + + +const notify = (data) => { + // TODO: throttle + wss.notifyAll(data) + console.log('notify', data) +} + +wss.on('message', ({socket, data}) => { + try { + const proto = socket.protocol.split('|') + const uid = proto[0] + const gid = proto[1] + const parsed = JSON.parse(data) + switch (parsed.type) { + case 'init': { + // a new player (or previous player) joined + players[uid] = players[uid] || {} + players[uid].id = uid + players[uid].tiles = players[uid].tiles || 0 + players[uid].m_x = players[uid].x || null + players[uid].m_y = players[uid].y || null + players[uid].m_d = false + console.log('init', players) + const puzzle = games[gid] ? games[gid].puzzle : null + console.log('init', games[gid]) + wss.notifyOne({type: 'init', puzzle: puzzle}, socket) + } break; + + case 'init_puzzle': { + games[gid] = { + puzzle: parsed.puzzle, + } + } break; + + case 'state': { + players[uid].m_x = parsed.state.m_x + players[uid].m_y = parsed.state.m_y + players[uid].m_d = parsed.state.m_d + } break; + + case 'change_tile': { + let idx = parsed.idx + let z = parsed.z + let finished = parsed.finished + let pos = { x: parsed.pos.x, y: parsed.pos.y } + let group = parsed.group + // games[gid].puzzle.tiles[idx] = games[gid].puzzle.tiles[idx] || {} + games[gid].puzzle.tiles[idx].pos = pos + games[gid].puzzle.tiles[idx].z = z + games[gid].puzzle.tiles[idx].finished = finished + games[gid].puzzle.tiles[idx].group = group + notify({type:'tile_changed', origin: uid, idx, pos, z, finished, group}) + } break; + } + } catch (e) { + console.error(e) + } +}) +wss.listen() diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..f615503 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,377 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://npm.stroeermediabrands.de/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://npm.stroeermediabrands.de/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://npm.stroeermediabrands.de/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://npm.stroeermediabrands.de/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://npm.stroeermediabrands.de/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://npm.stroeermediabrands.de/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://npm.stroeermediabrands.de/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://npm.stroeermediabrands.de/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://npm.stroeermediabrands.de/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://npm.stroeermediabrands.de/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://npm.stroeermediabrands.de/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://npm.stroeermediabrands.de/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://npm.stroeermediabrands.de/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://npm.stroeermediabrands.de/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://npm.stroeermediabrands.de/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://npm.stroeermediabrands.de/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://npm.stroeermediabrands.de/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://npm.stroeermediabrands.de/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://npm.stroeermediabrands.de/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://npm.stroeermediabrands.de/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://npm.stroeermediabrands.de/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://npm.stroeermediabrands.de/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://npm.stroeermediabrands.de/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://npm.stroeermediabrands.de/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://npm.stroeermediabrands.de/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://npm.stroeermediabrands.de/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://npm.stroeermediabrands.de/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://npm.stroeermediabrands.de/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://npm.stroeermediabrands.de/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://npm.stroeermediabrands.de/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://npm.stroeermediabrands.de/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://npm.stroeermediabrands.de/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://npm.stroeermediabrands.de/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://npm.stroeermediabrands.de/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://npm.stroeermediabrands.de/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://npm.stroeermediabrands.de/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://npm.stroeermediabrands.de/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://npm.stroeermediabrands.de/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://npm.stroeermediabrands.de/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://npm.stroeermediabrands.de/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://npm.stroeermediabrands.de/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://npm.stroeermediabrands.de/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://npm.stroeermediabrands.de/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://npm.stroeermediabrands.de/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://npm.stroeermediabrands.de/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://npm.stroeermediabrands.de/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://npm.stroeermediabrands.de/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://npm.stroeermediabrands.de/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://npm.stroeermediabrands.de/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://npm.stroeermediabrands.de/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "ws": { + "version": "7.3.1", + "resolved": "https://npm.stroeermediabrands.de/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..2d2cfa1 --- /dev/null +++ b/server/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "dependencies": { + "express": "^4.17.1", + "ws": "^7.3.1" + } +}