Modernise & make fun #3
28 changed files with 134 additions and 220 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -3,7 +3,7 @@
|
||||||
"Deserializable",
|
"Deserializable",
|
||||||
"Deserialization",
|
"Deserialization",
|
||||||
"Respawn",
|
"Respawn",
|
||||||
"decla",
|
"doppler",
|
||||||
"overridable",
|
"overridable",
|
||||||
"serializable"
|
"serializable"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# decla.red game server (the `declared-server` package).
|
# doppler game server (the `doppler-server` package).
|
||||||
# The frontend is a static site and is NOT built here — see .forgejo/workflows.
|
# The frontend is a static site and is NOT built here — see .forgejo/workflows.
|
||||||
|
|
||||||
# ---- Stage 1: build the shared lib, then bundle the server -------------------
|
# ---- Stage 1: build the shared lib, then bundle the server -------------------
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -1,8 +1,8 @@
|
||||||
# [decla.red](https://decla.red)
|
# doppler
|
||||||
|
|
||||||
A 2-dimensional multiplayer game utilising ray tracing.
|
A 2-dimensional multiplayer game utilising ray tracing.
|
||||||
|
|
||||||
> **Available at [decla.red](https://decla.red).**
|
> **Available at [doppler.schmelczer.dev](https://doppler.schmelczer.dev).**
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
@ -15,7 +15,7 @@ CI/CD runs on Forgejo Actions (`.forgejo/workflows/deploy.yml`). On a push to
|
||||||
`main` it:
|
`main` it:
|
||||||
|
|
||||||
- builds the static frontend and rsyncs `frontend/dist/` to the `/pages/declared`
|
- builds the static frontend and rsyncs `frontend/dist/` to the `/pages/declared`
|
||||||
mount on the runner host, and
|
mount on the runner host (the mount keeps its pre-rebrand name), and
|
||||||
- builds the server image from the root `Dockerfile` and pushes it to the Forgejo
|
- builds the server image from the root `Dockerfile` and pushes it to the Forgejo
|
||||||
container registry as `<registry>/<owner>/<repo>-server`.
|
container registry as `<registry>/<owner>/<repo>-server`.
|
||||||
|
|
||||||
|
|
@ -30,6 +30,6 @@ edit it to add or remove game-server origins.
|
||||||
Run the server image locally:
|
Run the server image locally:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker build -t declared-server .
|
docker build -t doppler-server .
|
||||||
docker run -p 3000:3000 declared-server --name "My server" --playerLimit 16
|
docker run -p 3000:3000 doppler-server --name "My server" --playerLimit 16
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# Setup a decla.red server on a fresh debian/ubuntu machine
|
|
||||||
|
|
||||||
> The method was tested on Debian 10 DigitalOcean droplets.
|
|
||||||
|
|
||||||
- Copy the files from this directory to the target machine.
|
|
||||||
- Execute the following commands.
|
|
||||||
- ```sh
|
|
||||||
chmod +x setup.sh
|
|
||||||
./setup.sh serverX
|
|
||||||
```
|
|
||||||
|
|
||||||
> Where `serverX` is the serverX.decla.red subdomain pointing to the server.
|
|
||||||
|
|
||||||
- Optionally, change the arguments in [declared-servers.sh](declared-servers.sh).
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=declared-server
|
|
||||||
|
|
||||||
Requires=basic.target network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/bin/sh /root/declared-servers.sh
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
NODE_ENV=production declared-server --port=3000 --name="Simonyi" --playerLimit=256 --npcCount=48 --scoreLimit=20000
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
user www-data;
|
|
||||||
worker_processes auto;
|
|
||||||
pid /run/nginx.pid;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 768;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
sendfile on;
|
|
||||||
tcp_nopush on;
|
|
||||||
tcp_nodelay on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
types_hash_max_size 2048;
|
|
||||||
server_tokens off;
|
|
||||||
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
|
||||||
ssl_prefer_server_ciphers on;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log;
|
|
||||||
error_log /var/log/nginx/error.log;
|
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
|
||||||
default upgrade;
|
|
||||||
'' close;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
|
|
||||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
|
||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
|
||||||
ssl_certificate /etc/letsencrypt/live/serverName.decla.red/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/serverName.decla.red/privkey.pem;
|
|
||||||
|
|
||||||
server_name serverName.decla.red;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:3000/;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
echo "Please specify a domain" 1>&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
domain="$1"
|
|
||||||
|
|
||||||
apt install -y curl
|
|
||||||
curl -sL https://deb.nodesource.com/setup_15.x | bash -
|
|
||||||
apt update
|
|
||||||
apt install -y nodejs nginx certbot python3-certbot-nginx
|
|
||||||
npm i -g declared-server
|
|
||||||
|
|
||||||
certbot certonly --nginx -m schmelczerandras@gmail.com -d "$domain".decla.red --agree-tos --non-interactive
|
|
||||||
|
|
||||||
mv nginx.conf /etc/nginx/nginx.conf
|
|
||||||
sed -i "s/serverName/$domain/g" /etc/nginx/nginx.conf
|
|
||||||
nginx -s reload
|
|
||||||
|
|
||||||
chmod +x /root/declared-servers.sh
|
|
||||||
chown root:root /root/declared-servers.sh
|
|
||||||
cp declared-servers.service /etc/systemd/system/declared-servers.service
|
|
||||||
systemctl enable declared-servers.service
|
|
||||||
systemctl start declared-servers
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "declared-server",
|
"name": "doppler-server",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Game server for decla.red",
|
"description": "Game server for doppler",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "András Schmelczer <andras@schmelczer.dev> (https://schmelczer.dev/)",
|
"author": "András Schmelczer <andras@schmelczer.dev> (https://schmelczer.dev/)",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"declared-server": "dist/main.js"
|
"doppler-server": "dist/main.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx webpack --mode production",
|
"build": "npx webpack --mode production",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Command } from 'shared';
|
||||||
|
|
||||||
export class GeneratePointsCommand extends Command {
|
export class GeneratePointsCommand extends Command {
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly decla: number,
|
public readonly blue: number,
|
||||||
public readonly red: number,
|
public readonly red: number,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export class GameServer extends CommandReceiver {
|
||||||
private deltaTimes!: Array<number>;
|
private deltaTimes!: Array<number>;
|
||||||
private deltaTimeCalculator!: DeltaTimeCalculator;
|
private deltaTimeCalculator!: DeltaTimeCalculator;
|
||||||
|
|
||||||
private declaPoints = 0;
|
private bluePoints = 0;
|
||||||
private redPoints = 0;
|
private redPoints = 0;
|
||||||
private matchPointAnnounced: Partial<Record<CharacterTeam, boolean>> = {};
|
private matchPointAnnounced: Partial<Record<CharacterTeam, boolean>> = {};
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ export class GameServer extends CommandReceiver {
|
||||||
);
|
);
|
||||||
this.deltaTimeCalculator = new DeltaTimeCalculator();
|
this.deltaTimeCalculator = new DeltaTimeCalculator();
|
||||||
this.deltaTimes = [];
|
this.deltaTimes = [];
|
||||||
this.declaPoints = 0;
|
this.bluePoints = 0;
|
||||||
this.redPoints = 0;
|
this.redPoints = 0;
|
||||||
this.matchPointAnnounced = {};
|
this.matchPointAnnounced = {};
|
||||||
this.isInEndGame = false;
|
this.isInEndGame = false;
|
||||||
|
|
@ -118,19 +118,19 @@ export class GameServer extends CommandReceiver {
|
||||||
this.handlePhysics();
|
this.handlePhysics();
|
||||||
}
|
}
|
||||||
|
|
||||||
private addPoints({ decla, red }: GeneratePointsCommand) {
|
private addPoints({ blue, red }: GeneratePointsCommand) {
|
||||||
if (this.isInEndGame) {
|
if (this.isInEndGame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.declaPoints += decla;
|
this.bluePoints += blue;
|
||||||
this.redPoints += red;
|
this.redPoints += red;
|
||||||
if (this.declaPoints >= this.options.scoreLimit) {
|
if (this.bluePoints >= this.options.scoreLimit) {
|
||||||
this.endGame(CharacterTeam.decla);
|
this.endGame(CharacterTeam.blue);
|
||||||
} else if (this.redPoints >= this.options.scoreLimit) {
|
} else if (this.redPoints >= this.options.scoreLimit) {
|
||||||
this.endGame(CharacterTeam.red);
|
this.endGame(CharacterTeam.red);
|
||||||
} else {
|
} else {
|
||||||
this.announceMatchPointOnce(CharacterTeam.decla, this.declaPoints);
|
this.announceMatchPointOnce(CharacterTeam.blue, this.bluePoints);
|
||||||
this.announceMatchPointOnce(CharacterTeam.red, this.redPoints);
|
this.announceMatchPointOnce(CharacterTeam.red, this.redPoints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +179,7 @@ export class GameServer extends CommandReceiver {
|
||||||
if ((this.timeSinceLastPointUpdate += delta) > 0.5) {
|
if ((this.timeSinceLastPointUpdate += delta) > 0.5) {
|
||||||
this.timeSinceLastPointUpdate = 0;
|
this.timeSinceLastPointUpdate = 0;
|
||||||
this.players.queueCommandForEachClient(
|
this.players.queueCommandForEachClient(
|
||||||
new UpdateGameState(this.declaPoints, this.redPoints, this.options.scoreLimit),
|
new UpdateGameState(this.bluePoints, this.redPoints, this.options.scoreLimit),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,7 +224,7 @@ export class GameServer extends CommandReceiver {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get gameProgress(): number {
|
private get gameProgress(): number {
|
||||||
return (Math.max(this.declaPoints, this.redPoints) / this.options.scoreLimit) * 100;
|
return (Math.max(this.bluePoints, this.redPoints) / this.options.scoreLimit) * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get serverInfo(): ServerInformation {
|
public get serverInfo(): ServerInformation {
|
||||||
|
|
|
||||||
|
|
@ -151,10 +151,10 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
private getPoints(game: CommandReceiver) {
|
private getPoints(game: CommandReceiver) {
|
||||||
if (!this.isAlive && !this.hasGeneratedPoints) {
|
if (!this.isAlive && !this.hasGeneratedPoints) {
|
||||||
this.hasGeneratedPoints = true;
|
this.hasGeneratedPoints = true;
|
||||||
const decla = this.team === CharacterTeam.decla ? 0 : settings.playerKillPoint;
|
const blue = this.team === CharacterTeam.blue ? 0 : settings.playerKillPoint;
|
||||||
const red = this.team === CharacterTeam.red ? 0 : settings.playerKillPoint;
|
const red = this.team === CharacterTeam.red ? 0 : settings.playerKillPoint;
|
||||||
|
|
||||||
game.handleCommand(new GeneratePointsCommand(decla, red));
|
game.handleCommand(new GeneratePointsCommand(blue, red));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -443,6 +443,8 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
|
|
||||||
this.setDirection(vec2.length(sumForce) === 0 ? vec2.fromValues(0, -1) : sumForce);
|
this.setDirection(vec2.length(sumForce) === 0 ? vec2.fromValues(0, -1) : sumForce);
|
||||||
} else {
|
} else {
|
||||||
|
this.carryWithRotatingPlanet(deltaTimeInSeconds);
|
||||||
|
|
||||||
const leftFootGravity = this.currentPlanet!.getForce(this.leftFoot.center);
|
const leftFootGravity = this.currentPlanet!.getForce(this.leftFoot.center);
|
||||||
const rightFootGravity = this.currentPlanet!.getForce(this.rightFoot.center);
|
const rightFootGravity = this.currentPlanet!.getForce(this.rightFoot.center);
|
||||||
|
|
||||||
|
|
@ -493,6 +495,38 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// While standing on a planet, ride its spin: rigidly rotate the whole body
|
||||||
|
// around the planet centre by the same per-tick angle the renderer and the
|
||||||
|
// collision SDF use, so the player is carried around and turned with the
|
||||||
|
// surface instead of it sliding frictionlessly underneath. The positional
|
||||||
|
// change becomes velocity in setPropertyUpdates, so the motion syncs and the
|
||||||
|
// client extrapolates it smoothly.
|
||||||
|
private carryWithRotatingPlanet(deltaTimeInSeconds: number) {
|
||||||
|
const planet = this.currentPlanet;
|
||||||
|
if (!planet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative: the rendered/collidable surface turns by R(-rotation) — see
|
||||||
|
// PlanetPhysical.distance (toLocalFrame) and planet-shape.ts.
|
||||||
|
const angle = -planet.angularVelocity * deltaTimeInSeconds;
|
||||||
|
const center = planet.center;
|
||||||
|
|
||||||
|
this.head.center = vec2.rotate(vec2.create(), this.head.center, center, angle);
|
||||||
|
this.leftFoot.center = vec2.rotate(
|
||||||
|
vec2.create(),
|
||||||
|
this.leftFoot.center,
|
||||||
|
center,
|
||||||
|
angle,
|
||||||
|
);
|
||||||
|
this.rightFoot.center = vec2.rotate(
|
||||||
|
vec2.create(),
|
||||||
|
this.rightFoot.center,
|
||||||
|
center,
|
||||||
|
angle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private keepPosture() {
|
private keepPosture() {
|
||||||
const center = this.center;
|
const center = this.center;
|
||||||
this.springMove(
|
this.springMove(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||||
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
||||||
|
import { depenetrateCircle } from '../physics/functions/depenetrate-circle';
|
||||||
import { moveCircle } from '../physics/functions/move-circle';
|
import { moveCircle } from '../physics/functions/move-circle';
|
||||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
||||||
|
|
@ -100,6 +101,8 @@ export class CirclePhysical extends CommandReceiver implements Circle, DynamicPh
|
||||||
.filter((b) => b.gameObject !== this.gameObject && b.canCollide);
|
.filter((b) => b.gameObject !== this.gameObject && b.canCollide);
|
||||||
this.radius -= vec2.length(delta);
|
this.radius -= vec2.length(delta);
|
||||||
|
|
||||||
|
depenetrateCircle(this, intersecting);
|
||||||
|
|
||||||
const { normal, hitSurface, hitObject } = moveCircle(this, delta, intersecting);
|
const { normal, hitSurface, hitObject } = moveCircle(this, delta, intersecting);
|
||||||
|
|
||||||
if (hitSurface) {
|
if (hitSurface) {
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,11 @@ export class PlayerContainer {
|
||||||
|
|
||||||
private getTeamOfNextPlayer(isNpc = false): CharacterTeam {
|
private getTeamOfNextPlayer(isNpc = false): CharacterTeam {
|
||||||
const players = isNpc ? this.players : this._players;
|
const players = isNpc ? this.players : this._players;
|
||||||
const declaCount = players.filter((p) => p.team === CharacterTeam.decla).length;
|
const blueCount = players.filter((p) => p.team === CharacterTeam.blue).length;
|
||||||
const redCount = players.filter((p) => p.team === CharacterTeam.red).length;
|
const redCount = players.filter((p) => p.team === CharacterTeam.red).length;
|
||||||
|
|
||||||
if ((declaCount === redCount && Random.getRandom() >= 0.5) || declaCount < redCount) {
|
if ((blueCount === redCount && Random.getRandom() >= 0.5) || blueCount < redCount) {
|
||||||
return CharacterTeam.decla;
|
return CharacterTeam.blue;
|
||||||
} else {
|
} else {
|
||||||
return CharacterTeam.red;
|
return CharacterTeam.red;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"projects": {
|
|
||||||
"default": "decla-red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"hosting": {
|
|
||||||
"public": "dist",
|
|
||||||
"ignore": [
|
|
||||||
"firebase.json",
|
|
||||||
"**/.*",
|
|
||||||
"**/node_modules/**"
|
|
||||||
],
|
|
||||||
"rewrites": [
|
|
||||||
{
|
|
||||||
"source": "**",
|
|
||||||
"destination": "/index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "decla.red-frontend",
|
"name": "doppler-frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "decla.red-frontend",
|
"name": "doppler-frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@plausible-analytics/tracker": "^0.4.5",
|
"@plausible-analytics/tracker": "^0.4.5",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "decla.red-frontend",
|
"name": "doppler-frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#b7455e" />
|
<meta name="theme-color" content="#b7455e" />
|
||||||
|
|
||||||
<meta property="og:url" content="https://decla.red" />
|
<meta property="og:url" content="https://doppler.schmelczer.dev" />
|
||||||
<meta property="og:image" content="https://decla.red/og-image.png" />
|
<meta property="og:image" content="https://doppler.schmelczer.dev/og-image.png" />
|
||||||
<meta property="og:image:width" content="2086" />
|
<meta property="og:image:width" content="2086" />
|
||||||
<meta property="og:image:height" content="940" />
|
<meta property="og:image:height" content="940" />
|
||||||
<meta
|
<meta
|
||||||
property="og:image:alt"
|
property="og:image:alt"
|
||||||
content="Dimly lit planets surrounding a text saying 'decla.red'"
|
content="Dimly lit planets surrounding a text saying 'doppler'"
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#b33951" />
|
<meta name="theme-color" content="#b33951" />
|
||||||
<meta
|
<meta
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="static/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="static/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="static/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="static/favicon-16x16.png" />
|
||||||
|
|
||||||
<title>decla.red</title>
|
<title>doppler</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
<div id="overlay"></div>
|
<div id="overlay"></div>
|
||||||
|
|
||||||
<section id="landing-ui">
|
<section id="landing-ui">
|
||||||
<h1>decla.<span class="red">red</span></h1>
|
<h1>doppler</h1>
|
||||||
<form id="join-game-form">
|
<form id="join-game-form">
|
||||||
<fieldset class="content">
|
<fieldset class="content">
|
||||||
<legend>Join a game</legend>
|
<legend>Join a game</legend>
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,18 @@ h1 {
|
||||||
font-size: 6rem;
|
font-size: 6rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-bottom: $medium-padding;
|
padding-bottom: $medium-padding;
|
||||||
|
width: fit-content;
|
||||||
|
margin-inline: auto;
|
||||||
|
background: linear-gradient(90deg, $bright-blue, $bright-red);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
|
||||||
|
&::selection {
|
||||||
|
color: white;
|
||||||
|
-webkit-text-fill-color: white;
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: $height-breakpoint) {
|
@media (max-width: $height-breakpoint) {
|
||||||
font-size: 4.5rem;
|
font-size: 4.5rem;
|
||||||
|
|
@ -51,15 +63,6 @@ h1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.red {
|
|
||||||
color: $accent;
|
|
||||||
|
|
||||||
&::selection {
|
|
||||||
color: $accent;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
canvas {
|
canvas {
|
||||||
|
|
@ -126,14 +129,14 @@ body {
|
||||||
border-radius: 1000px;
|
border-radius: 1000px;
|
||||||
transition: transform 200ms;
|
transition: transform 200ms;
|
||||||
|
|
||||||
&.decla {
|
&.blue {
|
||||||
color: $bright-decla;
|
color: $bright-blue;
|
||||||
|
|
||||||
.health {
|
.health {
|
||||||
background-color: $bright-decla;
|
background-color: $bright-blue;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
background-color: $bright-decla;
|
background-color: $bright-blue;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,8 +214,8 @@ body {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
animation: falling-point 2s ease-out forwards;
|
animation: falling-point 2s ease-out forwards;
|
||||||
|
|
||||||
&.decla {
|
&.blue {
|
||||||
color: $bright-decla;
|
color: $bright-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red {
|
&.red {
|
||||||
|
|
@ -232,8 +235,8 @@ body {
|
||||||
mask-image: url('../static/chevron.svg');
|
mask-image: url('../static/chevron.svg');
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
|
|
||||||
&.decla {
|
&.blue {
|
||||||
background-color: $bright-decla;
|
background-color: $bright-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red {
|
&.red {
|
||||||
|
|
@ -324,8 +327,8 @@ body {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.decla {
|
.blue {
|
||||||
color: $bright-decla;
|
color: $bright-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.red {
|
.red {
|
||||||
|
|
@ -374,10 +377,10 @@ body {
|
||||||
transition: width $animation-time;
|
transition: width $animation-time;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fill.decla {
|
.fill.blue {
|
||||||
right: 50%;
|
right: 50%;
|
||||||
background: $bright-decla;
|
background: $bright-blue;
|
||||||
color: $bright-decla;
|
color: $bright-blue;
|
||||||
border-radius: 1000px 0 0 1000px;
|
border-radius: 1000px 0 0 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,7 +428,7 @@ body {
|
||||||
font-size $animation-time;
|
font-size $animation-time;
|
||||||
}
|
}
|
||||||
|
|
||||||
.score.decla {
|
.score.blue {
|
||||||
right: calc(50% + #{$small-padding});
|
right: calc(50% + #{$small-padding});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -438,10 +441,10 @@ body {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.score.decla.leading {
|
.score.blue.leading {
|
||||||
text-shadow:
|
text-shadow:
|
||||||
0 0 3px rgba(0, 0, 0, 0.95),
|
0 0 3px rgba(0, 0, 0, 0.95),
|
||||||
0 0 8px $bright-decla;
|
0 0 8px $bright-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.score.red.leading {
|
.score.red.leading {
|
||||||
|
|
@ -471,9 +474,9 @@ body {
|
||||||
border-bottom: 5px solid currentColor;
|
border-bottom: 5px solid currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.decla {
|
&.blue {
|
||||||
right: calc(50% + #{$small-padding});
|
right: calc(50% + #{$small-padding});
|
||||||
color: $bright-decla;
|
color: $bright-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red {
|
&.red {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { init as plausibleInit } from '@plausible-analytics/tracker';
|
import { init as plausibleInit } from '@plausible-analytics/tracker';
|
||||||
|
|
||||||
const ANALYTICS_AUTO_CAPTURE_PAGEVIEWS = true;
|
const ANALYTICS_AUTO_CAPTURE_PAGEVIEWS = true;
|
||||||
const ANALYTICS_DOMAIN = 'decla.red';
|
const ANALYTICS_DOMAIN = 'doppler.schmelczer.dev';
|
||||||
const ANALYTICS_ENDPOINT = 'https://stats.schmelczer.dev/status';
|
const ANALYTICS_ENDPOINT = 'https://stats.schmelczer.dev/status';
|
||||||
const ANALYTICS_LOGGING = process.env.NODE_ENV !== 'production';
|
const ANALYTICS_LOGGING = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
/**
|
/**
|
||||||
* Hardcoded list of game servers the landing page offers to players.
|
* Hardcoded list of game servers the landing page offers to players.
|
||||||
*
|
*
|
||||||
* Each entry is the public origin of a dockerized `declared-server` instance.
|
* Each entry is the public origin of a dockerized `doppler-server` instance.
|
||||||
* The join screen polls `<origin>/state` (see `serverInformationEndpoint`) and
|
* The join screen polls `<origin>/state` (see `serverInformationEndpoint`) and
|
||||||
* only shows a server once it responds, so listing an offline origin here is
|
* only shows a server once it responds, so listing an offline origin here is
|
||||||
* harmless. Add or remove origins as you deploy more server containers.
|
* harmless. Add or remove origins as you deploy more server containers.
|
||||||
*/
|
*/
|
||||||
|
// This origin predates the rebrand; update it once the game server is
|
||||||
|
// redeployed under a doppler subdomain.
|
||||||
const productionServers: Array<string> = ['https://declared.schmelczer.dev'];
|
const productionServers: Array<string> = ['https://declared.schmelczer.dev'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the page is served from localhost (i.e. the webpack-dev-server), also
|
* When the page is served from localhost (i.e. the webpack-dev-server), also
|
||||||
* offer the local backend so a `npm start` server can be joined during
|
* offer the local backend so a `npm start` server can be joined during
|
||||||
* development. The backend's default port is 3000 (see backend/src/options.ts).
|
* development. The backend's default port is 3000 (see backend/src/options.ts).
|
||||||
* In production the hostname is `declared.schmelczer.dev`, so this never leaks.
|
* In production the page is served from its own hostname, so this never leaks.
|
||||||
*/
|
*/
|
||||||
const isDevelopment =
|
const isDevelopment =
|
||||||
typeof location !== 'undefined' &&
|
typeof location !== 'undefined' &&
|
||||||
|
|
|
||||||
|
|
@ -8,35 +8,35 @@ import { CharacterTeam, UpdateGameState, clamp, settings } from 'shared';
|
||||||
export class Scoreboard {
|
export class Scoreboard {
|
||||||
public readonly element = document.createElement('div');
|
public readonly element = document.createElement('div');
|
||||||
|
|
||||||
private readonly declaFill = document.createElement('div');
|
private readonly blueFill = document.createElement('div');
|
||||||
private readonly redFill = document.createElement('div');
|
private readonly redFill = document.createElement('div');
|
||||||
private readonly declaScore = document.createElement('span');
|
private readonly blueScore = document.createElement('span');
|
||||||
private readonly redScore = document.createElement('span');
|
private readonly redScore = document.createElement('span');
|
||||||
private readonly youMarker = document.createElement('div');
|
private readonly youMarker = document.createElement('div');
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.element.className = 'planet-progress';
|
this.element.className = 'planet-progress';
|
||||||
|
|
||||||
this.declaFill.className = 'fill decla';
|
this.blueFill.className = 'fill blue';
|
||||||
this.redFill.className = 'fill red';
|
this.redFill.className = 'fill red';
|
||||||
|
|
||||||
this.declaScore.className = 'score decla';
|
this.blueScore.className = 'score blue';
|
||||||
this.redScore.className = 'score red';
|
this.redScore.className = 'score red';
|
||||||
|
|
||||||
this.youMarker.className = 'you-marker';
|
this.youMarker.className = 'you-marker';
|
||||||
this.youMarker.innerText = 'YOU';
|
this.youMarker.innerText = 'YOU';
|
||||||
|
|
||||||
this.element.append(
|
this.element.append(
|
||||||
this.declaFill,
|
this.blueFill,
|
||||||
this.redFill,
|
this.redFill,
|
||||||
this.declaScore,
|
this.blueScore,
|
||||||
this.redScore,
|
this.redScore,
|
||||||
this.youMarker,
|
this.youMarker,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(
|
public update(
|
||||||
{ declaCount, redCount, limit }: UpdateGameState,
|
{ blueCount, redCount, limit }: UpdateGameState,
|
||||||
localTeam: CharacterTeam | undefined,
|
localTeam: CharacterTeam | undefined,
|
||||||
) {
|
) {
|
||||||
const { scoreboardHalfWidthPercent, scoreboardMinFillPercent } = settings;
|
const { scoreboardHalfWidthPercent, scoreboardMinFillPercent } = settings;
|
||||||
|
|
@ -46,23 +46,23 @@ export class Scoreboard {
|
||||||
clamp(count / limit, 0, 1) * scoreboardHalfWidthPercent,
|
clamp(count / limit, 0, 1) * scoreboardHalfWidthPercent,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.declaFill.style.width = fraction(declaCount) + '%';
|
this.blueFill.style.width = fraction(blueCount) + '%';
|
||||||
this.redFill.style.width = fraction(redCount) + '%';
|
this.redFill.style.width = fraction(redCount) + '%';
|
||||||
|
|
||||||
const isMatchPoint = (count: number) =>
|
const isMatchPoint = (count: number) =>
|
||||||
count / limit >= settings.matchPointScoreRatio;
|
count / limit >= settings.matchPointScoreRatio;
|
||||||
this.declaFill.classList.toggle('match-point', isMatchPoint(declaCount));
|
this.blueFill.classList.toggle('match-point', isMatchPoint(blueCount));
|
||||||
this.redFill.classList.toggle('match-point', isMatchPoint(redCount));
|
this.redFill.classList.toggle('match-point', isMatchPoint(redCount));
|
||||||
|
|
||||||
this.declaScore.innerText = String(Math.round(declaCount));
|
this.blueScore.innerText = String(Math.round(blueCount));
|
||||||
this.redScore.innerText = String(Math.round(redCount));
|
this.redScore.innerText = String(Math.round(redCount));
|
||||||
|
|
||||||
this.declaScore.classList.toggle('leading', declaCount > redCount);
|
this.blueScore.classList.toggle('leading', blueCount > redCount);
|
||||||
this.redScore.classList.toggle('leading', redCount > declaCount);
|
this.redScore.classList.toggle('leading', redCount > blueCount);
|
||||||
|
|
||||||
if (localTeam === CharacterTeam.decla || localTeam === CharacterTeam.red) {
|
if (localTeam === CharacterTeam.blue || localTeam === CharacterTeam.red) {
|
||||||
this.youMarker.style.display = '';
|
this.youMarker.style.display = '';
|
||||||
this.youMarker.classList.toggle('decla', localTeam === CharacterTeam.decla);
|
this.youMarker.classList.toggle('blue', localTeam === CharacterTeam.blue);
|
||||||
this.youMarker.classList.toggle('red', localTeam === CharacterTeam.red);
|
this.youMarker.classList.toggle('red', localTeam === CharacterTeam.red);
|
||||||
} else {
|
} else {
|
||||||
this.youMarker.style.display = 'none';
|
this.youMarker.style.display = 'none';
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export class Tutorial extends CommandReceiver {
|
||||||
}
|
}
|
||||||
if (standing) {
|
if (standing) {
|
||||||
const drift = standing.ownership - this.standingBaseline;
|
const drift = standing.ownership - this.standingBaseline;
|
||||||
const progress = player.team === CharacterTeam.decla ? -drift : drift;
|
const progress = player.team === CharacterTeam.blue ? -drift : drift;
|
||||||
if (progress > captureProgressThreshold) {
|
if (progress > captureProgressThreshold) {
|
||||||
this.finish();
|
this.finish();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ $breakpoint: 700px;
|
||||||
$height-breakpoint: 500px;
|
$height-breakpoint: 500px;
|
||||||
$large-icon: 48px;
|
$large-icon: 48px;
|
||||||
$small-icon: 32px;
|
$small-icon: 32px;
|
||||||
$bright-decla: #4069a5;
|
$bright-blue: #4069a5;
|
||||||
$bright-red: #d15652;
|
$bright-red: #d15652;
|
||||||
$bright-neutral: #ccc;
|
$bright-neutral: #ccc;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bright-decla: #{$bright-decla};
|
--bright-blue: #{$bright-blue};
|
||||||
--bright-red: #{$bright-red};
|
--bright-red: #{$bright-red};
|
||||||
--bright-neutral: #{$bright-neutral};
|
--bright-neutral: #{$bright-neutral};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Command } from '../command';
|
||||||
@serializable
|
@serializable
|
||||||
export class UpdateGameState extends Command {
|
export class UpdateGameState extends Command {
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly declaCount: number,
|
public readonly blueCount: number,
|
||||||
public readonly redCount: number,
|
public readonly redCount: number,
|
||||||
public readonly limit: number,
|
public readonly limit: number,
|
||||||
) {
|
) {
|
||||||
|
|
@ -12,6 +12,6 @@ export class UpdateGameState extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
public toArray(): Array<any> {
|
||||||
return [this.declaCount, this.redCount, this.limit];
|
return [this.blueCount, this.redCount, this.limit];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { serializable } from '../../serialization/serializable';
|
||||||
import { GameObject } from '../game-object';
|
import { GameObject } from '../game-object';
|
||||||
|
|
||||||
export enum CharacterTeam {
|
export enum CharacterTeam {
|
||||||
decla = 'decla',
|
blue = 'blue',
|
||||||
neutral = 'neutral',
|
neutral = 'neutral',
|
||||||
red = 'red',
|
red = 'red',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { rgb255 } from './helper/rgb255';
|
import { rgb255 } from './helper/rgb255';
|
||||||
import { CharacterTeam } from './objects/types/character-base';
|
import { CharacterTeam } from './objects/types/character-base';
|
||||||
|
|
||||||
const declaColor = rgb255(64, 105, 165);
|
const blueColor = rgb255(64, 105, 165);
|
||||||
const neutralColor = rgb255(82, 165, 64);
|
const neutralColor = rgb255(82, 165, 64);
|
||||||
const redColor = rgb255(209, 86, 82);
|
const redColor = rgb255(209, 86, 82);
|
||||||
const q = 2.5;
|
const q = 2.5;
|
||||||
const declaColorDim = rgb255(64 * q, 105 * q, 165 * q);
|
const blueColorDim = rgb255(64 * q, 105 * q, 165 * q);
|
||||||
const redColorDim = rgb255(209 * q, 86 * q, 82 * q);
|
const redColorDim = rgb255(209 * q, 86 * q, 82 * q);
|
||||||
const declaPlanetColor = declaColorDim;
|
const bluePlanetColor = blueColorDim;
|
||||||
const redPlanetColor = redColorDim;
|
const redPlanetColor = redColorDim;
|
||||||
|
|
||||||
export const settings = {
|
export const settings = {
|
||||||
|
|
@ -64,8 +64,8 @@ export const settings = {
|
||||||
chargeShotSpeedMax: 3400,
|
chargeShotSpeedMax: 3400,
|
||||||
playerColorIndexOffset: 3,
|
playerColorIndexOffset: 3,
|
||||||
backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)],
|
backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)],
|
||||||
declaColor,
|
blueColor,
|
||||||
declaPlanetColor,
|
bluePlanetColor,
|
||||||
npcNames: [
|
npcNames: [
|
||||||
'Adam',
|
'Adam',
|
||||||
'Andrew',
|
'Andrew',
|
||||||
|
|
@ -119,12 +119,12 @@ export const settings = {
|
||||||
redColor,
|
redColor,
|
||||||
redPlanetColor,
|
redPlanetColor,
|
||||||
colorIndices: {
|
colorIndices: {
|
||||||
[CharacterTeam.decla]: 0,
|
[CharacterTeam.blue]: 0,
|
||||||
[CharacterTeam.neutral]: 1,
|
[CharacterTeam.neutral]: 1,
|
||||||
[CharacterTeam.red]: 2,
|
[CharacterTeam.red]: 2,
|
||||||
},
|
},
|
||||||
palette: [declaColor, neutralColor, redColor],
|
palette: [blueColor, neutralColor, redColor],
|
||||||
paletteDim: [declaColorDim, neutralColor, redColorDim],
|
paletteDim: [blueColorDim, neutralColor, redColorDim],
|
||||||
targetPhysicsDeltaTimeInSeconds: 1 / 200,
|
targetPhysicsDeltaTimeInSeconds: 1 / 200,
|
||||||
inViewAreaSize: 1920 * 1080 * 4,
|
inViewAreaSize: 1920 * 1080 * 4,
|
||||||
scoreboardHalfWidthPercent: 50,
|
scoreboardHalfWidthPercent: 50,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue