Merge pull request 'Modernise & make fun' (#3) from asch/modernise into main
All checks were successful
Build & deploy / Build & publish server image (push) Successful in 1m26s
Build & deploy / Build & deploy website (push) Successful in 1m49s

Reviewed-on: https://home.schmelczer.dev/git/git/andras/decla-red/pulls/3
This commit is contained in:
Andras Schmelczer 2026-06-21 10:43:50 +01:00
commit 5d854440ec
132 changed files with 11445 additions and 20695 deletions

View file

@ -1,10 +0,0 @@
# Path to your oh-my-zsh installation.
export ZSH="/root/.oh-my-zsh"
# https://typewritten.dev/#/git_status_indicators
ZSH_THEME="typewritten"
# Which plugins would you like to load?
# plugins=(zsh-autosuggestions nvm git)
source $ZSH/oh-my-zsh.sh

View file

@ -1,16 +0,0 @@
ARG VARIANT="14-buster"
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
RUN apt update && export DEBIAN_FRONTEND=noninteractive \
&& apt -y install --no-install-recommends git-lfs \
&& rm -rf /var/lib/apt/lists/*
RUN git lfs install
RUN npm i && npm run init
ENV ZSH_CUSTOM /root/.oh-my-zsh/
RUN git clone https://github.com/reobin/typewritten.git $ZSH_CUSTOM/themes/typewritten
RUN ln -s "$ZSH_CUSTOM/themes/typewritten/typewritten.zsh-theme" "$ZSH_CUSTOM/themes/typewritten.zsh-theme"
RUN ln -s "$ZSH_CUSTOM/themes/typewritten/async.zsh" "$ZSH_CUSTOM/themes/async"
COPY .zshrc /root/.zshrc

View file

@ -1,52 +0,0 @@
{
"name": "Node.js & TypeScript",
"build": {
"dockerfile": "Dockerfile",
"args": {
"VARIANT": "14"
}
},
"settings": {
"terminal.integrated.shell.linux": "/bin/zsh",
"files.exclude": {
"**/node_modules": true,
"**/yarn.lock": true,
"**/dist": true,
"**/lib": true,
"**/.firebase": true
},
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": ["json"],
"markdown.extension.toc.levels": "2..4",
// <style>
"workbench.iconTheme": "material-icon-theme",
"workbench.colorTheme": "Community Material Theme Palenight High Contrast",
"editor.fontFamily": "'Fira Code'",
"editor.fontLigatures": true
// </style>
},
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"yzhang.markdown-all-in-one",
"hediet.vscode-drawio",
"pkief.material-icon-theme",
"equinusocio.vsc-community-material-theme"
],
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
"workspaceFolder": "/workspace",
"mounts": [
"source=decla-red-root-node_modules-volume4,target=/workspace/node_modules,type=volume",
"source=decla-red-frontend-node_modules-volume4,target=/workspace/frontend/node_modules,type=volume",
"source=decla-red-backend-node_modules-volume4,target=/workspace/backend/node_modules,type=volume",
"source=decla-red-shared-node_modules-volume4,target=/workspace/shared/node_modules,type=volume"
],
"forwardPorts": [3000, 8080],
"postCreateCommand": "chown node:node ./**/node_modules && npm run build"
}

View file

@ -1,4 +0,0 @@
**/node_modules/**/*.js
node_modules/**/*.js
**/package-lock.json
package-lock.json

View file

@ -1,37 +0,0 @@
{
"root": true,
"env": {
"browser": true,
"es2020": true
},
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"plugins": ["unused-imports", "@typescript-eslint", "json-format", "prettier"],
"settings": {
"json/sort-package-json": true
},
"rules": {
"prettier/prettier": "error",
"no-unused-vars": "off",
"unused-imports/no-unused-imports-ts": "error",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-var-requires": "off"
}
}

View file

@ -23,7 +23,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 14
node-version-file: .nvmrc
- name: Install dependencies
# No lockfiles are committed (see .gitignore), so use `npm install`.
@ -32,6 +32,12 @@ jobs:
- name: Build (shared -> frontend -> backend)
run: npm run build
- name: Test
run: npm test
- name: Lint
run: npm run lint:check
- name: Deploy to host pages mount
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: http://forgejo:3000/andras/ci-actions/deploy-pages@main

1
.node-version Normal file
View file

@ -0,0 +1 @@
22

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
22

