Collision detection experiments.

master
Ben Burlingham 5 years ago
parent 0681a9bc95
commit 97cf2dc632
  1. 184
      client/board.js
  2. 53
      client/controls.js
  3. 16
      client/cookie.js
  4. 3
      client/settings.js
  5. 52
      client/squares.js
  6. 67
      index.css
  7. 89
      index.html
  8. 12
      notes.txt
  9. 301
      ricochet.html
  10. 2
      server.js
  11. 20
      server/game.js

@ -0,0 +1,184 @@
const Board = function({ parent, squares }) {
this.parent = parent;
this.squares = squares;
this.blockers = {};
this.listeners = {};
this.drawSquares();
this.resetBlockers();
};
Board.prototype.resetBlockers = function() {
this.blockers = {
negativeI: null,
negativeJ: null,
positiveI: null,
positiveJ: null
};
};
Board.prototype.drawRobots = function(robots) {
robots.forEach(({ color, i, j }) => {
const { x, y } = this.squares.ijToXy({ i, j });
const s = this.squares.sideLength;
const id = color.replace('#', '').toUpperCase();
const robot = document.createElement('div');
robot.className = 'content-robot';
robot.style.background = color;
robot.style.borderRadius = (s / 2) + 'px';
robot.style.height = s + 'px';
robot.style.width = s + 'px';
robot.style.left = x + 'px';
robot.style.top = y + 'px';
robot.id = id
// Find if a robot is on a square: document.querySelector('[data-robot=i-j]')
robot.dataset.robot = `${i}-${j}`;
const shadow = document.createElement('div');
shadow.className = 'content-shadow';
shadow.style.background = color;
shadow.style.borderRadius = (s / 2) + 'px';
shadow.style.height = s + 'px';
shadow.style.width = s + 'px';
shadow.style.left = x + 'px';
shadow.style.top = y + 'px';
shadow.dataset.parentRobot = id;
this.parent.appendChild(robot);
this.parent.appendChild(shadow);
shadow.addEventListener('mousedown', this.onRobotDragStart.bind(this));
});
};
Board.prototype.drawSquares = function() {
for (let i = 0; i < this.squares.perSide; i++) {
for (let j = 0; j < this.squares.perSide; j++) {
// All squares are absolutely positioned relative to the viewport.
const { x, y } = this.squares.ijToXy({ i, j });
const square = document.createElement('div');
square.className = 'content-square';
square.style.height = this.squares.sideLength + 'px';
square.style.width = this.squares.sideLength + 'px';
square.style.left = x + 'px';
square.style.top = y + 'px';
this.parent.appendChild(square);
}
}
};
Board.prototype.drawWalls = function(edges) {
edges.forEach(edge => {
const id = `wall-${edge}`;
if (document.getElementById(id)) {
return;
}
const [i1, j1, i2, j2] = edge.split('-');
console.log(edge)
const wall = document.createElement('div');
wall.id = id;
wall.title = edge;
const { x, y } = this.squares.ijToXy({ i: i1, j: j1 });
wall.style.left = x + 'px';
wall.style.top = y + 'px';
// Find if edge has a wall: document.querySelector('[data-wall=i1-j1-i2-j2]')
wall.dataset.wall = edge;
if (i1 === i2) {
wall.className = 'content-wall-y';
wall.style.height = this.squares.sideLength + 'px';
} else {
wall.className = 'content-wall-x';
wall.style.width = this.squares.sideLength + 'px';
}
this.parent.appendChild(wall)
})
};
Board.prototype.getBlockers = function({ i1, j1, i2, j2 }) {
// const upperEdge = `${i1}-${j1}-${i2}-${j2}`;
// const lowerEdge = `${i1}-${j1}-${i2}-${j2}`;
if (i1 === i2 && j1 < j2) {
} else if (i1 === i2 && j1 > j2) {
} else if (j1 === j2 && i1 <= i2) {
this.blockers.positiveI && console.log(this.blockers.positiveI.i, i1)
if (this.blockers.positiveI && this.blockers.positiveI.i <= i1) {
return this.blockers.positiveI.blockage;
}
const rightEdge = `${i1 + 1}-${j1}-${i1 + 1}-${j1 + 1}`;
const wall = document.querySelector(`[data-wall='${rightEdge}']`);
if (wall) {
this.blockers.positiveI = { i: i1 + 1, blockage: wall };
return wall;
}
} else {
// const leftEdge = `${i1}-${j1}-${i2}-${j2}`;
// console.log("Left edge: ", leftEdge)
}
return null;
};
Board.prototype.onRobotDragStart = function(evt) {
evt.stopPropagation();
evt.preventDefault();
this.listeners.onRobotDragStop = this.onRobotDragStop.bind(this);
this.listeners.onRobotDrag = this.onRobotDrag.bind(this, evt.currentTarget);
document.body.addEventListener('mouseup', this.listeners.onRobotDragStop);
document.body.addEventListener('mousemove', this.listeners.onRobotDrag);
const { x, y } = this.squares.ijToXy(this.squares.xyToIj(evt));
evt.currentTarget.style.left = x + 'px';
evt.currentTarget.style.top = y + 'px';
};
Board.prototype.onRobotDrag = function(dragTarget, evt) {
const { i: i1, j: j1 } = this.squares.xyToIj({ x: evt.x - evt.movementX, y: evt.y - evt.movementY });
const { i: i2, j: j2 } = this.squares.xyToIj({ x: evt.x, y: evt.y });
const blockage = this.getBlockers({ i1, j1, i2, j2 });
// console.warn(blockers)
if (blockage) {
console.log(blockage)
console.log("NOPE")
// this.onRobotDragStop(); works but i don't like it
return;
}
const { x, y } = this.squares.ijToXy({ i: i2, j: j2 });
dragTarget.style.left = x + 'px';
dragTarget.style.top = y + 'px';
};
Board.prototype.onRobotDragStop = function(_) {
this.resetBlockers();
document.body.removeEventListener('mouseup', this.listeners.onRobotDragStop);
document.body.removeEventListener('mousemove', this.listeners.onRobotDrag);
};

