Compare commits
94 Commits
Author | SHA1 | Date | |
---|---|---|---|
ebd88fe643 | |||
5927290ba8 | |||
8a76347ca6 | |||
4037350e7e | |||
bf6e29bc1b | |||
cbe189bbed | |||
5d7c979752 | |||
04a365fbc9 | |||
39421c43b5 | |||
518a1f3ef8 | |||
|
5b8d347bae | ||
|
69ed3e56e2 | ||
|
7044b67cb0 | ||
|
fbf7ff07d4 | ||
|
0104b6d766 | ||
|
4946fe70cc | ||
|
b28d535903 | ||
|
3b3c8929e1 | ||
|
3d10dd527c | ||
|
01da4d3e55 | ||
|
b8b22ea3d4 | ||
|
60b5b98033 | ||
|
b9398212dc | ||
|
342c50a995 | ||
|
296c3d116a | ||
|
a572afebc7 | ||
|
a40cba4e5f | ||
|
0005563b46 | ||
|
c44446eb65 | ||
|
2244a6bb5d | ||
|
8ab91871ff | ||
|
efd60e662a | ||
|
a2cc7a92c3 | ||
|
d6918c06d7 | ||
|
182d3a1d76 | ||
|
ecf25f65a2 | ||
|
fe940a3182 | ||
|
277220ae14 | ||
|
a8a0ed36a9 | ||
|
539a370161 | ||
|
267a787387 | ||
|
8c1ad1aaa3 | ||
|
1ffd662600 | ||
|
1837919ee4 | ||
|
459c341be5 | ||
|
4f53513e4b | ||
|
d4f45d250b | ||
|
71075a53cf | ||
|
0abbd33cfa | ||
|
a9b9e8e895 | ||
|
28e8f611f2 | ||
|
b811b7d96f | ||
|
a3b2d660a7 | ||
|
728343ea0d | ||
|
33f6dbd00e | ||
|
afa909f9e2 | ||
|
d3aed9e5a5 | ||
|
73958b9252 | ||
|
df1dfa3062 | ||
|
2cf9221ac2 | ||
|
32449da6fb | ||
|
dad3583c68 | ||
|
a7d298f625 | ||
|
bb6cb3171e | ||
|
5c72a93605 | ||
|
2ea6801dcd | ||
|
b535f5c354 | ||
|
c9e8554d8c | ||
|
ed01c8a8ef | ||
|
c0869b47c5 | ||
|
783a06b63d | ||
|
57f28e279c | ||
|
a1887b2b7d | ||
|
2bea2e8b7a | ||
|
ebc5e93ae5 | ||
|
73bf9f86f2 | ||
|
64d9c5e6bc | ||
|
d83ec69553 | ||
|
efd5f58b9e | ||
|
1bb7997879 | ||
|
0df331b04a | ||
|
ca82979c7c | ||
|
be477f780e | ||
|
2ddfc7c0a0 | ||
|
d7f1af47e2 | ||
|
454fc98865 | ||
|
e78c78cefc | ||
|
893a19dbd8 | ||
|
870c939bcc | ||
|
858b8f0434 | ||
|
c8cd3d474b | ||
|
7ff47d8efc | ||
|
8d50dc1d54 | ||
|
075b55d5ec |
96
.gitignore
vendored
96
.gitignore
vendored
@ -1 +1,95 @@
|
||||
/node_modules
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Npm Lock files
|
||||
package-lock.json
|
||||
|
||||
# Dist
|
||||
dist/
|
||||
|
||||
# Backups and vim swp files
|
||||
*~
|
||||
*.sw?
|
||||
|
||||
.DS_Store
|
||||
|
||||
paper-io.log
|
||||
public/js/bundle.js
|
||||
|
63
README.md
63
README.md
@ -1,14 +1,67 @@
|
||||
# Blockly.IO
|
||||
# Paper.IO
|
||||
|
||||
This is a clone of the original Paper-IO released by Voodoo, except for one aspect. This will attempt to implement a multi-player aspect of the game (like a real IO game). Currently this has a playground at this [link] (https://thekidofarcrania.github.io/PaperIO-Web). It's a demo version of what to come. Hopefully by that time, the necessary server infrastructure could be obtained.
|
||||
> This is a clone of the original Paper-IO released by Voodoo, except for one aspect. This will attempt to implement a multi-player aspect of the game (like a real IO game). Currently this has a playground at this [link](https://thekidofarcrania.github.io/BlocklyIO). It's a demo version of what to come. Hopefully by that time, the necessary server infrastructure could be obtained.
|
||||
|
||||
This is just a fun side-project for me. If you would want to use this code, it would be nice to let me know.
|
||||
> This is just a fun side-project for me. If you would want to use this code, it would be nice to let me know.
|
||||
|
||||
## Running
|
||||
## Screenshots
|
||||
|
||||
![Screenshot](screenshot.png)
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Clone this repository
|
||||
git clone https://github.com/stevenjoezhang/paper.io.git
|
||||
# Go into the repository
|
||||
cd paper.io
|
||||
# Install dependencies
|
||||
npm install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
After cloning this repository, run the follow commands to install dependencies and set up server. Enjoy!
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
You can configure the game by editing `config.js`.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
**WARNING: Remember to build again after editing any file, include `config.js`.**
|
||||
|
||||
## Bots
|
||||
|
||||
Set `bots` in `config.js` to a non-zero value, or execute the command below:
|
||||
|
||||
```bash
|
||||
node paper-io-bot.js ws://localhost:8080
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
node bot.js ws://localhost:8080
|
||||
```
|
||||
|
||||
## Roadmap & TODO List
|
||||
|
||||
- [x] 统一配置文件
|
||||
- [x] 玩家观战模式
|
||||
- [ ] 更多游戏玩法
|
||||
- [ ] 多个游戏房间
|
||||
- [ ] 加快渲染速度
|
||||
- [ ] 优化胜负判定
|
||||
|
||||
## License
|
||||
|
||||
This repo is forked from [BlocklyIO](https://github.com/theKidOfArcrania/BlocklyIO) by theKidOfArcrania.
|
||||
|
||||
> This is licensed under MIT. As such, please provide due credit and link back to this repository if possible.
|
||||
|
284
bot.js
Normal file
284
bot.js
Normal file
@ -0,0 +1,284 @@
|
||||
if (process.argv.length < 3) {
|
||||
console.log("Usage: node bot.js <socket-url> [<name>]")
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
//TODO: add a land claiming algo (with coefficient parameters)
|
||||
//TODO: add weight to the max land area and last land area, and also the number of kills
|
||||
//TODO: genetic gene pooling
|
||||
|
||||
import { Grid } from "./src/core";
|
||||
|
||||
import client from "./src/game-client";
|
||||
import { consts } from "./config.js";
|
||||
|
||||
const MOVES = [[-1, 0], [0, 1], [1, 0], [0, -1]];
|
||||
|
||||
const AGGRESSIVE = Math.random();
|
||||
const THRESHOLD = 10;
|
||||
|
||||
let startFrame = -1;
|
||||
let endFrame = -1;
|
||||
const coeffs = [0.6164220147940495, -2.519369747858328, 0.9198978109542851, -1.2158956330674564, -3.072901620397528, 5, 4];
|
||||
let grid;
|
||||
let others;
|
||||
let user;
|
||||
const playerPortion = {};
|
||||
const DIST_TYPES = {
|
||||
land: {
|
||||
check: function(loc) {
|
||||
return grid.get(loc.row, loc.col) === user;
|
||||
},
|
||||
coeff: function() {
|
||||
return coeffs[0];
|
||||
}
|
||||
}, tail: {
|
||||
check: function(loc) {
|
||||
return tail(user, loc)
|
||||
},
|
||||
coeff: function() {
|
||||
return coeffs[1];
|
||||
}
|
||||
}, oTail: {
|
||||
check: foundProto(tail),
|
||||
coeff: function() {
|
||||
return AGGRESSIVE * coeffs[2];
|
||||
}
|
||||
}, other: {
|
||||
check: foundProto(function(other, loc) {
|
||||
return other.row === this.row && other.col === this.col;
|
||||
}),
|
||||
coeff: function() {
|
||||
return (1 - AGGRESSIVE) * coeffs[3];
|
||||
}
|
||||
}, edge: {
|
||||
check: function(loc) {
|
||||
return loc.row <= 1 || loc.col <= 1 || loc.row >= consts.GRID_COUNT - 1 || loc.col >= consts.GRID_COUNT - 1
|
||||
},
|
||||
coeff: function() {
|
||||
return coeffs[4];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function generateLandDirections() {
|
||||
function mod(x) {
|
||||
x %= 4;
|
||||
if (x < 0) x += 4;
|
||||
return x;
|
||||
}
|
||||
|
||||
const breadth = Math.floor(Math.random() * coeffs[5]) + 1;
|
||||
const spread = Math.floor(Math.random() * coeffs[6]) + 1;
|
||||
const extra = Math.floor(Math.random() * 2) + 1;
|
||||
const ccw = Math.floor(Math.random() * 2) * 2 - 1;
|
||||
|
||||
const dir = user.currentHeading;
|
||||
const turns = [dir, mod(dir + ccw), mod(dir + ccw * 2), mod(dir + ccw * 3)];
|
||||
const lengths = [breadth, spread, breadth + extra, spread];
|
||||
|
||||
const moves = [];
|
||||
for (let i = 0; i < turns.length; i++) {
|
||||
for (let j = 0; j < lengths[i]; j++) {
|
||||
moves.push(turns[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const LAND_CLAIMS = {
|
||||
rectDims: function() {},
|
||||
rectSpread: function() {}
|
||||
};
|
||||
|
||||
function foundProto(func) {
|
||||
return loc => {
|
||||
return others.some(other => {
|
||||
return func(other, loc);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function connect() {
|
||||
const prefixes = consts.PREFIXES.split(" ");
|
||||
const names = consts.NAMES.split(" ");
|
||||
const name = process.argv[3] || [prefixes[Math.floor(Math.random() * prefixes.length)], names[Math.floor(Math.random() * names.length)]].join(" ");
|
||||
client.connectGame(process.argv[2], "[BOT] " + name, (success, msg) => {
|
||||
if (!success) {
|
||||
console.error(msg);
|
||||
setTimeout(connect, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function Loc(row, col, step) {
|
||||
if (this.constructor != Loc) return new Loc(row, col, step);
|
||||
this.row = row;
|
||||
this.col = col;
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
//Projects vector b onto vector a
|
||||
function project(a, b) {
|
||||
const factor = (b[0] * a[0] + b[1] * a[1]) / (a[0] * a[0] + a[1] * a[1]);
|
||||
return [factor * a[0], factor * a[1]];
|
||||
}
|
||||
|
||||
function tail(player, loc) {
|
||||
return player.tail.hitsTail(loc);
|
||||
}
|
||||
|
||||
function traverseGrid(dir) {
|
||||
steps = new Array(consts.GRID_COUNT * consts.GRID_COUNT);
|
||||
for (let i in steps) {
|
||||
steps[i] = -1;
|
||||
}
|
||||
|
||||
distWeights = {};
|
||||
for (const type in DIST_TYPES) {
|
||||
distWeights[type] = 0;
|
||||
}
|
||||
|
||||
const { row, col } = user;
|
||||
const minRow = Math.max(0, row - 10), maxRow = Math.min(consts.GRID_COUNT, row + 10);
|
||||
const minCol = Math.max(0, col - 10), maxCol = Math.min(consts.GRID_COUNT, col + 10);
|
||||
|
||||
let proj = 0;
|
||||
for (let i = 1; i >= -1; i-=2) {
|
||||
proj = (1 + THRESHOLD) * i;
|
||||
while (proj != 0) {
|
||||
proj -= i;
|
||||
const normRange = Math.abs(proj);
|
||||
for (let norm = -normRange; norm <= normRange; norm++) {
|
||||
for (const distType in distWeights) {
|
||||
const move = MOVES[dir];
|
||||
const delta = THRESHOLD - Math.abs(proj);
|
||||
const dist = Math.sign(proj) * delta * delta / (Math.abs(norm) + 1);
|
||||
const loc = {row: proj * move[0] + norm * move[1], col: proj * move[1] + norm * move[0]};
|
||||
|
||||
loc.row += user.row;
|
||||
loc.col += user.col;
|
||||
|
||||
if (loc.row < 0 || loc.row >= consts.GRID_COUNT || loc.col < 0 || loc.col >= consts.GRID_COUNT) continue;
|
||||
if (DIST_TYPES[distType].check(loc)) distWeights[distType] += dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return distWeights;
|
||||
}
|
||||
|
||||
function printGrid() {
|
||||
const chars = new Grid(consts.GRID_COUNT);
|
||||
for (let r = 0; r < consts.GRID_COUNT; r++) {
|
||||
for (let c = 0; c < consts.GRID_COUNT; c++) {
|
||||
if (tail(user, {row: r, col: c})) chars.set(r, c, "t");
|
||||
else {
|
||||
const owner = grid.get(r, c);
|
||||
chars.set(r, c, owner ? "" + owner.num % 10 : ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of others) {
|
||||
chars.set(p.row, p.col, "x");
|
||||
}
|
||||
chars.set(user.row, user.col, "^>V<"[user.currentHeading]);
|
||||
|
||||
let str = "";
|
||||
for (let r = 0; r < consts.GRID_COUNT; r++) {
|
||||
str += "\n";
|
||||
for (let c = 0; c < consts.GRID_COUNT; c++) {
|
||||
str += chars.get(r, c);
|
||||
}
|
||||
}
|
||||
console.log(str);
|
||||
}
|
||||
|
||||
function update(frame) {
|
||||
if (startFrame == -1) startFrame = frame;
|
||||
endFrame = frame;
|
||||
if (frame % 6 == 1) {
|
||||
grid = client.grid;
|
||||
others = client.getOthers();
|
||||
//printGrid();
|
||||
const weights = [0, 0, 0, 0];
|
||||
for (let d of [3, 0, 1]) {
|
||||
let weight = 0;
|
||||
|
||||
d = (d + user.currentHeading) % 4;
|
||||
distWeights = traverseGrid(d);
|
||||
|
||||
let str = d + ": ";
|
||||
for (const distType in DIST_TYPES) {
|
||||
const point = distWeights[distType] * DIST_TYPES[distType].coeff();
|
||||
weight += point;
|
||||
str += distType + ": " + point + ", ";
|
||||
}
|
||||
//console.log(str);
|
||||
weights[d] = weight;
|
||||
}
|
||||
|
||||
const low = Math.min(0, Math.min.apply(this, weights));
|
||||
let total = 0;
|
||||
|
||||
weights[(user.currentHeading + 2) % 4] = low;
|
||||
for (let i = 0; i < weights.length; i++) {
|
||||
weights[i] -= low * (1 + Math.random());
|
||||
total += weights[i];
|
||||
}
|
||||
|
||||
if (total == 0) {
|
||||
for (let d of [-1, 0, 1]) {
|
||||
d = (d + user.currentHeading) % 4;
|
||||
while (d < 0) d += 4;
|
||||
weights[d] = 1;
|
||||
total++;
|
||||
}
|
||||
}
|
||||
//console.log(weights)
|
||||
//Choose a random direction from the weighted list
|
||||
let choice = Math.random() * total;
|
||||
let d = 0;
|
||||
while (choice > weights[d]) {
|
||||
choice -= weights[d++];
|
||||
}
|
||||
client.changeHeading(d);
|
||||
}
|
||||
}
|
||||
|
||||
function calcFavorability(params) {
|
||||
return params.portion + params.kills * 50 + params.survival / 100;
|
||||
}
|
||||
|
||||
client.setAllowAnimation(false);
|
||||
client.setRenderer({
|
||||
addPlayer: function(player) {
|
||||
playerPortion[player.num] = 0;
|
||||
},
|
||||
disconnect: function() {
|
||||
const dt = (endFrame - startFrame);
|
||||
startFrame = -1;
|
||||
console.log(`[${new Date()}] I died... (survived for ${dt} frames.)`);
|
||||
console.log(`[${new Date()}] I killed ${client.getKills()} player(s).`);
|
||||
console.log("Coefficients: " + coeffs);
|
||||
|
||||
const mutation = Math.min(10, Math.pow(2, calcFavorability(params)));
|
||||
for (let i = 0; i < coeffs.length; i++) {
|
||||
coeffs[i] += Math.random() * mutation * 2 - mutation;
|
||||
}
|
||||
connect();
|
||||
},
|
||||
removePlayer: function(player) {
|
||||
delete playerPortion[player.num];
|
||||
},
|
||||
setUser: function(u) {
|
||||
user = u;
|
||||
},
|
||||
update: update,
|
||||
updateGrid: function(row, col, before, after) {
|
||||
before && playerPortion[before.num]--;
|
||||
after && playerPortion[after.num]++;
|
||||
}
|
||||
});
|
||||
|
||||
connect();
|
103
client.js
Normal file
103
client.js
Normal file
@ -0,0 +1,103 @@
|
||||
import jquery from "jquery";
|
||||
import io from "socket.io-client/dist/socket.io.js";
|
||||
import * as client from "./src/game-client";
|
||||
import godRenderer from "./src/mode/god";
|
||||
import * as playerRenderer from "./src/mode/player";
|
||||
|
||||
const $ = jquery;
|
||||
|
||||
function run(flag) {
|
||||
client.setRenderer(flag ? godRenderer : playerRenderer);
|
||||
client.connectGame(io, "//" + location.host, $("#name").val(), (success, msg) => {
|
||||
if (success) {
|
||||
$("#main-ui").fadeIn(1000);
|
||||
$("#begin, #wasted").fadeOut(1000);
|
||||
}
|
||||
else {
|
||||
$("#error").text(msg);
|
||||
}
|
||||
}, flag);
|
||||
}
|
||||
|
||||
$(() => {
|
||||
const err = $("#error");
|
||||
if (!window.WebSocket) {
|
||||
err.text("Your browser does not support WebSockets!");
|
||||
return;
|
||||
}
|
||||
err.text("Loading... Please wait"); //TODO: show loading screen
|
||||
(() => {
|
||||
const socket = io(`//${location.host}`, {
|
||||
forceNew: true,
|
||||
upgrade: false,
|
||||
transports: ["websocket"]
|
||||
});
|
||||
socket.on("connect", () => {
|
||||
socket.emit("pings");
|
||||
});
|
||||
socket.on("pongs", () => {
|
||||
socket.disconnect();
|
||||
err.text("All done, have fun!");
|
||||
$("#name").on("keypress", evt => {
|
||||
if (evt.key === "Enter") run();
|
||||
});
|
||||
$(".start").removeAttr("disabled").on("click", evt => {
|
||||
run();
|
||||
});
|
||||
$(".spectate").removeAttr("disabled").click(evt => {
|
||||
run(true);
|
||||
});
|
||||
});
|
||||
socket.on("connect_error", () => {
|
||||
err.text("Cannot connect with server. This probably is due to misconfigured proxy server. (Try using a different browser)");
|
||||
});
|
||||
})();
|
||||
});
|
||||
//Event listeners
|
||||
$(document).on("keydown", e => {
|
||||
let newHeading = -1;
|
||||
switch (e.key) {
|
||||
case "w": case "ArrowUp":
|
||||
newHeading = 0; break; //UP (W)
|
||||
case "d": case "ArrowRight":
|
||||
newHeading = 1; break; //RIGHT (D)
|
||||
case "s": case "ArrowDown":
|
||||
newHeading = 2; break; //DOWN (S)
|
||||
case "a": case "ArrowLeft":
|
||||
newHeading = 3; break; //LEFT (A)
|
||||
default: return; //Exit handler for other keys
|
||||
}
|
||||
client.changeHeading(newHeading);
|
||||
//e.preventDefault();
|
||||
});
|
||||
|
||||
$(document).on("touchmove", e => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(document).on("touchstart", e1 => {
|
||||
const x1 = e1.targetTouches[0].pageX;
|
||||
const y1 = e1.targetTouches[0].pageY;
|
||||
$(document).one("touchend", e2 => {
|
||||
const x2 = e2.changedTouches[0].pageX;
|
||||
const y2 = e2.changedTouches[0].pageY;
|
||||
const deltaX = x2 - x1;
|
||||
const deltaY = y2 - y1;
|
||||
let newHeading = -1;
|
||||
if (deltaY < 0 && Math.abs(deltaY) > Math.abs(deltaX)) newHeading = 0;
|
||||
else if (deltaX > 0 && Math.abs(deltaY) < deltaX) newHeading = 1;
|
||||
else if (deltaY > 0 && Math.abs(deltaX) < deltaY) newHeading = 2;
|
||||
else if (deltaX < 0 && Math.abs(deltaX) > Math.abs(deltaY)) newHeading = 3;
|
||||
client.changeHeading(newHeading);
|
||||
});
|
||||
});
|
||||
|
||||
$(".menu").on("click", () => {
|
||||
client.disconnect();
|
||||
$("#main-ui, #wasted").fadeOut(1000);
|
||||
$("#begin").fadeIn(1000);
|
||||
});
|
||||
|
||||
$(".toggle").on("click", () => {
|
||||
$("#settings").slideToggle();
|
||||
});
|
100
color.js
100
color.js
@ -1,100 +0,0 @@
|
||||
|
||||
|
||||
function Color(h, s, l, a)
|
||||
{
|
||||
verifyRange(h, s, l);
|
||||
if (a === undefined) a = 1;
|
||||
else verifyRange(a);
|
||||
|
||||
Object.defineProperties(this, {
|
||||
"hue": {value: h, enumerable: true},
|
||||
"sat": {value: s, enumerable: true},
|
||||
"lum": {value: l, enumerable: true},
|
||||
"alpha": {value: a, enumerable: true},
|
||||
});
|
||||
}
|
||||
|
||||
Color.fromData = function(data) {
|
||||
return new Color(data.hue, data.sat, data.lum, data.alpha);
|
||||
};
|
||||
|
||||
function verifyRange()
|
||||
{
|
||||
for (var i = 0; i < arguments.length; i++)
|
||||
{
|
||||
if (arguments[i] < 0 || arguments[i] > 1)
|
||||
throw new RangeError("H, S, L, and A parameters must be between the range [0, 1]");
|
||||
}
|
||||
}
|
||||
|
||||
Color.prototype.interpolateToString = function(color, amount)
|
||||
{
|
||||
var rgbThis = hslToRgb(this.hue, this.sat, this.lum);
|
||||
var rgbThat = hslToRgb(color.hue, color.sat, color.lum);
|
||||
var rgb = [];
|
||||
|
||||
for (var i = 0; i < 3; i++)
|
||||
rgb[i] = Math.floor((rgbThat[i] - rgbThis[i]) * amount + rgbThis[i]);
|
||||
return {rgbString: function() {return 'rgb(' + rgb[0] + ', ' + rgb[1] + ', ' + rgb[2] + ')'}};
|
||||
}
|
||||
|
||||
Color.prototype.deriveLumination = function(amount)
|
||||
{
|
||||
var lum = this.lum + amount;
|
||||
lum = Math.min(Math.max(lum, 0), 1);
|
||||
return new Color(this.hue, this.sat, lum, this.alpha);
|
||||
};
|
||||
|
||||
Color.prototype.deriveHue = function(amount)
|
||||
{
|
||||
var hue = this.hue - amount;
|
||||
return new Color(hue - Math.floor(hue), this.sat, this.lum, this.alpha);
|
||||
};
|
||||
|
||||
Color.prototype.deriveSaturation = function(amount)
|
||||
{
|
||||
var sat = this.sat + amount;
|
||||
sat = Math.min(Math.max(sat, 0), 1);
|
||||
return new Color(this.hue, sat, this.lum, this.alpha);
|
||||
};
|
||||
|
||||
Color.prototype.deriveAlpha = function(newAlpha)
|
||||
{
|
||||
verifyRange(newAlpha);
|
||||
return new Color(this.hue, this.sat, this.lum, newAlpha);
|
||||
};
|
||||
|
||||
Color.prototype.rgbString = function() {
|
||||
var rgb = hslToRgb(this.hue, this.sat, this.lum);
|
||||
rgb[3] = this.a;
|
||||
return 'rgba(' + rgb[0] + ', ' + rgb[1] + ', ' + rgb[2] + ', ' + this.alpha + ')';
|
||||
};
|
||||
|
||||
|
||||
//http://stackoverflow.com/a/9493060/7344257
|
||||
function hslToRgb(h, s, l){
|
||||
var r, g, b;
|
||||
|
||||
if(s == 0){
|
||||
r = g = b = l; // achromatic
|
||||
}else{
|
||||
var hue2rgb = function hue2rgb(p, q, t){
|
||||
if(t < 0) t += 1;
|
||||
if(t > 1) t -= 1;
|
||||
if(t < 1/6) return p + (q - p) * 6 * t;
|
||||
if(t < 1/2) return q;
|
||||
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
|
||||
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
var p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1/3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1/3);
|
||||
}
|
||||
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
module.exports = Color;
|
18
config.js
Normal file
18
config.js
Normal file
@ -0,0 +1,18 @@
|
||||
export const config = {
|
||||
"dev": true,
|
||||
"port": 62787,
|
||||
"bots": 0,
|
||||
};
|
||||
|
||||
export const consts = {
|
||||
"GRID_COUNT": 128,
|
||||
"CELL_WIDTH": 40,
|
||||
"SPEED": 5,
|
||||
"BORDER_WIDTH": 20,
|
||||
"MAX_PLAYERS": 30,
|
||||
"NEW_PLAYER_LAG": 60,
|
||||
"LEADERBOARD_NUM": 5,
|
||||
"PREFIXES": "Cute Adorable Scratchy Angry",
|
||||
"NAMES": "Cat Kitty Kitten Feline"
|
||||
|
||||
};
|
352
game-client.js
352
game-client.js
@ -1,352 +0,0 @@
|
||||
/* global $ */
|
||||
var Player = require("./player.js");
|
||||
var renderer = require("./game-renderer.js");
|
||||
var consts = require("./game-consts.js");
|
||||
var core = require("./game-core.js");
|
||||
var io = require('socket.io-client');
|
||||
|
||||
var GRID_SIZE = consts.GRID_SIZE;
|
||||
var CELL_WIDTH = consts.CELL_WIDTH;
|
||||
|
||||
renderer.allowAnimation = true;
|
||||
|
||||
/**
|
||||
* Provides requestAnimationFrame in a cross browser way.
|
||||
* @author paulirish / http://paulirish.com/
|
||||
*/
|
||||
// window.requestAnimationFrame = function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) {
|
||||
// window.setTimeout( callback, 1000 / 60 );
|
||||
// };
|
||||
if ( !window.requestAnimationFrame ) {
|
||||
window.requestAnimationFrame = ( function() {
|
||||
return window.webkitRequestAnimationFrame ||
|
||||
window.mozRequestAnimationFrame ||
|
||||
window.oRequestAnimationFrame ||
|
||||
window.msRequestAnimationFrame ||
|
||||
function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) {
|
||||
window.setTimeout( callback, 1000 / 60 );
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
||||
var norun = false;
|
||||
function run() {
|
||||
if (norun)
|
||||
return; //Prevent multiple clicks.
|
||||
norun = true;
|
||||
|
||||
user = null;
|
||||
deadFrames = 0;
|
||||
|
||||
//Socket connection.
|
||||
//, {transports: ['websocket'], upgrade: false}
|
||||
connectServer();
|
||||
socket.emit('hello', {
|
||||
name: $("#name").val(),
|
||||
type: 0, //Free-for-all
|
||||
gameid: -1 //Requested game-id, or -1 for anyone.
|
||||
}, function(success, msg) {
|
||||
if (success)
|
||||
{
|
||||
console.info("Connected to game!");
|
||||
$("#begin").addClass("hidden");
|
||||
$("#begin").animate({
|
||||
opacity: 0
|
||||
}, 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.error("Unable to connect to game: " + msg);
|
||||
var error = $("#error");
|
||||
error.text(msg);
|
||||
norun = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(function() {
|
||||
var error = $("#error");
|
||||
|
||||
if (!window.WebSocket)
|
||||
{
|
||||
error.text("Your browser does not support WebSockets!");
|
||||
return;
|
||||
}
|
||||
|
||||
error.text("Loading..."); //TODO: show loading screen.
|
||||
var success = false;
|
||||
var socket = io('http://' + window.location.hostname + ':8081', {
|
||||
'forceNew': true,
|
||||
upgrade: false,
|
||||
transports: ['websocket']
|
||||
});
|
||||
|
||||
socket.on('connect_error', function() {
|
||||
if (!success)
|
||||
error.text("Cannot connect with server. This probably is due to misconfigured proxy server. (Try using a different browser)");
|
||||
})
|
||||
socket.emit("checkConn", function() {
|
||||
success = true;
|
||||
socket.disconnect();
|
||||
});
|
||||
setTimeout(function() {
|
||||
if (!success)
|
||||
error.text("Cannot connect with server. This probably is due to misconfigured proxy server. (Try using a different browser)");
|
||||
else
|
||||
{
|
||||
error.text("");
|
||||
$("input").keypress(function(evt) {
|
||||
if (evt.which === 13)
|
||||
requestAnimationFrame(run);
|
||||
});
|
||||
$("button").click(function(evt) {
|
||||
requestAnimationFrame(run);
|
||||
});
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
var user, socket, frame;
|
||||
|
||||
//Event listeners
|
||||
$(document).keydown(function(e) {
|
||||
if (!user || user.dead)
|
||||
return;
|
||||
var newHeading = -1;
|
||||
switch (e.which)
|
||||
{
|
||||
case 37: newHeading = 3; break; //LEFT
|
||||
case 38: newHeading = 0; break; //UP
|
||||
case 39: newHeading = 1; break; //RIGHT
|
||||
case 40: newHeading = 2; break; //DOWN
|
||||
default: return; //exit handler for other keys.
|
||||
}
|
||||
|
||||
if (newHeading === user.currentHeading || ((newHeading % 2 === 0) ^
|
||||
(user.currentHeading % 2 === 0)))
|
||||
{
|
||||
//user.heading = newHeading;
|
||||
if (socket)
|
||||
socket.emit("frame", {
|
||||
frame: frame,
|
||||
heading: newHeading
|
||||
}, function(success, msg) {
|
||||
if (!success)
|
||||
console.error(msg);
|
||||
});
|
||||
}
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
|
||||
|
||||
var grid = renderer.grid;
|
||||
var timeout = undefined;
|
||||
var dirty = false;
|
||||
var deadFrames = 0;
|
||||
var requesting = -1; //frame that we are requesting at.
|
||||
var frameCache = []; //Frames after our request.
|
||||
|
||||
//TODO: check if we can connect to server.
|
||||
function connectServer() {
|
||||
io.j = [];
|
||||
io.sockets = [];
|
||||
socket = io('http://' + window.location.hostname + ':8081', {
|
||||
'forceNew': true,
|
||||
upgrade: false,
|
||||
transports: ['websocket']
|
||||
});
|
||||
socket.on('connect', function(){
|
||||
console.info("Connected to server.");
|
||||
});
|
||||
socket.on('game', function(data) {
|
||||
if (timeout != undefined)
|
||||
clearTimeout(timeout);
|
||||
|
||||
//Initialize game.
|
||||
//TODO: display data.gameid --- game id #
|
||||
frame = data.frame;
|
||||
renderer.reset();
|
||||
|
||||
//Load players.
|
||||
data.players.forEach(function(p) {
|
||||
var pl = new Player(grid, p);
|
||||
renderer.addPlayer(pl);
|
||||
});
|
||||
user = renderer.getPlayerFromNum(data.num);
|
||||
if (!user) throw new Error();
|
||||
renderer.setUser(user);
|
||||
|
||||
//Load grid.
|
||||
var gridData = new Uint8Array(data.grid);
|
||||
for (var r = 0; r < grid.size; r++)
|
||||
for (var c = 0; c < grid.size; c++)
|
||||
{
|
||||
var ind = gridData[r * grid.size + c] - 1;
|
||||
grid.set(r, c, ind === -1 ? null : renderer.getPlayer(ind));
|
||||
}
|
||||
|
||||
renderer.paint();
|
||||
frame = data.frame;
|
||||
|
||||
if (requesting !== -1)
|
||||
{
|
||||
//Update those cache frames after we updated game.
|
||||
var minFrame = requesting;
|
||||
requesting = -1;
|
||||
while (frameCache.length > frame - minFrame)
|
||||
processFrame(frameCache[frame - minFrame]);
|
||||
frameCache = [];
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('notifyFrame', processFrame);
|
||||
|
||||
socket.on('dead', function() {
|
||||
socket.disconnect(); //In case we didn't get the disconnect call.
|
||||
});
|
||||
|
||||
socket.on('disconnect', function(){
|
||||
if (!user)
|
||||
return;
|
||||
console.info("Server has disconnected. Creating new game.");
|
||||
socket.disconnect();
|
||||
user.die();
|
||||
dirty = true;
|
||||
paintLoop();
|
||||
|
||||
//TODO: Show score stats.
|
||||
//Show score stats.
|
||||
$("#stats").removeClass("hidden");
|
||||
$("#stats").animate({
|
||||
opacity: .9999
|
||||
}, 3000, function() {
|
||||
showStats();
|
||||
});
|
||||
|
||||
//Then fade back into the login screen.
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function showStats() {
|
||||
$("#begin").removeClass("hidden");
|
||||
$("#begin").animate({
|
||||
opacity: .9999
|
||||
}, 1000, function() {
|
||||
$("#stats").addClass("hidden").css("opacity", 0);
|
||||
norun = false;
|
||||
});
|
||||
}
|
||||
|
||||
function processFrame(data)
|
||||
{
|
||||
if (timeout != undefined)
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (requesting !== -1 && requesting < data.frame)
|
||||
{
|
||||
frameCache.push(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.frame - 1 !== frame)
|
||||
{
|
||||
console.error("Frames don't match up!");
|
||||
socket.emit('requestFrame'); //Restore data.
|
||||
requesting = data.frame;
|
||||
frameCache.push(data);
|
||||
return;
|
||||
}
|
||||
|
||||
frame++;
|
||||
if (data.newPlayers)
|
||||
{
|
||||
data.newPlayers.forEach(function(p) {
|
||||
if (p.num === user.num)
|
||||
return;
|
||||
var pl = new Player(grid, p);
|
||||
renderer.addPlayer(pl);
|
||||
core.initPlayer(grid, pl);
|
||||
});
|
||||
}
|
||||
|
||||
var found = new Array(renderer.playerSize());
|
||||
data.moves.forEach(function(val, i) {
|
||||
var player = renderer.getPlayerFromNum(val.num);
|
||||
if (!player) return;
|
||||
if (val.left) player.die();
|
||||
found[i] = true;
|
||||
player.heading = val.heading;
|
||||
});
|
||||
for (var i = 0; i < renderer.playerSize(); i++)
|
||||
{
|
||||
//Implicitly leaving game.
|
||||
if (!found[i])
|
||||
{
|
||||
var player = renderer.getPlayer();
|
||||
player && player.die();
|
||||
}
|
||||
}
|
||||
|
||||
renderer.update();
|
||||
|
||||
var locs = {};
|
||||
for (var i = 0; i < renderer.playerSize(); i++)
|
||||
{
|
||||
var p = renderer.getPlayer(i);
|
||||
locs[p.num] = [p.posX, p.posY, p.waitLag];
|
||||
}
|
||||
|
||||
socket.emit("verify", {
|
||||
frame: frame,
|
||||
locs: locs
|
||||
}, function(frame, success, adviceFix, msg) {
|
||||
if (!success && requesting === -1)
|
||||
{
|
||||
console.error(frame + ": " + msg);
|
||||
if (adviceFix)
|
||||
socket.emit('requestFrame');
|
||||
}
|
||||
}.bind(this, frame));
|
||||
|
||||
dirty = true;
|
||||
requestAnimationFrame(function() {
|
||||
paintLoop();
|
||||
});
|
||||
timeout = setTimeout(function() {
|
||||
console.warn("Server has timed-out. Disconnecting.");
|
||||
socket.disconnect();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function paintLoop()
|
||||
{
|
||||
if (!dirty)
|
||||
return;
|
||||
renderer.paint();
|
||||
dirty = false;
|
||||
|
||||
if (user.dead)
|
||||
{
|
||||
if (timeout)
|
||||
clearTimeout(timeout);
|
||||
if (deadFrames === 60) //One second of frame
|
||||
{
|
||||
var before = renderer.allowAnimation;
|
||||
renderer.allowAnimation = false;
|
||||
renderer.update();
|
||||
renderer.paint();
|
||||
renderer.allowAnimation = before;
|
||||
user = null;
|
||||
deadFrames = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
socket.disconnect();
|
||||
deadFrames++;
|
||||
dirty = true;
|
||||
renderer.update();
|
||||
requestAnimationFrame(paintLoop);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
function constant(val)
|
||||
{
|
||||
return {
|
||||
value: val,
|
||||
enumerable: true
|
||||
};
|
||||
}
|
||||
|
||||
var consts = {
|
||||
GRID_SIZE: constant(80),
|
||||
CELL_WIDTH: constant(40),
|
||||
SPEED: constant(5),
|
||||
BORDER_WIDTH: constant(20),
|
||||
MAX_PLAYERS: constant(81)
|
||||
};
|
||||
|
||||
Object.defineProperties(module.exports, consts);
|
135
game-core.js
135
game-core.js
@ -1,135 +0,0 @@
|
||||
var ANIMATE_FRAMES = 24;
|
||||
var CELL_WIDTH = 40;
|
||||
|
||||
//TODO: remove constants.
|
||||
exports.initPlayer = function(grid, player)
|
||||
{
|
||||
for (var dr = -1; dr <= 1; dr++)
|
||||
for (var dc = -1; dc <= 1; dc++)
|
||||
if (!grid.isOutOfBounds(dr + player.row, dc + player.col))
|
||||
grid.set(dr + player.row, dc + player.col, player);
|
||||
};
|
||||
exports.updateFrame = function(grid, players, dead, notifyKill)
|
||||
{
|
||||
var adead = [];
|
||||
if (dead instanceof Array)
|
||||
adead = dead;
|
||||
|
||||
var kill;
|
||||
if (!notifyKill)
|
||||
kill = function() {};
|
||||
else
|
||||
kill = function(killer, other) {if (!removing[other]) notifyKill(killer, other);};
|
||||
|
||||
//Move players.
|
||||
var tmp = players.filter(function(val) {
|
||||
val.move();
|
||||
if (val.dead)
|
||||
adead.push(val);
|
||||
return !val.dead;
|
||||
});
|
||||
|
||||
//Remove players with collisions.
|
||||
var removing = new Array(players.length);
|
||||
for (var i = 0; i < players.length; i++)
|
||||
{
|
||||
for (var j = i; j < players.length; j++)
|
||||
{
|
||||
|
||||
//Remove those players when other players have hit their tail.
|
||||
if (!removing[j] && players[j].tail.hitsTail(players[i]))
|
||||
{
|
||||
kill(i, j);
|
||||
removing[j] = true;
|
||||
//console.log("TAIL");
|
||||
}
|
||||
if (!removing[i] && players[i].tail.hitsTail(players[j]))
|
||||
{
|
||||
kill(j, i);
|
||||
removing[i] = true;
|
||||
//console.log("TAIL");
|
||||
}
|
||||
|
||||
//Remove players with collisons...
|
||||
if (i !== j && squaresIntersect(players[i].posX, players[j].posX) &&
|
||||
squaresIntersect(players[i].posY, players[j].posY))
|
||||
{
|
||||
//...if one player is own his own territory, the other is out.
|
||||
if (grid.get(players[i].row, players[i].col) === players[i])
|
||||
{
|
||||
kill(i, j);
|
||||
removing[j] = true;
|
||||
}
|
||||
else if (grid.get(players[j].row, players[j].col) === players[j])
|
||||
{
|
||||
kill(j, i);
|
||||
removing[i] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//...otherwise, the one that sustains most of the collision will be removed.
|
||||
var areaI = area(players[i]);
|
||||
var areaJ = area(players[j]);
|
||||
|
||||
if (areaI === areaJ)
|
||||
{
|
||||
kill(i, j);
|
||||
kill(j, i);
|
||||
removing[i] = removing[j] = true;
|
||||
}
|
||||
else if (areaI > areaJ)
|
||||
{
|
||||
kill(j, i);
|
||||
removing[i] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
kill(i, j);
|
||||
removing[j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tmp = tmp.filter(function(val, i) {
|
||||
if (removing[i])
|
||||
{
|
||||
adead.push(val);
|
||||
val.die();
|
||||
}
|
||||
return !removing[i];
|
||||
});
|
||||
players.length = tmp.length;
|
||||
for (var i = 0; i < tmp.length; i++)
|
||||
players[i] = tmp[i];
|
||||
|
||||
//Remove dead squares.
|
||||
for (var r = 0; r < grid.size; r++)
|
||||
{
|
||||
for (var c = 0; c < grid.size; c++)
|
||||
{
|
||||
if (adead.indexOf(grid.get(r, c)) !== -1)
|
||||
grid.set(r, c, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function squaresIntersect(a, b)
|
||||
{
|
||||
if (a < b)
|
||||
return b < a + CELL_WIDTH;
|
||||
else
|
||||
return a < b + CELL_WIDTH;
|
||||
}
|
||||
|
||||
function area(player)
|
||||
{
|
||||
var xDest = player.col * CELL_WIDTH;
|
||||
var yDest = player.row * CELL_WIDTH;
|
||||
|
||||
if (player.posX === xDest)
|
||||
return Math.abs(player.posY - yDest);
|
||||
else
|
||||
return Math.abs(player.posX - xDest);
|
||||
}
|
459
game-renderer.js
459
game-renderer.js
@ -1,459 +0,0 @@
|
||||
/* global $ */
|
||||
var Rolling = require("./rolling.js");
|
||||
var Color = require("./color.js");
|
||||
var Grid = require("./grid.js");
|
||||
var consts = require("./game-consts.js");
|
||||
var core = require("./game-core.js");
|
||||
|
||||
var GRID_SIZE = consts.GRID_SIZE;
|
||||
var CELL_WIDTH = consts.CELL_WIDTH;
|
||||
var SPEED = consts.SPEED;
|
||||
var BORDER_WIDTH = consts.BORDER_WIDTH;
|
||||
var SHADOW_OFFSET = 5;
|
||||
var ANIMATE_FRAMES = 24;
|
||||
var BOUNCE_FRAMES = [8, 4];
|
||||
var DROP_HEIGHT = 24;
|
||||
var DROP_SPEED = 2;
|
||||
var MIN_BAR_WIDTH = 65;
|
||||
var BAR_HEIGHT = SHADOW_OFFSET + CELL_WIDTH;
|
||||
var BAR_WIDTH = 400;
|
||||
|
||||
|
||||
var canvasWidth, canvasHeight, gameWidth, gameHeight, ctx, offctx, offscreenCanvas;
|
||||
|
||||
$(function () {
|
||||
var canvas = $("#main-ui")[0];
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
offscreenCanvas = document.createElement("canvas");
|
||||
offctx = offscreenCanvas.getContext('2d');
|
||||
|
||||
canvasWidth = offscreenCanvas.width = canvas.width = window.innerWidth;
|
||||
canvasHeight = offscreenCanvas.height = canvas.height = window.innerHeight - 20;
|
||||
canvas.style.marginTop = 20 / 2;
|
||||
|
||||
gameWidth = canvasWidth;
|
||||
gameHeight = canvasHeight - BAR_HEIGHT;
|
||||
});
|
||||
|
||||
|
||||
var allowAnimation = true;
|
||||
var animateGrid, players, allPlayers, playerPortion, portionsRolling,
|
||||
barProportionRolling, grid, animateTo, offset, user, zoom, kills, showedDead;
|
||||
|
||||
grid = new Grid(GRID_SIZE, function(row, col, before, after) {
|
||||
//Keep track of areas.
|
||||
if (before)
|
||||
playerPortion[before.num]--;
|
||||
if (after)
|
||||
playerPortion[after.num]++;
|
||||
|
||||
//Queue animation
|
||||
if (before === after || !allowAnimation)
|
||||
return;
|
||||
animateGrid.set(row, col, {
|
||||
before: before,
|
||||
after: after,
|
||||
frame: 0
|
||||
});
|
||||
});
|
||||
|
||||
function init() {
|
||||
animateGrid = new Grid(GRID_SIZE);
|
||||
grid.reset();
|
||||
|
||||
players = [];
|
||||
allPlayers = [];
|
||||
playerPortion = [];
|
||||
portionsRolling = [];
|
||||
barProportionRolling = [];
|
||||
|
||||
animateTo = [0, 0];
|
||||
offset = [0, 0];
|
||||
|
||||
user = null;
|
||||
zoom = 1;
|
||||
kills = 0;
|
||||
showedDead = false;
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
//Paint methods.
|
||||
function paintGridBorder(ctx)
|
||||
{
|
||||
ctx.fillStyle = 'lightgray';
|
||||
var gridWidth = CELL_WIDTH * GRID_SIZE;
|
||||
|
||||
ctx.fillRect(-BORDER_WIDTH, 0, BORDER_WIDTH, gridWidth);
|
||||
ctx.fillRect(-BORDER_WIDTH, -BORDER_WIDTH, gridWidth + BORDER_WIDTH * 2, BORDER_WIDTH);
|
||||
ctx.fillRect(gridWidth, 0, BORDER_WIDTH, gridWidth);
|
||||
ctx.fillRect(-BORDER_WIDTH, gridWidth, gridWidth + BORDER_WIDTH * 2, BORDER_WIDTH);
|
||||
}
|
||||
|
||||
function paintGrid(ctx)
|
||||
{
|
||||
//Paint background.
|
||||
ctx.fillStyle = "#e2ebf3";
|
||||
ctx.fillRect(0, 0, CELL_WIDTH * GRID_SIZE, CELL_WIDTH * GRID_SIZE);
|
||||
|
||||
paintGridBorder(ctx);
|
||||
//paintGridLines(ctx);
|
||||
|
||||
//Get viewing limits
|
||||
var offsetX = (offset[0] - BORDER_WIDTH);
|
||||
var offsetY = (offset[1] - BORDER_WIDTH);
|
||||
|
||||
var minRow = Math.max(Math.floor(offsetY / CELL_WIDTH), 0);
|
||||
var minCol = Math.max(Math.floor(offsetX / CELL_WIDTH), 0);
|
||||
var maxRow = Math.min(Math.ceil((offsetY + gameHeight / zoom) / CELL_WIDTH), grid.size);
|
||||
var maxCol = Math.min(Math.ceil((offsetX + gameWidth / zoom) / CELL_WIDTH), grid.size);
|
||||
|
||||
//Paint occupied areas. (and fading ones).
|
||||
for (var r = minRow; r < maxRow; r++)
|
||||
{
|
||||
for (var c = minCol; c < maxCol; c++)
|
||||
{
|
||||
var p = grid.get(r, c);
|
||||
var x = c * CELL_WIDTH, y = r * CELL_WIDTH, baseColor, shadowColor;
|
||||
|
||||
var animateSpec = animateGrid.get(r, c);
|
||||
if (allowAnimation && animateSpec)
|
||||
{
|
||||
if (animateSpec.before) //fading animation
|
||||
{
|
||||
var frac = (animateSpec.frame / ANIMATE_FRAMES);
|
||||
var back = new Color(.58, .41, .92, 1);
|
||||
baseColor = animateSpec.before.baseColor.interpolateToString(back, frac);
|
||||
shadowColor = animateSpec.before.shadowColor.interpolateToString(back, frac);
|
||||
}
|
||||
else
|
||||
continue;
|
||||
}
|
||||
else if (p)
|
||||
{
|
||||
baseColor = p.baseColor;
|
||||
shadowColor = p.shadowColor;
|
||||
}
|
||||
else //No animation nor is this player owned.
|
||||
continue;
|
||||
|
||||
var hasBottom = !grid.isOutOfBounds(r + 1, c);
|
||||
var bottomAnimate = hasBottom && animateGrid.get(r + 1, c);
|
||||
var totalStatic = !bottomAnimate && !animateSpec;
|
||||
var bottomEmpty = totalStatic ? (hasBottom && !grid.get(r + 1, c)) :
|
||||
(!bottomAnimate || (bottomAnimate.after && bottomAnimate.before));
|
||||
if (hasBottom && ((!!bottomAnimate ^ !!animateSpec) || bottomEmpty))
|
||||
{
|
||||
|
||||
ctx.fillStyle = shadowColor.rgbString();
|
||||
ctx.fillRect(x, y + CELL_WIDTH, CELL_WIDTH + 1, SHADOW_OFFSET);
|
||||
}
|
||||
ctx.fillStyle = baseColor.rgbString();
|
||||
ctx.fillRect(x, y, CELL_WIDTH + 1, CELL_WIDTH + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allowAnimation)
|
||||
return;
|
||||
|
||||
//Paint squares with drop in animation.
|
||||
for (var r = 0; r < grid.size; r++)
|
||||
{
|
||||
for (var c = 0; c < grid.size; c++)
|
||||
{
|
||||
animateSpec = animateGrid.get(r, c);
|
||||
x = c * CELL_WIDTH, y = r * CELL_WIDTH;
|
||||
|
||||
if (animateSpec && allowAnimation)
|
||||
{
|
||||
var viewable = r >= minRow && r < maxRow && c >= minCol && c < maxCol;
|
||||
if (animateSpec.after && viewable)
|
||||
{
|
||||
//Bouncing the squares.
|
||||
var offsetBounce = getBounceOffset(animateSpec.frame);
|
||||
y -= offsetBounce;
|
||||
|
||||
shadowColor = animateSpec.after.shadowColor;
|
||||
baseColor = animateSpec.after.baseColor.deriveLumination(-(offsetBounce / DROP_HEIGHT) * .1);
|
||||
|
||||
ctx.fillStyle = shadowColor.rgbString();
|
||||
ctx.fillRect(x, y + CELL_WIDTH, CELL_WIDTH, SHADOW_OFFSET);
|
||||
ctx.fillStyle = baseColor.rgbString();
|
||||
ctx.fillRect(x, y, CELL_WIDTH + 1, CELL_WIDTH + 1);
|
||||
}
|
||||
|
||||
animateSpec.frame++;
|
||||
if (animateSpec.frame >= ANIMATE_FRAMES)
|
||||
animateGrid.set(r, c, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function paintUIBar(ctx)
|
||||
{
|
||||
//UI Bar background
|
||||
ctx.fillStyle = "#24422c";
|
||||
ctx.fillRect(0, 0, canvasWidth, BAR_HEIGHT);
|
||||
|
||||
var barOffset;
|
||||
ctx.fillStyle = "white";
|
||||
ctx.font = "24px Changa";
|
||||
barOffset = (user && user.name) ? (ctx.measureText(user.name).width + 20) : 0;
|
||||
ctx.fillText(user ? user.name : "", 5, CELL_WIDTH - 5);
|
||||
|
||||
//Draw filled bar.
|
||||
ctx.fillStyle = "rgba(180, 180, 180, .3)";
|
||||
ctx.fillRect(barOffset, 0, BAR_WIDTH, BAR_HEIGHT);
|
||||
|
||||
var userPortions = portionsRolling[user.num] ? portionsRolling[user.num].lag : 0;
|
||||
var barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * userPortions + MIN_BAR_WIDTH);
|
||||
ctx.fillStyle = user ? user.baseColor.rgbString() : "";
|
||||
ctx.fillRect(barOffset, 0, barSize, CELL_WIDTH);
|
||||
ctx.fillStyle = user ? user.shadowColor.rgbString() : "";
|
||||
ctx.fillRect(barOffset, CELL_WIDTH, barSize, SHADOW_OFFSET);
|
||||
|
||||
//TODO: dont reset kill count and zoom when we request frames.
|
||||
//Percentage
|
||||
ctx.fillStyle = "white";
|
||||
ctx.font = "18px Changa";
|
||||
ctx.fillText((userPortions * 100).toFixed(3) + "%", 5 + barOffset, CELL_WIDTH - 5);
|
||||
|
||||
//Number of kills
|
||||
var killsText = "Kills: " + kills;
|
||||
var killsOffset = 20 + BAR_WIDTH + barOffset;
|
||||
ctx.fillText(killsText, killsOffset, CELL_WIDTH - 5);
|
||||
|
||||
//Calcuate rank
|
||||
var sorted = [];
|
||||
players.forEach(function(val) {
|
||||
sorted.push({player: val, portion: playerPortion[val.num]});
|
||||
});
|
||||
sorted.sort(function(a, b) {
|
||||
if (a.portion === b.portion) return a.player.num - b.player.num;
|
||||
else return b.portion - a.portion;
|
||||
});
|
||||
|
||||
var rank = sorted.findIndex(function(val) {return val.player === user});
|
||||
ctx.fillText("Rank: " + (rank === -1 ? "--" : rank + 1) + " of " + sorted.length,
|
||||
ctx.measureText(killsText).width + killsOffset + 20, CELL_WIDTH - 5);
|
||||
|
||||
//Rolling the leaderboard bars.
|
||||
if (sorted.length > 0)
|
||||
{
|
||||
var maxPortion = sorted[0].portion;
|
||||
for (var i = 0; i < players.length; i++)
|
||||
{
|
||||
var rolling = barProportionRolling[players[i].num];
|
||||
rolling.value = playerPortion[players[i].num] / maxPortion;
|
||||
rolling.update();
|
||||
}
|
||||
}
|
||||
|
||||
//Show leaderboard.
|
||||
var leaderboardNum = Math.min(5, sorted.length);
|
||||
for (var i = 0; i < leaderboardNum; i++)
|
||||
{
|
||||
var player = sorted[i].player;
|
||||
var name = player.name || "Unnamed";
|
||||
var portion = barProportionRolling[player.num].lag;
|
||||
|
||||
var nameWidth = ctx.measureText(name).width;
|
||||
barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * portion + MIN_BAR_WIDTH);
|
||||
var barX = canvasWidth - barSize;
|
||||
var barY = BAR_HEIGHT * (i + 1);
|
||||
var offset = i == 0 ? 10 : 0;
|
||||
|
||||
ctx.fillStyle = 'rgba(10, 10, 10, .3)';
|
||||
ctx.fillRect(barX - 10, barY + 10 - offset, barSize + 10, BAR_HEIGHT + offset);
|
||||
ctx.fillStyle = player.baseColor.rgbString();
|
||||
ctx.fillRect(barX, barY, barSize, CELL_WIDTH);
|
||||
ctx.fillStyle = player.shadowColor.rgbString();
|
||||
ctx.fillRect(barX, barY + CELL_WIDTH, barSize, SHADOW_OFFSET);
|
||||
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fillText(name, barX - nameWidth - 15, barY + 27);
|
||||
|
||||
var percentage = (portionsRolling[player.num].lag * 100).toFixed(3) + "%";
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText(percentage, barX + 5, barY + CELL_WIDTH - 5);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function paint(ctx)
|
||||
{
|
||||
ctx.fillStyle = 'whitesmoke';
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
//Move grid to viewport as said with the offsets, below the stats
|
||||
ctx.save();
|
||||
ctx.translate(0, BAR_HEIGHT);
|
||||
ctx.beginPath();
|
||||
ctx.rect(0, 0, gameWidth, gameHeight);
|
||||
ctx.clip();
|
||||
|
||||
//Zoom in/out based on player stats.
|
||||
ctx.scale(zoom, zoom);
|
||||
ctx.translate(-offset[0] + BORDER_WIDTH, -offset[1] + BORDER_WIDTH);
|
||||
|
||||
paintGrid(ctx);
|
||||
players.forEach(function (p) {
|
||||
var fr = p.waitLag;
|
||||
if (fr < ANIMATE_FRAMES)
|
||||
p.render(ctx, fr / ANIMATE_FRAMES);
|
||||
else
|
||||
p.render(ctx);
|
||||
});
|
||||
|
||||
//Reset transform to paint fixed UI elements
|
||||
ctx.restore();
|
||||
paintUIBar(ctx);
|
||||
|
||||
if ((!user || user.dead) && !showedDead)
|
||||
{
|
||||
showedDead = true;
|
||||
console.log("You died!");
|
||||
//return;
|
||||
}
|
||||
}
|
||||
|
||||
function paintDoubleBuff()
|
||||
{
|
||||
paint(offctx);
|
||||
ctx.drawImage(offscreenCanvas, 0, 0);
|
||||
}
|
||||
|
||||
function update() {
|
||||
|
||||
//Change grid offsets.
|
||||
for (var i = 0; i <= 1; i++)
|
||||
{
|
||||
if (animateTo[i] !== offset[i])
|
||||
{
|
||||
if (allowAnimation)
|
||||
{
|
||||
var delta = animateTo[i] - offset[i];
|
||||
var dir = Math.sign(delta);
|
||||
var mag = Math.min(SPEED, Math.abs(delta));
|
||||
offset[i] += dir * mag;
|
||||
}
|
||||
else
|
||||
offset[i] = animateTo[i];
|
||||
}
|
||||
}
|
||||
|
||||
//Calculate player portions.
|
||||
for (var i = 0; i < players.length; i++)
|
||||
{
|
||||
var roll = portionsRolling[players[i].num];
|
||||
roll.value = playerPortion[players[i].num] / GRID_SIZE / GRID_SIZE;
|
||||
roll.update();
|
||||
}
|
||||
|
||||
//Zoom goes from 1 to .5, decreasing as portion goes up. TODO: maybe can modify this?
|
||||
if (portionsRolling[user.num])
|
||||
zoom = 1 / (portionsRolling[user.num].lag + 1);
|
||||
|
||||
var dead = [];
|
||||
core.updateFrame(grid, players, dead, function addKill(killer, other)
|
||||
{
|
||||
if (players[killer] === user && killer !== other)
|
||||
kills++;
|
||||
});
|
||||
dead.forEach(function(val) {
|
||||
console.log(val.name || "Unnamed" + " is dead");
|
||||
delete allPlayers[val.num];
|
||||
delete portionsRolling[val.num];
|
||||
});
|
||||
|
||||
//TODO: animate player is dead. (maybe explosion?), and tail rewinds itself.
|
||||
if (user) centerOnPlayer(user, animateTo);
|
||||
}
|
||||
|
||||
//Helper methods.
|
||||
function centerOnPlayer(player, pos)
|
||||
{
|
||||
var xOff = Math.floor(player.posX - (gameWidth / zoom - CELL_WIDTH) / 2);
|
||||
var yOff = Math.floor(player.posY - (gameHeight / zoom - CELL_WIDTH) / 2);
|
||||
var gridWidth = grid.size * CELL_WIDTH + BORDER_WIDTH * 2;
|
||||
pos[0] = Math.max(Math.min(xOff, gridWidth + (BAR_WIDTH + 100) / zoom - gameWidth / zoom), 0);
|
||||
pos[1] = Math.max(Math.min(yOff, gridWidth - gameHeight / zoom), 0);
|
||||
}
|
||||
|
||||
function getBounceOffset(frame)
|
||||
{
|
||||
var offsetBounce = ANIMATE_FRAMES;
|
||||
var bounceNum = BOUNCE_FRAMES.length - 1;
|
||||
while (bounceNum >= 0 && frame < offsetBounce - BOUNCE_FRAMES[bounceNum])
|
||||
{
|
||||
offsetBounce -= BOUNCE_FRAMES[bounceNum];
|
||||
bounceNum--;
|
||||
}
|
||||
|
||||
if (bounceNum === -1)
|
||||
{
|
||||
return (offsetBounce - frame) * DROP_SPEED;
|
||||
}
|
||||
else
|
||||
{
|
||||
offsetBounce -= BOUNCE_FRAMES[bounceNum];
|
||||
frame = frame - offsetBounce;
|
||||
var midFrame = BOUNCE_FRAMES[bounceNum] / 2;
|
||||
if (frame >= midFrame)
|
||||
return (BOUNCE_FRAMES[bounceNum] - frame) * DROP_SPEED;
|
||||
else
|
||||
return frame * DROP_SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = exports = {
|
||||
addPlayer: function(player) {
|
||||
if (allPlayers[player.num])
|
||||
return; //Already added.
|
||||
allPlayers[player.num] = players[players.length] = player;
|
||||
playerPortion[player.num] = 0;
|
||||
portionsRolling[player.num] = new Rolling(9 / GRID_SIZE / GRID_SIZE, ANIMATE_FRAMES);
|
||||
barProportionRolling[player.num] = new Rolling(0, ANIMATE_FRAMES);
|
||||
return players.length - 1;
|
||||
},
|
||||
getPlayer: function(ind) {
|
||||
if (ind < 0 || ind >= players.length)
|
||||
throw new RangeError("Player index out of bounds (" + ind + ").");
|
||||
return players[ind];
|
||||
},
|
||||
getPlayerFromNum: function(num) {
|
||||
return allPlayers[num];
|
||||
},
|
||||
playerSize: function() {
|
||||
return players.length;
|
||||
},
|
||||
setUser: function(player) {
|
||||
user = player;
|
||||
centerOnPlayer(user, offset);
|
||||
},
|
||||
incrementKill: function() {
|
||||
kills++;
|
||||
},
|
||||
reset: function() {
|
||||
init();
|
||||
},
|
||||
paint: paintDoubleBuff,
|
||||
update: update
|
||||
};
|
||||
|
||||
Object.defineProperties(exports, {
|
||||
allowAnimation: {
|
||||
get: function() { return allowAnimation; },
|
||||
set: function(val) { allowAnimation = !!val; },
|
||||
enumerable: true
|
||||
},
|
||||
grid: {
|
||||
get: function() { return grid; },
|
||||
enumerable: true
|
||||
}
|
||||
});
|
311
game-server.js
311
game-server.js
@ -1,311 +0,0 @@
|
||||
|
||||
var Color = require("./color");
|
||||
var Grid = require("./grid");
|
||||
var Player = require("./player");
|
||||
//var Gate = require("./gate");
|
||||
var core = require("./game-core");
|
||||
var consts = require("./game-consts");
|
||||
|
||||
var GRID_SIZE = consts.GRID_SIZE;
|
||||
var CELL_WIDTH = consts.CELL_WIDTH;
|
||||
var MAX_PLAYERS = consts.MAX_PLAYERS;
|
||||
|
||||
var HUES = [0, 10, 20, 25, 30, 35, 40, 45, 50, 60, 70, 100, 110, 120, 125, 130, 135, 140, 145, 150, 160, 170, 180, 190, 200, 210, 220].map(function(val) {return val / 240});
|
||||
var SATS = [192, 150, 100].map(function(val) {return val / 240});
|
||||
|
||||
|
||||
function Game(id)
|
||||
{
|
||||
//Shuffle the hues.
|
||||
for (var i = 0; i < HUES.length * 50; i++)
|
||||
{
|
||||
var a = Math.floor(Math.random() * HUES.length);
|
||||
var b = Math.floor(Math.random() * HUES.length);
|
||||
var tmp = HUES[a];
|
||||
HUES[a] = HUES[b];
|
||||
HUES[b] = tmp;
|
||||
}
|
||||
|
||||
var possColors = new Array(SATS.length * HUES.length);
|
||||
i = 0;
|
||||
for (var s = 0; s < SATS.length; s++)
|
||||
for (var h = 0; h < HUES.length; h++)
|
||||
possColors[i++] = new Color(HUES[h], SATS[s], .5, 1);
|
||||
|
||||
var nextInd = 0;
|
||||
var players = [];
|
||||
var newPlayers = [];
|
||||
var frameLocs = [];
|
||||
var frame = 0;
|
||||
|
||||
var filled = 0;
|
||||
var grid = new Grid(GRID_SIZE, function(row, col, before, after) {
|
||||
if (!!after ^ !!before)
|
||||
{
|
||||
if (after)
|
||||
filled++;
|
||||
else
|
||||
filled--;
|
||||
if (filled === GRID_SIZE * GRID_SIZE)
|
||||
console.log("FULL GAME");
|
||||
}
|
||||
});
|
||||
|
||||
this.id = id;
|
||||
|
||||
this.addPlayer = function(client, name) {
|
||||
if (players.length >= MAX_PLAYERS)
|
||||
return false;
|
||||
|
||||
var start = findEmpty(grid);
|
||||
if (!start)
|
||||
return false;
|
||||
|
||||
var params = {
|
||||
posX: start.col * CELL_WIDTH,
|
||||
posY: start.row * CELL_WIDTH,
|
||||
currentHeading: Math.floor(Math.random() * 4),
|
||||
name: name,
|
||||
num: nextInd,
|
||||
base: possColors.shift()
|
||||
};
|
||||
|
||||
var p = new Player(grid, params);
|
||||
p.tmpHeading = params.currentHeading;
|
||||
p.client = client;
|
||||
players.push(p);
|
||||
newPlayers.push(p);
|
||||
nextInd++;
|
||||
core.initPlayer(grid, p);
|
||||
|
||||
console.log((p.name || "Unnamed") + " (" + p.num + ") joined.");
|
||||
|
||||
client.on("requestFrame", function () {
|
||||
if (p.frame === frame)
|
||||
return;
|
||||
p.frame = frame; //Limit number of requests per frame. (One per frame);
|
||||
|
||||
var splayers = players.map(function(val) {return val.serialData();});
|
||||
client.emit("game", {
|
||||
"num": p.num,
|
||||
"gameid": id,
|
||||
"frame": frame,
|
||||
"players": splayers,
|
||||
"grid": gridSerialData(grid, players)
|
||||
});
|
||||
});
|
||||
|
||||
//Verifies that this client has executed this frame properly.
|
||||
client.on("verify", function(data, resp) {
|
||||
if (typeof resp !== "function")
|
||||
return;
|
||||
|
||||
if (!data.frame)
|
||||
resp(false, false, "No frame supplied");
|
||||
else if (!checkInt(data.frame, 0, frame + 1))
|
||||
resp(false, false, "Must be a valid frame number");
|
||||
else
|
||||
{
|
||||
verifyPlayerLocations(data.frame, data.locs, resp);
|
||||
}
|
||||
});
|
||||
|
||||
client.on("frame", function(data, errorHan){
|
||||
if (typeof data === "function")
|
||||
{
|
||||
errorHan(false, "No data supplied.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof errorHan !== "function")
|
||||
errorHan = function() {};
|
||||
|
||||
if (!data)
|
||||
errorHan(false, "No data supplied.");
|
||||
else if (!checkInt(data.frame, 0, Infinity))
|
||||
errorHan(false, "Requires a valid non-negative frame integer.");
|
||||
else if (data.frame > frame)
|
||||
errorHan(false, "Invalid frame received.");
|
||||
else
|
||||
{
|
||||
if (data.heading !== undefined)
|
||||
{
|
||||
if (checkInt(data.heading, 0, 4))
|
||||
{
|
||||
p.tmpHeading = data.heading;
|
||||
errorHan(true);
|
||||
}
|
||||
else
|
||||
errorHan(false, "New heading must be an integer of range [0, 4).");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.on('disconnect', function() {
|
||||
p.die(); //Die immediately if not already.
|
||||
p.disconnected = true;
|
||||
console.log((p.name || "Unnamed") + " (" + p.num + ") left.");
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
function pushPlayerLocations()
|
||||
{
|
||||
var locs = [];
|
||||
for (var p of players)
|
||||
locs[p.num] = [p.posX, p.posY, p.waitLag];
|
||||
locs.frame = frame;
|
||||
|
||||
if (frameLocs.length >= 300) //Give it 5 seconds of lag.
|
||||
frameLocs.shift();
|
||||
frameLocs.push(locs);
|
||||
}
|
||||
|
||||
function verifyPlayerLocations(fr, verify, resp)
|
||||
{
|
||||
var minFrame = frame - frameLocs.length + 1;
|
||||
if (fr < minFrame || fr > frame)
|
||||
{
|
||||
resp(false, false, "Frames out of reference");
|
||||
return;
|
||||
}
|
||||
|
||||
function string(loc)
|
||||
{
|
||||
return '(' + loc[0] + ', ' + loc[1] + ') [' + loc[2] + ']';
|
||||
}
|
||||
|
||||
var locs = frameLocs[fr - minFrame];
|
||||
if (locs.frame !== fr)
|
||||
{
|
||||
resp(false, false, locs.frame + " != " + fr);
|
||||
return;
|
||||
}
|
||||
for (var num in verify)
|
||||
{
|
||||
if (locs[num][0] !== verify[num][0] || locs[num][1] !== verify[num][1] || locs[num][2] !== verify[num][2])
|
||||
{
|
||||
resp(false, true, 'P' + num + ' ' + string(locs[num]) + ' !== ' + string(verify[num]));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
resp(true, false);
|
||||
}
|
||||
|
||||
function tick() {
|
||||
|
||||
//TODO: notify those players that this server automatically drops out.
|
||||
var splayers = players.map(function(val) {return val.serialData();});
|
||||
var snews = newPlayers.map(function(val) {
|
||||
//Emit game stats.
|
||||
val.client.emit("game", {
|
||||
"num": val.num,
|
||||
"gameid": id,
|
||||
"frame": frame,
|
||||
"players": splayers,
|
||||
"grid": gridSerialData(grid, players),
|
||||
});
|
||||
return val.serialData();
|
||||
});
|
||||
var moves = players.map(function(val) {
|
||||
//Account for race condition (when heading is set after emitting frames, and before updating).
|
||||
val.heading = val.tmpHeading;
|
||||
return {num: val.num, left: !!val.disconnected, heading: val.heading};
|
||||
});
|
||||
|
||||
update();
|
||||
|
||||
var data = {frame: frame + 1, moves: moves};
|
||||
if (snews.length > 0)
|
||||
{
|
||||
data.newPlayers = snews;
|
||||
newPlayers = [];
|
||||
}
|
||||
|
||||
for (var pl of players)
|
||||
pl.client.emit("notifyFrame", data);
|
||||
|
||||
frame++;
|
||||
pushPlayerLocations();
|
||||
}
|
||||
|
||||
this.tickFrame = tick;
|
||||
|
||||
function update()
|
||||
{
|
||||
var dead = [];
|
||||
core.updateFrame(grid, players, dead);
|
||||
for (var pl of dead)
|
||||
{
|
||||
if (!pl.handledDead)
|
||||
{
|
||||
possColors.unshift(pl.baseColor);
|
||||
pl.handledDead = true;
|
||||
}
|
||||
console.log((pl.name || "Unnamed") + " (" + pl.num + ") died.");
|
||||
pl.client.emit("dead");
|
||||
pl.client.disconnect(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkInt(value, min, max)
|
||||
{
|
||||
if (typeof value !== "number")
|
||||
return false;
|
||||
if (value < min || value >= max)
|
||||
return false;
|
||||
if (Math.floor(value) !== value)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function gridSerialData(grid, players)
|
||||
{
|
||||
var buff = Buffer.alloc(grid.size * grid.size);
|
||||
|
||||
var numToIndex = new Array(players.length > 0 ? players[players.length - 1].num + 1 : 0);
|
||||
for (var i = 0; i < players.length; i++)
|
||||
numToIndex[players[i].num] = i + 1;
|
||||
|
||||
for (var r = 0; r < grid.size; r++)
|
||||
for (var c = 0; c < grid.size; c++)
|
||||
{
|
||||
var ele = grid.get(r, c);
|
||||
buff[r * grid.size + c] = ele ? numToIndex[ele.num] : 0;
|
||||
}
|
||||
return buff;
|
||||
}
|
||||
|
||||
function findEmpty(grid)
|
||||
{
|
||||
var available = [];
|
||||
|
||||
for (var r = 1; r < grid.size - 1; r++)
|
||||
for (var c = 1; c < grid.size - 1; c++)
|
||||
{
|
||||
var cluttered = false;
|
||||
checkclutter: for (var dr = -1; dr <= 1; dr++)
|
||||
{
|
||||
for (var dc = -1; dc <= 1; dc++)
|
||||
{
|
||||
if (grid.get(r + dr, c + dc))
|
||||
{
|
||||
cluttered = true;
|
||||
break checkclutter;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cluttered)
|
||||
available.push({row: r, col: c});
|
||||
}
|
||||
|
||||
if (available.length === 0)
|
||||
return null;
|
||||
else
|
||||
return available[Math.floor(available.length * Math.random())];
|
||||
}
|
||||
|
||||
module.exports = Game;
|
47
gate.js
47
gate.js
@ -1,47 +0,0 @@
|
||||
var inherits = require('util').inherits;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
module.exports = Gate;
|
||||
|
||||
function Gate(awaiting)
|
||||
{
|
||||
var _this = this;
|
||||
if (!(this instanceof Gate))
|
||||
return new Gate(awaiting);
|
||||
|
||||
if (typeof awaiting !== "number")
|
||||
awaiting = 0;
|
||||
|
||||
var currentAwaiting = awaiting;
|
||||
var readyCount = 0;
|
||||
var ready = new Array(currentAwaiting);
|
||||
|
||||
this.setAwaiting = function(count) {
|
||||
awaiting = count;
|
||||
};
|
||||
this.ready = function(ind) {
|
||||
if (Math.floor(ind) != ind || ind >= readyCount)
|
||||
return false;
|
||||
|
||||
ready[ind] = true;
|
||||
readyCount++;
|
||||
|
||||
_this.emit("ready", ind);
|
||||
if (readyCount >= currentAwaiting)
|
||||
{
|
||||
_this.emit("allReady");
|
||||
_this.reset();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
this.reset = function() {
|
||||
_this.emit("reset");
|
||||
ready = new Array(currentAwaiting = awaiting);
|
||||
readyCount = 0;
|
||||
};
|
||||
|
||||
EventEmitter.call(this);
|
||||
}
|
||||
|
||||
inherits(Gate, EventEmitter);
|
56
grid.js
56
grid.js
@ -1,56 +0,0 @@
|
||||
function Grid(size, changeCallback)
|
||||
{
|
||||
var grid = new Array(size);
|
||||
var modified = false;
|
||||
|
||||
var data = {
|
||||
grid: grid,
|
||||
size: size
|
||||
};
|
||||
|
||||
this.get = function(row, col)
|
||||
{
|
||||
if (isOutOfBounds(data, row, col))
|
||||
throw new RangeError("Row or Column value out of bounds");
|
||||
return grid[row] && grid[row][col];
|
||||
}
|
||||
this.set = function(row, col, value)
|
||||
{
|
||||
if (isOutOfBounds(data, row, col))
|
||||
throw new RangeError("Row or Column value out of bounds");
|
||||
|
||||
if (!grid[row])
|
||||
grid[row] = new Array(size);
|
||||
var before = grid[row][col];
|
||||
grid[row][col] = value;
|
||||
|
||||
if (typeof changeCallback === "function")
|
||||
changeCallback(row, col, before, value);
|
||||
|
||||
modified = true;
|
||||
|
||||
return before;
|
||||
}
|
||||
this.reset = function() {
|
||||
if (modified)
|
||||
{
|
||||
grid = new Array(size);
|
||||
modified = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.isOutOfBounds = isOutOfBounds.bind(this, data);
|
||||
|
||||
Object.defineProperty(this, "size", {
|
||||
get: function() {return size; },
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function isOutOfBounds(data, row, col)
|
||||
{
|
||||
return row < 0 || row >= data.size || col < 0 || col >= data.size;
|
||||
}
|
||||
|
||||
module.exports = Grid;
|
39
package.json
39
package.json
@ -1,32 +1,37 @@
|
||||
{
|
||||
"name": "blockly-io",
|
||||
"version": "0.9.0",
|
||||
"name": "paper-io",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "An multiplayer-IO type game (cloned from Paper-IO)",
|
||||
"main": "",
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/theKidOfArcrania/Blockly-IO.git"
|
||||
"build": "rollup -c rollup.config.js -f iife | terser -c -m > public/js/bundle.js",
|
||||
"build-dev": "rollup -c rollup.config.js -f iife -o public/js/bundle.js -w"
|
||||
},
|
||||
"repository": "stevenjoezhang/paper.io",
|
||||
"keywords": [
|
||||
"Blockly-IO",
|
||||
"Paper-IO",
|
||||
"IO",
|
||||
"Game"
|
||||
],
|
||||
"author": "theKidOfArcrania",
|
||||
"author": "Mimi <stevenjoezhang@gmail.com> (https://zhangshuqiao.org)",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/theKidOfArcrania/Blockly-IO/issues"
|
||||
"url": "https://github.com/stevenjoezhang/paper.io/issues"
|
||||
},
|
||||
"homepage": "https://github.com/theKidOfArcrania/Blockly-IO#readme",
|
||||
"homepage": "https://github.com/stevenjoezhang/paper.io",
|
||||
"dependencies": {
|
||||
"finalhandler": "^1.0.0",
|
||||
"serve-static": "^1.11.2",
|
||||
"socket.io": "^1.7.3",
|
||||
"socket.io-client": "^1.7.3",
|
||||
"socket.io-proxy": "^1.0.3"
|
||||
"@fortawesome/fontawesome-free": "^6.2.0",
|
||||
"jquery": "^3.6.1",
|
||||
"mimi-server": "^0.0.1",
|
||||
"socket.io": "^4.5.3",
|
||||
"socket.io-client": "^4.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"rollup": "^3.3.0",
|
||||
"terser": "^5.15.1"
|
||||
}
|
||||
}
|
||||
|
216
paper-io-bot.js
Normal file
216
paper-io-bot.js
Normal file
@ -0,0 +1,216 @@
|
||||
if (process.argv.length < 3) {
|
||||
console.log("Usage: node paper-io-bot.js <socket-url> [<name>]")
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import io from "socket.io-client";
|
||||
import * as client from "./src/game-client.js";
|
||||
import { consts } from "./config.js";
|
||||
|
||||
const MOVES = [[-1, 0], [0, 1], [1, 0], [0, -1]];
|
||||
|
||||
let startFrame = -1;
|
||||
let endFrame = -1;
|
||||
let grid;
|
||||
let others;
|
||||
let user;
|
||||
const playerPortion = {};
|
||||
let claim = [];
|
||||
|
||||
function mod(x) {
|
||||
x %= 4;
|
||||
if (x < 0) x += 4;
|
||||
return x;
|
||||
}
|
||||
|
||||
function connect() {
|
||||
const prefixes = consts.PREFIXES.split(" ");
|
||||
const names = consts.NAMES.split(" ");
|
||||
const name = process.argv[3] || [prefixes[Math.floor(Math.random() * prefixes.length)], names[Math.floor(Math.random() * names.length)]].join(" ");
|
||||
client.connectGame(io, process.argv[2], "[BOT] " + name, function(success, msg) {
|
||||
if (!success) {
|
||||
console.error(msg);
|
||||
setTimeout(connect, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function Loc(row, col) {
|
||||
if (this.constructor != Loc) return new Loc(row, col);
|
||||
|
||||
this.row = row;
|
||||
this.col = col;
|
||||
}
|
||||
|
||||
function update(frame) {
|
||||
if (startFrame == -1) startFrame = frame;
|
||||
endFrame = frame;
|
||||
|
||||
if (frame % 6 == (startFrame + 1) % 6) {
|
||||
grid = client.grid;
|
||||
others = client.getOthers();
|
||||
|
||||
//Note: the code below isn't really my own code. This code is in fact the
|
||||
//approximate algorithm used by the paper.io game. It has been modified from
|
||||
//the original code (i.e. deobfuscating) and made more efficient in some
|
||||
//areas (and some tweaks), otherwise, the original logic is about the same.
|
||||
const row = user.row;
|
||||
|
||||
const col = user.col;
|
||||
let dir = user.currentHeading;
|
||||
const thres = (.05 + .1 * Math.random()) * consts.GRID_COUNT * consts.GRID_COUNT;
|
||||
|
||||
if (row < 0 || col < 0 || row >= consts.GRID_COUNT || col >= consts.GRID_COUNT) return;
|
||||
|
||||
if (grid.get(row, col) === user) {
|
||||
//When we are inside our territory
|
||||
claim = [];
|
||||
const weights = [25, 25, 25, 25];
|
||||
weights[dir] = 100;
|
||||
weights[mod(dir + 2)] = -9999;
|
||||
|
||||
for (var nd = 0; nd < 4; nd++) {
|
||||
for (var S = 1; S < 20; S++) {
|
||||
var nr = MOVES[nd][0] * S + row;
|
||||
var nc = MOVES[nd][1] * S + col;
|
||||
|
||||
if (nr < 0 || nc < 0 || nr >= consts.GRID_COUNT || nc >= consts.GRID_COUNT) {
|
||||
if (S > 1) weights[nd]--;
|
||||
else weights[nd] = -9999;
|
||||
}
|
||||
else {
|
||||
if (grid.get(nr, nc) !== user) weights[nd]--;
|
||||
|
||||
var tailed = undefined;
|
||||
for (var o of others) {
|
||||
if (o.tail.hitsTail(new Loc(nr, nc))) {
|
||||
tailed = o;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tailed) {
|
||||
if (o.name.indexOf("PAPER") != -1) weights[nd] += 3 * (30 - S); //Don't really try to kill our own kind
|
||||
else weights[nd] += 30 * (30 - S);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//View a selection of choices based on the weights we computed
|
||||
var choices = [];
|
||||
for (var d = 0; d < 4; d++) {
|
||||
for (var S = 1; S < weights[d]; S++) {
|
||||
choices.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
if (choices.length === 0) choices.push(dir);
|
||||
dir = choices[Math.floor(Math.random() * choices.length)];
|
||||
}
|
||||
else if (playerPortion[user.num] < thres) {
|
||||
//Claim some land if we are relatively tiny and have little to risk.
|
||||
if (claim.length === 0) {
|
||||
const breadth = 4 * Math.random() + 2;
|
||||
const length = 4 * Math.random() + 2;
|
||||
const ccw = 2 * Math.floor(2 * Math.random()) - 1;
|
||||
|
||||
const turns = [dir, mod(dir + ccw), mod(dir + ccw * 2), mod(dir + ccw * 3)];
|
||||
const lengths = [breadth, length, breadth + 2 * Math.random() + 1, length];
|
||||
|
||||
for (let i = 0; i < turns.length; i++) {
|
||||
for (let j = 0; j < lengths[i]; j++) {
|
||||
claim.push(turns[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (claim.length !== 0) dir = claim.shift();
|
||||
}
|
||||
else {
|
||||
claim = [];
|
||||
//We are playing a little bit more cautious when we are outside and have a
|
||||
//lot of land
|
||||
const weights = [5, 5, 5, 5];
|
||||
weights[dir] = 50;
|
||||
weights[mod(dir + 2)] = -9999;
|
||||
|
||||
for (var nd = 0; nd < 4; nd++) {
|
||||
for (var S = 1; S < 20; S++) {
|
||||
var nr = MOVES[nd][0] * S + row;
|
||||
var nc = MOVES[nd][1] * S + col;
|
||||
|
||||
if (nr < 0 || nc < 0 || nr >= consts.GRID_COUNT || nc >= consts.GRID_COUNT) {
|
||||
if (S > 1) weights[nd]--;
|
||||
else weights[nd] = -9999;
|
||||
}
|
||||
else {
|
||||
if (user.tail.hitsTail(new Loc(nr, nc))) {
|
||||
if (S > 1) weights[nd] -= 50 - S;
|
||||
else weights[nd] = -9999;
|
||||
}
|
||||
|
||||
if (grid.get(nr, nc) === user) weights[nd] += 10 + S;
|
||||
|
||||
var tailed = undefined;
|
||||
for (var o of others) {
|
||||
if (o.tail.hitsTail(new Loc(nr, nc))) {
|
||||
tailed = o;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tailed) {
|
||||
if (o.name.indexOf("PAPER") != -1) weights[nd] += 3 * (30 - S); //Don't really try to kill our own kind
|
||||
else weights[nd] += 30 * (30 - S);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//View a selection of choices based on the weights we computed
|
||||
var choices = [];
|
||||
for (var d = 0; d < 4; d++) {
|
||||
for (var S = 1; S < weights[d]; S++) {
|
||||
choices.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
if (choices.length === 0) choices.push(dir);
|
||||
dir = choices[Math.floor(Math.random() * choices.length)];
|
||||
}
|
||||
client.changeHeading(dir);
|
||||
}
|
||||
}
|
||||
|
||||
function calcFavorability(params) {
|
||||
return params.portion + params.kills * 50 + params.survival / 100;
|
||||
}
|
||||
|
||||
client.setAllowAnimation(false);
|
||||
client.setRenderer({
|
||||
addPlayer: function(player) {
|
||||
playerPortion[player.num] = 0;
|
||||
},
|
||||
disconnect: function() {
|
||||
const dt = (endFrame - startFrame);
|
||||
startFrame = -1;
|
||||
|
||||
console.log(`[${new Date()}] I died... (survived for ${dt} frames.)`);
|
||||
console.log(`[${new Date()}] I killed ${client.getKills()} player(s).`);
|
||||
setTimeout(connect, 5000);
|
||||
},
|
||||
removePlayer: function(player) {
|
||||
delete playerPortion[player.num];
|
||||
},
|
||||
setUser: function(u) {
|
||||
user = u;
|
||||
},
|
||||
update,
|
||||
updateGrid: function(row, col, before, after) {
|
||||
before && playerPortion[before.num]--;
|
||||
after && playerPortion[after.num]++;
|
||||
}
|
||||
});
|
||||
|
||||
connect();
|
502
player.js
502
player.js
@ -1,502 +0,0 @@
|
||||
var Stack = require("./stack.js");
|
||||
var Color = require("./color.js");
|
||||
var Grid = require("./grid.js");
|
||||
var consts = require("./game-consts.js");
|
||||
|
||||
var GRID_SIZE = consts.GRID_SIZE;
|
||||
var CELL_WIDTH = consts.CELL_WIDTH;
|
||||
var NEW_PLAYER_LAG = 60; //wait for a second at least.
|
||||
|
||||
function defineGetter(getter) {
|
||||
return {
|
||||
get: getter,
|
||||
enumerable: true
|
||||
};
|
||||
}
|
||||
|
||||
function defineInstanceMethods(thisobj, data /*, methods...*/)
|
||||
{
|
||||
for (var i = 2; i < arguments.length; i++)
|
||||
thisobj[arguments[i].name] = arguments[i].bind(this, data);
|
||||
}
|
||||
|
||||
function defineAccessorProperties(thisobj, data /*, names...*/)
|
||||
{
|
||||
var descript = {};
|
||||
function getAt(name) { return function() {return data[name] } }
|
||||
for (var i = 2; i < arguments.length; i++)
|
||||
descript[arguments[i]] = defineGetter(getAt(arguments[i]));
|
||||
Object.defineProperties(thisobj, descript);
|
||||
}
|
||||
|
||||
function TailMove(orientation)
|
||||
{
|
||||
this.move = 1;
|
||||
Object.defineProperty(this, "orientation", {
|
||||
value: orientation,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
function Tail(player, sdata)
|
||||
{
|
||||
var data = {
|
||||
tail: [],
|
||||
tailGrid: [],
|
||||
prev: null,
|
||||
startRow: 0,
|
||||
startCol: 0,
|
||||
prevRow: 0,
|
||||
prevCol: 0,
|
||||
player: player
|
||||
};
|
||||
|
||||
if (sdata)
|
||||
{
|
||||
data.startRow = data.prevRow = sdata.startRow || 0;
|
||||
data.startCol = data.prevCol = sdata.startCol || 0;
|
||||
sdata.tail.forEach(function(val) {
|
||||
addTail(data, val.orientation, val.move);
|
||||
});
|
||||
}
|
||||
data.grid = player.grid;
|
||||
|
||||
defineInstanceMethods(this, data, addTail, hitsTail, fillTail, renderTail, reposition, serialData);
|
||||
Object.defineProperty(this, "moves", {
|
||||
get: function() {return data.tail.slice(0);},
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
//Instance methods.
|
||||
function serialData(data) {
|
||||
return {
|
||||
tail: data.tail,
|
||||
startRow: data.startRow,
|
||||
startCol: data.startCol
|
||||
};
|
||||
}
|
||||
|
||||
function setTailGrid(data, tailGrid, r, c)
|
||||
{
|
||||
if (!tailGrid[r])
|
||||
tailGrid[r] = [];
|
||||
tailGrid[r][c] = true;
|
||||
}
|
||||
|
||||
function addTail(data, orientation, count)
|
||||
{
|
||||
if (count === undefined)
|
||||
count = 1;
|
||||
if (!count || count < 0)
|
||||
return;
|
||||
|
||||
var prev = data.prev;
|
||||
var r = data.prevRow, c = data.prevCol;
|
||||
if (data.tail.length === 0)
|
||||
setTailGrid(data, data.tailGrid, r, c);
|
||||
|
||||
if (!prev || prev.orientation !== orientation)
|
||||
{
|
||||
prev = data.prev = new TailMove(orientation);
|
||||
data.tail.push(prev);
|
||||
prev.move += count - 1;
|
||||
}
|
||||
else
|
||||
prev.move += count;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var pos = walk([data.prevRow, data.prevCol], null, orientation, 1);
|
||||
data.prevRow = pos[0];
|
||||
data.prevCol = pos[1];
|
||||
setTailGrid(data, data.tailGrid, pos[0], pos[1]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function reposition(data, row, col)
|
||||
{
|
||||
data.prevRow = data.startRow = row;
|
||||
data.prevCol = data.startCol = col;
|
||||
data.prev = null;
|
||||
if (data.tail.length === 0)
|
||||
return;
|
||||
else
|
||||
{
|
||||
var ret = data.tail;
|
||||
data.tail = [];
|
||||
data.tailGrid = [];
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
function render2(data, ctx)
|
||||
{
|
||||
ctx.fillStyle = data.player.tailColor.rgbString();
|
||||
for (var r = 0; r < data.tailGrid.length; r++)
|
||||
{
|
||||
if (!data.tailGrid[r])
|
||||
continue;
|
||||
for (var c = 0; c < data.tailGrid[r].length; c++)
|
||||
if (data.tailGrid[r][c])
|
||||
ctx.fillRect(c * CELL_WIDTH, r * CELL_WIDTH, CELL_WIDTH, CELL_WIDTH);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
//Helper methods.
|
||||
function renderTail(data, ctx)
|
||||
{
|
||||
if (data.tail.length === 0)
|
||||
return;
|
||||
|
||||
ctx.fillStyle = data.player.tailColor.rgbString();
|
||||
|
||||
var prevOrient = -1;
|
||||
var start = [data.startRow, data.startCol];
|
||||
|
||||
//fillTailRect(ctx, start, start);
|
||||
data.tail.forEach(function(tail) {
|
||||
var negDir = tail.orientation === 0 || tail.orientation === 3;
|
||||
|
||||
var back = start;
|
||||
if (!negDir)
|
||||
start = walk(start, null, tail.orientation, 1);
|
||||
var finish = walk(start, null, tail.orientation, tail.move - 1);
|
||||
|
||||
if (tail.move > 1)
|
||||
fillTailRect(ctx, start, finish);
|
||||
if (prevOrient !== -1)
|
||||
//Draw folding triangle.
|
||||
renderCorner(ctx, back, prevOrient, tail.orientation);
|
||||
|
||||
start = finish;
|
||||
if (negDir)
|
||||
walk(start, start, tail.orientation, 1);
|
||||
prevOrient = tail.orientation;
|
||||
});
|
||||
|
||||
var curOrient = data.player.currentHeading;
|
||||
if (prevOrient === curOrient)
|
||||
{
|
||||
fillTailRect(ctx, start, start);
|
||||
}
|
||||
else
|
||||
renderCorner(ctx, start, prevOrient, curOrient);
|
||||
}
|
||||
|
||||
function renderCorner(ctx, cornerStart, dir1, dir2)
|
||||
{
|
||||
if (dir1 === 0 || dir2 === 0)
|
||||
walk(cornerStart, cornerStart, 2, 1);
|
||||
if (dir1 === 3 || dir2 === 3)
|
||||
walk(cornerStart, cornerStart, 1, 1);
|
||||
|
||||
var a = walk(cornerStart, null, dir2, 1);
|
||||
var b = walk(a, null, dir1, 1);
|
||||
|
||||
var triangle = new Path2D();
|
||||
triangle.moveTo(cornerStart[1] * CELL_WIDTH, cornerStart[0] * CELL_WIDTH);
|
||||
triangle.lineTo(a[1] * CELL_WIDTH, a[0] * CELL_WIDTH);
|
||||
triangle.lineTo(b[1] * CELL_WIDTH, b[0] * CELL_WIDTH);
|
||||
triangle.closePath();
|
||||
for (var i = 0; i < 2; i++)
|
||||
ctx.fill(triangle);
|
||||
}
|
||||
|
||||
function walk(from, ret, orient, dist)
|
||||
{
|
||||
ret = ret || [];
|
||||
ret[0] = from[0];
|
||||
ret[1] = from[1];
|
||||
switch (orient)
|
||||
{
|
||||
case 0: ret[0] -= dist; break; //UP
|
||||
case 1: ret[1] += dist; break; //RIGHT
|
||||
case 2: ret[0] += dist; break; //DOWN
|
||||
case 3: ret[1] -= dist; break; //LEFT
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function fillTailRect(ctx, start, end)
|
||||
{
|
||||
var x = start[1] * CELL_WIDTH;
|
||||
var y = start[0] * CELL_WIDTH;
|
||||
var width = (end[1] - start[1]) * CELL_WIDTH;
|
||||
var height = (end[0] - start[0]) * CELL_WIDTH;
|
||||
|
||||
if (width === 0)
|
||||
width += CELL_WIDTH;
|
||||
if (height === 0)
|
||||
height += CELL_WIDTH;
|
||||
|
||||
if (width < 0)
|
||||
{
|
||||
x += width;
|
||||
width = -width;
|
||||
}
|
||||
if (height < 0)
|
||||
{
|
||||
y += height;
|
||||
height = -height;
|
||||
}
|
||||
ctx.fillRect(x, y, width, height);
|
||||
}
|
||||
|
||||
function fillTail(data)
|
||||
{
|
||||
if (data.tail.length === 0)
|
||||
return;
|
||||
|
||||
function onTail(c) { return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]]; }
|
||||
|
||||
var grid = data.grid;
|
||||
var start = [data.startRow, data.startCol];
|
||||
var been = new Grid(grid.size);
|
||||
var coords = [];
|
||||
|
||||
coords.push(start);
|
||||
while (coords.length > 0) //BFS for all tail spaces.
|
||||
{
|
||||
var coord = coords.shift();
|
||||
var r = coord[0];
|
||||
var c = coord[1];
|
||||
|
||||
if (grid.isOutOfBounds(r, c))
|
||||
continue;
|
||||
|
||||
if (been.get(r, c))
|
||||
continue;
|
||||
|
||||
if (onTail(coord)) //on the tail.
|
||||
{
|
||||
been.set(r, c, true);
|
||||
grid.set(r, c, data.player);
|
||||
|
||||
//Find all spots that this tail encloses.
|
||||
floodFill(data, grid, r + 1, c, been);
|
||||
floodFill(data, grid, r - 1, c, been);
|
||||
floodFill(data, grid, r, c + 1, been);
|
||||
floodFill(data, grid, r, c - 1, been);
|
||||
|
||||
coords.push([r + 1, c]);
|
||||
coords.push([r - 1, c]);
|
||||
coords.push([r, c + 1]);
|
||||
coords.push([r, c - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function floodFill(data, grid, row, col, been)
|
||||
{
|
||||
function onTail(c) { return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]]; }
|
||||
|
||||
var start = [row, col];
|
||||
if (grid.isOutOfBounds(row, col) || been.get(row, col) || onTail(start) || grid.get(row, col) === data.player)
|
||||
return; //Avoid allocating too many resources.
|
||||
|
||||
var coords = [];
|
||||
var filled = new Stack(GRID_SIZE * GRID_SIZE + 1);
|
||||
var surrounded = true;
|
||||
|
||||
coords.push(start);
|
||||
while (coords.length > 0)
|
||||
{
|
||||
var coord = coords.shift();
|
||||
var r = coord[0];
|
||||
var c = coord[1];
|
||||
|
||||
if (grid.isOutOfBounds(r, c))
|
||||
{
|
||||
surrounded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
//End this traverse on boundaries (where we been, on the tail, and when we enter our territory)
|
||||
if (been.get(r, c) || onTail(coord) || grid.get(r, c) === data.player)
|
||||
continue;
|
||||
|
||||
been.set(r, c, true);
|
||||
|
||||
if (surrounded)
|
||||
filled.push(coord);
|
||||
|
||||
coords.push([r + 1, c]);
|
||||
coords.push([r - 1, c]);
|
||||
coords.push([r, c + 1]);
|
||||
coords.push([r, c - 1]);
|
||||
}
|
||||
if (surrounded)
|
||||
{
|
||||
while (!filled.isEmpty())
|
||||
{
|
||||
coord = filled.pop();
|
||||
grid.set(coord[0], coord[1], data.player);
|
||||
}
|
||||
}
|
||||
|
||||
return surrounded;
|
||||
}
|
||||
|
||||
function hitsTail(data, other)
|
||||
{
|
||||
return (data.prevRow !== other.row || data.prevCol !== other.col) &&
|
||||
(data.startRow !== other.row || data.startCol !== other.col) &&
|
||||
!!(data.tailGrid[other.row] && data.tailGrid[other.row][other.col]);
|
||||
}
|
||||
|
||||
var SPEED = 5;
|
||||
var SHADOW_OFFSET = 10;
|
||||
|
||||
function Player(grid, sdata) {
|
||||
var data = {};
|
||||
|
||||
//Parameters
|
||||
data.num = sdata.num;
|
||||
data.name = sdata.name || ""; //|| "Player " + (data.num + 1);
|
||||
data.grid = grid;
|
||||
data.posX = sdata.posX;
|
||||
data.posY = sdata.posY;
|
||||
this.heading = data.currentHeading = sdata.currentHeading; //0 is up, 1 is right, 2 is down, 3 is left.
|
||||
data.waitLag = sdata.waitLag || 0;
|
||||
data.dead = false;
|
||||
|
||||
//Only need colors for client side.
|
||||
var base;
|
||||
if (sdata.base)
|
||||
base = this.baseColor = sdata.base instanceof Color ? sdata.base : Color.fromData(sdata.base);
|
||||
else
|
||||
{
|
||||
var hue = Math.random();
|
||||
this.baseColor = base = new Color(hue, .8, .5);
|
||||
}
|
||||
this.shadowColor = base.deriveLumination(-.3);
|
||||
this.tailColor = base.deriveLumination(.2).deriveAlpha(.5);
|
||||
|
||||
//Tail requires special handling.
|
||||
this.grid = grid; //Temporary
|
||||
if (sdata.tail)
|
||||
data.tail = new Tail(this, sdata.tail);
|
||||
else
|
||||
{
|
||||
data.tail = new Tail(this);
|
||||
data.tail.reposition(calcRow(data), calcCol(data));
|
||||
}
|
||||
|
||||
//Instance methods.
|
||||
this.move = move.bind(this, data);
|
||||
this.die = function() { data.dead = true;};
|
||||
this.serialData = function() {
|
||||
return {
|
||||
base: this.baseColor,
|
||||
num: data.num,
|
||||
name: data.name,
|
||||
posX: data.posX,
|
||||
posY: data.posY,
|
||||
currentHeading: data.currentHeading,
|
||||
tail: data.tail.serialData(),
|
||||
waitLag: data.waitLag
|
||||
};
|
||||
};
|
||||
|
||||
//Read-only Properties.
|
||||
defineAccessorProperties(this, data, "currentHeading", "dead", "name", "num", "posX", "posY", "grid", "tail", "waitLag");
|
||||
Object.defineProperties(this, {
|
||||
row: defineGetter(function() { return calcRow(data); }),
|
||||
col: defineGetter(function() { return calcCol(data); })
|
||||
});
|
||||
}
|
||||
|
||||
//Gets the next integer in positive or negative direction.
|
||||
function nearestInteger(positive, val)
|
||||
{
|
||||
return positive ? Math.ceil(val) : Math.floor(val);
|
||||
}
|
||||
|
||||
function calcRow(data)
|
||||
{
|
||||
return nearestInteger(data.currentHeading === 2 /*DOWN*/, data.posY / CELL_WIDTH);
|
||||
}
|
||||
|
||||
function calcCol(data)
|
||||
{
|
||||
return nearestInteger(data.currentHeading === 1 /*RIGHT*/, data.posX / CELL_WIDTH);
|
||||
}
|
||||
|
||||
//Instance methods
|
||||
Player.prototype.render = function(ctx, fade)
|
||||
{
|
||||
//Render tail.
|
||||
this.tail.renderTail(ctx);
|
||||
|
||||
//Render player.
|
||||
fade = fade || 1;
|
||||
ctx.fillStyle = this.shadowColor.deriveAlpha(fade).rgbString();
|
||||
ctx.fillRect(this.posX, this.posY, CELL_WIDTH, CELL_WIDTH);
|
||||
|
||||
var mid = CELL_WIDTH / 2;
|
||||
var grd = ctx.createRadialGradient(this.posX + mid, this.posY + mid - SHADOW_OFFSET, 1,
|
||||
this.posX + mid, this.posY + mid - SHADOW_OFFSET, CELL_WIDTH);
|
||||
grd.addColorStop(0, this.baseColor.deriveAlpha(fade).rgbString());
|
||||
grd.addColorStop(1, new Color(0, 0, 1, fade).rgbString());
|
||||
ctx.fillStyle = grd;
|
||||
ctx.fillRect(this.posX - 1, this.posY - SHADOW_OFFSET, CELL_WIDTH + 2, CELL_WIDTH);
|
||||
|
||||
//Render name
|
||||
ctx.fillStyle = this.shadowColor.deriveAlpha(fade).rgbString();
|
||||
ctx.textAlign = "center";
|
||||
|
||||
var yoff = -SHADOW_OFFSET * 2;
|
||||
if (this.row === 0)
|
||||
yoff = SHADOW_OFFSET * 2 + CELL_WIDTH;
|
||||
ctx.font = "18px Changa";
|
||||
ctx.fillText(this.name, this.posX + CELL_WIDTH / 2, this.posY + yoff);
|
||||
};
|
||||
|
||||
|
||||
function move(data)
|
||||
{
|
||||
if (data.waitLag < NEW_PLAYER_LAG)
|
||||
{
|
||||
data.waitLag++;
|
||||
return;
|
||||
}
|
||||
|
||||
//Move to new position.
|
||||
var heading = this.heading;
|
||||
if (this.posX % CELL_WIDTH !== 0 || this.posY % CELL_WIDTH !== 0)
|
||||
heading = data.currentHeading;
|
||||
else
|
||||
data.currentHeading = heading;
|
||||
switch (heading)
|
||||
{
|
||||
case 0: data.posY -= SPEED; break; //UP
|
||||
case 1: data.posX += SPEED; break; //RIGHT
|
||||
case 2: data.posY += SPEED; break; //DOWN
|
||||
case 3: data.posX -= SPEED; break; //LEFT
|
||||
}
|
||||
|
||||
//Check for out of bounds.
|
||||
var row = this.row, col = this.col;
|
||||
if (data.grid.isOutOfBounds(row, col))
|
||||
{
|
||||
data.dead = true;
|
||||
return;
|
||||
}
|
||||
|
||||
//Update tail position.
|
||||
if (data.grid.get(row, col) === this)
|
||||
{
|
||||
//Safe zone!
|
||||
this.tail.fillTail();
|
||||
this.tail.reposition(row, col);
|
||||
}
|
||||
//If we are completely in a new cell (not in our safe zone), we add to the tail.
|
||||
else if (this.posX % CELL_WIDTH === 0 && this.posY % CELL_WIDTH === 0)
|
||||
this.tail.addTail(heading);
|
||||
}
|
||||
|
||||
module.exports = Player;
|
@ -1,2 +0,0 @@
|
||||
//code.stephenmorley.org
|
||||
function Queue(){var a=[],b=0;this.getLength=function(){return a.length-b};this.isEmpty=function(){return 0==a.length};this.enqueue=function(b){a.push(b)};this.dequeue=function(){if(0!=a.length){var c=a[b];2*++b>=a.length&&(a=a.slice(b),b=0);return c}};this.peek=function(){return 0<a.length?a[b]:void 0}};
|
9654
public/bundle.js
9654
public/bundle.js
File diff suppressed because it is too large
Load Diff
@ -1,24 +0,0 @@
|
||||
/* arabic */
|
||||
@font-face {
|
||||
font-family: 'Changa';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Changa SemiBold'), local('Changa-SemiBold'), url(8-mw6umTgtMSI5PXqVIDMRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
|
||||
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+FB50-FDFF, U+FE80-FEFC;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Changa';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Changa SemiBold'), local('Changa-SemiBold'), url(l9R1mHXzJ6oxjfw8BkQIThJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Changa';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Changa SemiBold'), local('Changa-SemiBold'), url(aXdbZDB08TCIBRKa7Qgu_FtXRa8TVwTICgirnJhmVJw.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
|
||||
}
|
165
public/css/styles.css
Normal file
165
public/css/styles.css
Normal file
@ -0,0 +1,165 @@
|
||||
/* arabic */
|
||||
@font-face {
|
||||
font-family: "Changa";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local("Changa SemiBold"), local("Changa-SemiBold"), url(8-mw6umTgtMSI5PXqVIDMRJtnKITppOI_IvcXXDNrsc.woff2) format("woff2");
|
||||
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+FB50-FDFF, U+FE80-FEFC;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Changa";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local("Changa SemiBold"), local("Changa-SemiBold"), url(l9R1mHXzJ6oxjfw8BkQIThJtnKITppOI_IvcXXDNrsc.woff2) format("woff2");
|
||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Changa";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local("Changa SemiBold"), local("Changa-SemiBold"), url(aXdbZDB08TCIBRKa7Qgu_FtXRa8TVwTICgirnJhmVJw.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
|
||||
}
|
||||
body, html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
font-family: "Changa", "Sans Serif";
|
||||
user-select: none;
|
||||
}
|
||||
canvas {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#begin, #wasted {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, .8);
|
||||
}
|
||||
#github svg {
|
||||
transition: all 1s;
|
||||
fill: #222;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 0;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
#github:hover svg {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.center img {
|
||||
display: block;
|
||||
}
|
||||
.scale {
|
||||
animation: scale 5s infinite ease-in-out;
|
||||
}
|
||||
@keyframes scale {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
input, button {
|
||||
font-family: "Changa", "Sans Serif";
|
||||
font-size: 1.5rem;
|
||||
outline: none;
|
||||
border: 0;
|
||||
padding: .5rem 1rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
button {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:active {
|
||||
transform: translateY(4px);
|
||||
}
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
#name, #settings {
|
||||
background: #ededd1;
|
||||
border-bottom: 6px solid #a1a18d;
|
||||
}
|
||||
.yellow {
|
||||
background: #eaec4b;
|
||||
border-bottom: 6px solid #a1a130;
|
||||
color: #888a34;
|
||||
}
|
||||
.yellow:hover {
|
||||
background: #fafc5b;
|
||||
}
|
||||
.yellow:active {
|
||||
border-bottom: 2px solid #a1a130;
|
||||
}
|
||||
.orange {
|
||||
background: #FF972F;
|
||||
border-bottom: 6px solid #AE4E0D;
|
||||
color: #422100;
|
||||
}
|
||||
.orange:hover {
|
||||
background: #FFB76F;
|
||||
}
|
||||
.orange:active {
|
||||
border-bottom: 2px solid #AE4E0D;
|
||||
}
|
||||
.green {
|
||||
background: #7fed4c;
|
||||
border-bottom: 6px solid #56a130;
|
||||
color: #4f8a34;
|
||||
}
|
||||
.green:hover {
|
||||
background: #8efc5b;
|
||||
}
|
||||
.green:active {
|
||||
border-bottom: 2px solid #56a130;
|
||||
}
|
||||
span {
|
||||
color: red;
|
||||
}
|
||||
#wasted {
|
||||
display: none;
|
||||
}
|
||||
#wasted img {
|
||||
width: 650px;
|
||||
}
|
||||
.toggle {
|
||||
position: fixed;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
}
|
||||
#settings {
|
||||
display: grid;
|
||||
padding: 20px;
|
||||
}
|
||||
#settings > * {
|
||||
margin-top: 10px;
|
||||
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
public/image/logo.png
Normal file
BIN
public/image/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 367 KiB |
1
public/image/wasted.svg
Normal file
1
public/image/wasted.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 18 KiB |
@ -1,25 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="changa.css" rel="stylesheet">
|
||||
<link href="styles.css" rel="stylesheet">
|
||||
<script src="jquery.min.js"></script>
|
||||
<script src="bundle.js"></script>
|
||||
<title>Blockly.IO</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="user-scalable=no, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-touch-fullscreen" content="yes">
|
||||
<title>Paper CATS</title>
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
<link rel="stylesheet" href="/font/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="main-ui"></canvas>
|
||||
<div id="stats" style="opacity: 0" class="fullscreen hidden"></div>
|
||||
<div id="begin" style="opacity: .99999" class="fullscreen">
|
||||
<canvas id="main-ui"></canvas>
|
||||
<div id="begin">
|
||||
<a id="github" href="https://git.sdf.org/ilikecats/papercats" target="_blank" title="Visit the code on SDF Git!">
|
||||
<svg style="transform: rotate(90deg);" version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" style="enable-background:new 0 0 640 640;" xml:space="preserve" viewBox="5.67 143.05 628.65 387.55"> <g> <path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8 c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4 c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/> <g> <g> <path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2 c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5 c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5 c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3 c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1 C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4 c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7 S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55 c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8 l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"/> <path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4 c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1 c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9 c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3 c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3 c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29 c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8 C343.2,346.5,335,363.3,326.8,380.1z"/> </g> </g> </g> </svg>
|
||||
</a>
|
||||
<div class="center">
|
||||
<h1>Blockly.IO!</h1>
|
||||
<small>Todo: replace ^^^ with a picture. Work in progress.</small><br>
|
||||
|
||||
<img class="scale" src="image/logo.png">
|
||||
<h1>Enter your name</h1>
|
||||
<input autocomplete="off" id="name" placeholder="An awesome name!">
|
||||
<button type="submit">Play!</button>
|
||||
<input id="name" autocomplete="off" placeholder="An awesome name!">
|
||||
<button class="start yellow" disabled="disabled">Play!</button>
|
||||
<br>
|
||||
<br>
|
||||
<button id="mode" class="orange" disabled="disabled">Mode</button>
|
||||
<button id="skins" class="yellow" disabled="disabled">Skins</button>
|
||||
<button class="spectate green" disabled="disabled">Spectate</button>
|
||||
<div>
|
||||
<small id="error">Your browser does not support JavaScript!</small>
|
||||
<span id="error">Please wait... Maybe something's wrong!</span>
|
||||
<noscript>
|
||||
<span>Your browser does not support JavaScript!</span>
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wasted">
|
||||
<div class="center">
|
||||
<img class="scale" src="image/wasted.svg">
|
||||
<button class="start orange" disabled="disabled">Play Again</button>
|
||||
<button class="menu yellow">Main Menu</button>
|
||||
<button class="spectate green" disabled="disabled">Spectate</button>
|
||||
</div>
|
||||
<small>Visit the open-source code on <a href="https://github.com/theKidOfArcrania/PaperIO-Web">GitHub</a>!</small>
|
||||
</div>
|
||||
<button class="toggle yellow"><i class="fa fa-cogs"></i></button>
|
||||
<div id="settings" class="center" style="display: none;">
|
||||
<button class="menu yellow">Main Menu</button>
|
||||
<button class="yellow" onclick="JavaScript:alert('Want to play a game?\nUse arrows or WSAD to control your paper block\nDon\'t hit the walls\nBite enemy tails but don\'t let them bite yours!\nCompete against other players')">Hou To Play</button>
|
||||
<button class="yellow" onclick="JavaScript:alert('Paper.io play online\nMore! More! More territory! Take it all with new amazing game - Paper.io\nThis game concept is linked to old Xonix, which appeared in 1984. Like in any other IO game there are you and enemies willing to outwit you. But you\'ll prevail of course :) Take their terriory and destroy your enemies, but be careful, your tail is your weak point.\nThis game is very cool and has nice paper-like graphics and fluid animation. Enjoy!\nYou can play paper.io online and offline both on a mobile device and a desktop computer. Get paper.io and join the world gaming community. Manage a small board and win territory from your rivals.\nPaper.io 2 - behold the sequel to the popular game. Capture new territories and become the king of the map!\nThe more space you win the higher ranking and scores you get. You have to act and think quickly. Develop your own strategy and action plan.\nPaperio has simple rules but is very addictive in its simplicity. The competitors are also on guard. Watch out for your tail.\nThe game is very addictive and makes gamers from all over the world connect from home, from work, from campus or even the office!\nJoin the game, figure out your strategy and become the ultimate winner. Have fun!')">About</button>
|
||||
<audio loop="loop" preload="auto" controls>
|
||||
<source src="https://ilikecats.sdf.org/UNTALE/43%20Temmie%20Village.flac" type="audio/flac">
|
||||
</audio>
|
||||
</div>
|
||||
<script src="js/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
4
public/jquery.min.js
vendored
4
public/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
0
public/js/.gitkeep
Normal file
0
public/js/.gitkeep
Normal file
BIN
public/music/gwent.mp3
Executable file
BIN
public/music/gwent.mp3
Executable file
Binary file not shown.
BIN
public/music/gwent.ogg
Executable file
BIN
public/music/gwent.ogg
Executable file
Binary file not shown.
@ -1,71 +0,0 @@
|
||||
body, html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: black;
|
||||
|
||||
color: white;
|
||||
font-family: "Changa", "Sans Serif";
|
||||
}
|
||||
#error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: black;
|
||||
}
|
||||
|
||||
.center {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: "Changa", "Sans Serif";
|
||||
font-size: 24px;
|
||||
padding: .5rem 1rem;
|
||||
border: 4px solid lightgray;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
button {
|
||||
border-style: solid;
|
||||
border-color: #e6e699 #b8b814 #b8b814 #e6e699;
|
||||
border-width: 4px 4px 4px 4px;
|
||||
border-radius: 4px;
|
||||
background: #eded5e;
|
||||
font-family: "Changa", "Sans-serif";
|
||||
font-size: 24px;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
button:active {
|
||||
border-color: #73730d #cccc7d #cccc7d #73730d;
|
||||
background: #e8e830;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#stats
|
||||
{
|
||||
background: rgb(80, 80, 80);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
<script src="socket.io.min.js"></script>
|
||||
<script src="socket-test.js"></script>
|
27
rolling.js
27
rolling.js
@ -1,27 +0,0 @@
|
||||
|
||||
function Rolling(value, frames)
|
||||
{
|
||||
var lag = 0;
|
||||
|
||||
if (!frames)
|
||||
frames = 24;
|
||||
|
||||
this.value = value;
|
||||
|
||||
Object.defineProperty(this, "lag", {
|
||||
get: function() { return lag; },
|
||||
enumerable: true
|
||||
});
|
||||
this.update = function() {
|
||||
var delta = this.value - lag;
|
||||
var dir = Math.sign(delta);
|
||||
var speed = Math.abs(delta) / frames;
|
||||
var mag = Math.min(Math.abs(speed), Math.abs(delta));
|
||||
|
||||
lag += mag * dir;
|
||||
return lag;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Rolling;
|
||||
|
7
rollup.config.js
Normal file
7
rollup.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
|
||||
export default {
|
||||
input: 'client.js',
|
||||
plugins: [resolve(), commonjs()]
|
||||
};
|
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
75
server.js
75
server.js
@ -1,49 +1,54 @@
|
||||
//TODO: rename as "blockly.io".
|
||||
var hostname = process.argv[2] || "0.0.0.0";
|
||||
var port = parseInt(process.argv[3]) || 80;
|
||||
// https://github.com/socketio/socket.io/blob/master/examples/chat/index.js
|
||||
import MiServer from "mimi-server";
|
||||
import { Server } from "socket.io";
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import { exec, fork } from "child_process";
|
||||
import { config } from "./config.js";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
var finalhandler = require('finalhandler');
|
||||
var http = require('http');
|
||||
var serveStatic = require('serve-static');
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Serve up public/ftp folder
|
||||
var serve = serveStatic('public/', {'setHeaders': setHeaders});
|
||||
config.dev ? exec("npm run build-dev") : exec("npm run build");
|
||||
|
||||
function setHeaders(res, path) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=0');
|
||||
}
|
||||
const port = process.env.PORT || config.port;
|
||||
|
||||
// Create server
|
||||
var server = http.createServer(function onRequest (req, res) {
|
||||
serve(req, res, finalhandler(req, res));
|
||||
const { app, server } = new MiServer({
|
||||
port,
|
||||
static: path.join(__dirname, "public")
|
||||
});
|
||||
|
||||
// Listen
|
||||
server.listen(port, hostname);
|
||||
const io = new Server(server);
|
||||
|
||||
server = http.createServer();
|
||||
var io = require('socket.io')(server);
|
||||
io.set('transports', ['websocket']);
|
||||
// Routing
|
||||
app.use("/font", express.static(path.join(__dirname, "node_modules/@fortawesome/fontawesome-free")));
|
||||
|
||||
var Game = require('./game-server.js');
|
||||
var games = [new Game()];
|
||||
io.on('connection', function(socket){
|
||||
socket.on("hello", function(data, fn) {
|
||||
import Game from "./src/game-server.js";
|
||||
const game = new Game();
|
||||
|
||||
io.on("connection", socket => {
|
||||
socket.on("hello", (data, fn) => {
|
||||
//TODO: error checking.
|
||||
if (data.name && data.name.length > 32)
|
||||
fn(false, "Your name is too long!");
|
||||
else if (!games[0].addPlayer(socket, data.name))
|
||||
fn(false, "Game is too full!");
|
||||
else
|
||||
if (data.god && game.addGod(socket)) {
|
||||
fn(true);
|
||||
return;
|
||||
}
|
||||
if (data.name && data.name.length > 32) fn(false, "Your name is too long!");
|
||||
else if (!game.addPlayer(socket, data.name)) fn(false, "There're too many platers!");
|
||||
else fn(true);
|
||||
});
|
||||
socket.on("pings", (fn) => {
|
||||
socket.emit("pongs");
|
||||
socket.disconnect();
|
||||
});
|
||||
socket.on("checkConn", function(fn) { fn(); });
|
||||
});
|
||||
server.listen(8081);
|
||||
|
||||
function tick() {
|
||||
games[0].tickFrame();
|
||||
setTimeout(tick, 1000 / 60);
|
||||
setInterval(() => {
|
||||
game.tickFrame();
|
||||
}, 1000 / 60);
|
||||
|
||||
for (let i = 0; i < parseInt(config.bots); i++) {
|
||||
fork(path.join(__dirname, "paper-io-bot.js"), [`ws://localhost:${port}`], {
|
||||
stdio: "inherit"
|
||||
});
|
||||
}
|
||||
tick();
|
||||
//setTimeout(tick, 1000 / 60);
|
@ -1,13 +0,0 @@
|
||||
var ws = require("nodejs-websocket")
|
||||
|
||||
// Scream server example: "hi" -> "HI!!!"
|
||||
var server = ws.createServer(function (conn) {
|
||||
console.log("New connection")
|
||||
conn.on("text", function (str) {
|
||||
console.log("Received "+str)
|
||||
conn.sendText(str.toUpperCase()+"!!!")
|
||||
})
|
||||
conn.on("close", function (code, reason) {
|
||||
console.log("Connection closed")
|
||||
})
|
||||
}).listen(8081);
|
120
src/core/color.js
Normal file
120
src/core/color.js
Normal file
@ -0,0 +1,120 @@
|
||||
function verifyRange() {
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
if (arguments[i] < 0 || arguments[i] > 1) throw new RangeError("H, S, L, and A parameters must be between the range [0, 1]");
|
||||
}
|
||||
}
|
||||
// https://stackoverflow.com/a/9493060/7344257
|
||||
function hslToRgb(h, s, l) {
|
||||
let r, g, b;
|
||||
if (s == 0) r = g = b = l; //Achromatic
|
||||
else {
|
||||
const hue2rgb = function(p, q, t) {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
class Color {
|
||||
constructor(h, s, l, a) {
|
||||
verifyRange(h, s, l);
|
||||
if (a === undefined) a = 1;
|
||||
else verifyRange(a);
|
||||
Object.defineProperties(this, {
|
||||
"hue": {
|
||||
value: h,
|
||||
enumerable: true
|
||||
},
|
||||
"sat": {
|
||||
value: s,
|
||||
enumerable: true
|
||||
},
|
||||
"lum": {
|
||||
value: l,
|
||||
enumerable: true
|
||||
},
|
||||
"alpha": {
|
||||
value: a,
|
||||
enumerable: true
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interpolateToString(color, amount) {
|
||||
const rgbThis = hslToRgb(this.hue, this.sat, this.lum);
|
||||
const rgbThat = hslToRgb(color.hue, color.sat, color.lum);
|
||||
const rgb = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
rgb[i] = Math.floor((rgbThat[i] - rgbThis[i]) * amount + rgbThis[i]);
|
||||
}
|
||||
return {
|
||||
rgbString: function() {
|
||||
return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
deriveLumination(amount) {
|
||||
let lum = this.lum + amount;
|
||||
lum = Math.min(Math.max(lum, 0), 1);
|
||||
return new Color(this.hue, this.sat, lum, this.alpha);
|
||||
}
|
||||
|
||||
deriveHue(amount) {
|
||||
const hue = this.hue - amount;
|
||||
return new Color(hue - Math.floor(hue), this.sat, this.lum, this.alpha);
|
||||
}
|
||||
|
||||
deriveSaturation(amount) {
|
||||
let sat = this.sat + amount;
|
||||
sat = Math.min(Math.max(sat, 0), 1);
|
||||
return new Color(this.hue, sat, this.lum, this.alpha);
|
||||
}
|
||||
|
||||
deriveAlpha(newAlpha) {
|
||||
verifyRange(newAlpha);
|
||||
return new Color(this.hue, this.sat, this.lum, newAlpha);
|
||||
}
|
||||
|
||||
rgbString() {
|
||||
const rgb = hslToRgb(this.hue, this.sat, this.lum);
|
||||
rgb[3] = this.a;
|
||||
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${this.alpha})`;
|
||||
}
|
||||
}
|
||||
|
||||
Color.fromData = data => {
|
||||
return new Color(data.hue, data.sat, data.lum, data.alpha);
|
||||
};
|
||||
Color.possColors = () => {
|
||||
const SATS = [192, 150, 100].map(val => val / 240);
|
||||
const HUES = [0, 10, 20, 25, 30, 35, 40, 45, 50, 60, 70, 100, 110, 120, 125, 130, 135, 140, 145, 150, 160, 170, 180, 190, 200, 210, 220].map(val => val / 240);
|
||||
const possColors = new Array(SATS.length * HUES.length);
|
||||
let i = 0;
|
||||
for (let s = 0; s < SATS.length; s++) {
|
||||
for (let h = 0; h < HUES.length; h++) {
|
||||
possColors[i++] = new Color(HUES[h], SATS[s], .5, 1);
|
||||
}
|
||||
}
|
||||
//Shuffle the colors
|
||||
for (let i = 0; i < possColors.length * 50; i++) {
|
||||
const a = Math.floor(Math.random() * possColors.length);
|
||||
const b = Math.floor(Math.random() * possColors.length);
|
||||
const tmp = possColors[a];
|
||||
possColors[a] = possColors[b];
|
||||
possColors[b] = tmp;
|
||||
}
|
||||
return possColors;
|
||||
}
|
||||
|
||||
export default Color;
|
42
src/core/grid.js
Normal file
42
src/core/grid.js
Normal file
@ -0,0 +1,42 @@
|
||||
function Grid(size, changeCallback) {
|
||||
let grid = new Array(size);
|
||||
let modified = false;
|
||||
const data = {
|
||||
grid,
|
||||
size
|
||||
};
|
||||
|
||||
this.get = (row, col) => {
|
||||
if (isOutOfBounds(data, row, col)) throw new RangeError("Row or Column value out of bounds");
|
||||
return grid[row] && grid[row][col];
|
||||
}
|
||||
this.set = (row, col, value) => {
|
||||
if (isOutOfBounds(data, row, col)) throw new RangeError("Row or Column value out of bounds");
|
||||
if (!grid[row]) grid[row] = new Array(size);
|
||||
const before = grid[row][col];
|
||||
grid[row][col] = value;
|
||||
if (typeof changeCallback === "function") changeCallback(row, col, before, value);
|
||||
modified = true;
|
||||
return before;
|
||||
}
|
||||
this.reset = () => {
|
||||
if (modified) {
|
||||
grid = new Array(size);
|
||||
modified = false;
|
||||
}
|
||||
}
|
||||
this.isOutOfBounds = isOutOfBounds.bind(this, data);
|
||||
|
||||
Object.defineProperty(this, "size", {
|
||||
get: function() {
|
||||
return size;
|
||||
},
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
function isOutOfBounds(data, row, col) {
|
||||
return row < 0 || row >= data.size || col < 0 || col >= data.size;
|
||||
}
|
||||
|
||||
export default Grid;
|
109
src/core/index.js
Normal file
109
src/core/index.js
Normal file
@ -0,0 +1,109 @@
|
||||
import { consts } from "../../config.js";
|
||||
export { default as Color } from "./color.js";
|
||||
export { default as Grid } from "./grid.js";
|
||||
export { default as Player } from "./player.js";
|
||||
|
||||
export function initPlayer(grid, player) {
|
||||
for (let dr = -1; dr <= 1; dr++) {
|
||||
for (let dc = -1; dc <= 1; dc++) {
|
||||
if (!grid.isOutOfBounds(dr + player.row, dc + player.col)) grid.set(dr + player.row, dc + player.col, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateFrame(grid, players, dead, notifyKill) {
|
||||
let adead = [];
|
||||
if (dead instanceof Array) adead = dead;
|
||||
|
||||
//Move players
|
||||
let tmp = players.filter(val => {
|
||||
val.move();
|
||||
if (val.dead) adead.push(val);
|
||||
return !val.dead;
|
||||
});
|
||||
|
||||
//Remove players with collisions
|
||||
const removing = new Array(players.length);
|
||||
|
||||
const kill = (!notifyKill) ? () => { } : (killer, other) => {
|
||||
if (!removing[other]) notifyKill(killer, other);
|
||||
};
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
for (let j = i; j < players.length; j++) {
|
||||
|
||||
//Remove those players when other players have hit their tail
|
||||
if (!removing[j] && players[j].tail.hitsTail(players[i])) {
|
||||
kill(i, j);
|
||||
removing[j] = true;
|
||||
//console.log("TAIL");
|
||||
}
|
||||
if (!removing[i] && players[i].tail.hitsTail(players[j])) {
|
||||
kill(j, i);
|
||||
removing[i] = true;
|
||||
//console.log("TAIL");
|
||||
}
|
||||
|
||||
//Remove players with collisons...
|
||||
if (i !== j && squaresIntersect(players[i].posX, players[j].posX) &&
|
||||
squaresIntersect(players[i].posY, players[j].posY)) {
|
||||
//...if one player is own his own territory, the other is out
|
||||
if (grid.get(players[i].row, players[i].col) === players[i]) {
|
||||
kill(i, j);
|
||||
removing[j] = true;
|
||||
}
|
||||
else if (grid.get(players[j].row, players[j].col) === players[j]) {
|
||||
kill(j, i);
|
||||
removing[i] = true;
|
||||
}
|
||||
else {
|
||||
//...otherwise, the one that sustains most of the collision will be removed
|
||||
const areaI = area(players[i]);
|
||||
const areaJ = area(players[j]);
|
||||
|
||||
if (areaI === areaJ) {
|
||||
kill(i, j);
|
||||
kill(j, i);
|
||||
removing[i] = removing[j] = true;
|
||||
}
|
||||
else if (areaI > areaJ) {
|
||||
kill(j, i);
|
||||
removing[i] = true;
|
||||
}
|
||||
else {
|
||||
kill(i, j);
|
||||
removing[j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tmp = tmp.filter((val, i) => {
|
||||
if (removing[i]) {
|
||||
adead.push(val);
|
||||
val.die();
|
||||
}
|
||||
return !removing[i];
|
||||
});
|
||||
players.length = tmp.length;
|
||||
for (let i = 0; i < tmp.length; i++) {
|
||||
players[i] = tmp[i];
|
||||
}
|
||||
|
||||
//Remove dead squares
|
||||
for (let r = 0; r < grid.size; r++) {
|
||||
for (let c = 0; c < grid.size; c++) {
|
||||
if (adead.indexOf(grid.get(r, c)) !== -1) grid.set(r, c, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function squaresIntersect(a, b) {
|
||||
return (a < b) ? (b < a + consts.CELL_WIDTH) : (a < b + consts.CELL_WIDTH);
|
||||
}
|
||||
|
||||
function area(player) {
|
||||
const xDest = player.col * consts.CELL_WIDTH;
|
||||
const yDest = player.row * consts.CELL_WIDTH;
|
||||
return (player.posX === xDest) ? Math.abs(player.posY - yDest) : Math.abs(player.posX - xDest);
|
||||
}
|
424
src/core/player.js
Normal file
424
src/core/player.js
Normal file
@ -0,0 +1,424 @@
|
||||
import Stack from "./stack.js";
|
||||
import Color from "./color.js";
|
||||
import Grid from "./grid.js";
|
||||
import { consts } from "../../config.js";
|
||||
|
||||
function defineGetter(getter) {
|
||||
return {
|
||||
get: getter,
|
||||
enumerable: true
|
||||
};
|
||||
}
|
||||
|
||||
function defineInstanceMethods(thisobj, data /*, methods...*/) {
|
||||
for (let i = 2; i < arguments.length; i++) {
|
||||
thisobj[arguments[i].name] = arguments[i].bind(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
function defineAccessorProperties(thisobj, data /*, names...*/) {
|
||||
const descript = {};
|
||||
function getAt(name) {
|
||||
return () => {
|
||||
return data[name];
|
||||
};
|
||||
}
|
||||
for (let i = 2; i < arguments.length; i++) {
|
||||
descript[arguments[i]] = defineGetter(getAt(arguments[i]));
|
||||
}
|
||||
Object.defineProperties(thisobj, descript);
|
||||
}
|
||||
|
||||
function TailMove(orientation) {
|
||||
this.move = 1;
|
||||
Object.defineProperty(this, "orientation", {
|
||||
value: orientation,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
function Tail(player, sdata) {
|
||||
const data = {
|
||||
tail: [],
|
||||
tailGrid: [],
|
||||
prev: null,
|
||||
startRow: 0,
|
||||
startCol: 0,
|
||||
prevRow: 0,
|
||||
prevCol: 0,
|
||||
player
|
||||
};
|
||||
|
||||
if (sdata) {
|
||||
data.startRow = data.prevRow = sdata.startRow || 0;
|
||||
data.startCol = data.prevCol = sdata.startCol || 0;
|
||||
sdata.tail.forEach(val => {
|
||||
addTail(data, val.orientation, val.move);
|
||||
});
|
||||
}
|
||||
data.grid = player.grid;
|
||||
|
||||
defineInstanceMethods(this, data, addTail, hitsTail, fillTail, renderTail, reposition, serialData);
|
||||
Object.defineProperty(this, "moves", {
|
||||
get: function() {
|
||||
return data.tail.slice(0);
|
||||
},
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
//Instance methods
|
||||
function serialData(data) {
|
||||
return {
|
||||
tail: data.tail,
|
||||
startRow: data.startRow,
|
||||
startCol: data.startCol
|
||||
};
|
||||
}
|
||||
|
||||
function setTailGrid(data, tailGrid, r, c) {
|
||||
if (!tailGrid[r]) tailGrid[r] = [];
|
||||
tailGrid[r][c] = true;
|
||||
}
|
||||
|
||||
function addTail(data, orientation, count) {
|
||||
if (count === undefined) count = 1;
|
||||
if (!count || count < 0) return;
|
||||
|
||||
let prev = data.prev;
|
||||
const r = data.prevRow, c = data.prevCol;
|
||||
if (data.tail.length === 0) setTailGrid(data, data.tailGrid, r, c);
|
||||
|
||||
if (!prev || prev.orientation !== orientation) {
|
||||
prev = data.prev = new TailMove(orientation);
|
||||
data.tail.push(prev);
|
||||
prev.move += count - 1;
|
||||
}
|
||||
else prev.move += count;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const pos = walk([data.prevRow, data.prevCol], null, orientation, 1);
|
||||
data.prevRow = pos[0];
|
||||
data.prevCol = pos[1];
|
||||
setTailGrid(data, data.tailGrid, pos[0], pos[1]);
|
||||
}
|
||||
}
|
||||
|
||||
function reposition(data, row, col) {
|
||||
data.prevRow = data.startRow = row;
|
||||
data.prevCol = data.startCol = col;
|
||||
data.prev = null;
|
||||
if (data.tail.length === 0) return;
|
||||
else {
|
||||
const ret = data.tail;
|
||||
data.tail = [];
|
||||
data.tailGrid = [];
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
//Helper methods
|
||||
function renderTail(data, ctx) {
|
||||
if (data.tail.length === 0) return;
|
||||
|
||||
ctx.fillStyle = data.player.tailColor.rgbString();
|
||||
|
||||
let prevOrient = -1;
|
||||
let start = [data.startRow, data.startCol];
|
||||
|
||||
//fillTailRect(ctx, start, start);
|
||||
data.tail.forEach(tail => {
|
||||
const negDir = tail.orientation === 0 || tail.orientation === 3;
|
||||
|
||||
const back = start;
|
||||
if (!negDir) start = walk(start, null, tail.orientation, 1);
|
||||
const finish = walk(start, null, tail.orientation, tail.move - 1);
|
||||
|
||||
if (tail.move > 1) fillTailRect(ctx, start, finish);
|
||||
if (prevOrient !== -1) renderCorner(ctx, back, prevOrient, tail.orientation);
|
||||
//Draw folding triangle.
|
||||
start = finish;
|
||||
if (negDir) walk(start, start, tail.orientation, 1);
|
||||
prevOrient = tail.orientation;
|
||||
});
|
||||
|
||||
const curOrient = data.player.currentHeading;
|
||||
if (prevOrient === curOrient) fillTailRect(ctx, start, start);
|
||||
else renderCorner(ctx, start, prevOrient, curOrient);
|
||||
}
|
||||
|
||||
function renderCorner(ctx, cornerStart, dir1, dir2) {
|
||||
if (dir1 === 0 || dir2 === 0) walk(cornerStart, cornerStart, 2, 1);
|
||||
if (dir1 === 3 || dir2 === 3) walk(cornerStart, cornerStart, 1, 1);
|
||||
|
||||
const a = walk(cornerStart, null, dir2, 1);
|
||||
const b = walk(a, null, dir1, 1);
|
||||
|
||||
const triangle = new Path2D();
|
||||
triangle.moveTo(cornerStart[1] * consts.CELL_WIDTH, cornerStart[0] * consts.CELL_WIDTH);
|
||||
triangle.lineTo(a[1] * consts.CELL_WIDTH, a[0] * consts.CELL_WIDTH);
|
||||
triangle.lineTo(b[1] * consts.CELL_WIDTH, b[0] * consts.CELL_WIDTH);
|
||||
triangle.closePath();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
ctx.fill(triangle);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(from, ret, orient, dist) {
|
||||
ret = ret || [];
|
||||
ret[0] = from[0];
|
||||
ret[1] = from[1];
|
||||
switch (orient) {
|
||||
case 0: ret[0] -= dist; break; //UP
|
||||
case 1: ret[1] += dist; break; //RIGHT
|
||||
case 2: ret[0] += dist; break; //DOWN
|
||||
case 3: ret[1] -= dist; break; //LEFT
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function fillTailRect(ctx, start, end) {
|
||||
let x = start[1] * consts.CELL_WIDTH;
|
||||
let y = start[0] * consts.CELL_WIDTH;
|
||||
let width = (end[1] - start[1]) * consts.CELL_WIDTH;
|
||||
let height = (end[0] - start[0]) * consts.CELL_WIDTH;
|
||||
|
||||
if (width === 0) width += consts.CELL_WIDTH;
|
||||
if (height === 0) height += consts.CELL_WIDTH;
|
||||
|
||||
if (width < 0) {
|
||||
x += width;
|
||||
width = -width;
|
||||
}
|
||||
if (height < 0) {
|
||||
y += height;
|
||||
height = -height;
|
||||
}
|
||||
ctx.fillRect(x, y, width, height);
|
||||
}
|
||||
|
||||
function fillTail(data) {
|
||||
if (data.tail.length === 0) return;
|
||||
|
||||
function onTail(c) {
|
||||
return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]];
|
||||
}
|
||||
|
||||
const grid = data.grid;
|
||||
const start = [data.startRow, data.startCol];
|
||||
const been = new Grid(grid.size);
|
||||
const coords = [];
|
||||
|
||||
coords.push(start);
|
||||
while (coords.length > 0) { //BFS for all tail spaces
|
||||
const coord = coords.shift();
|
||||
const r = coord[0];
|
||||
const c = coord[1];
|
||||
|
||||
if (grid.isOutOfBounds(r, c) || been.get(r, c)) continue;
|
||||
|
||||
if (onTail(coord)) {//On the tail
|
||||
been.set(r, c, true);
|
||||
grid.set(r, c, data.player);
|
||||
|
||||
//Find all spots that this tail encloses
|
||||
floodFill(data, grid, r + 1, c, been);
|
||||
floodFill(data, grid, r - 1, c, been);
|
||||
floodFill(data, grid, r, c + 1, been);
|
||||
floodFill(data, grid, r, c - 1, been);
|
||||
|
||||
coords.push([r + 1, c]);
|
||||
coords.push([r - 1, c]);
|
||||
coords.push([r, c + 1]);
|
||||
coords.push([r, c - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function floodFill(data, grid, row, col, been) {
|
||||
function onTail(c) {
|
||||
return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]];
|
||||
}
|
||||
|
||||
const start = [row, col];
|
||||
if (grid.isOutOfBounds(row, col) || been.get(row, col) || onTail(start) || grid.get(row, col) === data.player) return;
|
||||
//Avoid allocating too many resources
|
||||
const coords = [];
|
||||
const filled = new Stack(consts.GRID_COUNT * consts.GRID_COUNT + 1);
|
||||
let surrounded = true;
|
||||
let coord;
|
||||
|
||||
coords.push(start);
|
||||
while (coords.length > 0) {
|
||||
coord = coords.shift();
|
||||
const r = coord[0];
|
||||
const c = coord[1];
|
||||
|
||||
if (grid.isOutOfBounds(r, c)) {
|
||||
surrounded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
//End this traverse on boundaries (where we been, on the tail, and when we enter our territory)
|
||||
if (been.get(r, c) || onTail(coord) || grid.get(r, c) === data.player) continue;
|
||||
|
||||
been.set(r, c, true);
|
||||
|
||||
if (surrounded) filled.push(coord);
|
||||
|
||||
coords.push([r + 1, c]);
|
||||
coords.push([r - 1, c]);
|
||||
coords.push([r, c + 1]);
|
||||
coords.push([r, c - 1]);
|
||||
}
|
||||
if (surrounded) {
|
||||
while (!filled.isEmpty()) {
|
||||
coord = filled.pop();
|
||||
grid.set(coord[0], coord[1], data.player);
|
||||
}
|
||||
}
|
||||
return surrounded;
|
||||
}
|
||||
|
||||
function hitsTail(data, other) {
|
||||
return (data.prevRow !== other.row || data.prevCol !== other.col)
|
||||
&& (data.startRow !== other.row || data.startCol !== other.col)
|
||||
&& !!(data.tailGrid[other.row] && data.tailGrid[other.row][other.col]);
|
||||
}
|
||||
|
||||
const SHADOW_OFFSET = 10;
|
||||
|
||||
function Player(grid, sdata) {
|
||||
const data = {};
|
||||
|
||||
//Parameters
|
||||
data.num = sdata.num;
|
||||
data.name = sdata.name || "Player " + (data.num + 1);
|
||||
data.grid = grid;
|
||||
data.posX = sdata.posX;
|
||||
data.posY = sdata.posY;
|
||||
this.heading = data.currentHeading = sdata.currentHeading; //0 is up, 1 is right, 2 is down, 3 is left
|
||||
data.waitLag = sdata.waitLag || 0;
|
||||
data.dead = false;
|
||||
|
||||
//Only need colors for client side
|
||||
let base;
|
||||
if (sdata.base) base = this.baseColor = sdata.base instanceof Color ? sdata.base : Color.fromData(sdata.base);
|
||||
else {
|
||||
const hue = Math.random();
|
||||
this.baseColor = base = new Color(hue, .8, .5);
|
||||
}
|
||||
this.lightBaseColor = base.deriveLumination(.1);
|
||||
this.shadowColor = base.deriveLumination(-.3);
|
||||
this.tailColor = base.deriveLumination(.2).deriveAlpha(0.98);
|
||||
|
||||
//Tail requires special handling
|
||||
this.grid = grid; //Temporary
|
||||
if (sdata.tail) data.tail = new Tail(this, sdata.tail);
|
||||
else {
|
||||
data.tail = new Tail(this);
|
||||
data.tail.reposition(calcRow(data), calcCol(data));
|
||||
}
|
||||
|
||||
//Instance methods
|
||||
this.move = move.bind(this, data);
|
||||
this.die = () => { data.dead = true; };
|
||||
this.serialData = function() {
|
||||
return {
|
||||
base: this.baseColor,
|
||||
num: data.num,
|
||||
name: data.name,
|
||||
posX: data.posX,
|
||||
posY: data.posY,
|
||||
currentHeading: data.currentHeading,
|
||||
tail: data.tail.serialData(),
|
||||
waitLag: data.waitLag
|
||||
};
|
||||
};
|
||||
|
||||
//Read-only Properties
|
||||
defineAccessorProperties(this, data, "currentHeading", "dead", "name", "num", "posX", "posY", "grid", "tail", "waitLag");
|
||||
Object.defineProperties(this, {
|
||||
row: defineGetter(() => calcRow(data)),
|
||||
col: defineGetter(() => calcCol(data))
|
||||
});
|
||||
}
|
||||
|
||||
//Gets the next integer in positive or negative direction
|
||||
function nearestInteger(positive, val) {
|
||||
return positive ? Math.ceil(val) : Math.floor(val);
|
||||
}
|
||||
|
||||
function calcRow(data) {
|
||||
return nearestInteger(data.currentHeading === 2 /*DOWN*/, data.posY / consts.CELL_WIDTH);
|
||||
}
|
||||
|
||||
function calcCol(data) {
|
||||
return nearestInteger(data.currentHeading === 1 /*RIGHT*/, data.posX / consts.CELL_WIDTH);
|
||||
}
|
||||
|
||||
//Instance methods
|
||||
Player.prototype.render = function(ctx, fade) {
|
||||
//Render tail
|
||||
this.tail.renderTail(ctx);
|
||||
//Render player
|
||||
fade = fade || 1;
|
||||
ctx.fillStyle = this.shadowColor.deriveAlpha(fade).rgbString();
|
||||
ctx.fillRect(this.posX, this.posY, consts.CELL_WIDTH, consts.CELL_WIDTH);
|
||||
|
||||
const mid = consts.CELL_WIDTH / 2;
|
||||
const grd = ctx.createRadialGradient(this.posX + mid, this.posY + mid - SHADOW_OFFSET, 1, this.posX + mid, this.posY + mid - SHADOW_OFFSET, consts.CELL_WIDTH);
|
||||
//grd.addColorStop(0, this.baseColor.deriveAlpha(fade).rgbString());
|
||||
//grd.addColorStop(1, new Color(0, 0, 1, fade).rgbString());
|
||||
//ctx.fillStyle = grd;
|
||||
ctx.fillStyle = this.shadowColor.deriveLumination(.2).rgbString();
|
||||
ctx.fillRect(this.posX - 1, this.posY - SHADOW_OFFSET, consts.CELL_WIDTH + 2, consts.CELL_WIDTH);
|
||||
|
||||
//Render name
|
||||
ctx.fillStyle = this.shadowColor.deriveAlpha(fade).rgbString();
|
||||
ctx.textAlign = "center";
|
||||
|
||||
let yoff = -SHADOW_OFFSET * 2;
|
||||
if (this.row === 0) yoff = SHADOW_OFFSET * 2 + consts.CELL_WIDTH;
|
||||
ctx.font = "18px Changa";
|
||||
ctx.fillText(this.name, this.posX + consts.CELL_WIDTH / 2, this.posY + yoff);
|
||||
};
|
||||
|
||||
function move(data) {
|
||||
if (data.waitLag < consts.NEW_PLAYER_LAG) { //Wait for a second at least
|
||||
data.waitLag++;
|
||||
return;
|
||||
}
|
||||
//Move to new position
|
||||
let { heading } = this;
|
||||
if (this.posX % consts.CELL_WIDTH !== 0 || this.posY % consts.CELL_WIDTH !== 0) heading = data.currentHeading;
|
||||
else data.currentHeading = heading;
|
||||
switch (heading) {
|
||||
case 0: data.posY -= consts.SPEED; break; //UP
|
||||
case 1: data.posX += consts.SPEED; break; //RIGHT
|
||||
case 2: data.posY += consts.SPEED; break; //DOWN
|
||||
case 3: data.posX -= consts.SPEED; break; //LEFT
|
||||
}
|
||||
//Check for out of bounds
|
||||
const { row, col } = this;
|
||||
if (data.grid.isOutOfBounds(row, col)) {
|
||||
switch (heading) {
|
||||
case 0: data.posY += consts.SPEED; break;
|
||||
case 1: data.posX -= consts.SPEED; break;
|
||||
case 2: data.posY -= consts.SPEED; break;
|
||||
case 3: data.posX += consts.SPEED; break;
|
||||
}
|
||||
//data.dead = true;
|
||||
return;
|
||||
}
|
||||
//Update tail position
|
||||
if (data.grid.get(row, col) === this) {
|
||||
//Safe zone!
|
||||
this.tail.fillTail();
|
||||
this.tail.reposition(row, col);
|
||||
}
|
||||
//If we are completely in a new cell (not in our safe zone), we add to the tail
|
||||
else if (this.posX % consts.CELL_WIDTH === 0 && this.posY % consts.CELL_WIDTH === 0) this.tail.addTail(heading);
|
||||
}
|
||||
|
||||
export default Player;
|
28
src/core/stack.js
Normal file
28
src/core/stack.js
Normal file
@ -0,0 +1,28 @@
|
||||
function Stack(initSize) {
|
||||
let len = 0;
|
||||
const arr = [];
|
||||
this.ensureCapacity = size => {
|
||||
arr.length = Math.max(arr.length, size || 0);
|
||||
};
|
||||
this.push = function(ele) {
|
||||
this[len] = ele;
|
||||
len++;
|
||||
};
|
||||
this.pop = function() {
|
||||
if (len === 0) return;
|
||||
len--;
|
||||
const tmp = this[len];
|
||||
this[len] = undefined;
|
||||
return tmp;
|
||||
};
|
||||
this.isEmpty = () => {
|
||||
return len === 0;
|
||||
}
|
||||
this.ensureCapacity(initSize);
|
||||
Object.defineProperty(this, "length", {
|
||||
get: function() {
|
||||
return len;
|
||||
}
|
||||
});
|
||||
}
|
||||
export default Stack;
|
279
src/game-client.js
Normal file
279
src/game-client.js
Normal file
@ -0,0 +1,279 @@
|
||||
import { Grid, Player, initPlayer, updateFrame } from "./core/index.js";
|
||||
import { consts } from "../config.js";
|
||||
let running = false;
|
||||
let user, socket, frame;
|
||||
let players, allPlayers;
|
||||
let kills;
|
||||
let timeout = undefined;
|
||||
let dirty = false;
|
||||
let deadFrames = 0;
|
||||
let requesting = -1; //Frame that we are requesting at
|
||||
let frameCache = []; //Frames after our request
|
||||
let _allowAnimation = true;
|
||||
const grid = new Grid(consts.GRID_COUNT, (row, col, before, after) => {
|
||||
invokeRenderer("updateGrid", [row, col, before, after]);
|
||||
});
|
||||
let renderer;
|
||||
|
||||
let requestAnimationFrame;
|
||||
try {
|
||||
requestAnimationFrame = window.requestAnimationFrame;
|
||||
} catch {
|
||||
requestAnimationFrame = callback => { setTimeout(callback, 1000 / 30) };
|
||||
}
|
||||
|
||||
//Public API
|
||||
function connectGame(io, url, name, callback, flag) {
|
||||
if (running) return; //Prevent multiple runs
|
||||
running = true;
|
||||
user = null;
|
||||
deadFrames = 0;
|
||||
const prefixes = consts.PREFIXES.split(" ");
|
||||
const names = consts.NAMES.split(" ");
|
||||
name = name || [prefixes[Math.floor(Math.random() * prefixes.length)], names[Math.floor(Math.random() * names.length)]].join(" ");
|
||||
//Socket connection
|
||||
io.j = [];
|
||||
io.sockets = [];
|
||||
socket = io(url, {
|
||||
"forceNew": true,
|
||||
upgrade: false,
|
||||
transports: ["websocket"]
|
||||
});
|
||||
socket.on("connect", () => {
|
||||
console.info("Connected to server.");
|
||||
});
|
||||
socket.on("game", data => {
|
||||
if (timeout != undefined) clearTimeout(timeout);
|
||||
//Initialize game
|
||||
//TODO: display data.gameid --- game id #
|
||||
frame = data.frame;
|
||||
reset();
|
||||
//Load players
|
||||
data.players.forEach(p => {
|
||||
const pl = new Player(grid, p);
|
||||
addPlayer(pl);
|
||||
});
|
||||
user = allPlayers[data.num];
|
||||
//if (!user) throw new Error();
|
||||
setUser(user);
|
||||
//Load grid
|
||||
const gridData = new Uint8Array(data.grid);
|
||||
for (let r = 0; r < grid.size; r++) {
|
||||
for (let c = 0; c < grid.size; c++) {
|
||||
const ind = gridData[r * grid.size + c] - 1;
|
||||
grid.set(r, c, ind === -1 ? null : players[ind]);
|
||||
}
|
||||
}
|
||||
invokeRenderer("paint", []);
|
||||
frame = data.frame;
|
||||
if (requesting !== -1) {
|
||||
//Update those cache frames after we updated game
|
||||
const minFrame = requesting;
|
||||
requesting = -1;
|
||||
while (frameCache.length > frame - minFrame) processFrame(frameCache[frame - minFrame]);
|
||||
frameCache = [];
|
||||
}
|
||||
});
|
||||
socket.on("notifyFrame", processFrame);
|
||||
socket.on("dead", () => {
|
||||
socket.disconnect(); //In case we didn"t get the disconnect call
|
||||
});
|
||||
socket.on("disconnect", () => {
|
||||
console.info("Server has disconnected. Creating new game.");
|
||||
socket.disconnect();
|
||||
if (!user) return;
|
||||
user.die();
|
||||
dirty = true;
|
||||
paintLoop();
|
||||
running = false;
|
||||
invokeRenderer("disconnect", []);
|
||||
});
|
||||
socket.emit("hello", {
|
||||
name: name,
|
||||
type: 0, //Free-for-all
|
||||
gameid: -1, //Requested game-id, or -1 for anyone
|
||||
god: flag
|
||||
}, (success, msg) => {
|
||||
if (success) console.info("Connected to game!");
|
||||
else {
|
||||
console.error("Unable to connect to game: " + msg);
|
||||
running = false;
|
||||
}
|
||||
if (callback) callback(success, msg);
|
||||
});
|
||||
}
|
||||
|
||||
function changeHeading(newHeading) {
|
||||
if (!user || user.dead) return;
|
||||
if (newHeading === user.currentHeading || ((newHeading % 2 === 0) ^ (user.currentHeading % 2 === 0))) {
|
||||
//user.heading = newHeading;
|
||||
if (socket) {
|
||||
socket.emit("frame", {
|
||||
frame: frame,
|
||||
heading: newHeading
|
||||
}, (success, msg) => {
|
||||
if (!success) console.error(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
function getPlayers() {
|
||||
return players.slice();
|
||||
}
|
||||
|
||||
function getOthers() {
|
||||
const ret = [];
|
||||
for (const p of players) {
|
||||
if (p !== user) ret.push(p);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
socket.disconnect();
|
||||
running = false;
|
||||
}
|
||||
|
||||
//Private API
|
||||
function addPlayer(player) {
|
||||
if (allPlayers[player.num]) return; //Already added
|
||||
allPlayers[player.num] = players[players.length] = player;
|
||||
invokeRenderer("addPlayer", [player]);
|
||||
return players.length - 1;
|
||||
}
|
||||
|
||||
function invokeRenderer(name, args) {
|
||||
if (renderer && typeof renderer[name] === "function") renderer[name].apply(null, args);
|
||||
}
|
||||
|
||||
function processFrame(data) {
|
||||
if (timeout != undefined) clearTimeout(timeout);
|
||||
if (requesting !== -1 && requesting < data.frame) {
|
||||
frameCache.push(data);
|
||||
return;
|
||||
}
|
||||
if (data.frame - 1 !== frame) {
|
||||
console.error("Frames don't match up!");
|
||||
socket.emit("requestFrame"); //Restore data
|
||||
requesting = data.frame;
|
||||
frameCache.push(data);
|
||||
return;
|
||||
}
|
||||
frame++;
|
||||
if (data.newPlayers) {
|
||||
data.newPlayers.forEach(p => {
|
||||
if (user && p.num === user.num) return;
|
||||
const pl = new Player(grid, p);
|
||||
addPlayer(pl);
|
||||
initPlayer(grid, pl);
|
||||
});
|
||||
}
|
||||
const found = new Array(players.length);
|
||||
data.moves.forEach((val, i) => {
|
||||
const player = allPlayers[val.num];
|
||||
if (!player) return;
|
||||
if (val.left) player.die();
|
||||
found[i] = true;
|
||||
player.heading = val.heading;
|
||||
});
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
//Implicitly leaving game
|
||||
if (!found[i]) {
|
||||
const player = players[i];
|
||||
player && player.die();
|
||||
}
|
||||
}
|
||||
update();
|
||||
const locs = {};
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
const p = players[i];
|
||||
locs[p.num] = [p.posX, p.posY, p.waitLag];
|
||||
}
|
||||
dirty = true;
|
||||
requestAnimationFrame(paintLoop);
|
||||
timeout = setTimeout(() => {
|
||||
console.warn("Server has timed-out. Disconnecting.");
|
||||
socket.disconnect();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function paintLoop() {
|
||||
if (!dirty) return;
|
||||
invokeRenderer("paint", []);
|
||||
dirty = false;
|
||||
if (user && user.dead) {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
if (deadFrames === 60) { //One second of frame
|
||||
const before = _allowAnimation;
|
||||
_allowAnimation = false;
|
||||
update();
|
||||
invokeRenderer("paint", []);
|
||||
_allowAnimation = before;
|
||||
user = null;
|
||||
deadFrames = 0;
|
||||
return;
|
||||
}
|
||||
socket.disconnect();
|
||||
deadFrames++;
|
||||
dirty = true;
|
||||
update();
|
||||
requestAnimationFrame(paintLoop);
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
user = null;
|
||||
grid.reset();
|
||||
players = [];
|
||||
allPlayers = [];
|
||||
kills = 0;
|
||||
invokeRenderer("reset");
|
||||
}
|
||||
|
||||
function setUser(player) {
|
||||
user = player;
|
||||
invokeRenderer("setUser", [player]);
|
||||
}
|
||||
|
||||
function update() {
|
||||
const dead = [];
|
||||
updateFrame(grid, players, dead, (killer, other) => { //addKill
|
||||
if (players[killer] === user && killer !== other) kills++;
|
||||
});
|
||||
dead.forEach(val => {
|
||||
console.log((val.name || "Unnamed") + " is dead");
|
||||
delete allPlayers[val.num];
|
||||
invokeRenderer("removePlayer", [val]);
|
||||
});
|
||||
invokeRenderer("update", [frame]);
|
||||
}
|
||||
|
||||
function setRenderer(r) {
|
||||
renderer = r;
|
||||
}
|
||||
|
||||
function setAllowAnimation(allow) {
|
||||
_allowAnimation = allow;
|
||||
}
|
||||
|
||||
function getKills() {
|
||||
return kills;
|
||||
}
|
||||
|
||||
// Export stuff
|
||||
export { connectGame, changeHeading, getUser, getPlayers, getOthers, disconnect, setRenderer, setAllowAnimation, getKills };
|
||||
export const allowAnimation = {
|
||||
get: function() {
|
||||
return _allowAnimation;
|
||||
},
|
||||
set: function(val) {
|
||||
_allowAnimation = !!val;
|
||||
},
|
||||
enumerable: true
|
||||
};
|
||||
export { grid };
|
239
src/game-server.js
Normal file
239
src/game-server.js
Normal file
@ -0,0 +1,239 @@
|
||||
import { Color, Grid, Player, initPlayer, updateFrame } from "./core/index.js";
|
||||
import { consts } from "../config.js";
|
||||
|
||||
function Game(id) {
|
||||
const possColors = Color.possColors();
|
||||
let nextInd = 0;
|
||||
const players = [];
|
||||
const gods = [];
|
||||
let newPlayers = [];
|
||||
const frameLocs = [];
|
||||
let frame = 0;
|
||||
let filled = 0;
|
||||
const grid = new Grid(consts.GRID_COUNT, (row, col, before, after) => {
|
||||
if (!!after ^ !!before) {
|
||||
if (after) filled++;
|
||||
else filled--;
|
||||
if (filled === consts.GRID_COUNT * consts.GRID_COUNT) console.log(`[${new Date()}] FULL GAME`);
|
||||
}
|
||||
});
|
||||
this.id = id;
|
||||
this.addPlayer = (client, name) => {
|
||||
if (players.length >= consts.MAX_PLAYERS) return false;
|
||||
const start = findEmpty(grid);
|
||||
if (!start) return false;
|
||||
const params = {
|
||||
posX: start.col * consts.CELL_WIDTH,
|
||||
posY: start.row * consts.CELL_WIDTH,
|
||||
currentHeading: Math.floor(Math.random() * 4),
|
||||
name,
|
||||
num: nextInd,
|
||||
base: possColors.shift()
|
||||
};
|
||||
const p = new Player(grid, params);
|
||||
p.tmpHeading = params.currentHeading;
|
||||
p.client = client;
|
||||
players.push(p);
|
||||
newPlayers.push(p);
|
||||
nextInd++;
|
||||
initPlayer(grid, p);
|
||||
if (p.name.indexOf("[BOT]") == -1) console.log(`[${new Date()}] ${p.name || "Unnamed"} (${p.num}) joined.`);
|
||||
client.on("requestFrame", () => {
|
||||
if (p.frame === frame) return;
|
||||
p.frame = frame; //Limit number of requests per frame (One per frame)
|
||||
const splayers = players.map(val => val.serialData());
|
||||
client.emit("game", {
|
||||
"num": p.num,
|
||||
"gameid": id,
|
||||
"frame": frame,
|
||||
"players": splayers,
|
||||
"grid": gridSerialData(grid, players)
|
||||
});
|
||||
});
|
||||
client.on("frame", (data, errorHan) => {
|
||||
if (typeof data === "function") {
|
||||
errorHan(false, "No data supplied.");
|
||||
return;
|
||||
}
|
||||
if (typeof errorHan !== "function") errorHan = () => {};
|
||||
if (!data) errorHan(false, "No data supplied.");
|
||||
else if (!checkInt(data.frame, 0, Infinity)) errorHan(false, "Requires a valid non-negative frame integer.");
|
||||
else if (data.frame > frame) errorHan(false, "Invalid frame received.");
|
||||
else {
|
||||
if (data.heading !== undefined) {
|
||||
if (checkInt(data.heading, 0, 4)) {
|
||||
p.tmpHeading = data.heading;
|
||||
errorHan(true);
|
||||
}
|
||||
else errorHan(false, "New heading must be an integer of range [0, 4).");
|
||||
}
|
||||
}
|
||||
});
|
||||
client.on("disconnect", () => {
|
||||
p.die(); //Die immediately if not already
|
||||
p.disconnected = true;
|
||||
if (p.name.indexOf("[BOT]") == -1) console.log(`[${new Date()}] ${p.name || "Unnamed"} (${p.num}) left.`);
|
||||
});
|
||||
return true;
|
||||
};
|
||||
this.addGod = client => {
|
||||
const g = {
|
||||
client,
|
||||
frame
|
||||
};
|
||||
gods.push(g);
|
||||
const splayers = players.map(val => val.serialData());
|
||||
client.emit("game", {
|
||||
"gameid": id,
|
||||
"frame": frame,
|
||||
"players": splayers,
|
||||
"grid": gridSerialData(grid, players)
|
||||
});
|
||||
client.on("requestFrame", () => {
|
||||
if (g.frame === frame) return;
|
||||
g.frame = frame; //Limit number of requests per frame (One per frame)
|
||||
const splayers = players.map(val => val.serialData());
|
||||
g.client.emit("game", {
|
||||
"gameid": id,
|
||||
"frame": frame,
|
||||
"players": splayers,
|
||||
"grid": gridSerialData(grid, players)
|
||||
});
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
function pushPlayerLocations() {
|
||||
const locs = [];
|
||||
for (const p of players) {
|
||||
locs[p.num] = [p.posX, p.posY, p.waitLag];
|
||||
}
|
||||
locs.frame = frame;
|
||||
if (frameLocs.length >= 300) frameLocs.shift(); //Give it 5 seconds of lag
|
||||
frameLocs.push(locs);
|
||||
}
|
||||
|
||||
function verifyPlayerLocations(fr, verify, resp) {
|
||||
const minFrame = frame - frameLocs.length + 1;
|
||||
if (fr < minFrame || fr > frame) {
|
||||
resp(false, false, "Frames out of reference");
|
||||
return;
|
||||
}
|
||||
|
||||
function string(loc) {
|
||||
return `(${loc[0]}, ${loc[1]}) [${loc[2]}]`;
|
||||
}
|
||||
const locs = frameLocs[fr - minFrame];
|
||||
if (locs.frame !== fr) {
|
||||
resp(false, false, locs.frame + " != " + fr);
|
||||
return;
|
||||
}
|
||||
for (const num in verify) {
|
||||
if (!locs[num]) continue;
|
||||
if (locs[num][0] !== verify[num][0] || locs[num][1] !== verify[num][1] || locs[num][2] !== verify[num][2]) {
|
||||
resp(false, true, "P" + num + " " + string(locs[num]) + " !== " + string(verify[num]));
|
||||
return;
|
||||
}
|
||||
}
|
||||
resp(true, false);
|
||||
}
|
||||
|
||||
function tick() {
|
||||
//TODO: notify those players that this server automatically drops out
|
||||
const splayers = players.map(val => val.serialData());
|
||||
const snews = newPlayers.map(val => {
|
||||
//Emit game stats.
|
||||
val.client.emit("game", {
|
||||
"num": val.num,
|
||||
"gameid": id,
|
||||
"frame": frame,
|
||||
"players": splayers,
|
||||
"grid": gridSerialData(grid, players),
|
||||
});
|
||||
return val.serialData();
|
||||
});
|
||||
const moves = players.map(val => {
|
||||
//Account for race condition (when heading is set after emitting frames, and before updating)
|
||||
val.heading = val.tmpHeading;
|
||||
return {
|
||||
num: val.num,
|
||||
left: !!val.disconnected,
|
||||
heading: val.heading
|
||||
};
|
||||
});
|
||||
update();
|
||||
const data = {
|
||||
frame: frame + 1,
|
||||
moves
|
||||
};
|
||||
if (snews.length > 0) {
|
||||
data.newPlayers = snews;
|
||||
newPlayers = [];
|
||||
}
|
||||
for (const p of players) {
|
||||
p.client.emit("notifyFrame", data);
|
||||
}
|
||||
for (const g of gods) {
|
||||
g.client.emit("notifyFrame", data);
|
||||
}
|
||||
frame++;
|
||||
pushPlayerLocations();
|
||||
}
|
||||
this.tickFrame = tick;
|
||||
|
||||
function update() {
|
||||
const dead = [];
|
||||
updateFrame(grid, players, dead);
|
||||
for (const p of dead) {
|
||||
if (!p.handledDead) {
|
||||
possColors.push(p.baseColor);
|
||||
p.handledDead = true;
|
||||
}
|
||||
if (p.name.indexOf("[BOT]") == -1) console.log(`${p.name || "Unnamed"} (${p.num}) died.`);
|
||||
p.client.emit("dead");
|
||||
p.client.disconnect(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkInt(value, min, max) {
|
||||
return !(typeof value !== "number" || value < min || value >= max || Math.floor(value) !== value);
|
||||
}
|
||||
|
||||
function gridSerialData(grid, players) {
|
||||
const buff = Buffer.alloc(grid.size * grid.size);
|
||||
const numToIndex = new Array(players.length > 0 ? players[players.length - 1].num + 1 : 0);
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
numToIndex[players[i].num] = i + 1;
|
||||
}
|
||||
for (let r = 0; r < grid.size; r++) {
|
||||
for (let c = 0; c < grid.size; c++) {
|
||||
const ele = grid.get(r, c);
|
||||
buff[r * grid.size + c] = ele ? numToIndex[ele.num] : 0;
|
||||
}
|
||||
}
|
||||
return buff;
|
||||
}
|
||||
|
||||
function findEmpty(grid) {
|
||||
const available = [];
|
||||
for (let r = 1; r < grid.size - 1; r++) {
|
||||
for (let c = 1; c < grid.size - 1; c++) {
|
||||
let cluttered = false;
|
||||
checkclutter: for (let dr = -1; dr <= 1; dr++) {
|
||||
for (let dc = -1; dc <= 1; dc++) {
|
||||
if (grid.get(r + dr, c + dc)) {
|
||||
cluttered = true;
|
||||
break checkclutter;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cluttered) available.push({
|
||||
row: r,
|
||||
col: c
|
||||
});
|
||||
}
|
||||
}
|
||||
return (available.length === 0) ? null : available[Math.floor(available.length * Math.random())];
|
||||
}
|
||||
export default Game;
|
334
src/mode/god.js
Normal file
334
src/mode/god.js
Normal file
@ -0,0 +1,334 @@
|
||||
import jquery from "jquery";
|
||||
|
||||
import { Grid, Color } from "../core";
|
||||
import * as client from "../game-client";
|
||||
import { consts } from "../../config.js";
|
||||
|
||||
const SHADOW_OFFSET = 5;
|
||||
const ANIMATE_FRAMES = 24;
|
||||
const BOUNCE_FRAMES = [8, 4];
|
||||
const DROP_HEIGHT = 24;
|
||||
const DROP_SPEED = 2;
|
||||
const MIN_BAR_WIDTH = 65;
|
||||
const BAR_HEIGHT = SHADOW_OFFSET + consts.CELL_WIDTH;
|
||||
const BAR_WIDTH = 400;
|
||||
|
||||
let canvas, ctx, offscreenCanvas, offctx, canvasWidth, canvasHeight, gameWidth, gameHeight;
|
||||
const $ = jquery;
|
||||
|
||||
$(() => {
|
||||
canvas = $("#main-ui")[0];
|
||||
ctx = canvas.getContext("2d");
|
||||
offscreenCanvas = document.createElement("canvas");
|
||||
offctx = offscreenCanvas.getContext("2d");
|
||||
updateSize();
|
||||
});
|
||||
|
||||
let animateGrid, playerPortion, portionsRolling, barProportionRolling, animateTo, offset, user, zoom, showedDead;
|
||||
const grid = client.grid;
|
||||
|
||||
function updateSize() {
|
||||
let changed = false;
|
||||
if (canvasWidth != window.innerWidth) {
|
||||
gameWidth = canvasWidth = offscreenCanvas.width = canvas.width = window.innerWidth;
|
||||
changed = true;
|
||||
}
|
||||
if (canvasHeight != window.innerHeight) {
|
||||
gameHeight = canvasHeight = offscreenCanvas.height = canvas.height = window.innerHeight;
|
||||
changed = true;
|
||||
}
|
||||
if (changed && user) centerOnPlayer(user, offset);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
animateGrid = new Grid(consts.GRID_COUNT);
|
||||
playerPortion = [];
|
||||
portionsRolling = [];
|
||||
barProportionRolling = [];
|
||||
animateTo = [0, 0];
|
||||
offset = [0, 0];
|
||||
user = null;
|
||||
zoom = (Math.min(canvasWidth, canvasHeight) - consts.BORDER_WIDTH) / (consts.CELL_WIDTH * consts.GRID_COUNT);
|
||||
showedDead = false;
|
||||
}
|
||||
|
||||
reset();
|
||||
|
||||
//Paint methods
|
||||
function paintGridBorder(ctx) {
|
||||
ctx.fillStyle = "lightgray";
|
||||
const gridWidth = consts.CELL_WIDTH * consts.GRID_COUNT;
|
||||
|
||||
ctx.fillRect(-consts.BORDER_WIDTH, 0, consts.BORDER_WIDTH, gridWidth);
|
||||
ctx.fillRect(-consts.BORDER_WIDTH, -consts.BORDER_WIDTH, gridWidth + consts.BORDER_WIDTH * 2, consts.BORDER_WIDTH);
|
||||
ctx.fillRect(gridWidth, 0, consts.BORDER_WIDTH, gridWidth);
|
||||
ctx.fillRect(-consts.BORDER_WIDTH, gridWidth, gridWidth + consts.BORDER_WIDTH * 2, consts.BORDER_WIDTH);
|
||||
}
|
||||
|
||||
function paintGrid(ctx) {
|
||||
//Paint background
|
||||
ctx.fillStyle = "rgb(211, 225, 237)";
|
||||
ctx.fillRect(0, 0, consts.CELL_WIDTH * consts.GRID_COUNT, consts.CELL_WIDTH * consts.GRID_COUNT);
|
||||
paintGridBorder(ctx);
|
||||
|
||||
//Get viewing limits
|
||||
const offsetX = (offset[0] - consts.BORDER_WIDTH);
|
||||
const offsetY = (offset[1] - consts.BORDER_WIDTH);
|
||||
const minRow = Math.max(Math.floor(offsetY / consts.CELL_WIDTH), 0);
|
||||
const minCol = Math.max(Math.floor(offsetX / consts.CELL_WIDTH), 0);
|
||||
const maxRow = Math.min(Math.ceil((offsetY + gameHeight / zoom) / consts.CELL_WIDTH), grid.size);
|
||||
const maxCol = Math.min(Math.ceil((offsetX + gameWidth / zoom) / consts.CELL_WIDTH), grid.size);
|
||||
let x, y, animateSpec, baseColor, shadowColor;
|
||||
|
||||
//Paint occupied areas (and fading ones)
|
||||
for (let r = minRow; r < maxRow; r++) {
|
||||
for (let c = minCol; c < maxCol; c++) {
|
||||
const p = grid.get(r, c);
|
||||
x = c * consts.CELL_WIDTH;
|
||||
y = r * consts.CELL_WIDTH;
|
||||
animateSpec = animateGrid.get(r, c);
|
||||
if (client.allowAnimation && animateSpec) {
|
||||
if (animateSpec.before) { //fading animation
|
||||
const frac = (animateSpec.frame / ANIMATE_FRAMES);
|
||||
const back = new Color(.58, .41, .92, 1);
|
||||
baseColor = animateSpec.before.lightBaseColor.interpolateToString(back, frac);
|
||||
shadowColor = animateSpec.before.shadowColor.interpolateToString(back, frac);
|
||||
}
|
||||
else continue;
|
||||
}
|
||||
else if (p) {
|
||||
baseColor = p.lightBaseColor;
|
||||
shadowColor = p.shadowColor;
|
||||
}
|
||||
else continue; //No animation nor is this player owned
|
||||
const hasBottom = !grid.isOutOfBounds(r + 1, c);
|
||||
const bottomAnimate = hasBottom && animateGrid.get(r + 1, c);
|
||||
const totalStatic = !bottomAnimate && !animateSpec;
|
||||
const bottomEmpty = totalStatic ? (hasBottom && !grid.get(r + 1, c)) : (!bottomAnimate || (bottomAnimate.after && bottomAnimate.before));
|
||||
if (hasBottom && ((!!bottomAnimate ^ !!animateSpec) || bottomEmpty)) {
|
||||
ctx.fillStyle = shadowColor.rgbString();
|
||||
ctx.fillRect(x, y + consts.CELL_WIDTH, consts.CELL_WIDTH + 1, SHADOW_OFFSET);
|
||||
}
|
||||
ctx.fillStyle = baseColor.rgbString();
|
||||
ctx.fillRect(x, y, consts.CELL_WIDTH + 1, consts.CELL_WIDTH + 1);
|
||||
}
|
||||
}
|
||||
if (!client.allowAnimation) return;
|
||||
|
||||
//Paint squares with drop in animation
|
||||
for (let r = 0; r < grid.size; r++) {
|
||||
for (let c = 0; c < grid.size; c++) {
|
||||
animateSpec = animateGrid.get(r, c);
|
||||
x = c * consts.CELL_WIDTH, y = r * consts.CELL_WIDTH;
|
||||
if (animateSpec && client.allowAnimation) {
|
||||
const viewable = r >= minRow && r < maxRow && c >= minCol && c < maxCol;
|
||||
if (animateSpec.after && viewable) {
|
||||
//Bouncing the squares.
|
||||
const offsetBounce = getBounceOffset(animateSpec.frame);
|
||||
y -= offsetBounce;
|
||||
shadowColor = animateSpec.after.shadowColor;
|
||||
baseColor = animateSpec.after.lightBaseColor.deriveLumination(-(offsetBounce / DROP_HEIGHT) * .1);
|
||||
ctx.fillStyle = shadowColor.rgbString();
|
||||
ctx.fillRect(x, y + consts.CELL_WIDTH, consts.CELL_WIDTH, SHADOW_OFFSET);
|
||||
ctx.fillStyle = baseColor.rgbString();
|
||||
ctx.fillRect(x, y, consts.CELL_WIDTH + 1, consts.CELL_WIDTH + 1);
|
||||
}
|
||||
animateSpec.frame++;
|
||||
if (animateSpec.frame >= ANIMATE_FRAMES) animateGrid.set(r, c, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function paintUIBar(ctx) {
|
||||
ctx.fillStyle = "white";
|
||||
ctx.font = "18px Changa";
|
||||
|
||||
//Calcuate rank
|
||||
const sorted = [];
|
||||
client.getPlayers().forEach(val => {
|
||||
sorted.push({player: val, portion: playerPortion[val.num]});
|
||||
});
|
||||
sorted.sort((a, b) => {
|
||||
return (a.portion === b.portion) ? a.player.num - b.player.num : b.portion - a.portion;
|
||||
});
|
||||
|
||||
//Rolling the leaderboard bars
|
||||
if (sorted.length > 0) {
|
||||
const maxPortion = sorted[0].portion;
|
||||
client.getPlayers().forEach(player => {
|
||||
const rolling = barProportionRolling[player.num];
|
||||
rolling.value = playerPortion[player.num] / maxPortion;
|
||||
rolling.update();
|
||||
});
|
||||
}
|
||||
|
||||
//Show leaderboard
|
||||
const leaderboardNum = Math.min(consts.LEADERBOARD_NUM, sorted.length);
|
||||
for (let i = 0; i < leaderboardNum; i++) {
|
||||
const { player } = sorted[i];
|
||||
const name = player.name || "Unnamed";
|
||||
const portion = barProportionRolling[player.num].lag;
|
||||
const nameWidth = ctx.measureText(name).width;
|
||||
const barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * portion + MIN_BAR_WIDTH);
|
||||
const barX = canvasWidth - barSize;
|
||||
const barY = BAR_HEIGHT * i;
|
||||
const offset = i == 0 ? 10 : 0;
|
||||
ctx.fillStyle = "rgba(10, 10, 10, .3)";
|
||||
ctx.fillRect(barX - 10, barY + 10 - offset, barSize + 10, BAR_HEIGHT + offset);
|
||||
ctx.fillStyle = player.baseColor.rgbString();
|
||||
ctx.fillRect(barX, barY, barSize, consts.CELL_WIDTH);
|
||||
ctx.fillStyle = player.shadowColor.rgbString();
|
||||
ctx.fillRect(barX, barY + consts.CELL_WIDTH, barSize, SHADOW_OFFSET);
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fillText(name, barX - nameWidth - 15, barY + 27);
|
||||
const percentage = (portionsRolling[player.num].lag * 100).toFixed(3) + "%";
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText(percentage, barX + 5, barY + consts.CELL_WIDTH - 5);
|
||||
}
|
||||
}
|
||||
|
||||
function paint(ctx) {
|
||||
ctx.fillStyle = "#e2ebf3"; //"whitesmoke";
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
//Move grid to viewport as said with the offsets, below the stats
|
||||
ctx.save();
|
||||
//ctx.translate(0, BAR_HEIGHT);
|
||||
ctx.beginPath();
|
||||
ctx.rect(0, 0, gameWidth, gameHeight);
|
||||
ctx.clip();
|
||||
|
||||
//Zoom in/out based on player stats
|
||||
ctx.scale(zoom, zoom);
|
||||
ctx.translate(consts.BORDER_WIDTH, consts.BORDER_WIDTH);
|
||||
|
||||
paintGrid(ctx);
|
||||
client.getPlayers().forEach(p => {
|
||||
const fr = p.waitLag;
|
||||
if (fr < ANIMATE_FRAMES) p.render(ctx, fr / ANIMATE_FRAMES);
|
||||
else p.render(ctx);
|
||||
});
|
||||
|
||||
//Reset transform to paint fixed UI elements
|
||||
ctx.restore();
|
||||
paintUIBar(ctx);
|
||||
|
||||
if ((!user || user.dead) && !showedDead) {
|
||||
showedDead = true;
|
||||
console.log("You died!");
|
||||
}
|
||||
}
|
||||
|
||||
function paintDoubleBuff() {
|
||||
paint(offctx);
|
||||
ctx.drawImage(offscreenCanvas, 0, 0);
|
||||
}
|
||||
|
||||
function update() {
|
||||
updateSize();
|
||||
|
||||
//Change grid offsets
|
||||
for (let i = 0; i <= 1; i++) {
|
||||
if (animateTo[i] !== offset[i]) {
|
||||
if (client.allowAnimation) {
|
||||
const delta = animateTo[i] - offset[i];
|
||||
const dir = Math.sign(delta);
|
||||
const mag = Math.min(consts.SPEED, Math.abs(delta));
|
||||
offset[i] += dir * mag;
|
||||
}
|
||||
else offset[i] = animateTo[i];
|
||||
}
|
||||
}
|
||||
|
||||
//Calculate player portions
|
||||
client.getPlayers().forEach(player => {
|
||||
const roll = portionsRolling[player.num];
|
||||
roll.value = playerPortion[player.num] / consts.GRID_COUNT / consts.GRID_COUNT;
|
||||
roll.update();
|
||||
});
|
||||
}
|
||||
|
||||
//Helper methods
|
||||
function centerOnPlayer(player, pos) {
|
||||
player = {
|
||||
posX: 0,
|
||||
posY: 0
|
||||
}
|
||||
const xOff = Math.floor(player.posX - (gameWidth / zoom - consts.CELL_WIDTH) / 2);
|
||||
const yOff = Math.floor(player.posY - (gameHeight / zoom - consts.CELL_WIDTH) / 2);
|
||||
const gridWidth = grid.size * consts.CELL_WIDTH + consts.BORDER_WIDTH * 2;
|
||||
pos[0] = xOff; //Math.max(Math.min(xOff, gridWidth + (BAR_WIDTH + 100) / zoom - gameWidth / zoom), 0);
|
||||
pos[1] = yOff; //Math.max(Math.min(yOff, gridWidth - gameHeight / zoom), 0);
|
||||
}
|
||||
|
||||
function getBounceOffset(frame) {
|
||||
let offsetBounce = ANIMATE_FRAMES;
|
||||
let bounceNum = BOUNCE_FRAMES.length - 1;
|
||||
while (bounceNum >= 0 && frame < offsetBounce - BOUNCE_FRAMES[bounceNum]) {
|
||||
offsetBounce -= BOUNCE_FRAMES[bounceNum];
|
||||
bounceNum--;
|
||||
}
|
||||
if (bounceNum === -1) return (offsetBounce - frame) * DROP_SPEED;
|
||||
else {
|
||||
offsetBounce -= BOUNCE_FRAMES[bounceNum];
|
||||
frame = frame - offsetBounce;
|
||||
const midFrame = BOUNCE_FRAMES[bounceNum] / 2;
|
||||
return (frame >= midFrame) ? (BOUNCE_FRAMES[bounceNum] - frame) * DROP_SPEED : frame * DROP_SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
function Rolling(value, frames) {
|
||||
let lag = 0;
|
||||
if (!frames) frames = 24;
|
||||
this.value = value;
|
||||
Object.defineProperty(this, "lag", {
|
||||
get: function() {
|
||||
return lag;
|
||||
},
|
||||
enumerable: true
|
||||
});
|
||||
this.update = function() {
|
||||
const delta = this.value - lag;
|
||||
const dir = Math.sign(delta);
|
||||
const speed = Math.abs(delta) / frames;
|
||||
const mag = Math.min(Math.abs(speed), Math.abs(delta));
|
||||
|
||||
lag += mag * dir;
|
||||
return lag;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
addPlayer: function(player) {
|
||||
playerPortion[player.num] = 0;
|
||||
portionsRolling[player.num] = new Rolling(9 / consts.GRID_COUNT / consts.GRID_COUNT, ANIMATE_FRAMES);
|
||||
barProportionRolling[player.num] = new Rolling(0, ANIMATE_FRAMES);
|
||||
},
|
||||
disconnect: function() {
|
||||
},
|
||||
removePlayer: function(player) {
|
||||
delete playerPortion[player.num];
|
||||
delete portionsRolling[player.num];
|
||||
delete barProportionRolling[player.num];
|
||||
},
|
||||
setUser: function(player) {
|
||||
user = player;
|
||||
centerOnPlayer(user, offset);
|
||||
},
|
||||
reset: reset,
|
||||
updateGrid: function(row, col, before, after) {
|
||||
//Keep track of areas
|
||||
if (before) playerPortion[before.num]--;
|
||||
if (after) playerPortion[after.num]++;
|
||||
//Queue animation
|
||||
if (before === after || !client.allowAnimation) return;
|
||||
animateGrid.set(row, col, {
|
||||
before: before,
|
||||
after: after,
|
||||
frame: 0
|
||||
});
|
||||
},
|
||||
paint: paintDoubleBuff,
|
||||
update: update
|
||||
};
|
375
src/mode/player.js
Normal file
375
src/mode/player.js
Normal file
@ -0,0 +1,375 @@
|
||||
import jquery from "jquery";
|
||||
|
||||
import { Color, Grid } from "../core";
|
||||
import * as client from "../game-client";
|
||||
import { consts } from "../../config.js";
|
||||
|
||||
const SHADOW_OFFSET = 5;
|
||||
const ANIMATE_FRAMES = 24;
|
||||
const BOUNCE_FRAMES = [8, 4];
|
||||
const DROP_HEIGHT = 24;
|
||||
const DROP_SPEED = 2;
|
||||
const MIN_BAR_WIDTH = 65;
|
||||
const BAR_HEIGHT = SHADOW_OFFSET + consts.CELL_WIDTH;
|
||||
const BAR_WIDTH = 400;
|
||||
|
||||
let canvas, ctx, offscreenCanvas, offctx, canvasWidth, canvasHeight, gameWidth, gameHeight;
|
||||
const $ = jquery;
|
||||
|
||||
$(() => {
|
||||
canvas = $("#main-ui")[0];
|
||||
ctx = canvas.getContext("2d");
|
||||
offscreenCanvas = document.createElement("canvas");
|
||||
offctx = offscreenCanvas.getContext("2d");
|
||||
updateSize();
|
||||
});
|
||||
|
||||
let animateGrid, playerPortion, portionsRolling, barProportionRolling, animateTo, offset, user, zoom, showedDead;
|
||||
const grid = client.grid;
|
||||
|
||||
function updateSize() {
|
||||
let changed = false;
|
||||
if (canvasWidth != window.innerWidth) {
|
||||
gameWidth = canvasWidth = offscreenCanvas.width = canvas.width = window.innerWidth;
|
||||
changed = true;
|
||||
}
|
||||
if (canvasHeight != window.innerHeight) {
|
||||
canvasHeight = offscreenCanvas.height = canvas.height = window.innerHeight;
|
||||
gameHeight = canvasHeight - BAR_HEIGHT;
|
||||
changed = true;
|
||||
}
|
||||
if (changed && user) centerOnPlayer(user, offset);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
animateGrid = new Grid(consts.GRID_COUNT);
|
||||
playerPortion = [];
|
||||
portionsRolling = [];
|
||||
barProportionRolling = [];
|
||||
animateTo = [0, 0];
|
||||
offset = [0, 0];
|
||||
user = null;
|
||||
zoom = 1;
|
||||
showedDead = false;
|
||||
}
|
||||
|
||||
reset();
|
||||
|
||||
//Paint methods
|
||||
function paintGridBorder(ctx) {
|
||||
ctx.fillStyle = "lightgray";
|
||||
const gridWidth = consts.CELL_WIDTH * consts.GRID_COUNT;
|
||||
|
||||
ctx.fillRect(-consts.BORDER_WIDTH, 0, consts.BORDER_WIDTH, gridWidth);
|
||||
ctx.fillRect(-consts.BORDER_WIDTH, -consts.BORDER_WIDTH, gridWidth + consts.BORDER_WIDTH * 2, consts.BORDER_WIDTH);
|
||||
ctx.fillRect(gridWidth, 0, consts.BORDER_WIDTH, gridWidth);
|
||||
ctx.fillRect(-consts.BORDER_WIDTH, gridWidth, gridWidth + consts.BORDER_WIDTH * 2, consts.BORDER_WIDTH);
|
||||
}
|
||||
|
||||
function paintGrid(ctx) {
|
||||
//Paint background
|
||||
ctx.fillStyle = "rgb(211, 225, 237)";
|
||||
ctx.fillRect(0, 0, consts.CELL_WIDTH * consts.GRID_COUNT, consts.CELL_WIDTH * consts.GRID_COUNT);
|
||||
paintGridBorder(ctx);
|
||||
|
||||
//Get viewing limits
|
||||
const offsetX = (offset[0] - consts.BORDER_WIDTH);
|
||||
const offsetY = (offset[1] - consts.BORDER_WIDTH);
|
||||
const minRow = Math.max(Math.floor(offsetY / consts.CELL_WIDTH), 0);
|
||||
const minCol = Math.max(Math.floor(offsetX / consts.CELL_WIDTH), 0);
|
||||
const maxRow = Math.min(Math.ceil((offsetY + gameHeight / zoom) / consts.CELL_WIDTH), grid.size);
|
||||
const maxCol = Math.min(Math.ceil((offsetX + gameWidth / zoom) / consts.CELL_WIDTH), grid.size);
|
||||
let x, y, animateSpec, baseColor, shadowColor;
|
||||
|
||||
//Paint occupied areas (and fading ones)
|
||||
for (let r = minRow; r < maxRow; r++) {
|
||||
for (let c = minCol; c < maxCol; c++) {
|
||||
const p = grid.get(r, c);
|
||||
x = c * consts.CELL_WIDTH;
|
||||
y = r * consts.CELL_WIDTH;
|
||||
animateSpec = animateGrid.get(r, c);
|
||||
if (client.allowAnimation && animateSpec) {
|
||||
if (animateSpec.before) { //fading animation
|
||||
const frac = (animateSpec.frame / ANIMATE_FRAMES);
|
||||
const back = new Color(.58, .41, .92, 1);
|
||||
baseColor = animateSpec.before.lightBaseColor.interpolateToString(back, frac);
|
||||
shadowColor = animateSpec.before.shadowColor.interpolateToString(back, frac);
|
||||
}
|
||||
else continue;
|
||||
}
|
||||
else if (p) {
|
||||
baseColor = p.lightBaseColor;
|
||||
shadowColor = p.shadowColor;
|
||||
}
|
||||
else continue; //No animation nor is this player owned
|
||||
const hasBottom = !grid.isOutOfBounds(r + 1, c);
|
||||
const bottomAnimate = hasBottom && animateGrid.get(r + 1, c);
|
||||
const totalStatic = !bottomAnimate && !animateSpec;
|
||||
const bottomEmpty = totalStatic ? (hasBottom && !grid.get(r + 1, c)) : (!bottomAnimate || (bottomAnimate.after && bottomAnimate.before));
|
||||
if (hasBottom && ((!!bottomAnimate ^ !!animateSpec) || bottomEmpty)) {
|
||||
ctx.fillStyle = shadowColor.rgbString();
|
||||
ctx.fillRect(x, y + consts.CELL_WIDTH, consts.CELL_WIDTH + 1, SHADOW_OFFSET);
|
||||
}
|
||||
ctx.fillStyle = baseColor.rgbString();
|
||||
ctx.fillRect(x, y, consts.CELL_WIDTH + 1, consts.CELL_WIDTH + 1);
|
||||
}
|
||||
}
|
||||
if (!client.allowAnimation) return;
|
||||
|
||||
//Paint squares with drop in animation
|
||||
for (let r = 0; r < grid.size; r++) {
|
||||
for (let c = 0; c < grid.size; c++) {
|
||||
animateSpec = animateGrid.get(r, c);
|
||||
x = c * consts.CELL_WIDTH, y = r * consts.CELL_WIDTH;
|
||||
if (animateSpec && client.allowAnimation) {
|
||||
const viewable = r >= minRow && r < maxRow && c >= minCol && c < maxCol;
|
||||
if (animateSpec.after && viewable) {
|
||||
//Bouncing the squares.
|
||||
const offsetBounce = getBounceOffset(animateSpec.frame);
|
||||
y -= offsetBounce;
|
||||
shadowColor = animateSpec.after.shadowColor;
|
||||
baseColor = animateSpec.after.lightBaseColor.deriveLumination(-(offsetBounce / DROP_HEIGHT) * .1);
|
||||
ctx.fillStyle = shadowColor.rgbString();
|
||||
ctx.fillRect(x, y + consts.CELL_WIDTH, consts.CELL_WIDTH, SHADOW_OFFSET);
|
||||
ctx.fillStyle = baseColor.rgbString();
|
||||
ctx.fillRect(x, y, consts.CELL_WIDTH + 1, consts.CELL_WIDTH + 1);
|
||||
}
|
||||
animateSpec.frame++;
|
||||
if (animateSpec.frame >= ANIMATE_FRAMES) animateGrid.set(r, c, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function paintUIBar(ctx) {
|
||||
//UI Bar background
|
||||
ctx.fillStyle = "#24422c";
|
||||
ctx.fillRect(0, 0, canvasWidth, BAR_HEIGHT);
|
||||
|
||||
let barOffset;
|
||||
ctx.fillStyle = "white";
|
||||
ctx.font = "24px Changa";
|
||||
barOffset = (user && user.name) ? (ctx.measureText(user.name).width + 20) : 0;
|
||||
ctx.fillText(user ? user.name : "", 5, consts.CELL_WIDTH - 5);
|
||||
|
||||
//Draw filled bar
|
||||
ctx.fillStyle = "rgba(180, 180, 180, .3)";
|
||||
ctx.fillRect(barOffset, 0, BAR_WIDTH, BAR_HEIGHT);
|
||||
|
||||
const userPortions = portionsRolling[user.num] ? portionsRolling[user.num].lag : 0;
|
||||
let barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * userPortions + MIN_BAR_WIDTH);
|
||||
ctx.fillStyle = user ? user.baseColor.rgbString() : "";
|
||||
ctx.fillRect(barOffset, 0, barSize, consts.CELL_WIDTH);
|
||||
ctx.fillStyle = user ? user.shadowColor.rgbString() : "";
|
||||
ctx.fillRect(barOffset, consts.CELL_WIDTH, barSize, SHADOW_OFFSET);
|
||||
|
||||
//TODO: dont reset kill count and zoom when we request frames.
|
||||
//Percentage
|
||||
ctx.fillStyle = "white";
|
||||
ctx.font = "18px Changa";
|
||||
ctx.fillText((userPortions * 100).toFixed(3) + "%", 5 + barOffset, consts.CELL_WIDTH - 5);
|
||||
|
||||
//Number of kills
|
||||
const killsText = "Kills: " + client.getKills();
|
||||
const killsOffset = 20 + BAR_WIDTH + barOffset;
|
||||
ctx.fillText(killsText, killsOffset, consts.CELL_WIDTH - 5);
|
||||
|
||||
//Calcuate rank
|
||||
const sorted = [];
|
||||
client.getPlayers().forEach(val => {
|
||||
sorted.push({player: val, portion: playerPortion[val.num]});
|
||||
});
|
||||
sorted.sort((a, b) => {
|
||||
return (a.portion === b.portion) ? a.player.num - b.player.num : b.portion - a.portion;
|
||||
});
|
||||
|
||||
const rank = sorted.findIndex(val => val.player === user);
|
||||
ctx.fillText("Rank: " + (rank === -1 ? "--" : rank + 1) + " of " + sorted.length,
|
||||
ctx.measureText(killsText).width + killsOffset + 20, consts.CELL_WIDTH - 5);
|
||||
|
||||
//Rolling the leaderboard bars
|
||||
if (sorted.length > 0) {
|
||||
const maxPortion = sorted[0].portion;
|
||||
client.getPlayers().forEach(player => {
|
||||
const rolling = barProportionRolling[player.num];
|
||||
rolling.value = playerPortion[player.num] / maxPortion;
|
||||
rolling.update();
|
||||
});
|
||||
}
|
||||
|
||||
//Show leaderboard
|
||||
const leaderboardNum = Math.min(consts.LEADERBOARD_NUM, sorted.length);
|
||||
for (let i = 0; i < leaderboardNum; i++) {
|
||||
const { player } = sorted[i];
|
||||
const name = player.name || "Unnamed";
|
||||
const portion = barProportionRolling[player.num].lag;
|
||||
const nameWidth = ctx.measureText(name).width;
|
||||
barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * portion + MIN_BAR_WIDTH);
|
||||
const barX = canvasWidth - barSize;
|
||||
const barY = BAR_HEIGHT * (i + 1);
|
||||
const offset = i == 0 ? 10 : 0;
|
||||
ctx.fillStyle = "rgba(10, 10, 10, .3)";
|
||||
ctx.fillRect(barX - 10, barY + 10 - offset, barSize + 10, BAR_HEIGHT + offset);
|
||||
ctx.fillStyle = player.baseColor.rgbString();
|
||||
ctx.fillRect(barX, barY, barSize, consts.CELL_WIDTH);
|
||||
ctx.fillStyle = player.shadowColor.rgbString();
|
||||
ctx.fillRect(barX, barY + consts.CELL_WIDTH, barSize, SHADOW_OFFSET);
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fillText(name, barX - nameWidth - 15, barY + 27);
|
||||
const percentage = (portionsRolling[player.num].lag * 100).toFixed(3) + "%";
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText(percentage, barX + 5, barY + consts.CELL_WIDTH - 5);
|
||||
}
|
||||
}
|
||||
|
||||
function paint(ctx) {
|
||||
ctx.fillStyle = "#e2ebf3"; //"whitesmoke";
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
//Move grid to viewport as said with the offsets, below the stats
|
||||
ctx.save();
|
||||
ctx.translate(0, BAR_HEIGHT);
|
||||
ctx.beginPath();
|
||||
ctx.rect(0, 0, gameWidth, gameHeight);
|
||||
ctx.clip();
|
||||
|
||||
//Zoom in/out based on player stats
|
||||
ctx.scale(zoom, zoom);
|
||||
ctx.translate(-offset[0] + consts.BORDER_WIDTH, -offset[1] + consts.BORDER_WIDTH);
|
||||
|
||||
paintGrid(ctx);
|
||||
client.getPlayers().forEach(p => {
|
||||
const fr = p.waitLag;
|
||||
if (fr < ANIMATE_FRAMES) p.render(ctx, fr / ANIMATE_FRAMES);
|
||||
else p.render(ctx);
|
||||
});
|
||||
|
||||
//Reset transform to paint fixed UI elements
|
||||
ctx.restore();
|
||||
paintUIBar(ctx);
|
||||
|
||||
if ((!user || user.dead) && !showedDead) {
|
||||
showedDead = true;
|
||||
console.log("You died!");
|
||||
}
|
||||
}
|
||||
|
||||
function paintDoubleBuff() {
|
||||
paint(offctx);
|
||||
ctx.drawImage(offscreenCanvas, 0, 0);
|
||||
}
|
||||
|
||||
function update() {
|
||||
updateSize();
|
||||
|
||||
//Change grid offsets
|
||||
for (let i = 0; i <= 1; i++) {
|
||||
if (animateTo[i] !== offset[i]) {
|
||||
if (client.allowAnimation) {
|
||||
const delta = animateTo[i] - offset[i];
|
||||
const dir = Math.sign(delta);
|
||||
const mag = Math.min(consts.SPEED, Math.abs(delta));
|
||||
offset[i] += dir * mag;
|
||||
}
|
||||
else offset[i] = animateTo[i];
|
||||
}
|
||||
}
|
||||
|
||||
//Calculate player portions
|
||||
client.getPlayers().forEach(player => {
|
||||
const roll = portionsRolling[player.num];
|
||||
roll.value = playerPortion[player.num] / consts.GRID_COUNT / consts.GRID_COUNT;
|
||||
roll.update();
|
||||
});
|
||||
|
||||
//Zoom goes from 1 to .5, decreasing as portion goes up. TODO: maybe can modify this?
|
||||
if (portionsRolling[user.num]) zoom = 1 / (portionsRolling[user.num].lag + 1);
|
||||
//TODO: animate player is dead. (maybe explosion?), and tail rewinds itself.
|
||||
if (user) centerOnPlayer(user, animateTo);
|
||||
}
|
||||
|
||||
//Helper methods
|
||||
function centerOnPlayer(player, pos) {
|
||||
const xOff = Math.floor(player.posX - (gameWidth / zoom - consts.CELL_WIDTH) / 2);
|
||||
const yOff = Math.floor(player.posY - (gameHeight / zoom - consts.CELL_WIDTH) / 2);
|
||||
const gridWidth = grid.size * consts.CELL_WIDTH + consts.BORDER_WIDTH * 2;
|
||||
pos[0] = xOff; //Math.max(Math.min(xOff, gridWidth + (BAR_WIDTH + 100) / zoom - gameWidth / zoom), 0);
|
||||
pos[1] = yOff; //Math.max(Math.min(yOff, gridWidth - gameHeight / zoom), 0);
|
||||
}
|
||||
|
||||
function getBounceOffset(frame) {
|
||||
let offsetBounce = ANIMATE_FRAMES;
|
||||
let bounceNum = BOUNCE_FRAMES.length - 1;
|
||||
while (bounceNum >= 0 && frame < offsetBounce - BOUNCE_FRAMES[bounceNum]) {
|
||||
offsetBounce -= BOUNCE_FRAMES[bounceNum];
|
||||
bounceNum--;
|
||||
}
|
||||
if (bounceNum === -1) return (offsetBounce - frame) * DROP_SPEED;
|
||||
else {
|
||||
offsetBounce -= BOUNCE_FRAMES[bounceNum];
|
||||
frame = frame - offsetBounce;
|
||||
const midFrame = BOUNCE_FRAMES[bounceNum] / 2;
|
||||
return (frame >= midFrame) ? (BOUNCE_FRAMES[bounceNum] - frame) * DROP_SPEED : frame * DROP_SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
function Rolling(value, frames) {
|
||||
let lag = 0;
|
||||
if (!frames) frames = 24;
|
||||
this.value = value;
|
||||
Object.defineProperty(this, "lag", {
|
||||
get: function() {
|
||||
return lag;
|
||||
},
|
||||
enumerable: true
|
||||
});
|
||||
this.update = function() {
|
||||
const delta = this.value - lag;
|
||||
const dir = Math.sign(delta);
|
||||
const speed = Math.abs(delta) / frames;
|
||||
const mag = Math.min(Math.abs(speed), Math.abs(delta));
|
||||
|
||||
lag += mag * dir;
|
||||
return lag;
|
||||
}
|
||||
}
|
||||
|
||||
export function addPlayer(player) {
|
||||
playerPortion[player.num] = 0;
|
||||
portionsRolling[player.num] = new Rolling(9 / consts.GRID_COUNT / consts.GRID_COUNT, ANIMATE_FRAMES);
|
||||
barProportionRolling[player.num] = new Rolling(0, ANIMATE_FRAMES);
|
||||
};
|
||||
|
||||
export function disconnect() {
|
||||
$("#wasted").fadeIn(1000);
|
||||
};
|
||||
|
||||
export function removePlayer(player) {
|
||||
delete playerPortion[player.num];
|
||||
delete portionsRolling[player.num];
|
||||
delete barProportionRolling[player.num];
|
||||
};
|
||||
|
||||
export function setUser(player) {
|
||||
user = player;
|
||||
centerOnPlayer(user, offset);
|
||||
};
|
||||
|
||||
export { reset };
|
||||
|
||||
export function updateGrid (row, col, before, after) {
|
||||
//Keep track of areas
|
||||
if (before) playerPortion[before.num]--;
|
||||
if (after) playerPortion[after.num]++;
|
||||
//Queue animation
|
||||
if (before === after || !client.allowAnimation) return;
|
||||
animateGrid.set(row, col, {
|
||||
before: before,
|
||||
after: after,
|
||||
frame: 0
|
||||
});
|
||||
};
|
||||
|
||||
export { paintDoubleBuff as paint };
|
||||
|
||||
export { update };
|
41
stack.js
41
stack.js
@ -1,41 +0,0 @@
|
||||
|
||||
|
||||
function Stack(initSize)
|
||||
{
|
||||
var len = 0;
|
||||
var arr = [];
|
||||
|
||||
this.ensureCapacity = function(size)
|
||||
{
|
||||
arr.length = Math.max(arr.length, size || 0);
|
||||
};
|
||||
|
||||
this.push = function(ele)
|
||||
{
|
||||
this[len] = ele;
|
||||
len++;
|
||||
};
|
||||
|
||||
this.pop = function()
|
||||
{
|
||||
if (len === 0)
|
||||
return;
|
||||
len--;
|
||||
var tmp = this[len];
|
||||
this[len] = undefined;
|
||||
return tmp;
|
||||
};
|
||||
|
||||
this.isEmpty = function() {
|
||||
return len === 0;
|
||||
}
|
||||
|
||||
this.ensureCapacity(initSize);
|
||||
|
||||
|
||||
Object.defineProperty(this, "length", {
|
||||
get: function() {return len;}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Stack;
|
Loading…
Reference in New Issue
Block a user