View file

@ -3,7 +3,7 @@
"Deserializable",
"Deserialization",
"Respawn",
"decla",
"doppler",
"overridable",
"serializable"
]

View file

@ -1,13 +1,13 @@
# 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.
# ---- Stage 1: build the shared lib, then bundle the server -------------------
# Node 14 matches the project toolchain (webpack 4 / TypeScript 4, see
# .devcontainer). The full (non-slim) image carries the build tools that
# Node 22 (current LTS) matches the project toolchain (webpack 5 / TypeScript 6,
# see .devcontainer). The full (non-slim) image carries the build tools that
# socket.io's optional native deps (bufferutil/utf-8-validate) compile against.
FROM node:14-bullseye AS build
FROM node:22-bookworm AS build
WORKDIR /app
# `shared` is consumed by the backend as `file:../shared` and is bundled into
@ -28,7 +28,7 @@ RUN cd backend && npm run build
RUN cd backend && npm prune --production
# ---- Stage 2: minimal runtime ----------------------------------------------
FROM node:14-bullseye-slim
FROM node:22-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
@ -46,5 +46,5 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/state',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"
ENTRYPOINT ["node", "dist/main.js"]
# Override these to tune the server, e.g. `--name`, `--playerLimit`, `--worldSize`.
# Override these to tune the server, e.g. `--name`, `--playerLimit`, `--scoreLimit`.
CMD ["--port", "3000"]

View file