53
client/controls.js vendored

@ -0,0 +1,53 @@
const Controls = {
guessBuild: () => {
const container = document.getElementById('controls-guesses');
container.querySelectorAll('.controls-guess').forEach(el => el.parentNode.removeChild(el));
for (let i = 1; i <= 30; i++) {
const guess = document.createElement('div');
guess.className = 'controls-guess';
guess.innerHTML = i;
guess.setAttribute('data-value', i);
guess.addEventListener('click', Controls.guessClick)
container.appendChild(guess);
}
},
guessClick: (evt) => {
alert(evt.currentTarget.dataset.value)
},
playerAdd: () => {
const rawInput = prompt("What is your name?");
connection.send(JSON.stringify({ head: { type: 'playerAdd' }, body: rawInput }))
},
playerRemove: (rawInput) => {
connection.send(JSON.stringify({ head: { type: 'playerRemove' }, body: rawInput }))
},
playersUpdate: (names) => {
const container = document.getElementById('controls-players');
console.log(names)
Object.keys(names).forEach(connectionId => {
const id = `player-${connectionId}`;
if (document.getElementById(id)) {
return;
}
const n = document.createElement('div');
n.id = id;
n.innerHTML = names[connectionId];
n.className = 'controls-player';
container.appendChild(n)
});
// container.querySelectorAll('.controls-player').forEach(el => {
// if (!names[el.id]) {
// container.removeChild(el);
// }
// })
},
};

@ -0,0 +1,16 @@
const Cookie = {
getCookie: function(name) {
var v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
return v ? decodeURI(v[2]) : null;
},
setCookie: function(name, value, days) {
var d = new Date;
d.setTime(d.getTime() + 24*60*60*1000*days);
document.cookie = name + "=" + encodeURI(value) + ";path=/;expires=" + d.toGMTString();
},
deleteCookie: function(name) {
Util.setCookie(name, '', -1);
}
};

@ -0,0 +1,3 @@
const Settings = function() {
this.squaresPerSide = 20;
}

