Compare commits
5 commits
a5d1ed6e29
...
7412bc8af5
| Author | SHA1 | Date | |
|---|---|---|---|
| 7412bc8af5 | |||
| 913abb7642 | |||
| 52ca1b8844 | |||
| 018df228df | |||
| 7a285549b5 |
47 changed files with 6236 additions and 19917 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
**/node_modules/**/*.js
|
||||
node_modules/**/*.js
|
||||
**/package-lock.json
|
||||
package-lock.json
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,10 @@
|
|||
# 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
|
||||
|
||||
|
|
|
|||
|
|
@ -10,39 +10,31 @@
|
|||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 decla: number,
|
||||
public readonly red: number,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { PhysicalContainer } from './physics/containers/physical-container';
|
||||
import ioserver from 'socket.io';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import {
|
||||
TransportEvents,
|
||||
deserialize,
|
||||
|
|
@ -62,7 +62,10 @@ export class GameServer extends CommandReceiver {
|
|||
[GeneratePointsCommand.type]: this.addPoints.bind(this),
|
||||
};
|
||||
|
||||
constructor(private readonly io: ioserver.Server, private options: Options) {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private options: Options,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.serverName = options.name;
|
||||
|
|
@ -70,7 +73,7 @@ export class GameServer extends CommandReceiver {
|
|||
|
||||
this.initialize();
|
||||
|
||||
io.on('connection', (socket: SocketIO.Socket) => {
|
||||
io.on('connection', (socket: Socket) => {
|
||||
socket.on(TransportEvents.PlayerJoining, (playerInfo: PlayerInformation) => {
|
||||
try {
|
||||
const player = this.players.createPlayer(playerInfo, socket);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import ioserver from 'socket.io';
|
||||
import { Server as IoServer } from 'socket.io';
|
||||
import express from 'express';
|
||||
import { Server } from 'http';
|
||||
import cors from 'cors';
|
||||
|
|
@ -22,7 +22,13 @@ Random.seed = options.seed;
|
|||
|
||||
const app = express();
|
||||
const server = new Server(app);
|
||||
const io = ioserver(server, { parser } as any);
|
||||
const io = new IoServer(server, {
|
||||
parser,
|
||||
cors: {
|
||||
origin: true,
|
||||
credentials: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const gameServer = new GameServer(io, options);
|
||||
|
||||
|
|
|
|||
|
|
@ -88,9 +88,10 @@ export class CirclePhysical extends CommandReceiver implements Circle, DynamicPh
|
|||
);
|
||||
}
|
||||
|
||||
public stepManually(
|
||||
deltaTimeInSeconds: number,
|
||||
): { hitObject: GameObject | undefined; velocity: vec2 } {
|
||||
public stepManually(deltaTimeInSeconds: number): {
|
||||
hitObject: GameObject | undefined;
|
||||
velocity: vec2;
|
||||
} {
|
||||
let delta = vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds);
|
||||
|
||||
this.radius += vec2.length(delta);
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
|||
return Math.abs(this.ownership - 0.5) < 0.1
|
||||
? CharacterTeam.neutral
|
||||
: this.ownership < 0.5
|
||||
? CharacterTeam.decla
|
||||
: CharacterTeam.red;
|
||||
? CharacterTeam.decla
|
||||
: CharacterTeam.red;
|
||||
}
|
||||
|
||||
private timeSinceLastPointGeneration = 0;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import { StaticPhysical } from '../physicals/static-physical';
|
|||
class Node {
|
||||
public left: Node | null = null;
|
||||
public right: Node | null = null;
|
||||
constructor(public object: StaticPhysical, public parent: Node | null) {}
|
||||
constructor(
|
||||
public object: StaticPhysical,
|
||||
public parent: Node | null,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class BoundingBoxTree {
|
||||
|
|
|
|||
|
|
@ -56,9 +56,8 @@ export abstract class PlayerBase extends CommandReceiver {
|
|||
);
|
||||
|
||||
const playerBoundingBox = getBoundingBoxOfCircle(playerBoundingCircle);
|
||||
const possibleIntersectors = this.objectContainer.findIntersecting(
|
||||
playerBoundingBox,
|
||||
);
|
||||
const possibleIntersectors =
|
||||
this.objectContainer.findIntersecting(playerBoundingBox);
|
||||
if (!isCircleIntersecting(playerBoundingCircle, possibleIntersectors)) {
|
||||
return playerPosition;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { CharacterTeam, PlayerInformation, Random, settings, Command } from 'shared';
|
||||
import { Socket } from 'socket.io';
|
||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||
import { NPC } from './npc';
|
||||
import { Player } from './player';
|
||||
|
|
@ -29,7 +30,7 @@ export class PlayerContainer {
|
|||
}
|
||||
}
|
||||
|
||||
public createPlayer(playerInfo: PlayerInformation, socket: SocketIO.Socket): Player {
|
||||
public createPlayer(playerInfo: PlayerInformation, socket: Socket): Player {
|
||||
if (this._players.length === this.playerMaxCount) {
|
||||
throw new Error('Too many players');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
PropertyUpdatesForObject,
|
||||
PrimaryActionCommand,
|
||||
} from 'shared';
|
||||
import { Socket } from 'socket.io';
|
||||
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||
import { CharacterPhysical } from '../objects/character-physical';
|
||||
|
|
@ -51,7 +52,7 @@ export class Player extends PlayerBase {
|
|||
playerContainer: PlayerContainer,
|
||||
objectContainer: PhysicalContainer,
|
||||
team: CharacterTeam,
|
||||
private readonly socket: SocketIO.Socket,
|
||||
private readonly socket: Socket,
|
||||
) {
|
||||
super(playerInfo, playerContainer, objectContainer, team);
|
||||
this.createCharacter();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"skipLibCheck": true,
|
||||
"target": "es6",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true,
|
||||
"moduleResolution": "node",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"module": "commonjs",
|
||||
"lib": ["dom", "es2017"],
|
||||
"typeRoots": ["./types", "./node_modules/@types"]
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ module.exports = (env, argv) => ({
|
|||
minimize: argv.mode !== 'development',
|
||||
minimizer: [
|
||||
new TerserJSPlugin({
|
||||
sourceMap: false,
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
// The custom serialization protocol keys on class names, so they must
|
||||
// survive minification (see shared/src/serialization).
|
||||
terserOptions: {
|
||||
keep_classnames: true,
|
||||
},
|
||||
|
|
@ -50,16 +50,6 @@ module.exports = (env, argv) => ({
|
|||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.html$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
query: {
|
||||
outputPath: '/',
|
||||
name: '[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: {
|
||||
|
|
|
|||
32
eslint.config.js
Normal file
32
eslint.config.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Flat config (ESLint 9+/10). Ported from the former .eslintrc.json.
|
||||
const tseslint = require('typescript-eslint');
|
||||
const prettierRecommended = require('eslint-plugin-prettier/recommended');
|
||||
const unusedImports = require('eslint-plugin-unused-imports');
|
||||
|
||||
module.exports = tseslint.config(
|
||||
{
|
||||
ignores: ['**/node_modules/**', '**/dist/**', '**/lib/**'],
|
||||
},
|
||||
...tseslint.configs.recommended,
|
||||
prettierRecommended,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
plugins: {
|
||||
'unused-imports': unusedImports,
|
||||
},
|
||||
languageOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'unused-imports/no-unused-imports': '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',
|
||||
// Renamed from the former no-var-requires; keep require() allowed.
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
25290
frontend/package-lock.json
generated
25290
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,7 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "npx webpack --mode production",
|
||||
"start": "npx webpack-dev-server --mode development",
|
||||
"dev": "npx webpack-dev-server --mode development",
|
||||
"try-build": "npm run build && cd dist && python3 -m http.server 8080"
|
||||
},
|
||||
"browserslist": [
|
||||
|
|
@ -22,32 +22,31 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/socket.io-client": "^1.4.34",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"common-config-webpack-plugin": "^2.0.1",
|
||||
"css-loader": "^1.0.1",
|
||||
"@plausible-analytics/tracker": "^0.4.5",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"css-loader": "^7.1.4",
|
||||
"file-loader": "^6.2.0",
|
||||
"gl-matrix": "3.3.0",
|
||||
"html-webpack-inline-source-plugin": "^1.0.0-beta.2",
|
||||
"html-webpack-inline-svg-plugin": "^2.3.0",
|
||||
"html-webpack-plugin": "^4.5.0",
|
||||
"image-config-webpack-plugin": "^2.0.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"html-inline-css-webpack-plugin": "^1.11.2",
|
||||
"html-webpack-plugin": "^5.6.7",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"postcss": "^8.5.15",
|
||||
"postcss-loader": "^8.2.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"resolve-url-loader": "^3.1.1",
|
||||
"sass": "^1.27.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sdf-2d": "^0.7.2",
|
||||
"sass": "^1.100.0",
|
||||
"sass-loader": "^17.0.0",
|
||||
"sdf-2d": "^0.7.6",
|
||||
"shared": "file:../shared",
|
||||
"socket.io-client": "^2.3.1",
|
||||
"socket.io-msgpack-parser": "^2.0.0",
|
||||
"source-map-loader": "^1.1.1",
|
||||
"svg-url-loader": "^6.0.0",
|
||||
"terser-webpack-plugin": "^4.2.2",
|
||||
"ts-config-webpack-plugin": "^2.0.0",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
"socket.io-client": "^4.8.3",
|
||||
"socket.io-msgpack-parser": "^3.0.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"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-dev-server": "^5.2.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
frontend/src/custom.d.ts
vendored
11
frontend/src/custom.d.ts
vendored
|
|
@ -2,3 +2,14 @@ declare module '*.mp3' {
|
|||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
// Asset imports handled by webpack loaders. TypeScript 5.x+ requires ambient
|
||||
// declarations for side-effect imports of these, so declare them here.
|
||||
declare module '*.png';
|
||||
declare module '*.ico';
|
||||
declare module '*.svg';
|
||||
declare module '*.scss';
|
||||
|
||||
// webpack's `mode` inlines `process.env.NODE_ENV` via DefinePlugin. Declare just
|
||||
// that one global so browser code can read it without pulling in all of @types/node.
|
||||
declare const process: { env: { NODE_ENV?: string } };
|
||||
|
|
|
|||
|
|
@ -68,13 +68,13 @@
|
|||
<img
|
||||
title="Enable sounds"
|
||||
alt="speaker with sound waves"
|
||||
src="../static/volume.svg"
|
||||
src="static/volume.svg"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label for="enable-music">
|
||||
<input id="enable-music" type="checkbox" />
|
||||
<img title="Enable music" alt="music note" src="../static/music.svg" />
|
||||
<img title="Enable music" alt="music note" src="static/music.svg" />
|
||||
</label>
|
||||
|
||||
<label for="enable-vibration">
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
<img
|
||||
title="Enable vibration on mobile"
|
||||
alt="vibrating mobile"
|
||||
src="../static/vibrate.svg"
|
||||
src="static/vibrate.svg"
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
title="Exit from current game"
|
||||
id="logout"
|
||||
alt="logout"
|
||||
src="../static/logout.svg"
|
||||
src="static/logout.svg"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
id="toggle-settings"
|
||||
class="icon"
|
||||
alt="toggle-settings"
|
||||
src="../static/settings.svg"
|
||||
src="static/settings.svg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -109,17 +109,17 @@
|
|||
class="full-screen-controllers"
|
||||
id="minimize"
|
||||
alt="minimize-application"
|
||||
src="../static/minimize.svg"
|
||||
src="static/minimize.svg"
|
||||
/>
|
||||
<img
|
||||
class="full-screen-controllers"
|
||||
id="maximize"
|
||||
alt="maximize-application"
|
||||
src="../static/maximize.svg"
|
||||
src="static/maximize.svg"
|
||||
/>
|
||||
|
||||
<div id="spinner-container">
|
||||
<img id="spinner" alt="waiting" src="../static/spinner.svg" />
|
||||
<img id="spinner" alt="waiting" src="static/spinner.svg" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
ProjectileBase,
|
||||
} from 'shared';
|
||||
import './main.scss';
|
||||
import './scripts/analytics';
|
||||
import '../static/og-image.png';
|
||||
import '../static/favicons/apple-touch-icon.png';
|
||||
import '../static/favicons/favicon-16x16.png';
|
||||
|
|
@ -16,9 +17,6 @@ import { LandingPageBackground } from './scripts/landing-page-background';
|
|||
import { JoinFormHandler } from './scripts/join-form-handler';
|
||||
import { handleFullScreen } from './scripts/helper/handle-full-screen';
|
||||
import { Game } from './scripts/game';
|
||||
import { handleInsights } from './scripts/handle-insights';
|
||||
import { getInsightsFromRenderer } from './scripts/get-insights-from-renderer';
|
||||
import { Renderer } from 'sdf-2d';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { OptionsHandler } from './scripts/options-handler';
|
||||
import { hide } from './scripts/helper/hide';
|
||||
|
|
@ -55,39 +53,6 @@ const enableMusic = document.querySelector('#enable-music') as HTMLInputElement;
|
|||
const enableVibration = document.querySelector('#enable-vibration') as HTMLInputElement;
|
||||
const spinner = document.querySelector('#spinner-container') as HTMLElement;
|
||||
|
||||
let isInGame = false;
|
||||
|
||||
const startInsights = (getRenderer: () => Renderer | undefined) => {
|
||||
const { vendor, renderer } = getInsightsFromRenderer(getRenderer());
|
||||
handleInsights(
|
||||
{
|
||||
vendor,
|
||||
renderer,
|
||||
referrer: document.referrer,
|
||||
connection: (navigator as any)?.connection?.effectiveType,
|
||||
devicePixelRatio: devicePixelRatio,
|
||||
},
|
||||
() => {
|
||||
const {
|
||||
fps,
|
||||
renderScale,
|
||||
lightScale,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
} = getInsightsFromRenderer(getRenderer());
|
||||
|
||||
return {
|
||||
isInGame,
|
||||
fps,
|
||||
renderScale,
|
||||
lightScale,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
};
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const toggleSettings = () => {
|
||||
settings.className = settings.className === 'open' ? '' : 'open';
|
||||
SoundHandler.play(Sounds.click);
|
||||
|
|
@ -157,9 +122,6 @@ const main = async () => {
|
|||
});
|
||||
window.onpopstate = () => game.destroy();
|
||||
|
||||
let backgroundRenderer: Renderer | undefined;
|
||||
startInsights(() => (isInGame ? game.renderer : backgroundRenderer));
|
||||
|
||||
for (;;) {
|
||||
show(spinner);
|
||||
hide(logoutButton, true);
|
||||
|
|
@ -168,7 +130,7 @@ const main = async () => {
|
|||
const background = new LandingPageBackground(canvas);
|
||||
const joinHandler = new JoinFormHandler(joinGameForm, serverContainer);
|
||||
|
||||
backgroundRenderer = await background.renderer;
|
||||
await background.renderer;
|
||||
hide(spinner);
|
||||
|
||||
const playerDecision = await joinHandler.getPlayerDecision();
|
||||
|
|
@ -185,11 +147,9 @@ const main = async () => {
|
|||
game = new Game(playerDecision, canvas, overlay);
|
||||
const gameOver = game.start();
|
||||
await game.started;
|
||||
isInGame = true;
|
||||
hide(spinner);
|
||||
show(logoutButton, true, 'block');
|
||||
await gameOver;
|
||||
isInGame = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
|||
17
frontend/src/scripts/analytics.ts
Normal file
17
frontend/src/scripts/analytics.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { init as plausibleInit } from '@plausible-analytics/tracker';
|
||||
|
||||
const ANALYTICS_AUTO_CAPTURE_PAGEVIEWS = true;
|
||||
const ANALYTICS_DOMAIN = 'decla.red';
|
||||
const ANALYTICS_ENDPOINT = 'https://stats.schmelczer.dev/status';
|
||||
const ANALYTICS_LOGGING = process.env.NODE_ENV !== 'production';
|
||||
|
||||
try {
|
||||
plausibleInit({
|
||||
domain: ANALYTICS_DOMAIN,
|
||||
endpoint: ANALYTICS_ENDPOINT,
|
||||
autoCapturePageviews: ANALYTICS_AUTO_CAPTURE_PAGEVIEWS,
|
||||
logging: ANALYTICS_LOGGING,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Could not initialize analytics.', error);
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { Command, CommandReceiver, serialize, TransportEvents } from 'shared';
|
||||
import { Socket } from 'socket.io-client';
|
||||
|
||||
export class CommandSocket extends CommandReceiver {
|
||||
constructor(private readonly socket: SocketIOClient.Socket) {
|
||||
constructor(private readonly socket: Socket) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { CommandGenerator, PrimaryActionCommand, SecondaryActionCommand } from '
|
|||
import { Game } from '../game';
|
||||
|
||||
export class MouseListener extends CommandGenerator {
|
||||
constructor(private target: HTMLElement, private readonly game: Game) {
|
||||
constructor(
|
||||
private target: HTMLElement,
|
||||
private readonly game: Game,
|
||||
) {
|
||||
super();
|
||||
|
||||
target.addEventListener('mousedown', this.mouseDownListener);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,21 @@
|
|||
* 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.
|
||||
*/
|
||||
const servers: Array<string> = ['https://server.decla.red'];
|
||||
const productionServers: Array<string> = ['https://declared.schmelczer.dev'];
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
const isDevelopment =
|
||||
typeof location !== 'undefined' &&
|
||||
(location.hostname === 'localhost' || location.hostname === '127.0.0.1');
|
||||
|
||||
const servers: Array<string> = isDevelopment
|
||||
? [`http://${location.hostname}:3000`, ...productionServers]
|
||||
: productionServers;
|
||||
|
||||
export abstract class Configuration {
|
||||
public static async initialize(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import {
|
|||
Command,
|
||||
settings,
|
||||
} from 'shared';
|
||||
import io from 'socket.io-client';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { KeyboardListener } from './commands/keyboard-listener';
|
||||
import { MouseListener } from './commands/mouse-listener';
|
||||
import { TouchListener } from './commands/touch-listener';
|
||||
|
|
@ -38,7 +38,7 @@ import { StepCommand } from './commands/types/step';
|
|||
export class Game extends CommandReceiver {
|
||||
public gameObjects = new GameObjectContainer(this);
|
||||
public renderer?: Renderer;
|
||||
private socket!: SocketIOClient.Socket;
|
||||
private socket!: Socket;
|
||||
private isBetweenGames = false;
|
||||
|
||||
public started: Promise<void>;
|
||||
|
|
@ -92,7 +92,9 @@ export class Game extends CommandReceiver {
|
|||
parser,
|
||||
} as any);
|
||||
|
||||
this.socket.on('reconnect_attempt', () => {
|
||||
// In socket.io-client v4 reconnection events are emitted by the Manager
|
||||
// (`socket.io`), not the Socket itself.
|
||||
this.socket.io.on('reconnect_attempt', () => {
|
||||
this.socket.io.opts.transports = ['polling', 'websocket'];
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import { Renderer } from 'sdf-2d';
|
||||
|
||||
export const getInsightsFromRenderer = (
|
||||
renderer?: Renderer,
|
||||
): {
|
||||
vendor?: string;
|
||||
renderer?: string;
|
||||
fps?: number;
|
||||
renderScale?: number;
|
||||
lightScale?: number;
|
||||
canvasWidth?: number;
|
||||
canvasHeight?: number;
|
||||
} => ({
|
||||
fps: renderer?.insights?.fps,
|
||||
vendor: renderer?.insights?.vendor,
|
||||
renderer: renderer?.insights?.renderer,
|
||||
renderScale: renderer?.insights?.renderPasses.distance.renderScale,
|
||||
lightScale: renderer?.insights?.renderPasses.lights.renderScale,
|
||||
canvasWidth: renderer?.canvasSize.x,
|
||||
canvasHeight: renderer?.canvasSize.y,
|
||||
});
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
const baseUri = 'https://insights.decla.red';
|
||||
const type = 'decla-red-frontend';
|
||||
const updateTime = 15000;
|
||||
|
||||
export const handleInsights = async (initialData: any, getFrameData: () => any) => {
|
||||
const sessionId = await createSession(initialData);
|
||||
setInterval(() => createFrame(sessionId, getFrameData()), updateTime);
|
||||
};
|
||||
|
||||
const createSession = async (data: any): Promise<string> => {
|
||||
const response = await sendPostRequest(`${baseUri}/${type}/sessions/`, data);
|
||||
const { sessionId } = await response.json();
|
||||
return sessionId;
|
||||
};
|
||||
|
||||
const createFrame = async (sessionId: string, data: any): Promise<unknown> =>
|
||||
await sendPostRequest(`${baseUri}/${type}/sessions/${sessionId}`, data);
|
||||
|
||||
const sendPostRequest = async (uri: string, data: any): Promise<Response> =>
|
||||
await fetch(uri, {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
redirect: 'follow',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
|
@ -42,7 +42,7 @@ export const handleFullScreen = (
|
|||
});
|
||||
|
||||
addEventListener('resize', () => {
|
||||
if (isInFullScreen && currentWindowHeight > innerHeight) {
|
||||
if (isInFullScreen() && currentWindowHeight > innerHeight) {
|
||||
followToggle();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ServerInformation, serverInformationEndpoint, TransportEvents } from 'shared';
|
||||
import io from 'socket.io-client';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { Configuration } from './configuration';
|
||||
import parser from 'socket.io-msgpack-parser';
|
||||
import { SoundHandler, Sounds } from './sound-handler';
|
||||
|
|
@ -21,7 +21,10 @@ export class JoinFormHandler {
|
|||
}
|
||||
};
|
||||
|
||||
constructor(private form: HTMLFormElement, private readonly container: HTMLElement) {
|
||||
constructor(
|
||||
private form: HTMLFormElement,
|
||||
private readonly container: HTMLElement,
|
||||
) {
|
||||
this.joinButton = form.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||
this.joinButton.disabled = true;
|
||||
this.waitingForDecision = new Promise((r) => (this.resolvePlayerDecision = r));
|
||||
|
|
@ -32,9 +35,9 @@ export class JoinFormHandler {
|
|||
|
||||
form.onsubmit = (e) => {
|
||||
SoundHandler.play(Sounds.click);
|
||||
const result: PlayerDecision = (Array.from(
|
||||
(new FormData(form) as any).entries(),
|
||||
) as Array<[string, any]>).reduce((result, [name, value]) => {
|
||||
const result: PlayerDecision = (
|
||||
Array.from((new FormData(form) as any).entries()) as Array<[string, any]>
|
||||
).reduce((result, [name, value]) => {
|
||||
(result as any)[name] = value;
|
||||
return result;
|
||||
}, {}) as any;
|
||||
|
|
@ -116,7 +119,7 @@ class ServerChooserOption {
|
|||
private serverNameElement = document.createElement('span');
|
||||
private completionElement = document.createElement('span');
|
||||
|
||||
private socket: SocketIOClient.Socket;
|
||||
private socket: Socket;
|
||||
|
||||
constructor(
|
||||
private content: ServerInformation,
|
||||
|
|
@ -145,8 +148,9 @@ class ServerChooserOption {
|
|||
parser,
|
||||
} as any);
|
||||
|
||||
// `connect_timeout` was removed in socket.io-client v3+; connection
|
||||
// timeouts now surface through `connect_error` (reconnection is disabled).
|
||||
this.socket.on('connect_error', this.destroy.bind(this));
|
||||
this.socket.on('connect_timeout', this.destroy.bind(this));
|
||||
this.socket.on('disconnect', this.destroy.bind(this));
|
||||
this.socket.emit(TransportEvents.SubscribeForServerInfoUpdates);
|
||||
this.socket.on(
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ export abstract class OptionsHandler {
|
|||
musicEnabled: true,
|
||||
};
|
||||
|
||||
public static initialize(
|
||||
inputElements: { [k in Extract<keyof Options, string>]: HTMLInputElement },
|
||||
) {
|
||||
public static initialize(inputElements: {
|
||||
[k in Extract<keyof Options, string>]: HTMLInputElement;
|
||||
}) {
|
||||
if (localStorage.getItem('options')) {
|
||||
const stored: Partial<Options> | null = JSON.parse(
|
||||
localStorage.getItem('options')!,
|
||||
|
|
@ -44,7 +44,11 @@ export abstract class OptionsHandler {
|
|||
}
|
||||
|
||||
if (k === 'musicEnabled') {
|
||||
this.checked ? SoundHandler.playAmbient() : SoundHandler.stopAmbient();
|
||||
if (this.checked) {
|
||||
SoundHandler.playAmbient();
|
||||
} else {
|
||||
SoundHandler.stopAmbient();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.checked && k === 'vibrationEnabled') {
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ export class PlanetShape extends PolygonFactory(settings.planetEdgeCount, 0) {
|
|||
if (dist < minDistance) {
|
||||
minDistance = dist;
|
||||
color = mix(${colorToString(settings.declaPlanetColor)}, ${colorToString(
|
||||
settings.redPlanetColor,
|
||||
)}, planetColorMixQ[j]);
|
||||
settings.redPlanetColor,
|
||||
)}, planetColorMixQ[j]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +113,10 @@ export class PlanetShape extends PolygonFactory(settings.planetEdgeCount, 0) {
|
|||
|
||||
public randomOffset = 0;
|
||||
|
||||
constructor(public vertices: Array<vec2>, public colorMixQ: number) {
|
||||
constructor(
|
||||
public vertices: Array<vec2>,
|
||||
public colorMixQ: number,
|
||||
) {
|
||||
super(vertices);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
@use 'sass:math';
|
||||
@use 'vars' as *;
|
||||
@use 'mixins' as *;
|
||||
|
||||
form {
|
||||
@include background;
|
||||
|
||||
padding: $small-padding / 2 $small-padding $small-padding $small-padding;
|
||||
padding: math.div($small-padding, 2) $small-padding $small-padding $small-padding;
|
||||
border-radius: $border-radius;
|
||||
|
||||
input:-webkit-autofill,
|
||||
|
|
@ -38,7 +39,7 @@ form {
|
|||
position: relative;
|
||||
padding: 15px 0 0;
|
||||
margin-top: 10px;
|
||||
margin: 10px $small-padding / 2 $small-padding $small-padding / 2;
|
||||
margin: 10px math.div($small-padding, 2) $small-padding math.div($small-padding, 2);
|
||||
|
||||
input {
|
||||
font-family: inherit;
|
||||
|
|
@ -99,7 +100,7 @@ form {
|
|||
|
||||
cursor: pointer;
|
||||
margin: $border-width-focused - $border-width $border-width-focused -
|
||||
$border-width + $small-padding / 2;
|
||||
$border-width + math.div($small-padding, 2);
|
||||
|
||||
.completion {
|
||||
font-size: 0.7em;
|
||||
|
|
@ -115,7 +116,7 @@ form {
|
|||
&:checked + label {
|
||||
border-color: $accent;
|
||||
border-width: $border-width-focused;
|
||||
margin: 0 $small-padding / 2;
|
||||
margin: 0 math.div($small-padding, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
@use 'sass:math';
|
||||
@use 'vars' as *;
|
||||
@use 'mixins' as *;
|
||||
|
||||
|
|
@ -107,7 +108,7 @@
|
|||
|
||||
$height: 4px;
|
||||
width: $height;
|
||||
top: $height / 2;
|
||||
top: math.div($height, 2);
|
||||
|
||||
right: 0;
|
||||
transform-origin: top right;
|
||||
|
|
@ -119,7 +120,7 @@
|
|||
@media (max-width: $breakpoint) {
|
||||
$height: 2px;
|
||||
width: $height;
|
||||
top: $height / 2;
|
||||
top: math.div($height, 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,11 +153,11 @@
|
|||
label:not(:first-child) {
|
||||
input[type='checkbox']:after {
|
||||
$height: 4px;
|
||||
top: $height / 2 + $small-padding;
|
||||
top: math.div($height, 2) + $small-padding;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
right: $small-padding;
|
||||
top: $height / 2;
|
||||
top: math.div($height, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"target": "es6",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true,
|
||||
"moduleResolution": "node",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"skipLibCheck": true,
|
||||
"module": "commonjs",
|
||||
"lib": ["dom", "es2017"],
|
||||
|
|
|
|||
|
|
@ -1,110 +1,148 @@
|
|||
const CleanWebpackPlugin = require('clean-webpack-plugin').CleanWebpackPlugin;
|
||||
const path = require('path');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const TsConfigWebpackPlugin = require('ts-config-webpack-plugin');
|
||||
const HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const TerserJSPlugin = require('terser-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const HTMLInlineCSSWebpackPlugin = require('html-inline-css-webpack-plugin').default;
|
||||
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
|
||||
const Sass = require('sass');
|
||||
|
||||
module.exports = (env, argv) => ({
|
||||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
disableHostCheck: true,
|
||||
module.exports = (env, argv) => {
|
||||
const isProduction = argv.mode !== 'development';
|
||||
|
||||
return {
|
||||
entry: {
|
||||
main: path.resolve(__dirname, 'src/index.ts'),
|
||||
},
|
||||
output: {
|
||||
filename: 'main.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: 'all',
|
||||
},
|
||||
// webpack-dev-server 5 no longer accepts `watchOptions` under `devServer`;
|
||||
// polling is configured on the compiler instead (needed for some FS mounts).
|
||||
watchOptions: {
|
||||
poll: true,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new MiniCssExtractPlugin(),
|
||||
//new BundleAnalyzerPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/index.html',
|
||||
inlineSource: argv.mode === 'development' ? '' : '.(css)$',
|
||||
}),
|
||||
new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin),
|
||||
new HtmlWebpackInlineSVGPlugin({
|
||||
inlineAll: true,
|
||||
svgoConfig: [
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new MiniCssExtractPlugin(),
|
||||
//new BundleAnalyzerPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/index.html',
|
||||
}),
|
||||
// The SVG UI icons used to be inlined into the HTML by the (abandoned,
|
||||
// webpack-4-only) html-webpack-inline-svg-plugin. They are now emitted as
|
||||
// static files and referenced via `static/<name>.svg` from index.html.
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{ from: 'static/*.svg', to: 'static/[name][ext]' }],
|
||||
}),
|
||||
// Inline the extracted CSS into the HTML for production builds (replaces
|
||||
// the abandoned html-webpack-inline-source-plugin). In development the CSS
|
||||
// stays a separate, linked file for faster rebuilds.
|
||||
...(isProduction ? [new HTMLInlineCSSWebpackPlugin()] : []),
|
||||
],
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserJSPlugin({
|
||||
exclude: /node_modules/,
|
||||
// The custom serialization protocol keys on class names, so they must
|
||||
// survive minification (see shared/src/serialization).
|
||||
terserOptions: {
|
||||
keep_classnames: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
removeViewBox: false,
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
use: ['source-map-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.scss$/i,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'postcss-loader',
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
implementation: Sass,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// SVGs referenced from CSS (`mask-image`/`background-image`) and JS
|
||||
// must be inlined as real `data:` URIs. svg-url-loader emits a CJS
|
||||
// module (`module.exports = "data:..."`) which webpack 5 + css-loader 7
|
||||
// write out verbatim as a `.svg` asset file, so the browser fetches JS
|
||||
// instead of an image and the mask/background silently fails. webpack 5's
|
||||
// built-in `asset/inline` produces a proper data URI instead.
|
||||
// (HTML <img> icons are unaffected: they are copied by CopyWebpackPlugin
|
||||
// and referenced by literal `static/*.svg` paths, not through this rule.)
|
||||
test: /\.svg$/,
|
||||
type: 'asset/inline',
|
||||
},
|
||||
{
|
||||
// Use oneOf so each asset matches exactly one rule (og-image.png would
|
||||
// otherwise match both the generic png rule and its own rule).
|
||||
// The directory is encoded in `name` (not `outputPath`) and has no
|
||||
// leading slash: with the default `publicPath: 'auto'` a leading slash
|
||||
// produces a doubled slash in the emitted URL (e.g. `//static/x.mp3`).
|
||||
oneOf: [
|
||||
{
|
||||
test: /og-image\.png$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.ico$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(mp3|png)$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: 'static/[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
new TsConfigWebpackPlugin(),
|
||||
],
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserJSPlugin({
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
terserOptions: {
|
||||
keep_classnames: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
use: ['source-map-loader'],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
alias: {
|
||||
// sdf-2d's package.json `exports` only declares an `import` condition,
|
||||
// which webpack 5 cannot resolve for the production (require) build.
|
||||
// Point straight at its entry to bypass package exports resolution.
|
||||
'sdf-2d$': path.resolve(__dirname, 'node_modules/sdf-2d/lib/main.js'),
|
||||
},
|
||||
{
|
||||
test: /\.scss$/i,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'postcss-loader',
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
implementation: Sass,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: {
|
||||
loader: 'svg-url-loader',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(mp3|png)$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
query: {
|
||||
outputPath: '/static',
|
||||
name: '[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.ico$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
query: {
|
||||
outputPath: '/',
|
||||
name: '[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /og-image.png$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
query: {
|
||||
outputPath: '/',
|
||||
name: '[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
23
package.json
23
package.json
|
|
@ -2,22 +2,19 @@
|
|||
"name": "root",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^3.9.1",
|
||||
"@typescript-eslint/parser": "^3.9.1",
|
||||
"concurrently": "^5.3.0",
|
||||
"eslint": "^7.2.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.21.2",
|
||||
"eslint-plugin-json-format": "^2.0.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-unused-imports": "^0.1.3",
|
||||
"prettier": "^2.0.5",
|
||||
"typescript": "^4.0.3"
|
||||
"concurrently": "^10.0.3",
|
||||
"eslint": "^10.4.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.60.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cd shared && npm run build && cd ../frontend && npm run build && cd ../backend && npm run build",
|
||||
"lint": "eslint ./**/src/**/*.{js,ts,json} --fix && prettier --write ./**/src/**/*.{js,ts,json}",
|
||||
"lint": "eslint ./**/src/**/*.{js,ts} --fix && prettier --write ./**/src/**/*.{js,ts,json}",
|
||||
"init": "cd shared && npm install && cd ../frontend && npm install && cd ../backend && npm install",
|
||||
"start": "concurrently --kill-others-on-fail \"cd shared && npm start\" \"cd backend && npm start\" \"cd frontend && npm start\""
|
||||
"dev": "concurrently --kill-others-on-fail \"cd shared && npm dev\" \"cd backend && npm dev\" \"cd frontend && npm dev\""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,18 +9,18 @@
|
|||
],
|
||||
"scripts": {
|
||||
"build": "npx webpack --mode production",
|
||||
"start": "npx webpack --mode development --watch",
|
||||
"dev": "npx webpack --mode development --watch",
|
||||
"try-build-before": "npx webpack --mode production"
|
||||
},
|
||||
"devDependencies": {
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"file-loader": "^6.1.0",
|
||||
"gl-matrix": "3.3.0",
|
||||
"prettier": "^2.0.5",
|
||||
"terser-webpack-plugin": "^2.3.8",
|
||||
"ts-loader": "^8.0.3",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.12"
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"gl-matrix": "^3.3.0",
|
||||
"prettier": "^3.8.3",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
"ts-loader": "^9.6.0",
|
||||
"typescript": "^6.0.3",
|
||||
"webpack": "^5.107.2",
|
||||
"webpack-cli": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import { Command } from '../command';
|
|||
|
||||
@serializable
|
||||
export class RemoteCallsForObject {
|
||||
constructor(public readonly id: Id, public readonly calls: Array<RemoteCall>) {}
|
||||
constructor(
|
||||
public readonly id: Id,
|
||||
public readonly calls: Array<RemoteCall>,
|
||||
) {}
|
||||
|
||||
public toArray(): Array<any> {
|
||||
return [this.id, this.calls];
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { serializable } from '../serialization/serializable';
|
|||
|
||||
@serializable
|
||||
export class Circle {
|
||||
constructor(public center: vec2, public radius: number) {}
|
||||
constructor(
|
||||
public center: vec2,
|
||||
public radius: number,
|
||||
) {}
|
||||
|
||||
public distance(target: vec2): number {
|
||||
return vec2.distance(this.center, target) - this.radius;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { serializable } from '../serialization/serializable';
|
|||
|
||||
@serializable
|
||||
export class Rectangle {
|
||||
constructor(public topLeft = vec2.create(), public size = vec2.create()) {}
|
||||
constructor(
|
||||
public topLeft = vec2.create(),
|
||||
public size = vec2.create(),
|
||||
) {}
|
||||
|
||||
public toArray(): Array<any> {
|
||||
return [this.topLeft, this.size];
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import { serializable } from '../serialization/serializable';
|
|||
|
||||
@serializable
|
||||
export class RemoteCall {
|
||||
constructor(public readonly functionName: string, public readonly args: Array<any>) {}
|
||||
constructor(
|
||||
public readonly functionName: string,
|
||||
public readonly args: Array<any>,
|
||||
) {}
|
||||
|
||||
public toArray(): Array<any> {
|
||||
return [this.functionName, this.args];
|
||||
|
|
@ -48,9 +51,9 @@ export abstract class GameObject extends CommandReceiver {
|
|||
|
||||
public processRemoteCalls(remoteCalls: Array<RemoteCall>) {
|
||||
remoteCalls.forEach((r) =>
|
||||
((this[r.functionName as keyof this] as unknown) as (
|
||||
...args: Array<any>
|
||||
) => unknown)(...r.args),
|
||||
(this[r.functionName as keyof this] as unknown as (...args: Array<any>) => unknown)(
|
||||
...r.args,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ import { GameObject } from '../game-object';
|
|||
|
||||
@serializable
|
||||
export class LampBase extends GameObject {
|
||||
constructor(id: Id, public center: vec2, public color: vec3, public lightness: number) {
|
||||
constructor(
|
||||
id: Id,
|
||||
public center: vec2,
|
||||
public color: vec3,
|
||||
public lightness: number,
|
||||
) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true,
|
||||
"moduleResolution": "node",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"module": "commonjs",
|
||||
"skipLibCheck": true,
|
||||
"composite": true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue