From d592cef494f59a41a44c600a20b20bcb7bcfed40 Mon Sep 17 00:00:00 2001 From: Zutatensuppe Date: Thu, 12 Nov 2020 19:19:02 +0100 Subject: [PATCH] everything --- common/Geometry.js | 50 +++ common/package.json | 3 + game/Camera.js | 48 ++- game/Communication.js | 40 +-- game/Graphics.js | 2 - game/index.js | 521 +++++--------------------------- server/Game.js | 426 ++++++++++++++++++++++++++ server/{puzzle.js => Puzzle.js} | 2 +- server/index.js | 66 ++-- 9 files changed, 619 insertions(+), 539 deletions(-) create mode 100644 common/Geometry.js create mode 100644 common/package.json create mode 100644 server/Game.js rename server/{puzzle.js => Puzzle.js} (99%) diff --git a/common/Geometry.js b/common/Geometry.js new file mode 100644 index 0000000..9eb4323 --- /dev/null +++ b/common/Geometry.js @@ -0,0 +1,50 @@ +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 } +} + +function pointDistance(a, b) { + const diffX = a.x - b.x + const diffY = a.y - b.y + return Math.sqrt(diffX * diffX + diffY * diffY) +} + +function pointInBounds(pt, rect) { + return pt.x >= rect.x + && pt.x <= rect.x + rect.w + && pt.y >= rect.y + && pt.y <= rect.y + rect.h +} + +function rectCenter(rect) { + return { + x: rect.x + (rect.w / 2), + y: rect.y + (rect.h / 2), + } +} + +function rectMoved(rect, x, y) { + return { + x: rect.x + x, + y: rect.y + y, + w: rect.w, + h: rect.h, + } +} + +function rectCenterDistance(rectA, rectB) { + return pointDistance(rectCenter(rectA), rectCenter(rectB)) +} + +export default { + pointSub, + pointAdd, + pointDistance, + pointInBounds, + rectCenter, + rectMoved, + rectCenterDistance, +} diff --git a/common/package.json b/common/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/common/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/game/Camera.js b/game/Camera.js index b51adc5..0f32c2a 100644 --- a/game/Camera.js +++ b/game/Camera.js @@ -28,30 +28,26 @@ export default class Camera { 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 - } + setZoom(newzoom) { + const zoom = Math.min(Math.max(newzoom, this.minZoom), this.maxZoom) + if (zoom == this.zoom) { return false + } + + // centered zoom + this.x -= Math.round(((this.width / this.zoom) - (this.width / zoom)) / 2) + this.y -= Math.round(((this.height / this.zoom) - (this.height / zoom)) / 2) + + this.zoom = zoom + return true + } + + zoomOut() { + return this.setZoom(this.zoom - this.zoomStep) } 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 + return this.setZoom(this.zoom + this.zoomStep) } /** @@ -61,8 +57,8 @@ export default class Camera { */ viewportToWorld(coord) { return { - x: (coord.x / this.zoom) - this.x, - y: (coord.y / this.zoom) - this.y, + x: Math.round((coord.x / this.zoom) - this.x), + y: Math.round((coord.y / this.zoom) - this.y), } } @@ -73,15 +69,15 @@ export default class Camera { */ worldToViewport(coord) { return { - x: (coord.x + this.x) * this.zoom, - y: (coord.y + this.y) * this.zoom, + x: Math.round((coord.x + this.x) * this.zoom), + y: Math.round((coord.y + this.y) * this.zoom), } } worldDimToViewport(dim) { return { - w: dim.w * this.zoom, - h: dim.h * this.zoom, + w: Math.round(dim.w * this.zoom), + h: Math.round(dim.h * this.zoom), } } } diff --git a/game/Communication.js b/game/Communication.js index fe6c1cc..04cdf8d 100644 --- a/game/Communication.js +++ b/game/Communication.js @@ -1,5 +1,10 @@ import WsClient from './WsClient.js' +const EV_SERVER_STATE_CHANGED = 1 +const EV_SERVER_INIT = 4 +const EV_CLIENT_MOUSE = 2 +const EV_CLIENT_INIT = 3 + let conn let changesCallback = () => {} @@ -11,39 +16,26 @@ function connect(gameId, playerId) { conn = new WsClient(WS_ADDRESS, playerId + '|' + gameId) return new Promise(r => { conn.connect() - conn.send(JSON.stringify({ type: 'init' })) + conn.send(JSON.stringify([EV_CLIENT_INIT])) conn.onSocket('message', async ({ data }) => { - const d = JSON.parse(data) - if (d.type === 'init') { - r(d.game) - } else if (d.type === 'state_changed' && d.origin !== playerId) { - changesCallback(d.changes) + const [type, typeData] = JSON.parse(data) + if (type === EV_SERVER_INIT) { + const game = typeData + r(game) + } else if (type === EV_SERVER_STATE_CHANGED) { + const changes = typeData + changesCallback(changes) } }) }) } -const _STATE = { - changed: false, - changes: [], -} - -function addChange(change) { - _STATE.changes.push(change) - _STATE.changed = true -} - -function sendChanges() { - if (_STATE.changed) { - conn.send(JSON.stringify({ type: 'state', state: _STATE })) - _STATE.changes = [] - _STATE.changed = false - } +function addMouse(mouse) { + conn.send(JSON.stringify([EV_CLIENT_MOUSE, mouse])) } export default { connect, onChanges, - addChange, - sendChanges, + addMouse, } diff --git a/game/Graphics.js b/game/Graphics.js index ed09947..95b313f 100644 --- a/game/Graphics.js +++ b/game/Graphics.js @@ -1,5 +1,3 @@ -// import Bitmap from './Bitmap.js' - function createCanvas(width = 0, height = 0) { const c = document.createElement('canvas') c.width = width === 0 ? window.innerWidth : width diff --git a/game/index.js b/game/index.js index 0b5bb27..2c3c2ee 100644 --- a/game/index.js +++ b/game/index.js @@ -5,6 +5,7 @@ import EventAdapter from './EventAdapter.js' import Graphics from './Graphics.js' import Debug from './Debug.js' import Communication from './Communication.js' +import Geometry from './../common/Geometry.js' if (typeof GAME_ID === 'undefined') throw '[ GAME_ID not set ]' if (typeof WS_ADDRESS === 'undefined') throw '[ WS_ADDRESS not set ]' @@ -16,55 +17,6 @@ function addCanvasToDom(canvas) { return canvas } -function pointDistance(a, b) { - const diffX = a.x - b.x - const diffY = a.y - b.y - return Math.sqrt(diffX * diffX + diffY * diffY) -} - -function rectMoved(rect, x, y) { - return { - x: rect.x + x, - y: rect.y + y, - w: rect.w, - h: rect.h, - } -} - -const rectCenter = (rect) => { - return { - x: rect.x + (rect.w / 2), - y: rect.y + (rect.h / 2), - } -} - -function rectCenterDistance(a, b) { - return pointDistance(rectCenter(a), rectCenter(b)) -} - -function pointInBounds(pt, rect) { - return pt.x >= rect.x - && pt.x <= rect.x + rect.w - && pt.y >= rect.y - && pt.y <= rect.y + rect.h -} - -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] - ] -} - async function createPuzzleTileBitmaps(img, tiles, info) { var tileSize = info.tileSize var tileMarginWidth = info.tileMarginWidth @@ -93,30 +45,30 @@ async function createPuzzleTileBitmaps(img, tiles, info) { const topLeftEdge = { x: tileMarginWidth, y: tileMarginWidth } path.moveTo(topLeftEdge.x, topLeftEdge.y) for (let i = 0; i < curvyCoords.length / 6; i++) { - const p1 = pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.top * curvyCoords[i * 6 + 1] * tileRatio }) - const p2 = pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.top * curvyCoords[i * 6 + 3] * tileRatio }) - const p3 = pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.top * curvyCoords[i * 6 + 5] * tileRatio }) + const p1 = Geometry.pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.top * curvyCoords[i * 6 + 1] * tileRatio }) + const p2 = Geometry.pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.top * curvyCoords[i * 6 + 3] * tileRatio }) + const p3 = Geometry.pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.top * curvyCoords[i * 6 + 5] * tileRatio }) path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); } - const topRightEdge = pointAdd(topLeftEdge, { x: tileSize, y: 0 }) + const topRightEdge = Geometry.pointAdd(topLeftEdge, { x: tileSize, y: 0 }) for (let i = 0; i < curvyCoords.length / 6; i++) { - const p1 = pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio }) - const p2 = pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio }) - const p3 = pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio }) + const p1 = Geometry.pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio }) + const p2 = Geometry.pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio }) + const p3 = Geometry.pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio }) path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); } - const bottomRightEdge = pointAdd(topRightEdge, { x: 0, y: tileSize }) + const bottomRightEdge = Geometry.pointAdd(topRightEdge, { x: 0, y: tileSize }) for (let i = 0; i < curvyCoords.length / 6; i++) { - let p1 = pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 1] * tileRatio }) - let p2 = pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 3] * tileRatio }) - let p3 = pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 5] * tileRatio }) + let p1 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 1] * tileRatio }) + let p2 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 3] * tileRatio }) + let p3 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 5] * tileRatio }) path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); } - const bottomLeftEdge = pointSub(bottomRightEdge, { x: tileSize, y: 0 }) + const bottomLeftEdge = Geometry.pointSub(bottomRightEdge, { x: tileSize, y: 0 }) for (let i = 0; i < curvyCoords.length / 6; i++) { - let p1 = pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio }) - let p2 = pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio }) - let p3 = pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio }) + let p1 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio }) + let p2 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio }) + let p3 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio }) path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); } paths[key] = path @@ -169,64 +121,6 @@ function srcRectByIdx(puzzleInfo, idx) { } } -const pointSub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y }) - -const pointAdd = (a, b) => ({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++) { - const tile = puzzle.tiles[idx] - if (tile.owner === -1) { - continue - } - - const collisionRect = { - x: tile.pos.x, - y: tile.pos.y, - w: puzzle.info.tileSize, - h: puzzle.info.tileSize, - } - if (pointInBounds(pos, collisionRect)) { - if (maxZ === -1 || tile.z > maxZ) { - maxZ = tile.z - tileIdx = idx - } - } - } - return tileIdx -} - -class DirtyRect { - constructor() { - this.reset() - } - get () { - return this.x0 === null ? null : [ - {x0: this.x0, x1: this.x1, y0: this.y0, y1: this.y1} - ] - } - add (pos, offset) { - const x0 = pos.x - offset - const x1 = pos.x + offset - const y0 = pos.y - offset - const y1 = pos.y + offset - this.x0 = this.x0 === null ? x0 : Math.min(this.x0, x0) - this.x1 = this.x1 === null ? x1 : Math.max(this.x1, x1) - this.y0 = this.y0 === null ? y0 : Math.min(this.y0, y0) - this.y1 = this.y1 === null ? y1 : Math.max(this.y1, y1) - } - reset () { - this.x0 = null - this.x1 = null - this.y0 = null - this.y1 = null - } -} - async function loadPuzzleBitmaps(puzzle) { // load bitmap, to determine the original size of the image const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl) @@ -252,6 +146,15 @@ function initme() { return ID } +const getFirstOwnedTile = (puzzle, userId) => { + for (let t of puzzle.tiles) { + if (t.owner === userId) { + return t + } + } + return null +} + async function main () { let gameId = GAME_ID let me = initme() @@ -265,30 +168,12 @@ async function main () { const puzzle = game.puzzle const players = game.players - // information for next render cycle - let rectPlayer = new DirtyRect() - let rerenderPlayer = true - let rectTable = new DirtyRect() - let rerenderTable = true let rerender = true const changePlayer = (change) => { for (let k of Object.keys(change)) { players[me][k] = change[k] } - Communication.addChange({type: 'change_player', player: players[me]}) - } - const changeData = (change) => { - for (let k of Object.keys(change)) { - puzzle.data[k] = change[k] - } - Communication.addChange({type: 'change_data', data: puzzle.data}) - } - const changeTile = (t, change) => { - for (let k of Object.keys(change)) { - t[k] = change[k] - } - Communication.addChange({type: 'change_tile', tile: t}) } // Create a dom and attach adapters to it so we can work with it @@ -299,311 +184,67 @@ async function main () { // initialize some view data // this global data will change according to input events const viewport = new Camera(canvas) + // center viewport + viewport.move( + -(puzzle.info.table.width - viewport.width) /2, + -(puzzle.info.table.height - viewport.height) /2 + ) Communication.onChanges((changes) => { - for (let change of changes) { - switch (change.type) { - case 'change_player': { - if (players[change.player.id]) { - rectPlayer.add(viewport.worldToViewport(players[change.player.id]), cursorGrab.width) + for (let [type, typeData] of changes) { + switch (type) { + case 'player': { + if (typeData.id !== me) { + players[typeData.id] = typeData + rerender = true } - - players[change.player.id] = change.player - - rectPlayer.add(viewport.worldToViewport(players[change.player.id]), cursorGrab.width) } break; - - case 'change_tile': { - rectTable.add(puzzle.tiles[change.tile.idx].pos, puzzle.info.tileDrawSize) - - puzzle.tiles[change.tile.idx] = change.tile - - rectTable.add(puzzle.tiles[change.tile.idx].pos, puzzle.info.tileDrawSize) + case 'tile': { + puzzle.tiles[typeData.idx] = typeData + rerender = true } break; - case 'change_data': { - puzzle.data = change.data + case 'data': { + puzzle.data = typeData + rerender = true } break; } } }) - // Information about what tile is the player currently grabbing - let grabbingTileIdx = -1 - - // 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 = '#222' - const puzzleTable = await Graphics.createBitmap( - puzzle.info.table.width, - puzzle.info.table.height, - 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 = '#505050' - const board = await Graphics.createBitmap( - 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 { - changeData({ maxGroup: puzzle.data.maxGroup + 1 }) - targetGroup = puzzle.data.maxGroup - } - - changeTile(tile, { group: targetGroup }) - changeTile(other, { group: targetGroup }) - - if (searchGroups.length > 0) { - for (let tmp of puzzle.tiles) { - if (searchGroups.includes(tmp.group)) { - changeTile(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 rectCenter(tileRectByTile(tile)) - } - - // get the would-be visible bounding rect if a tile was - // in given position - const tileRectByPos = (pos) => { - return { - x: pos.x, - y: pos.y, - w: puzzle.info.tileSize, - h: 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)) { - changeTile(t, { z: zIndex }) - } - } - const setGroupedOwner = (tile, owner) => { - for (let t of getGroupedTiles(tile)) { - // may only change own tiles or untaken tiles - if (t.owner === me || t.owner === 0) { - changeTile(t, { owner: owner }) - } - } - } - - const moveGroupedTilesDiff = (tile, diffX, diffY) => { - for (let t of getGroupedTiles(tile)) { - changeTile(t, { pos: pointAdd(t.pos, { x: diffX, y: diffY }) }) - - // TODO: instead there could be a function to - // get min/max x/y of a group - rectTable.add(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)) { - changeTile(t, { owner: -1, z: 1 }) - } - } - // --------------------------------------------------------------- - - - - - - - - let _last_mouse = null let _last_mouse_down = null const onUpdate = () => { - let last_x = null - let last_y = null - - if (_last_mouse_down !== null) { - last_x = _last_mouse_down.x - last_y = _last_mouse_down.y - } for (let mouse of evts.consumeAll()) { const tp = viewport.viewportToWorld(mouse) + if (mouse.type === 'move') { + Communication.addMouse(['move', tp.x, tp.y]) + rerender = true changePlayer({ x: tp.x, y: tp.y }) - if (_last_mouse) { - rectPlayer.add(_last_mouse, cursorGrab.width) - } - rectPlayer.add(mouse, cursorGrab.width) - if (_last_mouse_down !== null) { - _last_mouse_down = mouse - - if (last_x === null || last_y === null) { - last_x = mouse.x - last_y = mouse.y - } - - if (grabbingTileIdx >= 0) { - const tp_last = viewport.viewportToWorld({ x: last_x, y: last_y }) - const diffX = tp.x - tp_last.x - const diffY = tp.y - tp_last.y - - const t = puzzle.tiles[grabbingTileIdx] - moveGroupedTilesDiff(t, diffX, diffY) - - // todo: dont +- tileDrawSize, we can work with less? - rectTable.add(tp, puzzle.info.tileDrawSize) - rectTable.add(tp_last, puzzle.info.tileDrawSize) - } else { + if (_last_mouse_down && !getFirstOwnedTile(puzzle, me)) { // move the cam - const diffX = Math.round(mouse.x - last_x) - const diffY = Math.round(mouse.y - last_y) + const diffX = Math.round(mouse.x - _last_mouse_down.x) + const diffY = Math.round(mouse.y - _last_mouse_down.y) viewport.move(diffX, diffY) - rerender = true - } + + _last_mouse_down = mouse } } else if (mouse.type === 'down') { - changePlayer({ down: true }) - rectPlayer.add(mouse, cursorGrab.width) - + Communication.addMouse(['down', tp.x, tp.y]) _last_mouse_down = mouse - if (last_x === null || last_y === null) { - last_x = mouse.x - last_y = mouse.y - } - - grabbingTileIdx = unfinishedTileByPos(puzzle, tp) - console.log(grabbingTileIdx) - if (grabbingTileIdx >= 0) { - changeData({ maxZ: puzzle.data.maxZ + 1 }) - setGroupedZIndex(puzzle.tiles[grabbingTileIdx], puzzle.data.maxZ) - setGroupedOwner(puzzle.tiles[grabbingTileIdx], me) - } - } else if (mouse.type === 'up') { - changePlayer({ down: false }) - if (_last_mouse) { - rectPlayer.add(_last_mouse, cursorGrab.width) - } - rectPlayer.add(mouse, cursorGrab.width) - + Communication.addMouse(['up', tp.x, tp.y]) _last_mouse_down = 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] - setGroupedOwner(tile, 0) - let pt = pointSub(tile.pos, boardPos) - let dst = tileRectByPos(pt) - let srcRect = srcRectByIdx(puzzle.info, grabbingTileIdx) - if (rectCenterDistance(srcRect, dst) < puzzle.info.snapDistance) { - // Snap the tile to the final destination - moveGroupedTiles(tile, { - x: srcRect.x + boardPos.x, - y: srcRect.y + boardPos.y, - }) - finishGroupedTiles(tile) - rectTable.add(tp, puzzle.info.tileDrawSize) - } else { - // Snap to other tiles - const check = (t, off, other) => { - if (!other || (other.owner === -1) || areGrouped(t, other)) { - return false - } - const trec_ = tileRectByTile(t) - const otrec = rectMoved( - tileRectByTile(other), - off[0] * puzzle.info.tileSize, - off[1] * puzzle.info.tileSize - ) - if (rectCenterDistance(trec_, otrec) < puzzle.info.snapDistance) { - moveGroupedTiles(t, { x: otrec.x, y: otrec.y }) - groupTiles(t, other) - setGroupedZIndex(t, t.z) - rectTable.add(tileCenterPos(t), puzzle.info.tileDrawSize) - return true - } - return false - } - - for (let t of getGroupedTiles(tile)) { - let others = getSurroundingTilesByIdx(puzzle, t.idx) - if ( - check(t, [0, 1], others[0]) // top - || check(t, [-1, 0], others[1]) // right - || check(t, [0, -1], others[2]) // bottom - || check(t, [1, 0], others[3]) // left - ) { - break - } - } - } - } - grabbingTileIdx = -1 } else if (mouse.type === 'wheel') { if ( mouse.deltaY < 0 && viewport.zoomIn() @@ -611,27 +252,13 @@ async function main () { ) { rerender = true changePlayer({ x: tp.x, y: tp.y }) - if (_last_mouse) { - rectPlayer.add(_last_mouse, cursorGrab.width) - } - rectPlayer.add(mouse, cursorGrab.width) } } - // console.log(mouse) - _last_mouse = mouse } - if (rectTable.get()) { - rerenderTable = true - } - if (rectPlayer.get()) { - rerenderPlayer = true - } - - Communication.sendChanges() } const onRender = () => { - if (!rerenderTable && !rerenderPlayer && !rerender) { + if (!rerender) { return } @@ -640,24 +267,33 @@ async function main () { if (DEBUG) Debug.checkpoint_start(0) - ctx.fillStyle = puzzleTableColor - ctx.fillRect(0, 0, canvas.width, canvas.height) + // CLEAR CTX + // --------------------------------------------------------------- + ctx.clearRect(0, 0, canvas.width, canvas.height) if (DEBUG) Debug.checkpoint('clear done') + // --------------------------------------------------------------- + // DRAW BOARD // --------------------------------------------------------------- - pos = viewport.worldToViewport(boardPos) - dim = viewport.worldDimToViewport({w: board.width, h: board.height}) - ctx.drawImage(board, - 0, 0, board.width, board.height, - pos.x, pos.y, dim.w, dim.h - ) + pos = viewport.worldToViewport({ + x: (puzzle.info.table.width - puzzle.info.width) / 2, + y: (puzzle.info.table.height - puzzle.info.height) / 2 + }) + dim = viewport.worldDimToViewport({ + w: puzzle.info.width, + h: puzzle.info.height, + }) + ctx.fillStyle = boardColor + ctx.fillRect(pos.x, pos.y, dim.w, dim.h) if (DEBUG) Debug.checkpoint('board done') + // --------------------------------------------------------------- + // DRAW TILES // --------------------------------------------------------------- for (let tile of tilesSortedByZIndex()) { - let bmp = bitmaps[tile.idx] + const bmp = bitmaps[tile.idx] pos = viewport.worldToViewport({ x: puzzle.info.tileDrawOffset + tile.pos.x, y: puzzle.info.tileDrawOffset + tile.pos.y, @@ -672,6 +308,8 @@ async function main () { ) } if (DEBUG) Debug.checkpoint('tiles done') + // --------------------------------------------------------------- + // DRAW PLAYERS // --------------------------------------------------------------- @@ -679,19 +317,16 @@ async function main () { const p = players[id] const cursor = p.down ? cursorGrab : cursorHand const pos = viewport.worldToViewport(p) - ctx.drawImage(cursor, pos.x, pos.y) + ctx.drawImage(cursor, + Math.round(pos.x - cursor.width/2), + Math.round(pos.y - cursor.height/2) + ) } - // if (DEBUG) Debug.checkpoint('players done') + // --------------------------------------------------------------- - if (DEBUG) Debug.checkpoint('all done') - - rerenderTable = false - rerenderPlayer = false rerender = false - rectTable.reset() - rectPlayer.reset() } run({ diff --git a/server/Game.js b/server/Game.js new file mode 100644 index 0000000..3cf3b21 --- /dev/null +++ b/server/Game.js @@ -0,0 +1,426 @@ +import { createPuzzle } from './Puzzle.js' +import Geometry from './../common/Geometry.js' + +const GAMES = {} + +function exists(gameId) { + return (!!GAMES[gameId]) || false +} + +async function createGame(gameId, targetTiles, image) { + GAMES[gameId] = { + puzzle: await createPuzzle(targetTiles, image), + players: {}, + + sockets: [], + evtInfos: {}, + } +} + +function addPlayer(gameId, playerId) { + GAMES[gameId].players[playerId] = { + id: playerId, + x: 0, + y: 0, + down: false + } + GAMES[gameId].evtInfos[playerId] = { + _last_mouse: null, + _last_mouse_down: null, + } +} + +function addSocket(gameId, socket) { + const sockets = GAMES[gameId].sockets + + if (!sockets.includes(socket)) { + sockets.push(socket) + } +} + +function get(gameId) { + return GAMES[gameId] +} + +function getSockets(gameId) { + return GAMES[gameId].sockets +} + +function changePlayer(gameId, playerId, change) { + for (let k of Object.keys(change)) { + GAMES[gameId].players[playerId][k] = change[k] + } +} + +function changeData(gameId, change) { + for (let k of Object.keys(change)) { + GAMES[gameId].puzzle.data[k] = change[k] + } +} + +function changeTile(gameId, tileIdx, change) { + for (let k of Object.keys(change)) { + GAMES[gameId].puzzle.tiles[tileIdx][k] = change[k] + } +} + +const getTile = (gameId, tileIdx) => { + return GAMES[gameId].puzzle.tiles[tileIdx] +} + +const getTileGroup = (gameId, tileIdx) => { + const tile = getTile(gameId, tileIdx) + return tile.group +} + +const getFinalTilePos = (gameId, tileIdx) => { + const info = GAMES[gameId].puzzle.info + const boardPos = { + x: (info.table.width - info.width) / 2, + y: (info.table.height - info.height) / 2 + } + const srcPos = srcPosByTileIdx(gameId, tileIdx) + return Geometry.pointAdd(boardPos, srcPos) +} + +const getTilePos = (gameId, tileIdx) => { + const tile = getTile(gameId, tileIdx) + return tile.pos +} + +const getTileZIndex = (gameId, tileIdx) => { + const tile = getTile(gameId, tileIdx) + return tile.z +} + +const getFirstOwnedTileIdx = (gameId, userId) => { + for (let t of GAMES[gameId].puzzle.tiles) { + if (t.owner === userId) { + return t.idx + } + } + return -1 +} + +const getMaxGroup = (gameId) => { + return GAMES[gameId].puzzle.data.maxGroup +} + +const getMaxZIndex = (gameId) => { + return GAMES[gameId].puzzle.data.maxZ +} + +const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => { + let maxZ = 0 + for (let tileIdx of tileIdxs) { + let tileZIndex = getTileZIndex(gameId, tileIdx) + if (tileZIndex > maxZ) { + maxZ = tileZIndex + } + } + return maxZ +} + +function srcPosByTileIdx(gameId, tileIdx) { + const info = GAMES[gameId].puzzle.info + + const c = info.coords[tileIdx] + const cx = c.x * info.tileSize + const cy = c.y * info.tileSize + + return { x: cx, y: cy } +} + +function getSurroundingTilesByIdx(gameId, tileIdx) { + const info = GAMES[gameId].puzzle.info + + const _X = info.coords[tileIdx].x + const _Y = info.coords[tileIdx].y + + return [ + // top + (_Y > 0) ? (tileIdx - info.tiles_x) : -1, + // right + (_X < info.tiles_x - 1) ? (tileIdx + 1) : -1, + // bottom + (_Y < info.tiles_y - 1) ? (tileIdx + info.tiles_x) : -1, + // left + (_X > 0) ? (tileIdx - 1) : -1, + ] +} + +const setTilesZIndex = (gameId, tileIdxs, zIndex) => { + for (let tilesIdx of tileIdxs) { + changeTile(gameId, tilesIdx, { z: zIndex }) + } +} + +const moveTileDiff = (gameId, tileIdx, diff) => { + const oldPos = getTilePos(gameId, tileIdx) + const pos = Geometry.pointAdd(oldPos, diff) + changeTile(gameId, tileIdx, { pos }) +} + +const moveTilesDiff = (gameId, tileIdxs, diff) => { + for (let tileIdx of tileIdxs) { + moveTileDiff(gameId, tileIdx, diff) + } +} + +const finishTiles = (gameId, tileIdxs) => { + for (let tileIdx of tileIdxs) { + changeTile(gameId, tileIdx, { owner: -1, z: 1 }) + } +} + +const setTilesOwner = (gameId, tileIdxs, owner) => { + for (let tileIdx of tileIdxs) { + changeTile(gameId, tileIdx, { owner }) + } +} + +function pointInBounds(pt, rect) { + return pt.x >= rect.x + && pt.x <= rect.x + rect.w + && pt.y >= rect.y + && pt.y <= rect.y + rect.h +} + +// get all grouped tiles for a tile +function getGroupedTileIdxs(gameId, tileIdx) { + const tiles = GAMES[gameId].puzzle.tiles + const tile = tiles[tileIdx] + + const grouped = [] + if (tile.group) { + for (let other of tiles) { + if (other.group === tile.group) { + grouped.push(other.idx) + } + } + } else { + grouped.push(tile.idx) + } + return grouped +} + +// Returns the index of the puzzle tile with the highest z index +// that is not finished yet and that matches the position +const freeTileIdxByPos = (gameId, pos) => { + let info = GAMES[gameId].puzzle.info + let tiles = GAMES[gameId].puzzle.tiles + + let maxZ = -1 + let tileIdx = -1 + for (let idx = 0; idx < tiles.length; idx++) { + const tile = tiles[idx] + if (tile.owner !== 0) { + continue + } + + const collisionRect = { + x: tile.pos.x, + y: tile.pos.y, + w: info.tileSize, + h: info.tileSize, + } + if (pointInBounds(pos, collisionRect)) { + if (maxZ === -1 || tile.z > maxZ) { + maxZ = tile.z + tileIdx = idx + } + } + } + return tileIdx +} + +// determine if two tiles are grouped together +const areGrouped = (gameId, tileIdx1, tileIdx2) => { + const g1 = getTileGroup(gameId, tileIdx1) + const g2 = getTileGroup(gameId, tileIdx2) + return g1 && g1 === g2 +} + +function handleInput(gameId, playerId, input) { + let puzzle = GAMES[gameId].puzzle + let players = GAMES[gameId].players + let evtInfo = GAMES[gameId].evtInfos[playerId] + + let changes = [] + + const _dataChange = () => { + changes.push(['data', puzzle.data]) + } + + const _tileChange = (tileIdx) => { + changes.push(['tile', getTile(gameId, tileIdx)]) + } + + const _tileChanges = (tileIdxs) => { + for (let tileIdx of tileIdxs) { + _tileChange(tileIdx) + } + } + + const _playerChange = () => { + changes.push(['player', players[playerId]]) + } + + // put both tiles (and their grouped tiles) in the same group + const groupTiles = (gameId, tileIdx1, tileIdx2) => { + let tiles = GAMES[gameId].puzzle.tiles + let group1 = getTileGroup(gameId, tileIdx1) + let group2 = getTileGroup(gameId, tileIdx2) + + let group + let searchGroups = [] + if (group1) { + searchGroups.push(group1) + } + if (group2) { + searchGroups.push(group2) + } + if (group1) { + group = group1 + } else if (group2) { + group = group2 + } else { + let maxGroup = getMaxGroup(gameId) + 1 + changeData(gameId, { maxGroup }) + _dataChange() + group = getMaxGroup(gameId) + } + + changeTile(gameId, tileIdx1, { group }) + _tileChange(tileIdx1) + changeTile(gameId, tileIdx2, { group }) + _tileChange(tileIdx2) + + // TODO: strange + if (searchGroups.length > 0) { + for (let tile of tiles) { + if (searchGroups.includes(tile.group)) { + changeTile(gameId, tile.idx, { group }) + _tileChange(tile.idx) + } + } + } + } + + let [type, x, y] = input + let pos = {x, y} + if (type === 'down') { + changePlayer(gameId, playerId, { down: true }) + _playerChange() + evtInfo._last_mouse_down = pos + + const tileIdxAtPos = freeTileIdxByPos(gameId, pos) + if (tileIdxAtPos >= 0) { + console.log('tile: ', tileIdxAtPos) + let maxZ = getMaxZIndex(gameId) + 1 + changeData(gameId, { maxZ }) + _dataChange() + const tileIdxs = getGroupedTileIdxs(gameId, tileIdxAtPos) + setTilesZIndex(gameId, tileIdxs, getMaxZIndex(gameId)) + setTilesOwner(gameId, tileIdxs, playerId) + _tileChanges(tileIdxs) + } + + } else if (type === 'move') { + changePlayer(gameId, playerId, pos) + _playerChange() + + if (evtInfo._last_mouse_down !== null) { + let tileIdx = getFirstOwnedTileIdx(gameId, playerId) + if (tileIdx >= 0) { + const diffX = x - evtInfo._last_mouse_down.x + const diffY = y - evtInfo._last_mouse_down.y + const diff = { x: diffX, y: diffY } + const tileIdxs = getGroupedTileIdxs(gameId, tileIdx) + moveTilesDiff(gameId, tileIdxs, diff) + _tileChanges(tileIdxs) + } + + evtInfo._last_mouse_down = pos + } + } else if (type === 'up') { + changePlayer(gameId, playerId, { down: false }) + _playerChange() + evtInfo._last_mouse_down = null + + let tileIdx = getFirstOwnedTileIdx(gameId, playerId) + if (tileIdx >= 0) { + // drop the tile(s) + let tileIdxs = getGroupedTileIdxs(gameId, tileIdx) + setTilesOwner(gameId, tileIdxs, 0) + _tileChanges(tileIdxs) + + // Check if the tile was dropped near the final location + let tilePos = getTilePos(gameId, tileIdx) + let finalPos = getFinalTilePos(gameId, tileIdx) + if (Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance) { + let diff = Geometry.pointSub(finalPos, tilePos) + // Snap the tile to the final destination + moveTilesDiff(gameId, tileIdxs, diff) + finishTiles(gameId, tileIdxs) + _tileChanges(tileIdxs) + } else { + // Snap to other tiles + const check = (gameId, tileIdx, otherTileIdx, off) => { + let info = GAMES[gameId].puzzle.info + if (otherTileIdx < 0) { + return false + } + if (areGrouped(gameId, tileIdx, otherTileIdx)) { + return false + } + const tilePos = getTilePos(gameId, tileIdx) + const dstPos = Geometry.pointAdd( + getTilePos(gameId, otherTileIdx), + {x: off[0] * info.tileSize, y: off[1] * info.tileSize} + ) + if (Geometry.pointDistance(tilePos, dstPos) < info.snapDistance) { + let diff = Geometry.pointSub(dstPos, tilePos) + let tileIdxs = getGroupedTileIdxs(gameId, tileIdx) + moveTilesDiff(gameId, tileIdxs, diff) + groupTiles(gameId, tileIdx, otherTileIdx) + tileIdxs = getGroupedTileIdxs(gameId, tileIdx) + const zIndex = getMaxZIndexByTileIdxs(gameId, tileIdxs) + console.log('z:' , zIndex, tileIdxs) + setTilesZIndex(gameId, tileIdxs, zIndex) + _tileChanges(tileIdxs) + return true + } + return false + } + + for (let tileIdxTmp of getGroupedTileIdxs(gameId, tileIdx)) { + let othersIdxs = getSurroundingTilesByIdx(gameId, tileIdxTmp) + if ( + check(gameId, tileIdxTmp, othersIdxs[0], [0, 1]) // top + || check(gameId, tileIdxTmp, othersIdxs[1], [-1, 0]) // right + || check(gameId, tileIdxTmp, othersIdxs[2], [0, -1]) // bottom + || check(gameId, tileIdxTmp, othersIdxs[3], [1, 0]) // left + ) { + break + } + } + } + } + } + // console.log(mouse) + evtInfo._last_mouse = pos + + return changes +} + + +export default { + createGame, + exists, + addPlayer, + addSocket, + get, + getSockets, + handleInput, +} diff --git a/server/puzzle.js b/server/Puzzle.js similarity index 99% rename from server/puzzle.js rename to server/Puzzle.js index 2ac4592..d45ef09 100644 --- a/server/puzzle.js +++ b/server/Puzzle.js @@ -201,5 +201,5 @@ const coordsByNum = (puzzleInfo) => { } export { - createPuzzle + createPuzzle, } diff --git a/server/index.js b/server/index.js index cee0dca..da9a4e4 100644 --- a/server/index.js +++ b/server/index.js @@ -1,13 +1,18 @@ import WebSocketServer from './WebSocketServer.js' import express from 'express' -import { createPuzzle } from './puzzle.js' import config from './config.js' import { uniqId, choice } from './util.js' +import Game from './Game.js' + +const EV_SERVER_STATE_CHANGED = 1 +const EV_SERVER_INIT = 4 +const EV_CLIENT_MOUSE = 2 +const EV_CLIENT_INIT = 3 // desired number of tiles // actual calculated number can be higher -const TARGET_TILES = 500 +const TARGET_TILES = 1000 const IMAGES = [ '/example-images/ima_86ec3fa.jpeg', @@ -37,6 +42,7 @@ app.use('/g/:gid', (req, res, next) => { `) }) +app.use('/common/', express.static('./../common/')) app.use('/', (req, res, next) => { if (req.path === '/') { res.send(` @@ -63,57 +69,31 @@ const notify = (data, sockets) => { for (let socket of sockets) { wss.notifyOne(data, socket) } - console.log('notify', data) + // console.log('notify', data) } wss.on('message', async ({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 - games[gid] = games[gid] || { - puzzle: await createPuzzle(TARGET_TILES, choice(IMAGES)), - players: {}, - sockets: [] + const playerId = proto[0] + const gameId = proto[1] + const [type, typeData] = JSON.parse(data) + switch (type) { + case EV_CLIENT_INIT: { + if (!Game.exists(gameId)) { + await Game.createGame(gameId, TARGET_TILES, choice(IMAGES)) } - if (!games[gid].sockets.includes(socket)) { - games[gid].sockets.push(socket) - } - games[gid].players[uid] = {id: uid, x: 0, y: 0, down: false} + Game.addPlayer(gameId, playerId) + Game.addSocket(gameId, socket) - wss.notifyOne({ - type: 'init', - game: { - puzzle: games[gid].puzzle, - players: games[gid].players, - }, - }, socket) + wss.notifyOne([EV_SERVER_INIT, Game.get(gameId)], socket) } break; - // somebody has changed the state - case 'state': { - for (let change of parsed.state.changes) { - switch (change.type) { - case 'change_player': { - games[gid].players[uid] = change.player - } break; - case 'change_tile': { - games[gid].puzzle.tiles[change.tile.idx] = change.tile - } break; - case 'change_data': { - games[gid].puzzle.data = change.data - } break; - } + case EV_CLIENT_MOUSE: { + const changes = Game.handleInput(gameId, playerId, typeData) + if (changes.length > 0) { + notify([EV_SERVER_STATE_CHANGED, changes], Game.getSockets(gameId)) } - notify({ - type:'state_changed', - origin: uid, - changes: parsed.state.changes, - }, games[gid].sockets) } break; } } catch (e) {