@ -0,0 +1,52 @@
// This layer acts to convert squares and their edges to pixel coords and back.
// i and j are notations for the grid of squares.
// x and y are notations for absolute pixels.
//
// Grid: XY pair. 0-i on x and 0-j on y.
// Absolute: XY pair. 0-width and 0-height, relative to viewport
// Edge: A string. i1-j1-i1-j2.
const Squares = function() {
const controlBounds = document.getElementById('controls-container').getBoundingClientRect();
const contentBounds = document.getElementById('content-container').getBoundingClientRect();
const h = contentBounds.height - 40;
const w = contentBounds.width - controlBounds.right - 40;
const min = Math.min(h, w);
// Centering
const offsetX = (min === h) ? ((w - min) / 2) : 0;
const offsetY = (min === h) ? 0 : ((h - min) / 2);
this.perSide = 20;
this.sideLength = Math.floor(min / this.perSide);
// Origin (top left)
this.x0 = controlBounds.left + controlBounds.width + 40 + offsetX;
this.y0 = contentBounds.top + 40 + offsetY;
}
Squares.prototype.ijToXy = function({ i, j }) {
if ((i !== 0 && !i) || (j !== 0 && !j)) {
return
}
return {
x: this.x0 + i * this.sideLength,
y: this.y0 + j * this.sideLength
}
}
Squares.prototype.xyToIj = function({ x, y }) {
const i = Math.floor((x - this.x0) / this.sideLength);
const j = Math.floor((y - this.y0) / this.sideLength);
const min = 0;
const max = this.perSide - 1;
// Coalesce outlying results to grid edges.
return {
i: Math.min(Math.max(min, i), max),
j: Math.min(Math.max(min, j), max),
}
}

@ -3,32 +3,44 @@
font-family: Montserrat; font-family: Montserrat;
} }
.controls-container { body {
overflow: hidden;
}
/*
4D3243
9A748C
639699
364B4D
*/
#controls-container {
background: #e7e7e7; background: #e7e7e7;
bottom: 0; bottom: 0;
left: 0; left: 20px;
position: absolute; position: absolute;
top: 0; top: 0;
width: 300px; width: 300px;
z-index: 1;
} }
.controls-subtitle { .controls-subtitle {
background-color: #555; background-color: #4D3243;
color: #fff; color: #fff;
padding: 12px;
} }
.controls-title { .controls-title {
background-color: #222; background-color: #639978;
background-image: url('sprite-robots.png'); background-image: url('sprite-robots.png');
color: #fff; /*color: #fff;*/
font-size: 12px; font-size: 12px;
line-height: 48px; line-height: 48px;
margin-bottom: 24px;
text-align: center; text-align: center;
} }
.controls-room { .controls-room {
line-height: 48px;
text-align: center;
} }
.controls-room span { .controls-room span {
@ -61,61 +73,44 @@
background: orange; background: orange;
} }
.board-container { #content-container {
background: conic-gradient(lime 0 25%, yellow 25% 50%, red 50% 75%, blue 75% 100%); background-color: #28313b;
background-image: linear-gradient(315deg, #28313b 0%, #1A2026 74%);
bottom: 0; bottom: 0;
left: 300px; left: 0;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
z-index: 0;
} }
#board-squares { .content-square {
border-color: #aaa;
border-style: solid;
border-width: 1px 0 0 1px;
position: relative;
}
.board-square{
background: #ddd; background: #ddd;
border-style: solid; border-style: solid;
border-color: #aaa; border-color: #aaa;
border-width: 0 1px 1px 0; border-width: 0 1px 1px 0;
float: left; position: absolute;
height: 40px;
width: 40px;
} }
.board-wall-x { .content-wall-x {
background: #222; background: #222;
height: 8px; height: 8px;
margin-top: -4px; margin-top: -4px;
position: absolute; position: absolute;
width: 40px;
} }
.board-wall-y { .content-wall-y {
background: #222; background: #222;
height: 40px;
margin-left: -4px; margin-left: -4px;
position: absolute; position: absolute;
width: 8px; width: 8px;
} }
.board-robot { .content-robot {
/*border-radius: 20px;*/ opacity: 0.25;
border: 2px solid transparent;
height: 36px;
margin: 2px;
position: absolute; position: absolute;
width: 36px;
} }
.board-robot-shadow { .content-shadow {
background: red;
height: 36px;
opacity: 0.75;
position: absolute; position: absolute;
width: 36px;
} }

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="index.css">
<link href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap" rel="stylesheet">
<script type='text/javascript' src='client/board.js'></script>
<script type='text/javascript' src='client/controls.js'></script>
<script type='text/javascript' src='client/cookie.js'></script>
<script type='text/javascript' src='client/squares.js'></script>
</head>
<body>
<div id="controls-container">
<div class='controls-title'>Puzzle Robots</div>
<div class="controls-room">
<div class='controls-subtitle'>Room</div>
<input type="text" id='game-id'>
<button type='button'>&gt;</button>
</div>
<!-- <div class="rounds">
<button type='button' id='game-start'>Start New Round</button>
<div class="timer">0:42</div>
</div> -->
<div id="controls-players">
<div class='controls-subtitle'>Players</div>
</div>
<div id="controls-guesses">
<div class='controls-subtitle'>Guess</div>
</div>
</div>
<div id="content-container">
</div>
<script>
window.addEventListener('load', () => {
const squares = new Squares();
const board = new Board({ parent: document.getElementById('content-container'), squares });
var connection = new WebSocket('ws://localhost:8080/ricochet', ['soap', 'xmpp']);
// connection.onopen = Controls.playerAdd;
connection.onerror = console.error;
connection.onmessage = function (msg) {
const data = JSON.parse(msg.data)
if (!data.head || !data.body) {
console.warn("Unprocessable entity: ", msg)
return;
}
console.log(msg)
switch(data.head.type) {
case 'connect':
break;
case 'disconnect':
break;
case 'players':
// Controls.playersUpdate(data.body)
break;
case 'robots':
board.drawRobots(data.body);
break;
case 'walls':
board.drawWalls(data.body);
break;
default:
console.warn("Unhandled message: ", msg)
}
};
})
</script>
</body>
</html>

