Add backend to repository
This commit is contained in:
parent
13de88a77f
commit
163d11432e
5
backend/.dockerignore
Normal file
5
backend/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile
|
||||
.git
|
||||
.gitignore
|
44
backend/.eslintrc.json
Normal file
44
backend/.eslintrc.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": [ 1, "double", "avoid-escape" ],
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"js": "never",
|
||||
"jsx": "never",
|
||||
"ts": "never",
|
||||
"tsx": "never"
|
||||
}
|
||||
]
|
||||
},
|
||||
"globals": {
|
||||
"__dirname": true
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
backend/.gitattributes
vendored
Normal file
24
backend/.gitattributes
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
* text eol=lf
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.c text
|
||||
*.h text
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sln text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
||||
# Force batch scripts to always use CRLF line endings so that if a repo is accessed
|
||||
# in Windows via a file share from Linux, the scripts will work.
|
||||
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
|
||||
# Force bash scripts to always use LF line endings so that if a repo is accessed
|
||||
# in Unix via a file share from Windows, the scripts will work.
|
||||
*.sh text eol=lf
|
117
backend/.gitignore
vendored
Normal file
117
backend/.gitignore
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Local Terraform Files
|
||||
**.tfstate*
|
||||
terraform-provider**
|
||||
lock.json
|
||||
.terraform/
|
||||
|
||||
#batect
|
||||
.batect/caches*
|
||||
|
||||
# Typescript
|
||||
build
|
||||
build/**
|
30
backend/Dockerfile
Normal file
30
backend/Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
FROM node:14.18.1-alpine3.13 AS builder
|
||||
|
||||
RUN mkdir /home/node/app/ && chown -R node:node /home/node/app
|
||||
|
||||
WORKDIR /home/node/app
|
||||
|
||||
COPY --chown=node:node package.json ./
|
||||
COPY --chown=node:node yarn.lock ./
|
||||
|
||||
USER node
|
||||
RUN yarn install --non-interactive --dev && yarn cache clean
|
||||
COPY ./src/ ./src
|
||||
COPY ./tsconfig.json ./
|
||||
RUN yarn run build
|
||||
|
||||
FROM builder AS install
|
||||
ENV NODE_ENV production
|
||||
WORKDIR /home/node/app
|
||||
RUN rm -rf ./node_modules && yarn install --non-interactive --prod --frozen-lockfile && yarn cache clean
|
||||
|
||||
FROM node:14.18.1-alpine3.13
|
||||
USER node
|
||||
ENV NODE_ENV production
|
||||
WORKDIR /home/node/app
|
||||
COPY --chown=node:node package.json ./
|
||||
COPY --chown=node:node yarn.lock ./
|
||||
COPY --chown=node:node --from=builder /home/node/app/build ./build
|
||||
COPY --chown=node:node --from=install /home/node/app/node_modules ./node_modules
|
||||
|
||||
CMD ["node", "--es-module-specifier-resolution=node", "./build/index.js"]
|
26
backend/Makefile
Normal file
26
backend/Makefile
Normal file
@ -0,0 +1,26 @@
|
||||
.SILENT:
|
||||
|
||||
## RECOMMENDED
|
||||
|
||||
obr:
|
||||
docker compose run --rm --service-ports --name "obr-service" owlbearrodeo
|
||||
|
||||
## DEVELOPER HELPERS
|
||||
|
||||
new:
|
||||
docker compose run --service-ports --name "obr-service" owlbearrodeo
|
||||
|
||||
build:
|
||||
docker compose build --no-cache owlbearrodeo
|
||||
|
||||
start:
|
||||
docker start obr-service
|
||||
|
||||
stop:
|
||||
docker stop obr-service
|
||||
|
||||
dev:
|
||||
docker compose rm -s -f owlbearrodeo && make start
|
||||
|
||||
remove:
|
||||
docker rm obr-service
|
7
backend/README.md
Normal file
7
backend/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Owlbear Broker
|
||||
|
||||
Node.js backend for Owlbear Rodeo
|
||||
|
||||
## Requirements:
|
||||
|
||||
- Docker
|
10
backend/docker-compose.yml
Normal file
10
backend/docker-compose.yml
Normal file
@ -0,0 +1,10 @@
|
||||
services:
|
||||
owlbearrodeo:
|
||||
build: ./
|
||||
stop_grace_period: 30s
|
||||
init: true
|
||||
ports:
|
||||
- 9000:9000
|
||||
environment:
|
||||
PORT: 9000
|
||||
ALLOW_ORIGIN: ".*"
|
49
backend/package.json
Normal file
49
backend/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "owlbear-broker",
|
||||
"version": "2.0.0",
|
||||
"description": "",
|
||||
"main": "./src/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "tsc --project ./",
|
||||
"start:dev": "node --es-module-specifier-resolution=node -r dotenv/config ./build/index.js",
|
||||
"start": "node --es-module-specifier-resolution=node ./build/index.js",
|
||||
"lint": "eslint src/**/*.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mitchemmc/owlbear-rodeo.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mitchemmc/owlbear-rodeo/issues"
|
||||
},
|
||||
"homepage": "https://github.com/mitchemmc/owlbear-rodeo#readme",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"deep-diff": "^1.0.2",
|
||||
"express": "^4.17.3",
|
||||
"helmet": "^5.0.2",
|
||||
"lodash.get": "^4.4.2",
|
||||
"socket.io": "^4.5.4",
|
||||
"socket.io-msgpack-parser": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
"@types/component-emitter": "^1.2.11",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/deep-diff": "^1.0.1",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/node": "^15.6.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"eslint": "^8.12.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"typescript": "^4.9.3"
|
||||
}
|
||||
}
|
56
backend/src/controllers/Controller.ts
Normal file
56
backend/src/controllers/Controller.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Response, Request, NextFunction, Router } from "express";
|
||||
|
||||
export enum Methods {
|
||||
GET = "GET",
|
||||
POST = "POST",
|
||||
PUT = "PUT",
|
||||
DELETE = "DELETE",
|
||||
}
|
||||
|
||||
interface IRoute {
|
||||
path: string;
|
||||
method: Methods;
|
||||
handler: (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => void | Promise<void>;
|
||||
localMiddleware: ((
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => void)[];
|
||||
}
|
||||
|
||||
export default abstract class Controller {
|
||||
public router: Router = Router();
|
||||
public abstract path: string;
|
||||
protected routes: Array<IRoute> = [];
|
||||
|
||||
public setRoutes = (): Router => {
|
||||
for (const route of this.routes) {
|
||||
for (const mw of route.localMiddleware) {
|
||||
this.router.use(route.path, mw);
|
||||
}
|
||||
switch (route.method) {
|
||||
case "GET":
|
||||
this.router.get(route.path, route.handler);
|
||||
break;
|
||||
case "POST":
|
||||
this.router.post(route.path, route.handler);
|
||||
break;
|
||||
case "PUT":
|
||||
this.router.put(route.path, route.handler);
|
||||
break;
|
||||
case "DELETE":
|
||||
this.router.delete(route.path, route.handler);
|
||||
break;
|
||||
default:
|
||||
console.log("not a valid method");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.router;
|
||||
};
|
||||
}
|
30
backend/src/controllers/HealthcheckController.ts
Normal file
30
backend/src/controllers/HealthcheckController.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import Controller, { Methods } from "./Controller";
|
||||
|
||||
export default class HealthcheckController extends Controller {
|
||||
path = "/";
|
||||
|
||||
routes = [
|
||||
{
|
||||
path: "/",
|
||||
method: Methods.GET,
|
||||
handler: this.handleHome.bind(this),
|
||||
localMiddleware: []
|
||||
},
|
||||
{
|
||||
path: "/health",
|
||||
method: Methods.GET,
|
||||
handler: this.handleHealthcheck.bind(this),
|
||||
localMiddleware: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
async handleHome(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
async handleHealthcheck(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
res.sendStatus(200);
|
||||
}
|
||||
}
|
36
backend/src/controllers/IceServerController.ts
Normal file
36
backend/src/controllers/IceServerController.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import IceServer from "../entities/IceServer";
|
||||
import Controller, { Methods } from "./Controller";
|
||||
|
||||
export default class IceServerController extends Controller {
|
||||
iceServer: IceServer;
|
||||
|
||||
path = "/";
|
||||
|
||||
routes = [
|
||||
{
|
||||
path: "/iceservers",
|
||||
method: Methods.GET,
|
||||
handler: this.handleIceServerConnection.bind(this),
|
||||
localMiddleware: [],
|
||||
},
|
||||
];
|
||||
|
||||
constructor(iceServer: IceServer) {
|
||||
super();
|
||||
this.iceServer = iceServer;
|
||||
}
|
||||
|
||||
async handleIceServerConnection(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
try {
|
||||
const servers = await this.iceServer.getIceServers();
|
||||
res.send(JSON.stringify(servers));
|
||||
} catch (error) {
|
||||
res.status(500).send({ error });
|
||||
}
|
||||
}
|
||||
}
|
33
backend/src/entities/AppServer.ts
Normal file
33
backend/src/entities/AppServer.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Application, RequestHandler } from "express";
|
||||
import http, { Server } from "http";
|
||||
import Controller from "../controllers/Controller";
|
||||
|
||||
export default class AppServer extends Server {
|
||||
private app: Application;
|
||||
|
||||
private readonly port: string | number;
|
||||
|
||||
constructor(app: Application, port: string | number) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public run(): http.Server {
|
||||
return this.app.listen(this.port, () => {
|
||||
console.log(`The server is running on port ${this.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
public loadMiddleware(middlewares: Array<RequestHandler>): void {
|
||||
middlewares.forEach((middleware) => {
|
||||
this.app.use(middleware);
|
||||
});
|
||||
}
|
||||
|
||||
public loadControllers(controllers: Array<Controller>): void {
|
||||
controllers.forEach((controller) => {
|
||||
this.app.use(controller.path, controller.setRoutes());
|
||||
});
|
||||
}
|
||||
}
|
16
backend/src/entities/Auth.ts
Normal file
16
backend/src/entities/Auth.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
export default class Auth implements Auth {
|
||||
async createPasswordHash(
|
||||
password: string,
|
||||
saltRounds: number = 10
|
||||
): Promise<string> {
|
||||
const hash = await bcrypt.hash(password, saltRounds);
|
||||
return hash;
|
||||
}
|
||||
|
||||
async checkPassword(password: string, hash: string): Promise<boolean> {
|
||||
const result = await bcrypt.compare(password, hash);
|
||||
return result;
|
||||
}
|
||||
}
|
65
backend/src/entities/Game.ts
Normal file
65
backend/src/entities/Game.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { PlayerState } from "../types/PlayerState";
|
||||
import { PartyState } from "../types/PartyState";
|
||||
import { MapState } from "../types/MapState";
|
||||
import { Manifest } from "../types/Manifest";
|
||||
import { Map } from "../types/Map";
|
||||
|
||||
export default class Game {
|
||||
gameId: string;
|
||||
partyState: PartyState;
|
||||
passwordHash: string;
|
||||
state: Record<string, MapState | Manifest | Map>;
|
||||
|
||||
constructor(gameId: string, hash: string) {
|
||||
this.gameId = gameId;
|
||||
this.partyState = {};
|
||||
this.passwordHash = hash;
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
getPartyState(): PartyState {
|
||||
// const result = await this.redis.json_hgetall(`game:${gameId}:partyState`);
|
||||
const result = this.partyState;
|
||||
return result;
|
||||
}
|
||||
|
||||
getGamePasswordHash(): string {
|
||||
// const result = await this.redis.get(`game:${gameId}:passwordHash`);
|
||||
|
||||
const result = this.passwordHash;
|
||||
return result;
|
||||
}
|
||||
|
||||
setGamePasswordHash(hash: string): void {
|
||||
// const result = await this.redis.set(`game:${gameId}:passwordHash`, hash);
|
||||
|
||||
this.passwordHash = hash;
|
||||
}
|
||||
|
||||
setPlayerState(playerState: PlayerState, playerId: string): void {
|
||||
this.partyState[playerId] = playerState;
|
||||
}
|
||||
|
||||
deletePlayer(playerId: string): void {
|
||||
delete this.partyState[playerId];
|
||||
}
|
||||
|
||||
deleteGameData(): void {
|
||||
this.state = {};
|
||||
this.partyState = {};
|
||||
}
|
||||
|
||||
setState(field: "map" | "mapState" | "manifest", value: any): void {
|
||||
if (!value) {
|
||||
delete this.state[field];
|
||||
}
|
||||
|
||||
this.state[field] = value;
|
||||
}
|
||||
|
||||
getState(field: "map" | "mapState" | "manifest"): MapState | Manifest | Map {
|
||||
const result = this.state[field];
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
87
backend/src/entities/GameRepository.ts
Normal file
87
backend/src/entities/GameRepository.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { PlayerState } from "../types/PlayerState";
|
||||
import { PartyState } from "../types/PartyState";
|
||||
import { MapState } from "../types/MapState";
|
||||
import { Manifest } from "../types/Manifest";
|
||||
import { Map } from "../types/Map";
|
||||
import Game from "./Game";
|
||||
|
||||
export default class GameRepository {
|
||||
games: Record<string, Game>;
|
||||
|
||||
constructor() {
|
||||
this.games = {};
|
||||
}
|
||||
|
||||
setGameCreation(gameId: string, hash: string): void {
|
||||
const game = new Game(gameId, hash);
|
||||
|
||||
this.games[gameId] = game;
|
||||
}
|
||||
|
||||
isGameCreated(gameId: string): boolean {
|
||||
if (this.games[gameId] === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getPartyState(gameId: string): PartyState {
|
||||
const game = this.games[gameId];
|
||||
|
||||
const result = game.getPartyState();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getGamePasswordHash(gameId: string): string {
|
||||
const game = this.games[gameId];
|
||||
|
||||
const result = game.getGamePasswordHash();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
setGamePasswordHash(gameId: string, hash: string): void {
|
||||
const game = this.games[gameId];
|
||||
|
||||
game.setGamePasswordHash(hash);
|
||||
}
|
||||
|
||||
setPlayerState(
|
||||
gameId: string,
|
||||
playerState: PlayerState,
|
||||
playerId: string
|
||||
): void {
|
||||
const game = this.games[gameId];
|
||||
game.setPlayerState(playerState, playerId);
|
||||
}
|
||||
|
||||
deletePlayer(gameId: string, playerId: string): void {
|
||||
const game = this.games[gameId];
|
||||
game.deletePlayer(playerId);
|
||||
}
|
||||
|
||||
deleteGameData(gameId: string): void {
|
||||
const game = this.games[gameId];
|
||||
game.deleteGameData();
|
||||
}
|
||||
|
||||
setState(
|
||||
gameId: string,
|
||||
field: "map" | "mapState" | "manifest",
|
||||
value: any
|
||||
): void {
|
||||
const game = this.games[gameId];
|
||||
game.setState(field, value);
|
||||
}
|
||||
|
||||
getState(
|
||||
gameId: string,
|
||||
field: "map" | "mapState" | "manifest"
|
||||
): MapState | Manifest | Map {
|
||||
const game = this.games[gameId];
|
||||
const result = game.getState(field);
|
||||
return result;
|
||||
}
|
||||
}
|
260
backend/src/entities/GameServer.ts
Normal file
260
backend/src/entities/GameServer.ts
Normal file
@ -0,0 +1,260 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { Server as HttpServer } from "http";
|
||||
import { Socket, Server as IOServer } from "socket.io";
|
||||
import Auth from "./Auth";
|
||||
import GameRepository from "./GameRepository";
|
||||
import GameState from "./GameState";
|
||||
import { Update } from "../helpers/diff";
|
||||
import { Map } from "../types/Map";
|
||||
import { MapState } from "../types/MapState";
|
||||
import { PlayerState } from "../types/PlayerState";
|
||||
import { Manifest } from "../types/Manifest";
|
||||
import { Pointer } from "../types/Pointer";
|
||||
|
||||
export default class GameServer {
|
||||
private readonly io: IOServer;
|
||||
private gameRepo;
|
||||
|
||||
constructor(io: IOServer) {
|
||||
this.io = io;
|
||||
this.gameRepo = new GameRepository();
|
||||
}
|
||||
|
||||
public initaliseSocketServer(httpServer: HttpServer) {
|
||||
this.io.listen(httpServer);
|
||||
}
|
||||
|
||||
public run(): void {
|
||||
this.io.on("connect", async (socket: Socket) => {
|
||||
const gameState = new GameState(this.io, socket, this.gameRepo);
|
||||
let _gameId: string;
|
||||
|
||||
socket.on("signal", (data: string) => {
|
||||
try {
|
||||
const { to, signal } = JSON.parse(data);
|
||||
this.io.to(to).emit("signal", { from: socket.id, signal });
|
||||
} catch (error) {
|
||||
console.error("SIGNAL_ERROR", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("disconnecting", async () => {
|
||||
try {
|
||||
let gameId: string;
|
||||
if (_gameId) {
|
||||
gameId = _gameId;
|
||||
} else {
|
||||
const result = gameState.getGameId();
|
||||
if (result) {
|
||||
gameId = result;
|
||||
_gameId = result;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
socket.to(gameId).emit("player_left", socket.id);
|
||||
// Delete player state from game
|
||||
this.gameRepo.deletePlayer(gameId, socket.id);
|
||||
|
||||
// Update party state
|
||||
const partyState = this.gameRepo.getPartyState(gameId);
|
||||
socket.to(gameId).emit("party_state", partyState);
|
||||
} catch (error) {
|
||||
console.error("DISCONNECT_ERROR", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("join_game", async (gameId: string, password: string) => {
|
||||
const auth = new Auth();
|
||||
_gameId = gameId;
|
||||
|
||||
try {
|
||||
if (typeof gameId !== "string" || typeof password !== "string") {
|
||||
console.log("invalid type in party credentials");
|
||||
socket.emit("auth_error");
|
||||
return;
|
||||
}
|
||||
|
||||
const created = this.gameRepo.isGameCreated(gameId);
|
||||
if (!created) {
|
||||
// Create a game and join
|
||||
const hash = await auth.createPasswordHash(password);
|
||||
this.gameRepo.setGameCreation(gameId, hash);
|
||||
await gameState.joinGame(gameId);
|
||||
} else {
|
||||
// Join existing game
|
||||
const hash = this.gameRepo.getGamePasswordHash(gameId);
|
||||
const res = await auth.checkPassword(password, hash);
|
||||
if (res) {
|
||||
await gameState.joinGame(gameId);
|
||||
} else {
|
||||
socket.emit("auth_error");
|
||||
}
|
||||
}
|
||||
this.io.to(gameId).emit("joined_game", socket.id);
|
||||
} catch (error) {
|
||||
console.error("JOIN_ERROR", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("map", async (map: Map) => {
|
||||
try {
|
||||
let gameId: string;
|
||||
if (_gameId) {
|
||||
gameId = _gameId;
|
||||
} else {
|
||||
const result = gameState.getGameId();
|
||||
if (result) {
|
||||
gameId = result;
|
||||
_gameId = result;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.gameRepo.setState(gameId, "map", map);
|
||||
const state = this.gameRepo.getState(gameId, "map");
|
||||
socket.broadcast.to(gameId).emit("map", state);
|
||||
} catch (error) {
|
||||
console.error("MAP_ERROR", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("map_state", async (mapState: MapState) => {
|
||||
try {
|
||||
let gameId: string;
|
||||
if (_gameId) {
|
||||
gameId = _gameId;
|
||||
} else {
|
||||
const result = gameState.getGameId();
|
||||
if (result) {
|
||||
gameId = result;
|
||||
_gameId = result;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.gameRepo.setState(gameId, "mapState", mapState);
|
||||
const state = this.gameRepo.getState(gameId, "mapState");
|
||||
socket.broadcast.to(gameId).emit("map_state", state);
|
||||
} catch (error) {
|
||||
console.error("MAP_STATE_ERROR", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("map_state_update", async (update: Update<MapState>) => {
|
||||
try {
|
||||
let gameId: string;
|
||||
if (_gameId) {
|
||||
gameId = _gameId;
|
||||
} else {
|
||||
const result = gameState.getGameId();
|
||||
if (result) {
|
||||
gameId = result;
|
||||
_gameId = result;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (await gameState.updateState(gameId, "mapState", update)) {
|
||||
socket.to(gameId).emit("map_state_update", update);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("MAP_STATE_UPDATE_ERROR", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("player_state", async (playerState: PlayerState) => {
|
||||
try {
|
||||
let gameId: string;
|
||||
if (_gameId) {
|
||||
gameId = _gameId;
|
||||
} else {
|
||||
const result = gameState.getGameId();
|
||||
if (result) {
|
||||
gameId = result;
|
||||
_gameId = result;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.gameRepo.setPlayerState(gameId, playerState, socket.id);
|
||||
await gameState.broadcastPlayerState(gameId, socket, "party_state");
|
||||
} catch (error) {
|
||||
console.error("PLAYER_STATE_ERROR", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("manifest", async (manifest: Manifest) => {
|
||||
try {
|
||||
let gameId: string;
|
||||
if (_gameId) {
|
||||
gameId = _gameId;
|
||||
} else {
|
||||
const result = gameState.getGameId();
|
||||
if (result) {
|
||||
gameId = result;
|
||||
_gameId = result;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.gameRepo.setState(gameId, "manifest", manifest);
|
||||
const state = this.gameRepo.getState(gameId, "manifest");
|
||||
socket.broadcast.to(gameId).emit("manifest", state);
|
||||
} catch (error) {
|
||||
console.error("MANIFEST_ERROR", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("manifest_update", async (update: Update<Manifest>) => {
|
||||
try {
|
||||
let gameId: string;
|
||||
if (_gameId) {
|
||||
gameId = _gameId;
|
||||
} else {
|
||||
const result = gameState.getGameId();
|
||||
if (result) {
|
||||
gameId = result;
|
||||
_gameId = result;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (await gameState.updateState(gameId, "manifest", update)) {
|
||||
socket.to(gameId).emit("manifest_update", update);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("MANIFEST_UPDATE_ERROR", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("player_pointer", async (playerPointer: Pointer) => {
|
||||
try {
|
||||
let gameId: string;
|
||||
if (_gameId) {
|
||||
gameId = _gameId;
|
||||
} else {
|
||||
const result = gameState.getGameId();
|
||||
if (result) {
|
||||
gameId = result;
|
||||
_gameId = result;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
socket.to(gameId).emit("player_pointer", playerPointer);
|
||||
} catch (error) {
|
||||
console.error("POINTER_ERROR", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
67
backend/src/entities/GameState.ts
Normal file
67
backend/src/entities/GameState.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { Server, Socket } from "socket.io";
|
||||
import { applyChanges, Update } from "../helpers/diff";
|
||||
import GameRepository from "./GameRepository";
|
||||
|
||||
export default class GameState {
|
||||
io: Server;
|
||||
socket: Socket;
|
||||
gameRepository: GameRepository;
|
||||
|
||||
constructor(io: Server, socket: Socket, gameRepository: GameRepository) {
|
||||
this.io = io;
|
||||
this.socket = socket;
|
||||
this.gameRepository = gameRepository;
|
||||
}
|
||||
|
||||
getGameId(): string | undefined {
|
||||
let gameId;
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const room of this.socket.rooms) {
|
||||
if (room !== this.socket.id) {
|
||||
gameId = room;
|
||||
}
|
||||
}
|
||||
return gameId;
|
||||
}
|
||||
|
||||
async joinGame(gameId: string): Promise<void> {
|
||||
await this.socket.join(gameId);
|
||||
|
||||
const partyState = this.gameRepository.getPartyState(gameId);
|
||||
this.socket.emit("party_state", partyState);
|
||||
|
||||
const mapState = this.gameRepository.getState(gameId, "mapState");
|
||||
this.socket.emit("map_state", mapState);
|
||||
|
||||
const map = this.gameRepository.getState(gameId, "map");
|
||||
this.socket.emit("map", map);
|
||||
|
||||
const manifest = this.gameRepository.getState(gameId, "manifest");
|
||||
this.socket.emit("manifest", manifest);
|
||||
|
||||
this.socket.to(gameId).emit("player_joined", this.socket.id);
|
||||
}
|
||||
|
||||
async broadcastPlayerState(
|
||||
gameId: string,
|
||||
socket: Socket,
|
||||
eventName: string
|
||||
): Promise<void> {
|
||||
const state = this.gameRepository.getPartyState(gameId);
|
||||
socket.broadcast.to(gameId).emit(eventName, state);
|
||||
}
|
||||
|
||||
async updateState(
|
||||
gameId: string,
|
||||
field: "map" | "mapState" | "manifest",
|
||||
update: Update<any>
|
||||
): Promise<boolean> {
|
||||
const state = this.gameRepository.getState(gameId, field) as any;
|
||||
if (state && !(state instanceof Map) && update.id === state["mapId"]) {
|
||||
applyChanges(state, update.changes);
|
||||
this.gameRepository.setState(gameId, field, state);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
12
backend/src/entities/Global.ts
Normal file
12
backend/src/entities/Global.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export default class Global {
|
||||
static ORIGIN_WHITELIST: string = process.env.ALLOW_ORIGIN!!;
|
||||
|
||||
static CONNECTION_PORT: string | number = process.env.PORT || 9000;
|
||||
|
||||
static ICE_SERVERS: string = `
|
||||
{
|
||||
"iceServers": [
|
||||
]
|
||||
}
|
||||
`;
|
||||
}
|
9
backend/src/entities/IceServer.ts
Normal file
9
backend/src/entities/IceServer.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import Global from "./Global";
|
||||
|
||||
export default class IceServer {
|
||||
async getIceServers(): Promise<any> {
|
||||
const servers = Global.ICE_SERVERS;
|
||||
const data = JSON.parse(servers);
|
||||
return data;
|
||||
}
|
||||
}
|
22
backend/src/helpers/diff.ts
Normal file
22
backend/src/helpers/diff.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import diff, { Diff } from "deep-diff";
|
||||
import get from "lodash.get";
|
||||
const { applyChange } = diff;
|
||||
|
||||
export function applyChanges<LHS>(target: LHS, changes: Diff<LHS, any>[]) {
|
||||
for (let change of changes) {
|
||||
if (change.path && (change.kind === "E" || change.kind === "A")) {
|
||||
// If editing an object or array ensure that the value exists
|
||||
const valid = get(target, change.path) !== undefined;
|
||||
if (valid) {
|
||||
applyChange(target, true, change);
|
||||
}
|
||||
} else {
|
||||
applyChange(target, true, change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Update<T> = {
|
||||
id: string;
|
||||
changes: Diff<T>[];
|
||||
};
|
79
backend/src/index.ts
Normal file
79
backend/src/index.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import cors, { CorsOptions } from "cors";
|
||||
import express, { Application, RequestHandler } from "express";
|
||||
import helmet from "helmet";
|
||||
import { Server } from "socket.io";
|
||||
// @ts-ignore
|
||||
import msgParser from "socket.io-msgpack-parser";
|
||||
import AppServer from "./entities/AppServer";
|
||||
import Controller from "./controllers/Controller";
|
||||
import GameServer from "./entities/GameServer";
|
||||
import Global from "./entities/Global";
|
||||
import HealthcheckController from "./controllers/HealthcheckController";
|
||||
import IceServer from "./entities/IceServer";
|
||||
import IceServerController from "./controllers/IceServerController";
|
||||
|
||||
const app: Application = express();
|
||||
const port: string | number = Global.CONNECTION_PORT;
|
||||
const server = new AppServer(app, port);
|
||||
|
||||
const whitelist = new RegExp(Global.ORIGIN_WHITELIST);
|
||||
|
||||
const io = new Server(server, {
|
||||
cookie: false,
|
||||
cors: {
|
||||
origin: whitelist,
|
||||
methods: ["GET", "POST"],
|
||||
credentials: true,
|
||||
},
|
||||
serveClient: false,
|
||||
maxHttpBufferSize: 1e7,
|
||||
parser: msgParser,
|
||||
});
|
||||
|
||||
const corsConfig: CorsOptions = {
|
||||
origin: function (origin: any, callback: any) {
|
||||
if (!origin || whitelist.test(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
const msg =
|
||||
"The CORS policy for this site does not allow access from the specified Origin.";
|
||||
return callback(new Error(msg), false);
|
||||
},
|
||||
};
|
||||
|
||||
const iceServer = new IceServer();
|
||||
|
||||
const globalMiddleware: Array<RequestHandler> = [helmet(), cors(corsConfig)];
|
||||
|
||||
const controllers: Array<Controller> = [
|
||||
new HealthcheckController(),
|
||||
new IceServerController(iceServer),
|
||||
];
|
||||
|
||||
server.loadMiddleware(globalMiddleware);
|
||||
server.loadControllers(controllers);
|
||||
const httpServer = server.run();
|
||||
const game = new GameServer(io);
|
||||
game.initaliseSocketServer(httpServer);
|
||||
game.run();
|
||||
|
||||
process.once("SIGTERM", () => {
|
||||
console.log("sigterm event");
|
||||
server.close(() => {
|
||||
console.log("http server closed");
|
||||
});
|
||||
|
||||
io.close(() => {
|
||||
console.log("socket server closed");
|
||||
io.sockets.emit("server shutdown");
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000).unref();
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (reason, p) => {
|
||||
console.log("Unhandled Rejection at: Promise", p, "reason:", reason);
|
||||
// application specific logging, throwing an error, or other logic here
|
||||
});
|
15
backend/src/types/Color.ts
Normal file
15
backend/src/types/Color.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export type Color = {
|
||||
blue: string;
|
||||
orange: string;
|
||||
red: string;
|
||||
yellow: string;
|
||||
purple: string;
|
||||
green: string;
|
||||
pink: string;
|
||||
teal: string;
|
||||
black: string;
|
||||
darkGray: string;
|
||||
lightGray: string;
|
||||
white: string;
|
||||
primary: string;
|
||||
}
|
13
backend/src/types/Dice.ts
Normal file
13
backend/src/types/Dice.ts
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
export type DiceType = "d4" | "d6" | "d8" | "d10" | "d12" | "d20" | "d100";
|
||||
|
||||
export type DiceRoll = {
|
||||
type: DiceType;
|
||||
roll: number | "unknown";
|
||||
};
|
||||
|
||||
export type Dice = {
|
||||
share: boolean;
|
||||
rolls: DiceRoll[];
|
||||
};
|
70
backend/src/types/Drawing.ts
Normal file
70
backend/src/types/Drawing.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { Vector2 } from "./Vector2";
|
||||
import { Color } from "./Color";
|
||||
|
||||
export type PointsData = {
|
||||
points: Vector2[];
|
||||
};
|
||||
|
||||
export type RectData = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type CircleData = {
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
};
|
||||
|
||||
export type ShapeData = PointsData | RectData | CircleData;
|
||||
|
||||
export type BaseDrawing = {
|
||||
blend: boolean;
|
||||
color: Color;
|
||||
id: string;
|
||||
strokeWidth: number;
|
||||
};
|
||||
|
||||
export type BaseShape = BaseDrawing & {
|
||||
type: "shape";
|
||||
};
|
||||
|
||||
export type Line = BaseShape & {
|
||||
shapeType: "line";
|
||||
data: PointsData;
|
||||
};
|
||||
|
||||
export type Rectangle = BaseShape & {
|
||||
shapeType: "rectangle";
|
||||
data: RectData;
|
||||
};
|
||||
|
||||
export type Circle = BaseShape & {
|
||||
shapeType: "circle";
|
||||
data: CircleData;
|
||||
};
|
||||
|
||||
export type Triangle = BaseShape & {
|
||||
shapeType: "triangle";
|
||||
data: PointsData;
|
||||
};
|
||||
|
||||
export type ShapeType =
|
||||
| Line["shapeType"]
|
||||
| Rectangle["shapeType"]
|
||||
| Circle["shapeType"]
|
||||
| Triangle["shapeType"];
|
||||
|
||||
export type Shape = Line | Rectangle | Circle | Triangle;
|
||||
|
||||
export type Path = BaseDrawing & {
|
||||
type: "path";
|
||||
pathType: "fill" | "stroke";
|
||||
data: PointsData;
|
||||
};
|
||||
|
||||
export type Drawing = Shape | Path;
|
||||
|
||||
export type DrawingState = Record<string, Drawing>;
|
18
backend/src/types/Fog.ts
Normal file
18
backend/src/types/Fog.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Vector2 } from "./Vector2";
|
||||
import { Color } from "./Color";
|
||||
|
||||
export type FogData = {
|
||||
points: Vector2[];
|
||||
holes: Vector2[][];
|
||||
};
|
||||
|
||||
export type Fog = {
|
||||
color: Color;
|
||||
data: FogData;
|
||||
id: string;
|
||||
strokeWidth: number;
|
||||
type: "fog";
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
export type FogState = Record<string, Fog>;
|
39
backend/src/types/Grid.ts
Normal file
39
backend/src/types/Grid.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Vector2 } from "./Vector2";
|
||||
|
||||
export type GridInset = {
|
||||
/** Top left position of the inset */
|
||||
topLeft: Vector2;
|
||||
/** Bottom right position of the inset */
|
||||
bottomRight: Vector2;
|
||||
};
|
||||
|
||||
export type GridMeasurementType =
|
||||
| "chebyshev"
|
||||
| "alternating"
|
||||
| "euclidean"
|
||||
| "manhattan";
|
||||
|
||||
export type GridMeasurement = {
|
||||
type: GridMeasurementType;
|
||||
scale: string;
|
||||
};
|
||||
|
||||
export type GridType = "square" | "hexVertical" | "hexHorizontal";
|
||||
|
||||
export type Grid = {
|
||||
/** The inset of the grid from the map */
|
||||
inset: GridInset;
|
||||
/** The number of columns and rows of the grid as `x` and `y` */
|
||||
size: Vector2;
|
||||
type: GridType;
|
||||
measurement: GridMeasurement;
|
||||
};
|
||||
|
||||
export type GridScale = {
|
||||
/** The number multiplier of the scale */
|
||||
multiplier: number;
|
||||
/** The unit of the scale */
|
||||
unit: string;
|
||||
/** The precision of the scale */
|
||||
digits: number;
|
||||
};
|
6
backend/src/types/Manifest.ts
Normal file
6
backend/src/types/Manifest.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export type ManifestAsset = {
|
||||
id: string;
|
||||
owner: string;
|
||||
};
|
||||
export type ManifestAssets = Record<string, ManifestAsset>;
|
||||
export type Manifest = { mapId: string; assets: ManifestAssets };
|
38
backend/src/types/Map.ts
Normal file
38
backend/src/types/Map.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { Grid } from "./Grid";
|
||||
|
||||
export type BaseMap = {
|
||||
id: string;
|
||||
name: string;
|
||||
owner: string;
|
||||
grid: Grid;
|
||||
width: number;
|
||||
height: number;
|
||||
lastModified: number;
|
||||
created: number;
|
||||
showGrid: boolean;
|
||||
snapToGrid: boolean;
|
||||
};
|
||||
|
||||
export type DefaultMap = BaseMap & {
|
||||
type: "default";
|
||||
key: string;
|
||||
};
|
||||
|
||||
export type FileMapResolutions = {
|
||||
low?: string;
|
||||
medium?: string;
|
||||
high?: string;
|
||||
ultra?: string;
|
||||
};
|
||||
|
||||
export type MapQuality = keyof FileMapResolutions | "original";
|
||||
|
||||
export type FileMap = BaseMap & {
|
||||
type: "file";
|
||||
file: string;
|
||||
resolutions: FileMapResolutions;
|
||||
thumbnail: string;
|
||||
quality: MapQuality;
|
||||
};
|
||||
|
||||
export type Map = DefaultMap | FileMap;
|
15
backend/src/types/MapState.ts
Normal file
15
backend/src/types/MapState.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { DrawingState } from "./Drawing";
|
||||
import { FogState } from "./Fog";
|
||||
import { Notes } from "./Note";
|
||||
import { TokenStates } from "./TokenState";
|
||||
|
||||
export type EditFlag = "drawing" | "tokens" | "notes" | "fog";
|
||||
|
||||
export type MapState = {
|
||||
tokens: TokenStates;
|
||||
drawings: DrawingState;
|
||||
fogs: FogState;
|
||||
editFlags: Array<EditFlag>;
|
||||
notes: Notes;
|
||||
mapId: string;
|
||||
};
|
18
backend/src/types/Note.ts
Normal file
18
backend/src/types/Note.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Color } from "./Color";
|
||||
|
||||
export type Note = {
|
||||
id: string;
|
||||
color: Color;
|
||||
lastModified: number;
|
||||
lastModifiedBy: string;
|
||||
locked: boolean;
|
||||
size: number;
|
||||
text: string;
|
||||
textOnly: boolean;
|
||||
visible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
};
|
||||
|
||||
export type Notes = Record<string, Note>;
|
21
backend/src/types/Outline.ts
Normal file
21
backend/src/types/Outline.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export type CircleOutline = {
|
||||
type: "circle";
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
};
|
||||
|
||||
export type RectOutline = {
|
||||
type: "rect";
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type PathOutline = {
|
||||
type: "path";
|
||||
points: number[];
|
||||
};
|
||||
|
||||
export type Outline = CircleOutline | RectOutline | PathOutline;
|
3
backend/src/types/PartyState.ts
Normal file
3
backend/src/types/PartyState.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { PlayerState } from "./PlayerState";
|
||||
|
||||
export type PartyState = Record<string, PlayerState>;
|
10
backend/src/types/PlayerState.ts
Normal file
10
backend/src/types/PlayerState.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Timer } from "./Timer";
|
||||
import { Dice } from "./Dice";
|
||||
|
||||
export type PlayerState = {
|
||||
nickname: string;
|
||||
timer?: Timer;
|
||||
dice: Dice;
|
||||
sessionId?: string;
|
||||
userId?: string;
|
||||
};
|
10
backend/src/types/Pointer.ts
Normal file
10
backend/src/types/Pointer.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Vector2 } from "./Vector2";
|
||||
import { Color } from "./Color";
|
||||
|
||||
export type Pointer = {
|
||||
position: Vector2;
|
||||
visible: boolean;
|
||||
id: string;
|
||||
color: Color;
|
||||
time: number;
|
||||
};
|
4
backend/src/types/Timer.ts
Normal file
4
backend/src/types/Timer.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type Timer = {
|
||||
current: number;
|
||||
max: number;
|
||||
};
|
31
backend/src/types/Token.ts
Normal file
31
backend/src/types/Token.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Outline } from "./Outline";
|
||||
|
||||
export type TokenCategory = "character" | "vehicle" | "prop" | "attachment";
|
||||
|
||||
export type BaseToken = {
|
||||
id: string;
|
||||
name: string;
|
||||
defaultSize: number;
|
||||
defaultCategory: TokenCategory;
|
||||
defaultLabel: string;
|
||||
hideInSidebar: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
owner: string;
|
||||
created: number;
|
||||
lastModified: number;
|
||||
outline: Outline;
|
||||
};
|
||||
|
||||
export type DefaultToken = BaseToken & {
|
||||
type: "default";
|
||||
key: string;
|
||||
};
|
||||
|
||||
export type FileToken = BaseToken & {
|
||||
type: "file";
|
||||
file: string;
|
||||
thumbnail: string;
|
||||
};
|
||||
|
||||
export type Token = DefaultToken | FileToken;
|
37
backend/src/types/TokenState.ts
Normal file
37
backend/src/types/TokenState.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { Color } from "./Color";
|
||||
import { Outline } from "./Outline";
|
||||
import { TokenCategory } from "./Token";
|
||||
|
||||
export type BaseTokenState = {
|
||||
id: string;
|
||||
tokenId: string;
|
||||
owner: string;
|
||||
size: number;
|
||||
category: TokenCategory;
|
||||
label: string;
|
||||
statuses: Color[];
|
||||
x: number;
|
||||
y: number;
|
||||
lastModifiedBy: string;
|
||||
lastModified: number;
|
||||
rotation: number;
|
||||
locked: boolean;
|
||||
visible: boolean;
|
||||
outline: Outline;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type DefaultTokenState = BaseTokenState & {
|
||||
type: "default";
|
||||
key: string;
|
||||
};
|
||||
|
||||
export type FileTokenState = BaseTokenState & {
|
||||
type: "file";
|
||||
file: string;
|
||||
};
|
||||
|
||||
export type TokenState = DefaultTokenState | FileTokenState;
|
||||
|
||||
export type TokenStates = Record<string, TokenState>;
|
4
backend/src/types/Vector2.ts
Normal file
4
backend/src/types/Vector2.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type Vector2 = {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
17
backend/tsconfig.json
Normal file
17
backend/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./build",
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es6",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noEmitOnError": true,
|
||||
"pretty": true,
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["node_modules/**/*", "./build/**/*"]
|
||||
}
|
2271
backend/yarn.lock
Normal file
2271
backend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user