feature/authoritative-server #1

Merged
para merged 6 commits from feature/authoritative-server into master 2020-11-17 21:39:26 +00:00
9 changed files with 619 additions and 539 deletions
Showing only changes of commit d592cef494 - Show all commits

50
common/Geometry.js Normal file
View file

@ -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,
}

3
common/package.json Normal file
View file

@ -0,0 +1,3 @@
{
"type": "module"
}

View file

@ -28,30 +28,26 @@ export default class Camera {
this.y += y / this.zoom this.y += y / this.zoom
} }
zoomOut() { setZoom(newzoom) {
const newzoom = Math.max(this.zoom - this.zoomStep, this.minZoom) const zoom = Math.min(Math.max(newzoom, this.minZoom), this.maxZoom)
if (newzoom !== this.zoom) { if (zoom == this.zoom) {
// centered zoom return false
this.x -= ((this.width / this.zoom) - (this.width / newzoom)) / 2 }
this.y -= ((this.height / this.zoom) - (this.height / newzoom)) / 2
this.zoom = newzoom // 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 return true
} }
return false
zoomOut() {
return this.setZoom(this.zoom - this.zoomStep)
} }
zoomIn() { zoomIn() {
const newzoom = Math.min(this.zoom + this.zoomStep, this.maxZoom) return this.setZoom(this.zoom + this.zoomStep)
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
} }
/** /**
@ -61,8 +57,8 @@ export default class Camera {
*/ */
viewportToWorld(coord) { viewportToWorld(coord) {
return { return {
x: (coord.x / this.zoom) - this.x, x: Math.round((coord.x / this.zoom) - this.x),
y: (coord.y / this.zoom) - this.y, y: Math.round((coord.y / this.zoom) - this.y),
} }
} }
@ -73,15 +69,15 @@ export default class Camera {
*/ */
worldToViewport(coord) { worldToViewport(coord) {
return { return {
x: (coord.x + this.x) * this.zoom, x: Math.round((coord.x + this.x) * this.zoom),
y: (coord.y + this.y) * this.zoom, y: Math.round((coord.y + this.y) * this.zoom),
} }
} }
worldDimToViewport(dim) { worldDimToViewport(dim) {
return { return {
w: dim.w * this.zoom, w: Math.round(dim.w * this.zoom),
h: dim.h * this.zoom, h: Math.round(dim.h * this.zoom),
} }
} }
} }

View file

@ -1,5 +1,10 @@
import WsClient from './WsClient.js' 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 conn
let changesCallback = () => {} let changesCallback = () => {}
@ -11,39 +16,26 @@ function connect(gameId, playerId) {
conn = new WsClient(WS_ADDRESS, playerId + '|' + gameId) conn = new WsClient(WS_ADDRESS, playerId + '|' + gameId)
return new Promise(r => { return new Promise(r => {
conn.connect() conn.connect()
conn.send(JSON.stringify({ type: 'init' })) conn.send(JSON.stringify([EV_CLIENT_INIT]))
conn.onSocket('message', async ({ data }) => { conn.onSocket('message', async ({ data }) => {
const d = JSON.parse(data) const [type, typeData] = JSON.parse(data)
if (d.type === 'init') { if (type === EV_SERVER_INIT) {
r(d.game) const game = typeData
} else if (d.type === 'state_changed' && d.origin !== playerId) { r(game)
changesCallback(d.changes) } else if (type === EV_SERVER_STATE_CHANGED) {
const changes = typeData
changesCallback(changes)
} }
}) })
}) })
} }
const _STATE = { function addMouse(mouse) {
changed: false, conn.send(JSON.stringify([EV_CLIENT_MOUSE, mouse]))
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
}
} }
export default { export default {
connect, connect,
onChanges, onChanges,
addChange, addMouse,
sendChanges,
} }

View file