@ -0,0 +1,12 @@
// TODO dynamic sizing of squares based on available height
// TODO dynamic move population
// TODO move websocket server to /core
// TODO dynamic socket server resolution
// TODO namespace server to /ricochet
// TODO [soap, xmpp]
// TODO a message must have a head and a body
// TODO your favorite games
// TODO no cancel from name prompt
// TODO limit concurrent players
// TODO window resize update board
// TODO donate link

@ -1,301 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="ricochet.css">
<link href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap" rel="stylesheet">
</head>
<body>
<div class="controls-container">
<div class='controls-title'>Puzzle Robots</div>
<div class="controls-room">
<span>Room ID</span>
<input type="text" id='game-id'>
<button type='button'>&gt;</button>
</div>
<!-- <div class="rounds">
<button type='button' id='game-start'>Start New Round</button>
<div class="timer">0:42</div>
</div> -->
<div id="controls-players">
<div class='controls-subtitle'>Players</div>
</div>
<div id="controls-guesses">
<div class='controls-subtitle'>Guess</div>
</div>
</div>
<div class="board-container">
<div id="board-squares"></div>
</div>
<script>
// TODO dynamic sizing of squares based on available height
// TODO dynamic move population
// TODO move websocket server to /core
// TODO dynamic socket server resolution
// TODO namespace server to /ricochet
// TODO [soap, xmpp]
// TODO a message must have a head and a body
// TODO your favorite games
// TODO no cancel from name prompt
// TODO limit concurrent players
// TODO window resize update board
const Cookie = {
getCookie: function(name) {
var v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
return v ? decodeURI(v[2]) : null;
},
setCookie: function(name, value, days) {
var d = new Date;
d.setTime(d.getTime() + 24*60*60*1000*days);
document.cookie = name + "=" + encodeURI(value) + ";path=/;expires=" + d.toGMTString();
},
deleteCookie: function(name) {
Util.setCookie(name, '', -1);
}
};
const Controls = {
guessBuild: () => {
const container = document.getElementById('controls-guesses');
container.querySelectorAll('.controls-guess').forEach(el => el.parentNode.removeChild(el));
for (let i = 1; i <= 30; i++) {
const guess = document.createElement('div');
guess.className = 'controls-guess';
guess.innerHTML = i;
guess.setAttribute('data-value', i);
guess.addEventListener('click', Controls.guessClick)
container.appendChild(guess);
}
},
guessClick: (evt) => {
alert(evt.currentTarget.dataset.value)
},
playerAdd: () => {
const rawInput = prompt("What is your name?");
connection.send(JSON.stringify({ head: { type: 'playerAdd' }, body: rawInput }))
},
playerRemove: (rawInput) => {
connection.send(JSON.stringify({ head: { type: 'playerRemove' }, body: rawInput }))
},
playersUpdate: (names) => {
const container = document.getElementById('controls-players');
console.log(names)
Object.keys(names).forEach(connectionId => {
const id = `player-${connectionId}`;
if (document.getElementById(id)) {
return;
}
const n = document.createElement('div');
n.id = id;
n.innerHTML = names[connectionId];
n.className = 'controls-player';
container.appendChild(n)
});
// container.querySelectorAll('.controls-player').forEach(el => {
// if (!names[el.id]) {
// container.removeChild(el);
// }
// })
},
};
const Robot = function([color, x, y]) {
this.color = color;
const el = document.createElement('div');
el.className = 'board-robot';
el.style.background = color;
el.style.left = 40 * x + 'px';
el.style.top = 40 * y + 'px';
el.addEventListener('mousedown', this.onMouseDown.bind(this));
this.shadow = document.createElement('div');
this.shadow.className = 'board-robot-shadow';
this.shadow.style.display = 'none';
Board.el.appendChild(el);
Board.el.appendChild(this.shadow);
};
Robot.prototype.onMouseDown = function(evt) {
document.body.addEventListener('mouseup', this.onMouseUp.bind(this));
document.body.addEventListener('mousemove', this.onDrag.bind(this));
this.shadow.style.display = 'block';
this.shadow.style.left = (this.snapX(evt.pageX) + 2) + 'px';
this.shadow.style.top = (this.snapY(evt.pageY) + 2) + 'px';
};
Robot.prototype.onDrag = function(evt) {
// this.shadow.style.left = (evt.pageX - Board.bounds.left - 18) + 'px';
// this.shadow.style.top = (evt.pageY - Board.bounds.top - 18) + 'px';
this.shadow.style.left = (this.snapX(evt.pageX) + 2) + 'px';
this.shadow.style.top = (this.snapY(evt.pageY) + 2) + 'px';
};
Robot.prototype.onMouseUp = function(evt) {
console.log('mouseup')
this.shadow.style.display = 'none';
document.removeEventListener('mouseup', this.onMouseUp);
document.removeEventListener('mousemove', this.onDrag);
};
Robot.prototype.snapX = (x) => {
const relativeX = Math.floor((x - Board.bounds.left) / Board.squareSize) * Board.squareSize;
const maxX = Board.squareSize * (Board.squaresPerSide - 1);
return Math.min(Math.max(0, relativeX), maxX);
};
Robot.prototype.snapY = (y) => {
const relativeY = Math.floor((y - Board.bounds.top) / Board.squareSize) * Board.squareSize;
const maxY = Board.squareSize * (Board.squaresPerSide - 1);
return Math.min(Math.max(0, relativeY), maxY);
};
const Board = {
bounds: document.getElementById('board-squares').getBoundingClientRect(),
el: document.getElementById('board-squares'),
squaresPerSide: 20,
squareSize: 40,
placeSquares: () => {
Board.el.style.width = (Board.squaresPerSide * Board.squareSize + 1) + 'px';
Board.el.style.height = (Board.squaresPerSide * Board.squareSize + 1) + 'px';
while (Board.el.hasChildElements) {
Board.el.removeChild(board.firstChild);
}
for (let i = 0; i < Board.squaresPerSide; i++) {
for (let j = 0; j < Board.squaresPerSide; j++) {
const square = document.createElement('div');
square.className = 'board-square';
Board.el.appendChild(square);
}
}
},
placeRobots: (robots) => {
robots.forEach(r => new Robot(r))
},
placeWall: (id, className, x, y) => {
if (document.getElementById(id)) {
return;
}
const w = document.createElement('wall');
w.id = id;
w.style.top = x + 'px';
w.style.left = y + 'px';
w.className = className;
Board.el.appendChild(w);
},
placeWalls: (walls) => {
walls.forEach(wall => {
const [x, y, north, south, east, west] = wall;
const pxX = 40 * x;
const pxY = 40 * y;
if (north) {
Board.placeWall(`${x}-${y}-n`, "board-wall-y", pxX, pxY);
}
if (south) {
Board.placeWall(`${x}-${y}-s`, "board-wall-y", pxX, pxY + Board.squareSize);
}
if (east) {
Board.placeWall(`${x}-${y}-e`, "board-wall-x", pxX, pxY);
}
if (west) {
Board.placeWall(`${x}-${y}-e`, "board-wall-x", pxX + Board.squareSize, pxY);
}
})
},
removeWalls: () => {
const walls = document.querySelector('board-wall-x').concat(document.querySelectorAll('board-wall-y'))
}
}
/////////////////////
window.addEventListener('load', () => {
Controls.guessBuild();
Board.placeSquares();
})
var connection = new WebSocket('ws://localhost:8080/ricochet', ['soap', 'xmpp']);
connection.onopen = Controls.playerAdd;
connection.onerror = console.error;
connection.onmessage = function (msg) {
const data = JSON.parse(msg.data)
if (!data.head || !data.body) {
console.warn("Unprocessable entity: ", msg)
return;
}
console.log(msg)
switch(data.head.type) {
case 'connect':
break;
case 'disconnect':
break;
case 'players':
Controls.playersUpdate(data.body)
break;
case 'robots':
Board.placeRobots(data.body);
break;
case 'walls':
Board.placeWalls(data.body);
break;
default:
console.warn("Unhandled message: ", msg)
}
};
</script>
</body>
</html>

