diff --git a/common/GameCommon.js b/common/GameCommon.js new file mode 100644 index 0000000..0e886df --- /dev/null +++ b/common/GameCommon.js @@ -0,0 +1,412 @@ +import Geometry from './Geometry.js' + +const GAMES = {} + +function exists(gameId) { + return (!!GAMES[gameId]) || false +} + +function setGame(gameId, game) { + GAMES[gameId] = game +} + +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.tilesX) : -1, + // right + (_X < info.tilesX - 1) ? (tileIdx + 1) : -1, + // bottom + (_Y < info.tilesY - 1) ? (tileIdx + info.tilesX) : -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 }) + } +} + +// 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 (Geometry.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 { + setGame, + exists, + addPlayer, + addSocket, + get, + getSockets, + handleInput, +} 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/Protocol.js b/common/Protocol.js new file mode 100644 index 0000000..07f4ccb --- /dev/null +++ b/common/Protocol.js @@ -0,0 +1,51 @@ +/* +SERVER_CLIENT_MESSAGE_PROTOCOL +NOTE: clients always send game id and their id + when creating sockets (via socket.protocol), so + this doesn't need to be set in each message data + +NOTE: first value in the array is always the type of event/message + when describing them below, the value each has is used + instead of writing EVENT_TYPE or something ismilar + + +EV_CLIENT_EVENT: event triggered by clients and sent to server +[ + EV_CLIENT_EVENT, // constant value, type of event + CLIENT_SEQ, // sequence number sent by client. + EV_DATA, // (eg. mouse input info) +] + +EV_SERVER_EVENT: event sent to clients after recieving a client + event and processing it +[ + EV_SERVER_EVENT, // constant value, type of event + CLIENT_ID, // user who sent the client event + CLIENT_SEQ, // sequence number of the client event msg + CHANGES_TRIGGERED_BY_CLIENT_EVENT, +] + +EV_CLIENT_INIT: event sent by client to enter a game +[ + EV_CLIENT_INIT, // constant value, type of event +] + +EV_SERVER_INIT: event sent to one client after that client + connects to a game +[ + EV_SERVER_INIT, // constant value, type of event + GAME, // complete game instance required by + // client to build client side of the game +] +*/ +const EV_SERVER_EVENT = 1 +const EV_SERVER_INIT = 4 +const EV_CLIENT_EVENT = 2 +const EV_CLIENT_INIT = 3 + +export default { + EV_SERVER_EVENT, + EV_SERVER_INIT, + EV_CLIENT_EVENT, + EV_CLIENT_INIT, +} diff --git a/server/util.js b/common/Util.js similarity index 99% rename from server/util.js rename to common/Util.js index 4d0293c..0d20aa1 100644 --- a/server/util.js +++ b/common/Util.js @@ -1,4 +1,3 @@ - // get a unique id export const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2) 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..bb92a67 100644 --- a/game/Camera.js +++ b/game/Camera.js @@ -28,30 +28,27 @@ 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 + // TODO: mouse-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 +58,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 +70,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..0c9ea5c 100644 --- a/game/Communication.js +++ b/game/Communication.js @@ -1,49 +1,55 @@ import WsClient from './WsClient.js' +import Protocol from './../common/Protocol.js' +/** @type WsClient */ let conn let changesCallback = () => {} -function onChanges(callback) { +function onServerChange(callback) { changesCallback = callback } -function connect(gameId, playerId) { - conn = new WsClient(WS_ADDRESS, playerId + '|' + gameId) +function send(message) { + conn.send(JSON.stringify(message)) +} + +let clientSeq +let events +function connect(gameId, clientId) { + clientSeq = 0 + events = {} + conn = new WsClient(WS_ADDRESS, clientId + '|' + gameId) return new Promise(r => { conn.connect() - conn.send(JSON.stringify({ type: 'init' })) + send([Protocol.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 msg = JSON.parse(data) + const msgType = msg[0] + if (msgType === Protocol.EV_SERVER_INIT) { + const game = msg[1] + r(game) + } else if (msgType === Protocol.EV_SERVER_EVENT) { + const msgClientId = msg[1] + const msgClientSeq = msg[2] + if (msgClientId === clientId && events[msgClientSeq]) { + delete events[msgClientSeq] + } + changesCallback(msg) } }) }) } -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 sendClientEvent(mouse) { + // when sending event, increase number of sent events + // and add the event locally + clientSeq++; + events[clientSeq] = mouse + send([Protocol.EV_CLIENT_EVENT, clientSeq, events[clientSeq]]) } export default { connect, - onChanges, - addChange, - sendChanges, + onServerChange, + sendClientEvent, } diff --git a/game/EventAdapter.js b/game/EventAdapter.js deleted file mode 100644 index 806959b..0000000 --- a/game/EventAdapter.js +++ /dev/null @@ -1,38 +0,0 @@ -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', deltaY: e.deltaY, x: e.offsetX, y: e.offsetY}) - } -} diff --git a/game/Game.js b/game/Game.js new file mode 100644 index 0000000..93800b7 --- /dev/null +++ b/game/Game.js @@ -0,0 +1,6 @@ +import GameCommon from './../common/GameCommon.js' + +export default { + createGame: GameCommon.setGame, + handleInput: GameCommon.handleInput, +} 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/PuzzleGraphics.js b/game/PuzzleGraphics.js new file mode 100644 index 0000000..29dc7a7 --- /dev/null +++ b/game/PuzzleGraphics.js @@ -0,0 +1,119 @@ +import Geometry from '../common/Geometry.js' +import Graphics from './Graphics.js' + +async function createPuzzleTileBitmaps(img, tiles, info) { + 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) + + const paths = {} + function pathForShape(shape) { + const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}` + if (paths[key]) { + return paths[key] + } + + const path = new Path2D() + const topLeftEdge = { x: tileMarginWidth, y: tileMarginWidth } + path.moveTo(topLeftEdge.x, topLeftEdge.y) + for (let i = 0; i < curvyCoords.length / 6; i++) { + 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 = Geometry.pointAdd(topLeftEdge, { x: tileSize, y: 0 }) + for (let i = 0; i < curvyCoords.length / 6; i++) { + 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 = Geometry.pointAdd(topRightEdge, { x: 0, y: tileSize }) + for (let i = 0; i < curvyCoords.length / 6; i++) { + 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 = Geometry.pointSub(bottomRightEdge, { x: tileSize, y: 0 }) + for (let i = 0; i < curvyCoords.length / 6; i++) { + 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 + return path + } + + for (let tile of tiles) { + const srcRect = srcRectByIdx(info, tile.idx) + const path = pathForShape(info.shapes[tile.idx]) + + const c = Graphics.createCanvas(tileDrawSize, tileDrawSize) + const ctx = c.getContext('2d') + // ----------------------------------------------------------- + // ----------------------------------------------------------- + ctx.lineWidth = 2 + ctx.stroke(path) + // ----------------------------------------------------------- + // ----------------------------------------------------------- + ctx.save(); + ctx.clip(path) + ctx.drawImage( + img, + srcRect.x - tileMarginWidth, + srcRect.y - tileMarginWidth, + tileDrawSize, + tileDrawSize, + 0, + 0, + tileDrawSize, + tileDrawSize, + ) + ctx.stroke(path) + ctx.restore(); + + bitmaps[tile.idx] = await createImageBitmap(c) + } + + return bitmaps +} + +function srcRectByIdx(puzzleInfo, idx) { + const c = puzzleInfo.coords[idx] + return { + x: c.x * puzzleInfo.tileSize, + y: c.y * puzzleInfo.tileSize, + w: puzzleInfo.tileSize, + h: puzzleInfo.tileSize, + } +} + +async function loadPuzzleBitmaps(puzzle) { + // load bitmap, to determine the original size of the image + const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl) + + // creation of tile bitmaps + // then create the final puzzle bitmap + // NOTE: this can decrease OR increase in size! + const bmpResized = await Graphics.resizeBitmap(bmp, puzzle.info.width, puzzle.info.height) + return await createPuzzleTileBitmaps(bmpResized, puzzle.tiles, puzzle.info) +} + +export default { + loadPuzzleBitmaps, +} diff --git a/game/index.js b/game/index.js index 0b5bb27..637c39f 100644 --- a/game/index.js +++ b/game/index.js @@ -1,10 +1,12 @@ "use strict" import {run} from './gameloop.js' import Camera from './Camera.js' -import EventAdapter from './EventAdapter.js' import Graphics from './Graphics.js' import Debug from './Debug.js' import Communication from './Communication.js' +import Util from './../common/Util.js' +import PuzzleGraphics from './PuzzleGraphics.js' +import Game from './Game.js' if (typeof GAME_ID === 'undefined') throw '[ GAME_ID not set ]' if (typeof WS_ADDRESS === 'undefined') throw '[ WS_ADDRESS not set ]' @@ -16,622 +18,199 @@ 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 - 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) - - const paths = {} - function pathForShape(shape) { - const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}` - if (paths[key]) { - return paths[key] - } - - const path = new Path2D() - 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 }) - path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); - } - const topRightEdge = 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 }) - path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); - } - const bottomRightEdge = 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 }) - path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); - } - const bottomLeftEdge = 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 }) - path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); - } - paths[key] = path - return path - } - - for (let tile of tiles) { - const srcRect = srcRectByIdx(info, tile.idx) - const path = pathForShape(info.shapes[tile.idx]) - - const c = Graphics.createCanvas(tileDrawSize, tileDrawSize) - const ctx = c.getContext('2d') - // ----------------------------------------------------------- - // ----------------------------------------------------------- - ctx.lineWidth = 2 - ctx.stroke(path) - // ----------------------------------------------------------- - // ----------------------------------------------------------- - ctx.save(); - ctx.clip(path) - ctx.drawImage( - img, - srcRect.x - tileMarginWidth, - srcRect.y - tileMarginWidth, - tileDrawSize, - tileDrawSize, - 0, - 0, - tileDrawSize, - tileDrawSize, - ) - ctx.stroke(path) - ctx.restore(); - - bitmaps[tile.idx] = await createImageBitmap(c) - } - - return bitmaps -} - -function srcRectByIdx(puzzleInfo, idx) { - let c = puzzleInfo.coords[idx] - let cx = c.x * puzzleInfo.tileSize - let cy = c.y * puzzleInfo.tileSize - return { - x: cx, - y: cy, - w: puzzleInfo.tileSize, - h: puzzleInfo.tileSize, - } -} - -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) - - // creation of tile bitmaps - // then create the final puzzle bitmap - // NOTE: this can decrease OR increase in size! - const bmpResized = await Graphics.resizeBitmap(bmp, puzzle.info.width, puzzle.info.height) - return await createPuzzleTileBitmaps(bmpResized, puzzle.tiles, puzzle.info) -} - -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() + ID = Util.uniqId() localStorage.setItem("ID", ID) } return ID } -async function main () { +const getFirstOwnedTile = (puzzle, userId) => { + for (let t of puzzle.tiles) { + if (t.owner === userId) { + return t + } + } + return null +} + +export default class EventAdapter { + constructor(canvas, viewport) { + this._mouseEvts = [] + this._viewport = viewport + 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) { + const pos = this._viewport.viewportToWorld({ + x: e.offsetX, + y: e.offsetY, + }) + this._mouseEvts.push(['down', pos.x, pos.y]) + } + } + + _mouseUp(e) { + if (e.button === 0) { + const pos = this._viewport.viewportToWorld({ + x: e.offsetX, + y: e.offsetY, + }) + this._mouseEvts.push(['up', pos.x, pos.y]) + } + } + + _mouseMove(e) { + const pos = this._viewport.viewportToWorld({ + x: e.offsetX, + y: e.offsetY, + }) + this._mouseEvts.push(['move', pos.x, pos.y]) + } + + _wheel(e) { + const pos = this._viewport.viewportToWorld({ + x: e.offsetX, + y: e.offsetY, + }) + const evt = e.deltaY < 0 ? 'zoomin' : 'zoomout' + this._mouseEvts.push([evt, pos.x, pos.y]) + } +} + +async function main() { let gameId = GAME_ID - let me = initme() + let CLIENT_ID = initme() let cursorGrab = await Graphics.loadImageToBitmap('/grab.png') let cursorHand = await Graphics.loadImageToBitmap('/hand.png') - const game = await Communication.connect(gameId, me) + const game = await Communication.connect(gameId, CLIENT_ID) + Game.createGame(GAME_ID, game); - const bitmaps = await loadPuzzleBitmaps(game.puzzle) + const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(game.puzzle) 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] + players[CLIENT_ID][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 const canvas = addCanvasToDom(Graphics.createCanvas()) const ctx = canvas.getContext('2d') - const evts = new EventAdapter(canvas) // 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) + const evts = new EventAdapter(canvas, viewport) + + Communication.onServerChange((msg) => { + const msgType = msg[0] + const evClientId = msg[1] + const evClientSeq = msg[2] + const evChanges = msg[3] + for(let [changeType, changeData] of evChanges) { + switch (changeType) { + case 'player': { + if (changeData.id !== CLIENT_ID) { + players[changeData.id] = changeData + 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[changeData.idx] = changeData + rerender = true } break; - case 'change_data': { - puzzle.data = change.data + case 'data': { + puzzle.data = changeData + 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 + for (let evt of evts.consumeAll()) { - 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') { - changePlayer({ x: tp.x, y: tp.y }) - if (_last_mouse) { - rectPlayer.add(_last_mouse, cursorGrab.width) - } - rectPlayer.add(mouse, cursorGrab.width) + // LOCAL ONLY CHANGES + // ------------------------------------------------------------- + const type = evt[0] + const pos = {x: evt[1], y: evt[2]} + if (type === 'move') { + rerender = true + changePlayer(pos) - 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, CLIENT_ID)) { // move the cam - const diffX = Math.round(mouse.x - last_x) - const diffY = Math.round(mouse.y - last_y) + const mouse = viewport.worldToViewport(pos) + 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 - } - } - } else if (mouse.type === 'down') { - changePlayer({ down: true }) - rectPlayer.add(mouse, cursorGrab.width) - _last_mouse_down = mouse - if (last_x === null || last_y === null) { - last_x = mouse.x - last_y = mouse.y + _last_mouse_down = mouse } - - 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) - + } else if (type === 'down') { + _last_mouse_down = viewport.worldToViewport(pos) + } else if (type === 'up') { _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() - || mouse.deltaY > 0 && viewport.zoomOut() - ) { + } else if (type === 'zoomin') { + if (viewport.zoomIn()) { rerender = true - changePlayer({ x: tp.x, y: tp.y }) - if (_last_mouse) { - rectPlayer.add(_last_mouse, cursorGrab.width) - } - rectPlayer.add(mouse, cursorGrab.width) + changePlayer(pos) + } + } else if (type === 'zoomout') { + if (viewport.zoomOut()) { + rerender = true + changePlayer(pos) } } - // console.log(mouse) - _last_mouse = mouse - } - if (rectTable.get()) { - rerenderTable = true - } - if (rectPlayer.get()) { - rerenderPlayer = true - } - Communication.sendChanges() + // LOCAL + SERVER CHANGES + // ------------------------------------------------------------- + Game.handleInput(GAME_ID, CLIENT_ID, evt) + Communication.sendClientEvent(evt) + } } const onRender = () => { - if (!rerenderTable && !rerenderPlayer && !rerender) { + if (!rerender) { return } @@ -640,24 +219,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 +260,8 @@ async function main () { ) } if (DEBUG) Debug.checkpoint('tiles done') + // --------------------------------------------------------------- + // DRAW PLAYERS // --------------------------------------------------------------- @@ -679,19 +269,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..5d722dc --- /dev/null +++ b/server/Game.js @@ -0,0 +1,23 @@ +import { createPuzzle } from './Puzzle.js' +import GameCommon from './../common/GameCommon.js' + +async function createGame(gameId, targetTiles, image) { + const game = { + puzzle: await createPuzzle(targetTiles, image), + players: {}, + + sockets: [], + evtInfos: {}, + } + GameCommon.setGame(gameId, game) +} + +export default { + createGame, + exists: GameCommon.exists, + addPlayer: GameCommon.addPlayer, + addSocket: GameCommon.addSocket, + get: GameCommon.get, + getSockets: GameCommon.getSockets, + handleInput: GameCommon.handleInput, +} diff --git a/server/puzzle.js b/server/Puzzle.js similarity index 66% rename from server/puzzle.js rename to server/Puzzle.js index 2ac4592..0a45cab 100644 --- a/server/puzzle.js +++ b/server/Puzzle.js @@ -1,25 +1,23 @@ import sizeOf from 'image-size' -import { choice, shuffle } from './util.js' +import Util from './../common/Util.js' // cut size of each puzzle tile in the // final resized version of the puzzle image const TILE_SIZE = 64 async function createPuzzle(targetTiles, image) { - const imgPath = './../game' + image - const imgUrl = image + const imagePath = './../game' + image + const imageUrl = image // load bitmap, to determine the original size of the image - let dim = sizeOf(imgPath) + const dim = sizeOf(imagePath) // determine puzzle information from the bitmap - let info = determinePuzzleInfo(dim.width, dim.height, targetTiles) + const info = determinePuzzleInfo(dim.width, dim.height, targetTiles) let tiles = new Array(info.tiles) for (let i = 0; i < tiles.length; i++) { - tiles[i] = { - idx: i, - } + tiles[i] = { idx: i } } const shapes = determinePuzzleTileShapes(info) @@ -33,43 +31,46 @@ async function createPuzzle(targetTiles, image) { } } - let tableWidth = info.width * 3 - let tableHeight = info.height * 3 + const tableWidth = info.width * 3 + const tableHeight = info.height * 3 - let off = (info.tileSize * 1.5) - let last = {x: info.width - (1 * off), y: info.height - (2 * off) } - let count_x = Math.ceil(info.width / off) + 2 - let count_y = Math.ceil(info.height / off) + 2 + const off = info.tileSize * 1.5 + let last = { + x: info.width - (1 * off), + y: info.height - (2 * off), + } + let countX = Math.ceil(info.width / off) + 2 + let countY = Math.ceil(info.height / off) + 2 - let diff_x = off - let diff_y = 0 + let diffX = off + let diffY = 0 let index = 0 for (let pos of positions) { pos.x = last.x pos.y = last.y - last.x+=diff_x - last.y+=diff_y + last.x+=diffX + last.y+=diffY index++ // did we move horizontally? - if (diff_x !== 0) { - if (index === count_x) { - diff_y = diff_x - count_y ++ - diff_x = 0 + if (diffX !== 0) { + if (index === countX) { + diffY = diffX + countY++ + diffX = 0 index = 0 } } else { - if (index === count_y) { - diff_x = -diff_y - count_x ++ - diff_y = 0 + if (index === countY) { + diffX = -diffY + countX++ + diffY = 0 index = 0 } } } // then shuffle the positions - positions = shuffle(positions) + positions = Util.shuffle(positions) tiles = tiles.map(tile => { return { @@ -91,7 +92,7 @@ async function createPuzzle(targetTiles, image) { }) // Complete puzzle object - const p = { + return { // tiles array tiles, // game data for puzzle, data changes during the game @@ -109,7 +110,7 @@ async function createPuzzle(targetTiles, image) { }, // information that was used to create the puzzle targetTiles: targetTiles, - imageUrl: imgUrl, + imageUrl, width: info.width, // actual puzzle width (same as bitmap.width) height: info.height, // actual puzzle height (same as bitmap.height) @@ -122,8 +123,8 @@ async function createPuzzle(targetTiles, image) { // 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 + tilesX: info.tilesX, // number of tiles each row + tilesY: info.tilesY, // 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 @@ -133,7 +134,6 @@ async function createPuzzle(targetTiles, image) { shapes: shapes, // tile shapes }, } - return p } function determinePuzzleTileShapes(info) { @@ -142,34 +142,38 @@ function determinePuzzleTileShapes(info) { const shapes = new Array(info.tiles) for (let i = 0; i < info.tiles; i++) { shapes[i] = { - top: info.coords[i].y === 0 ? 0 : shapes[i - info.tiles_x].bottom * -1, - right: info.coords[i].x === info.tiles_x - 1 ? 0 : choice(tabs), + top: info.coords[i].y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1, + right: info.coords[i].x === info.tilesX - 1 ? 0 : Util.choice(tabs), left: info.coords[i].x === 0 ? 0 : shapes[i - 1].right * -1, - bottom: info.coords[i].y === info.tiles_y - 1 ? 0 : choice(tabs), + bottom: info.coords[i].y === info.tilesY - 1 ? 0 : Util.choice(tabs), } } return shapes } -const determinePuzzleInfo = (w, h, targetTiles) => { - let tileSize = 0 +const determineTilesXY = (w, h, targetTiles) => { + const w_ = w < h ? (w * h) : (w * w) + const h_ = w < h ? (h * h) : (w * h) + let size = 0 let tiles = 0 do { - tileSize++ - tiles = tilesFit(w, h, tileSize) + size++ + tiles = Math.floor(w_ / size) * Math.floor(h_ / size) } while (tiles >= targetTiles) - tileSize-- + size-- + return { + tilesX: Math.round(w_ / size), + tilesY: Math.round(h_ / size), + } +} - tiles = tilesFit(w, h, tileSize) - const tiles_x = Math.round(w / tileSize) - const 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 - const width = tiles_x * tileSize - const height = tiles_y * tileSize - const coords = coordsByNum({ width, height, tileSize, tiles }) +const determinePuzzleInfo = (w, h, targetTiles) => { + const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles) + const tiles = tilesX * tilesY + const tileSize = TILE_SIZE + const width = tilesX * tileSize + const height = tilesY * tileSize + const coords = buildCoords({ width, height, tileSize, tiles }) const tileMarginWidth = tileSize * .5; const tileDrawSize = Math.round(tileSize + tileMarginWidth * 2) @@ -181,25 +185,23 @@ const determinePuzzleInfo = (w, h, targetTiles) => { tileMarginWidth, tileDrawSize, tiles, - tiles_x, - tiles_y, + tilesX, + tilesY, coords, } } -const tilesFit = (w, h, size) => Math.floor(w / size) * Math.floor(h / size) - -const coordsByNum = (puzzleInfo) => { - const w_tiles = puzzleInfo.width / puzzleInfo.tileSize +const buildCoords = (puzzleInfo) => { + const wTiles = 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 + const y = Math.floor(i / wTiles) + const x = i % wTiles coords[i] = { x, y } } return coords } export { - createPuzzle + createPuzzle, } diff --git a/server/index.js b/server/index.js index cee0dca..0c3224c 100644 --- a/server/index.js +++ b/server/index.js @@ -1,13 +1,14 @@ 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 Protocol from './../common/Protocol.js' +import Util from './../common/Util.js' +import Game from './Game.js' // 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 +38,7 @@ app.use('/g/:gid', (req, res, next) => { `) }) +app.use('/common/', express.static('./../common/')) app.use('/', (req, res, next) => { if (req.path === '/') { res.send(` @@ -44,7 +46,7 @@ app.use('/', (req, res, next) => { html,body {margin: 0; overflow: hidden;} html, body, #main { background: #222 } - New game :P + New game :P ${Object.keys(games).map(k => { return `Game ${k}` })} @@ -59,61 +61,41 @@ app.use('/', (req, res, next) => { const wss = new WebSocketServer(config.ws); const notify = (data, sockets) => { - // TODO: throttle + // TODO: throttle? for (let socket of sockets) { wss.notifyOne(data, socket) } - 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 clientId = proto[0] + const gameId = proto[1] + const msg = JSON.parse(data) + const msgType = msg[0] + switch (msgType) { + case Protocol.EV_CLIENT_INIT: { + if (!Game.exists(gameId)) { + await Game.createGame(gameId, TARGET_TILES, Util.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, clientId) + Game.addSocket(gameId, socket) - wss.notifyOne({ - type: 'init', - game: { - puzzle: games[gid].puzzle, - players: games[gid].players, - }, - }, socket) + notify( + [Protocol.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; - } - } - notify({ - type:'state_changed', - origin: uid, - changes: parsed.state.changes, - }, games[gid].sockets) + case Protocol.EV_CLIENT_EVENT: { + const clientSeq = msg[1] + const clientEvtData = msg[2] + const changes = Game.handleInput(gameId, clientId, clientEvtData) + notify( + [Protocol.EV_SERVER_EVENT, clientId, clientSeq, changes], + Game.getSockets(gameId) + ) } break; } } catch (e) {