@ -1,5 +1,3 @@
// import Bitmap from './Bitmap.js'
function createCanvas(width = 0, height = 0) { function createCanvas(width = 0, height = 0) {
const c = document.createElement('canvas') const c = document.createElement('canvas')
c.width = width === 0 ? window.innerWidth : width c.width = width === 0 ? window.innerWidth : width

View file

@ -5,6 +5,7 @@ import EventAdapter from './EventAdapter.js'
import Graphics from './Graphics.js' import Graphics from './Graphics.js'
import Debug from './Debug.js' import Debug from './Debug.js'
import Communication from './Communication.js' import Communication from './Communication.js'
import Geometry from './../common/Geometry.js'
if (typeof GAME_ID === 'undefined') throw '[ GAME_ID not set ]' if (typeof GAME_ID === 'undefined') throw '[ GAME_ID not set ]'
if (typeof WS_ADDRESS === 'undefined') throw '[ WS_ADDRESS not set ]' if (typeof WS_ADDRESS === 'undefined') throw '[ WS_ADDRESS not set ]'
@ -16,55 +17,6 @@ function addCanvasToDom(canvas) {
return 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) { async function createPuzzleTileBitmaps(img, tiles, info) {
var tileSize = info.tileSize var tileSize = info.tileSize
var tileMarginWidth = info.tileMarginWidth var tileMarginWidth = info.tileMarginWidth
@ -93,30 +45,30 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
const topLeftEdge = { x: tileMarginWidth, y: tileMarginWidth } const topLeftEdge = { x: tileMarginWidth, y: tileMarginWidth }
path.moveTo(topLeftEdge.x, topLeftEdge.y) path.moveTo(topLeftEdge.x, topLeftEdge.y)
for (let i = 0; i < curvyCoords.length / 6; i++) { 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 p1 = Geometry.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 p2 = Geometry.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 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); 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++) { 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 p1 = Geometry.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 p2 = Geometry.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 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); 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++) { 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 p1 = Geometry.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 p2 = Geometry.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 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); 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++) { 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 p1 = Geometry.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 p2 = Geometry.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 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); path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
} }
paths[key] = path 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) { async function loadPuzzleBitmaps(puzzle) {
// load bitmap, to determine the original size of the image // load bitmap, to determine the original size of the image
const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl) const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl)
@ -252,6 +146,15 @@ function initme() {
return ID return ID
} }
const getFirstOwnedTile = (puzzle, userId) => {
for (let t of puzzle.tiles) {
if (t.owner === userId) {
return t
}
}
return null
}
async function main () { async function main () {
let gameId = GAME_ID let gameId = GAME_ID
let me = initme() let me = initme()
@ -265,30 +168,12 @@ async function main () {
const puzzle = game.puzzle const puzzle = game.puzzle
const players = game.players 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 let rerender = true
const changePlayer = (change) => { const changePlayer = (change) => {
for (let k of Object.keys(change)) { for (let k of Object.keys(change)) {
players[me][k] = change[k] 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 // 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 // initialize some view data
// this global data will change according to input events // this global data will change according to input events
const viewport = new Camera(canvas) 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) => { Communication.onChanges((changes) => {
for (let change of changes) { for (let [type, typeData] of changes) {
switch (change.type) { switch (type) {
case 'change_player': { case 'player': {
if (players[change.player.id]) { if (typeData.id !== me) {
rectPlayer.add(viewport.worldToViewport(players[change.player.id]), cursorGrab.width) players[typeData.id] = typeData
rerender = true
} }
players[change.player.id] = change.player
rectPlayer.add(viewport.worldToViewport(players[change.player.id]), cursorGrab.width)
} break; } break;
case 'tile': {
case 'change_tile': { puzzle.tiles[typeData.idx] = typeData
rectTable.add(puzzle.tiles[change.tile.idx].pos, puzzle.info.tileDrawSize) rerender = true
puzzle.tiles[change.tile.idx] = change.tile
rectTable.add(puzzle.tiles[change.tile.idx].pos, puzzle.info.tileDrawSize)
} break; } break;
case 'change_data': { case 'data': {
puzzle.data = change.data puzzle.data = typeData
rerender = true
} break; } 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 // In the middle of the table, there is a board. this is to
// tell the player where to place the final puzzle // tell the player where to place the final puzzle
const boardColor = '#505050' 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 tilesSortedByZIndex = () => {
const sorted = puzzle.tiles.slice() const sorted = puzzle.tiles.slice()
return sorted.sort((t1, t2) => t1.z - t2.z) 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 let _last_mouse_down = null
const onUpdate = () => { 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()) { for (let mouse of evts.consumeAll()) {
const tp = viewport.viewportToWorld(mouse) const tp = viewport.viewportToWorld(mouse)
if (mouse.type === 'move') { if (mouse.type === 'move') {
changePlayer({ x: tp.x, y: tp.y }) Communication.addMouse(['move', tp.x, 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 {
// move the cam
const diffX = Math.round(mouse.x - last_x)
const diffY = Math.round(mouse.y - last_y)
viewport.move(diffX, diffY)
rerender = true rerender = true
} changePlayer({ x: tp.x, y: tp.y })
if (_last_mouse_down && !getFirstOwnedTile(puzzle, me)) {
// move the cam
const diffX = Math.round(mouse.x - _last_mouse_down.x)
const diffY = Math.round(mouse.y - _last_mouse_down.y)
viewport.move(diffX, diffY)
_last_mouse_down = mouse
} }
} else if (mouse.type === 'down') { } else if (mouse.type === 'down') {
changePlayer({ down: true }) Communication.addMouse(['down', tp.x, tp.y])
rectPlayer.add(mouse, cursorGrab.width)
_last_mouse_down = mouse _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') { } else if (mouse.type === 'up') {
changePlayer({ down: false }) Communication.addMouse(['up', tp.x, tp.y])
if (_last_mouse) {
rectPlayer.add(_last_mouse, cursorGrab.width)
}
rectPlayer.add(mouse, cursorGrab.width)
_last_mouse_down = null _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') { } else if (mouse.type === 'wheel') {
if ( if (
mouse.deltaY < 0 && viewport.zoomIn() mouse.deltaY < 0 && viewport.zoomIn()
@ -611,27 +252,13 @@ async function main () {
) { ) {
rerender = true rerender = true
changePlayer({ x: tp.x, y: tp.y }) 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 = () => { const onRender = () => {
if (!rerenderTable && !rerenderPlayer && !rerender) { if (!rerender) {
return return
} }
@ -640,24 +267,33 @@ async function main () {
if (DEBUG) Debug.checkpoint_start(0) if (DEBUG) Debug.checkpoint_start(0)
ctx.fillStyle = puzzleTableColor // CLEAR CTX
ctx.fillRect(0, 0, canvas.width, canvas.height) // ---------------------------------------------------------------
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (DEBUG) Debug.checkpoint('clear done') if (DEBUG) Debug.checkpoint('clear done')
// ---------------------------------------------------------------
// DRAW BOARD // DRAW BOARD
// --------------------------------------------------------------- // ---------------------------------------------------------------
pos = viewport.worldToViewport(boardPos) pos = viewport.worldToViewport({
dim = viewport.worldDimToViewport({w: board.width, h: board.height}) x: (puzzle.info.table.width - puzzle.info.width) / 2,
ctx.drawImage(board, y: (puzzle.info.table.height - puzzle.info.height) / 2
0, 0, board.width, board.height, })
pos.x, pos.y, dim.w, dim.h 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') if (DEBUG) Debug.checkpoint('board done')
// ---------------------------------------------------------------
// DRAW TILES // DRAW TILES
// --------------------------------------------------------------- // ---------------------------------------------------------------
for (let tile of tilesSortedByZIndex()) { for (let tile of tilesSortedByZIndex()) {
let bmp = bitmaps[tile.idx] const bmp = bitmaps[tile.idx]
pos = viewport.worldToViewport({ pos = viewport.worldToViewport({
x: puzzle.info.tileDrawOffset + tile.pos.x, x: puzzle.info.tileDrawOffset + tile.pos.x,
y: puzzle.info.tileDrawOffset + tile.pos.y, y: puzzle.info.tileDrawOffset + tile.pos.y,
@ -672,6 +308,8 @@ async function main () {
) )
} }
if (DEBUG) Debug.checkpoint('tiles done') if (DEBUG) Debug.checkpoint('tiles done')
// ---------------------------------------------------------------
// DRAW PLAYERS // DRAW PLAYERS
// --------------------------------------------------------------- // ---------------------------------------------------------------
@ -679,19 +317,16 @@ async function main () {
const p = players[id] const p = players[id]
const cursor = p.down ? cursorGrab : cursorHand const cursor = p.down ? cursorGrab : cursorHand
const pos = viewport.worldToViewport(p) 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('players done')
// ---------------------------------------------------------------
if (DEBUG) Debug.checkpoint('all done')
rerenderTable = false
rerenderPlayer = false
rerender = false rerender = false
rectTable.reset()
rectPlayer.reset()
} }
run({ run({

426
server/Game.js Normal file
View file

@ -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,
}

View file

@ -201,5 +201,5 @@ const coordsByNum = (puzzleInfo) => {
} }
export { export {
createPuzzle createPuzzle,
} }

View file

@ -1,13 +1,18 @@
import WebSocketServer from './WebSocketServer.js' import WebSocketServer from './WebSocketServer.js'
import express from 'express' import express from 'express'
import { createPuzzle } from './puzzle.js'
import config from './config.js' import config from './config.js'
import { uniqId, choice } from './util.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 // desired number of tiles
// actual calculated number can be higher // actual calculated number can be higher
const TARGET_TILES = 500 const TARGET_TILES = 1000
const IMAGES = [ const IMAGES = [
'/example-images/ima_86ec3fa.jpeg', '/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) => { app.use('/', (req, res, next) => {
if (req.path === '/') { if (req.path === '/') {
res.send(` res.send(`
@ -63,57 +69,31 @@ const notify = (data, sockets) => {
for (let socket of sockets) { for (let socket of sockets) {
wss.notifyOne(data, socket) wss.notifyOne(data, socket)
} }
console.log('notify', data) // console.log('notify', data)
} }
wss.on('message', async ({socket, data}) => { wss.on('message', async ({socket, data}) => {
try { try {
const proto = socket.protocol.split('|') const proto = socket.protocol.split('|')
const uid = proto[0] const playerId = proto[0]
const gid = proto[1] const gameId = proto[1]
const parsed = JSON.parse(data) const [type, typeData] = JSON.parse(data)
switch (parsed.type) { switch (type) {
case 'init': { case EV_CLIENT_INIT: {
// a new player (or previous player) joined if (!Game.exists(gameId)) {
games[gid] = games[gid] || { await Game.createGame(gameId, TARGET_TILES, choice(IMAGES))
puzzle: await createPuzzle(TARGET_TILES, choice(IMAGES)),
players: {},
sockets: []
} }
if (!games[gid].sockets.includes(socket)) { Game.addPlayer(gameId, playerId)
games[gid].sockets.push(socket) Game.addSocket(gameId, socket)
}
games[gid].players[uid] = {id: uid, x: 0, y: 0, down: false}
wss.notifyOne({ wss.notifyOne([EV_SERVER_INIT, Game.get(gameId)], socket)
type: 'init',
game: {
puzzle: games[gid].puzzle,
players: games[gid].players,
},
}, socket)
} break; } break;
// somebody has changed the state case EV_CLIENT_MOUSE: {
case 'state': { const changes = Game.handleInput(gameId, playerId, typeData)
for (let change of parsed.state.changes) { if (changes.length > 0) {
switch (change.type) { notify([EV_SERVER_STATE_CHANGED, changes], Game.getSockets(gameId))
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)
} break; } break;
} }
} catch (e) { } catch (e) {