@ -1,7 +1,7 @@
const WebSocket = require('ws'); const WebSocket = require('ws');
const uuid = require('node-uuid'); const uuid = require('node-uuid');
const jsDir = `${__dirname}/`; const jsDir = `${__dirname}/server`;
const Game = require(`${jsDir}/game.js`); const Game = require(`${jsDir}/game.js`);
const wss = new WebSocket.Server({ port: 8080 }); const wss = new WebSocket.Server({ port: 8080 });

@ -22,23 +22,19 @@ Game.prototype.getPlayers = function() {
} }
Game.prototype.getRobots = function() { Game.prototype.getRobots = function() {
const robots = [ return [
{x: 3, y: 1, color: 'red' }, {i: 4, j: 3, color: '#E00000' },
{x: 19, y: 0, color: 'silver' }, {i: 1, j: 3, color: '#00C000' },
{x: 1, y: 19, color: 'green' } {i: 1, j: 19, color: '#0000C0' }
]; ];
return robots.map(r => [r.color, r.x, r.y]);
} }
Game.prototype.getWalls = function() { Game.prototype.getWalls = function() {
const walls = [ // Edge IDs are of the form [i1-j1-i2-j2]. Top left is 0, 0.
{x: 8, y: 9, n: 1, e: 0, s: 0, w: 1 }, return [
{x: 18, y: 9, n: 1, e: 0, s: 0, w: 1 }, "4-8-5-8",
{x: 4, y: 19, n: 1, e: 0, s: 0, w: 1 } "8-3-8-4"
]; ];
return walls.map(w => [w.x, w.y, w.n, w.s, w.e, w.w]);
}; };
module.exports = Game; module.exports = Game;
Loading…
Cancel
Save