@ -1,8 +1,8 @@
# [decla.red](https://decla.red)
# doppler
A 2-dimensional multiplayer game utilising ray tracing.
> **Available at [decla.red](https://decla.red).**
> **Available at [doppler.schmelczer.dev](https://doppler.schmelczer.dev).**
![screenshot of in-game fight](media/game-pc.png)
![three screenshots taken at iPhone SE screen size](media/collage-iphone.png)
@ -15,7 +15,7 @@ CI/CD runs on Forgejo Actions (`.forgejo/workflows/deploy.yml`). On a push to
`main` it:
- 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
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:
```sh
docker build -t declared-server .
docker run -p 3000:3000 declared-server --name "My server" --playerLimit 16
docker build -t doppler-server .
docker run -p 3000:3000 doppler-server --name "My server" --playerLimit 16
```

View file

@ -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).

View file

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

View file

@ -1,5 +0,0 @@
#!/bin/sh
set -e
NODE_ENV=production declared-server --port=3000 --name="Simonyi" --playerLimit=256 --npcCount=48 --scoreLimit=20000

View file

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

View file

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

View file

@ -1,48 +1,43 @@
{
"name": "declared-server",
"name": "doppler-server",
"version": "0.1.0",
"description": "Game server for decla.red",
"description": "Game server for doppler",
"keywords": [],
"author": "András Schmelczer <andras@schmelczer.dev> (https://schmelczer.dev/)",
"main": "dist/main.js",
"bin": {
"declared-server": "dist/main.js"
"doppler-server": "dist/main.js"
},
"engines": {
"node": ">=20"
},
"scripts": {
"build": "npx webpack --mode production",
"start": "concurrently --kill-others-on-fail \"webpack --mode development -w\" \"nodemon --legacy-watch dist/main.js\"",
"dev": "concurrently --kill-others-on-fail \"webpack --mode development -w\" \"nodemon --legacy-watch dist/main.js\"",
"try-build": "npm run build && cd dist && node main.js && cd -"
},
"dependencies": {
"@types/config": "0.0.36",
"cors": "^2.8.5",
"express": "^4.17.1",
"cors": "^2.8.6",
"express": "^5.2.1",
"gl-matrix": "3.3.0",
"minimist": "^1.2.5",
"socket.io": "^2.3.0",
"socket.io-msgpack-parser": "^2.0.0"
"minimist": "^1.2.8",
"socket.io": "^4.8.3",
"socket.io-msgpack-parser": "^3.0.2"
},
"devDependencies": {
"@types/cors": "^2.8.7",
"@types/express": "^4.17.8",
"@types/minimist": "^1.2.0",
"@types/node": "^14.11.2",
"@types/socket.io": "^2.1.11",
"clean-webpack-plugin": "^3.0.0",
"concurrently": "^5.3.0",
"file-loader": "^6.1.0",
"html-webpack-plugin": "^4.5.0",
"nodemon": "^2.0.4",
"raw-loader": "^4.0.1",
"resolve-url-loader": "^3.1.1",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/minimist": "^1.2.5",
"@types/node": "^22.0.0",
"clean-webpack-plugin": "^4.0.0",
"concurrently": "^10.0.3",
"nodemon": "^3.1.14",
"shared": "file:../shared",
"terser-webpack-plugin": "^2.3.5",
"ts-config-webpack-plugin": "^2.0.0",
"ts-loader": "^8.0.3",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",
"webpack-node-externals": "^2.5.2"
"terser-webpack-plugin": "^5.6.1",
"ts-loader": "^9.6.0",
"typescript": "^6.0.3",
"webpack": "^5.107.2",
"webpack-cli": "^7.0.3",
"webpack-node-externals": "^3.0.0"
}
}

View file

@ -0,0 +1,9 @@
import { Command } from 'shared';
// Internal request from a game object (e.g. a keystone planet flipping) asking
// the GameServer to broadcast a one-off announcement to every connected client.
export class AnnounceCommand extends Command {
public constructor(public readonly text: string) {
super();
}
}

View file

@ -1,7 +1,10 @@
import { Command } from 'shared';
export class GeneratePointsCommand extends Command {
public constructor(public readonly decla: number, public readonly red: number) {
public constructor(
public readonly blue: number,
public readonly red: number,
) {
super();
}
}

View file

@ -1,16 +1,15 @@
import { vec2 } from 'gl-matrix';
import { Random, PlanetBase, hsl, settings } from 'shared';
import { Random, PlanetBase, hsl, settings, evaluateSdf } from 'shared';
import { LampPhysical } from './objects/lamp-physical';
import { PlanetPhysical } from './objects/planet-physical';
import { PhysicalContainer } from './physics/containers/physical-container';
import { evaluateSdf } from './physics/functions/evaluate-sdf';
import { Physical } from './physics/physicals/physical';
export const createWorld = (objectContainer: PhysicalContainer, worldRadius: number) => {
export const createWorld = (objectContainer: PhysicalContainer) => {
const objects: Array<Physical> = [];
const lights: Array<Physical> = [];
for (let r = 0; r < worldRadius; r += settings.radiusSteps) {
for (let r = 0; r < settings.worldRadius; r += settings.radiusSteps) {
const circumference = 2 * Math.PI * r;
const stepCount = circumference * settings.objectsOnCircleLength;
for (let rad = 0; rad < 2 * Math.PI; rad += (2 * Math.PI) / stepCount) {
@ -45,13 +44,16 @@ export const createWorld = (objectContainer: PhysicalContainer, worldRadius: num
) {
const planet =
objects.length === 0
? new PlanetPhysical(
? // The first, central giant is the keystone "Heart": a named,
// always-contested focal objective the whole match orbits.
new PlanetPhysical(
PlanetBase.createPlanetVertices(
position,
Random.getRandomInRange(1600, 2400),
Random.getRandomInRange(1600, 2400),
Random.getRandomInRange(80, 300),
),
true,
)
: new PlanetPhysical(
PlanetBase.createPlanetVertices(
@ -67,8 +69,29 @@ export const createWorld = (objectContainer: PhysicalContainer, worldRadius: num
}
}
}
console.info('Generated planet count', objects.length);
console.info('Generated light count', lights.length);
console.info(`Generated ${objects.length} planets`);
console.info(`Generated ${lights.length} light`);
// Associate each lamp with its NEAREST planet, so a planet can repaint "its"
// lamps to the owning team's colour when it flips. Lamps are already placed by
// proximity during world-gen, so the nearest planet is the one whose capture
// they should advertise. Distances use the planet SDF (negative inside), which
// is exactly the "closest planet" metric we want.
const planets = objects.filter((o): o is PlanetPhysical => o instanceof PlanetPhysical);
lights
.filter((l): l is LampPhysical => l instanceof LampPhysical)
.forEach((lamp) => {
let nearest: PlanetPhysical | undefined;
let nearestDistance = Infinity;
planets.forEach((planet) => {
const distance = planet.distance(lamp.center);
if (distance < nearestDistance) {
nearestDistance = distance;
nearest = planet;
}
});
nearest?.addLamp(lamp);
});
[...objects, ...lights].forEach((o) => objectContainer.addObject(o));
};

Some files were not shown because too many files have changed in this diff Show more