diff --git a/backend/package.json b/backend/package.json index 43627a7..c40b15a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,20 +1,24 @@ { - "name": "decla.red-server", - "private": true, + "name": "declared-server", "description": "Game server for decla.red", "keywords": [], "author": "AndrĂ¡s Schmelczer (https://schmelczer.dev/)", - "main": "index.js", + "version": "0.0.1", + "main": "dist/main.js", "scripts": { "build": "npx webpack --mode production", "initialize": "npm install", - "start": "concurrently --kill-others \"npx webpack --mode development -w\" \"nodemon --legacy-watch dist/main.js\"", - "try-build": "npm run build && node dist/main.js" + "start": "concurrently --kill-others \"npx webpack --mode development -w\" \"cd dist && nodemon --legacy-watch main.js\"", + "try-build": "npm run build && cd dist && node main.js && cd -" + }, + "bin": { + "declared-server": "./dist/main.js" }, "dependencies": { + "@types/config": "0.0.36", "cors": "^2.8.5", "express": "^4.17.1", - "http": "0.0.1-security", + "minimist": "^1.2.5", "socket.io": "^2.3.0", "uws": "^10.148.1", "webpack-node-externals": "^2.5.2" diff --git a/backend/src/default-options.ts b/backend/src/default-options.ts new file mode 100644 index 0000000..4ce4c30 --- /dev/null +++ b/backend/src/default-options.ts @@ -0,0 +1,8 @@ +import { Options } from './options'; + +export const defaultOptions: Options = { + port: 3000, + name: 'Localhost', + playerLimit: 16, + seed: 51, +}; diff --git a/backend/src/game-server.ts b/backend/src/game-server.ts new file mode 100644 index 0000000..74c134a --- /dev/null +++ b/backend/src/game-server.ts @@ -0,0 +1,88 @@ +import { PhysicalContainer } from './physics/containers/physical-container'; +import { Player } from './players/player'; +import ioserver from 'socket.io'; +import { TransportEvents, deserialize, settings, ServerInformation } from 'shared'; +import { createWorld } from './map/create-world'; +import { DeltaTimeCalculator } from './helper/delta-time-calculator'; +import { Options } from './options'; + +export class GameServer { + private objects = new PhysicalContainer(); + private players: Array = []; + private deltaTimes: Array = []; + private deltaTimeCalculator = new DeltaTimeCalculator(); + + private serverName: string; + private playerLimit: number; + constructor(io: ioserver.Server, options: Options) { + this.serverName = options.name; + this.playerLimit = options.playerLimit; + + createWorld(this.objects); + this.objects.initialize(); + + io.on('connection', (socket: SocketIO.Socket) => { + socket.on(TransportEvents.PlayerJoining, () => { + const player = new Player(this.objects, socket); + this.players.push(player); + socket.on(TransportEvents.PlayerToServer, (json: string) => { + const command = deserialize(json); + player.sendCommand(command); + }); + + socket.on('disconnect', () => { + player.destroy(); + this.players = this.players.filter((p) => p !== player); + }); + }); + + socket.on('join', (room_name: string) => { + socket.join(room_name); + }); + }); + } + + public start() { + this.handlePhysics(); + } + + private handlePhysics() { + const delta = this.deltaTimeCalculator.getNextDeltaTimeInMilliseconds(); + this.deltaTimes.push(delta); + const framesBetweenDeltaTimeCalculation = 1000; + + if (this.deltaTimes.length > framesBetweenDeltaTimeCalculation) { + this.deltaTimes.sort((a, b) => a - b); + console.log( + `Median physics time: ${this.deltaTimes[ + framesBetweenDeltaTimeCalculation + ].toFixed(2)} ms`, + ); + console.log( + `Memory used: ${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)} MB`, + ); + this.deltaTimes = []; + console.log(this.players.map((p) => p.latency)); + } + + this.objects.stepObjects(delta / 1000); + this.players.forEach((p) => p.step(delta / 1000)); + + const physicsDelta = this.deltaTimeCalculator.getDeltaTimeInMilliseconds(); + this.deltaTimes.push(physicsDelta); + const sleepTime = settings.targetPhysicsDeltaTimeInMilliseconds - physicsDelta; + if (sleepTime >= settings.minPhysicsSleepTime) { + setTimeout(this.handlePhysics.bind(this), sleepTime); + } else { + setImmediate(this.handlePhysics.bind(this)); + } + } + + public get serverInfo(): ServerInformation { + return { + serverName: this.serverName, + playerCount: this.players.length, + playerLimit: this.playerLimit, + }; + } +} diff --git a/backend/src/index.html b/backend/src/index.html deleted file mode 100644 index 17aef93..0000000 --- a/backend/src/index.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - Server info - - - - - - - \ No newline at end of file diff --git a/backend/src/main.ts b/backend/src/main.ts index c2adba2..d1a3abd 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,36 +1,32 @@ -import ioserver, { Socket } from 'socket.io'; +import ioserver from 'socket.io'; import express from 'express'; import { Server } from 'http'; import cors from 'cors'; -import { - applyArrayPlugins, - Random, - TransportEvents, - deserialize, - settings, -} from 'shared'; -import './index.html'; -import { Player } from './players/player'; -import { PhysicalContainer } from './physics/containers/physical-container'; -import { createDungeon } from './map/create-dungeon'; +import { applyArrayPlugins, Random, serverInformationEndpoint } from 'shared'; +import minimist from 'minimist'; + import { glMatrix } from 'gl-matrix'; -import { DeltaTimeCalculator } from './helper/delta-time-calculator'; + +import { GameServer } from './game-server'; + +import { defaultOptions } from './default-options'; glMatrix.setMatrixArrayType(Array); - applyArrayPlugins(); -Random.seed = 44; +const optionOverrides = minimist(process.argv.slice(2)); +const options = { + ...defaultOptions, + ...optionOverrides, +}; -const objects = new PhysicalContainer(); -createDungeon(objects); - -objects.initialize(); - -let players: Array = []; +Random.seed = options.seed; const app = express(); -const deltaTimeCalculator = new DeltaTimeCalculator(); +const server = new Server(app); +const io = ioserver(server); + +const gameServer = new GameServer(io, options); app.use( cors({ @@ -41,70 +37,12 @@ app.use( }), ); -const port = 3000; -const server = new Server(app); -const io = ioserver(server); - -/* -const log = (text: string) => { - io.to('insights').emit('insights', text + '\n'); -}; -*/ - -app.get('/', function (req, res) { - res.sendFile('dist/index.html', { root: '.' }); +app.get(serverInformationEndpoint, (req, res) => { + res.json(gameServer.serverInfo); }); -io.on('connection', (socket: SocketIO.Socket) => { - socket.on(TransportEvents.PlayerJoining, () => { - const player = new Player(objects, socket); - players.push(player); - socket.on(TransportEvents.PlayerToServer, (json: string) => { - const command = deserialize(json); - player.sendCommand(command); - }); - - socket.on('disconnect', () => { - player.destroy(); - players = players.filter((p) => p !== player); - }); - }); - - socket.on('join', (room_name: string) => { - socket.join(room_name); - }); +server.listen(options.port, () => { + console.log(`server started at http://localhost:${options.port}`); }); -server.listen(port, () => { - console.log(`server started at http://localhost:${port}`); -}); - -let deltas: Array = []; - -const handlePhysics = () => { - const delta = deltaTimeCalculator.getNextDeltaTimeInMilliseconds(); - deltas.push(delta); - if (deltas.length > 1000) { - deltas.sort((a, b) => a - b); - console.log(`Median physics time: ${deltas[50].toFixed(2)} ms`); - console.log( - `Memory used: ${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)} MB`, - ); - deltas = []; - console.log(players.map((p) => p.latency)); - } - - objects.stepObjects(delta / 1000); - players.forEach((p) => p.step(delta / 1000)); - - const physicsDelta = deltaTimeCalculator.getDeltaTimeInMilliseconds(); - deltas.push(physicsDelta); - const sleepTime = settings.targetPhysicsDeltaTimeInMilliseconds - physicsDelta; - if (sleepTime >= settings.minPhysicsSleepTime) { - setTimeout(handlePhysics, sleepTime); - } else { - setImmediate(handlePhysics); - } -}; - -handlePhysics(); +gameServer.start(); diff --git a/backend/src/map/create-dungeon.ts b/backend/src/map/create-world.ts similarity index 68% rename from backend/src/map/create-dungeon.ts rename to backend/src/map/create-world.ts index f7475f8..d6e9e71 100644 --- a/backend/src/map/create-dungeon.ts +++ b/backend/src/map/create-world.ts @@ -1,12 +1,12 @@ import { vec2, vec3 } from 'gl-matrix'; -import { Random, settings } from 'shared'; +import { Random, settings, PlanetBase } from 'shared'; import { LampPhysical } from '../objects/lamp-physical'; import { PlanetPhysical } from '../objects/planet-physical'; import { PhysicalContainer } from '../physics/containers/physical-container'; import { evaluateSdf } from '../physics/functions/evaluate-sdf'; import { Physical } from '../physics/physicals/physical'; -export const createDungeon = (objectContainer: PhysicalContainer) => { +export const createWorld = (objectContainer: PhysicalContainer) => { const objects: Array = []; const lights: Array = []; @@ -33,11 +33,13 @@ export const createDungeon = (objectContainer: PhysicalContainer) => { ); objects.push( - createBlob( - position, - Random.getRandomInRange(300, 800), - Random.getRandomInRange(300, 800), - Random.getRandomInRange(20, 40), + new PlanetPhysical( + PlanetBase.createPlanetVertices( + position, + Random.getRandomInRange(300, 800), + Random.getRandomInRange(300, 800), + Random.getRandomInRange(20, 40), + ), ), ); } @@ -72,26 +74,3 @@ export const createDungeon = (objectContainer: PhysicalContainer) => { [...objects, ...lights].forEach((o) => objectContainer.addObject(o)); }; - -const createBlob = ( - center: vec2, - width: number, - height: number, - randomness: number, -): PlanetPhysical => { - const vertices = []; - - for (let i = 0; i < settings.polygonEdgeCount; i++) { - vertices.push( - vec2.fromValues( - center.x + - (width / 2) * Math.cos((i / settings.polygonEdgeCount) * -Math.PI * 2) + - Random.getRandomInRange(-randomness, randomness), - center.y + - (height / 2) * Math.sin((i / settings.polygonEdgeCount) * -Math.PI * 2) + - Random.getRandomInRange(-randomness, randomness), - ), - ); - } - return new PlanetPhysical(vertices); -}; diff --git a/backend/src/options.ts b/backend/src/options.ts new file mode 100644 index 0000000..c5fc944 --- /dev/null +++ b/backend/src/options.ts @@ -0,0 +1,6 @@ +export interface Options { + port: number; + name: string; + playerLimit: number; + seed: number; +} diff --git a/frontend/src/index.html b/frontend/src/index.html index d488574..667f31e 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -15,10 +15,33 @@ - +
+

decla.red

+
+
+ Join a game + +
+ + +
+ +
+ +
+
+
+
- + diff --git a/frontend/src/index.ts b/frontend/src/index.ts index b5f2bc9..876a70c 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,12 +1,15 @@ import { glMatrix } from 'gl-matrix'; import { CharacterBase, LampBase, overrideDeserialization, PlanetBase } from 'shared'; import { ProjectileBase } from 'shared/src/objects/types/projectile-base'; -import { Game } from './scripts/game'; + import { CharacterView } from './scripts/objects/character-view'; import { LampView } from './scripts/objects/lamp-view'; import { ProjectileView } from './scripts/objects/projectile-view'; import { PlanetView } from './scripts/objects/planet-view'; import './styles/main.scss'; +import { LandingPageBackground } from './scripts/landing-page-background'; +import { JoinFormHandler } from './scripts/join-form-handler'; +import { Game } from './scripts/game'; glMatrix.setMatrixArrayType(Array); @@ -15,9 +18,35 @@ overrideDeserialization(PlanetBase, PlanetView); overrideDeserialization(LampBase, LampView); overrideDeserialization(ProjectileBase, ProjectileView); +const addSupportForTabNavigation = () => + (document.onkeydown = (e) => { + if (e.key === ' ') { + (document.activeElement as HTMLElement)?.click(); + e.preventDefault(); + } + }); + +/*const removeUnnecessaryOutlines = () => + (document.onclick = (e) => { + (e.target as HTMLElement)?.blur(); + }); +*/ +addSupportForTabNavigation(); +//removeUnnecessaryOutlines(); + const main = async () => { try { - await new Game().start(); + const landingUI = document.querySelector('#landing-ui') as HTMLElement; + const background = new LandingPageBackground(); + const joinHandler = new JoinFormHandler( + document.querySelector('#join-game-form') as HTMLFormElement, + document.querySelector('#server-container') as HTMLElement, + ); + const playerDecision = await joinHandler.getPlayerDecision(); + landingUI.style.display = 'none'; + console.log(playerDecision); + background.destroy(); + await new Game(playerDecision).start(); } catch (e) { console.error(e); alert(e); diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 66bda2c..2c11171 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -23,8 +23,8 @@ import { MouseListener } from './commands/generators/mouse-listener'; import { TouchListener } from './commands/generators/touch-listener'; import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket'; -import { Configuration } from './config/configuration'; import { DeltaTimeCalculator } from './helper/delta-time-calculator'; +import { PlayerDecision } from './join-form-handler'; import { GameObjectContainer } from './objects/game-object-container'; import { BlobShape } from './shapes/blob-shape'; import { Circle } from './shapes/circle'; @@ -32,18 +32,23 @@ import { Polygon } from './shapes/polygon'; export class Game { public readonly gameObjects = new GameObjectContainer(this); - private readonly canvas: HTMLCanvasElement = document.querySelector( - 'canvas#main', - ) as HTMLCanvasElement; + private readonly canvas = document.querySelector('canvas') as HTMLCanvasElement; private renderer!: Renderer; private socket!: SocketIOClient.Socket; + private promises: Promise<[void, void]>; private deltaTimeCalculator = new DeltaTimeCalculator(); private overlay: HTMLElement = document.querySelector('#overlay') as HTMLDivElement; - private async setupCommunication(): Promise { - await Configuration.initialize(); + constructor(playerDecision: PlayerDecision) { + console.log(playerDecision.server); + this.promises = Promise.all([ + this.setupCommunication(playerDecision.server), + this.setupRenderer(), + ]); + } - this.socket = io(Configuration.servers[1], { + private async setupCommunication(serverUrl: string): Promise { + this.socket = io(serverUrl, { reconnectionDelayMax: 10000, transports: ['websocket'], }); @@ -81,7 +86,7 @@ export class Game { [ { ...Polygon.descriptor, - shaderCombinationSteps: [0, 2, 6, 16], + shaderCombinationSteps: [0, 1, 2, 3], }, { ...BlobShape.descriptor, @@ -93,11 +98,11 @@ export class Game { }, { ...CircleLight.descriptor, - shaderCombinationSteps: [0, 1, 2, 4, 8], + shaderCombinationSteps: [0, 1, 2, 4, 8, 16], }, { ...Flashlight.descriptor, - shaderCombinationSteps: [0, 1], + shaderCombinationSteps: [0], }, ], { @@ -131,7 +136,7 @@ export class Game { } public async start(): Promise { - await Promise.all([this.setupCommunication(), this.setupRenderer()]); + await this.promises; requestAnimationFrame(this.gameLoop.bind(this)); } diff --git a/frontend/src/scripts/join-form-handler.ts b/frontend/src/scripts/join-form-handler.ts new file mode 100644 index 0000000..3d9e149 --- /dev/null +++ b/frontend/src/scripts/join-form-handler.ts @@ -0,0 +1,72 @@ +import { ServerInformation, serverInformationEndpoint } from 'shared'; + +import { Configuration } from './config/configuration'; + +export type PlayerDecision = { + playerName: string; + server: string; +}; + +export class JoinFormHandler { + private waitingForDecision: Promise; + private resolvePlayerDecision!: (d: PlayerDecision) => void; + + constructor(form: HTMLFormElement, private readonly container: HTMLElement) { + this.waitingForDecision = new Promise((r) => (this.resolvePlayerDecision = r)); + + new FormData(form); + + document.addEventListener('keyup', (e) => { + if (e.key === 'enter') { + form.submit(); + } + }); + + form.onsubmit = (e) => { + 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; + + this.resolvePlayerDecision(result); + + e.preventDefault(); + }; + + this.loadServers(); + } + + private async loadServers() { + await Configuration.initialize(); + const serverList = Configuration.servers; + + serverList.map(async (url) => { + const response = await fetch(url + serverInformationEndpoint); + if (response.ok) { + const content: ServerInformation = await response.json(); + this.displayNewServerInfo(content, url); + } + }); + } + + public async getPlayerDecision(): Promise { + return this.waitingForDecision; + } + + private isFirstServer = true; + private displayNewServerInfo(content: ServerInformation, url: string) { + this.container.innerHTML += ` +
+ + +
+ `; + this.isFirstServer = false; + } +} diff --git a/frontend/src/scripts/landing-page-background.ts b/frontend/src/scripts/landing-page-background.ts new file mode 100644 index 0000000..5037fac --- /dev/null +++ b/frontend/src/scripts/landing-page-background.ts @@ -0,0 +1,165 @@ +import { vec2 } from 'gl-matrix'; +import { + CircleLight, + compile, + FilteringOptions, + hsl, + NoisyPolygonFactory, + Renderer, + renderNoise, + WrapOptions, +} from 'sdf-2d'; +import { settings, rgb, PlanetBase, Random } from 'shared'; + +const landingPageVertexCount = 9; +const LangindPagePolygon = NoisyPolygonFactory( + landingPageVertexCount, + rgb(0.5, 0.4, 0.7), +); + +export class LandingPageBackground { + private readonly canvas = document.querySelector('canvas') as HTMLCanvasElement; + private renderer!: Renderer; + + constructor() { + this.start(); + } + + private async start(): Promise { + const noiseTexture = await renderNoise([256, 256], 1.2, 2); + + this.renderer = await compile( + this.canvas, + [ + { + ...LangindPagePolygon.descriptor, + shaderCombinationSteps: [0, 1, 2], + }, + { + ...CircleLight.descriptor, + shaderCombinationSteps: [0, 2], + }, + ], + { + shadowTraceCount: 16, + paletteSize: 1, + }, + ); + + this.renderer.setRuntimeSettings({ + ambientLight: rgb(0, 0, 0), + lightCutoffDistance: settings.lightCutoffDistance, + textures: { + noiseTexture: { + source: noiseTexture, + overrides: { + maxFilter: FilteringOptions.LINEAR, + wrapS: WrapOptions.MIRRORED_REPEAT, + wrapT: WrapOptions.MIRRORED_REPEAT, + }, + }, + }, + }); + + requestAnimationFrame(this.gameLoop.bind(this)); + } + + public destroy() { + this.renderer.destroy(); + } + + private gameLoop(time: DOMHighResTimeStamp) { + Random.seed = 42; + + this.renderer.setViewArea( + vec2.fromValues(0, this.renderer.canvasSize.y), + this.renderer.canvasSize, + ); + + const topPlanetPosition = vec2.fromValues( + 0.7 * this.renderer.canvasSize.x, + 0.7 * this.renderer.canvasSize.y, + ); + + const topPlanet = new LangindPagePolygon( + PlanetBase.createPlanetVertices( + topPlanetPosition, + Random.getRandomInRange(150, 400), + Random.getRandomInRange(150, 400), + Random.getRandomInRange(10, 20), + landingPageVertexCount, + ), + ); + + (topPlanet as any).randomOffset = 0.5 + time / 3500; + + const bottomPlanetPosition = vec2.fromValues( + 0.3 * this.renderer.canvasSize.x, + 0.3 * this.renderer.canvasSize.y, + ); + + const bottomPlanet = new LangindPagePolygon( + PlanetBase.createPlanetVertices( + bottomPlanetPosition, + Random.getRandomInRange(150, 800), + Random.getRandomInRange(150, 400), + Random.getRandomInRange(10, 40), + landingPageVertexCount, + ), + ); + + (bottomPlanet as any).randomOffset = time / 2500; + + const planetDistance = vec2.subtract( + vec2.create(), + topPlanetPosition, + bottomPlanetPosition, + ); + const planetDistanceLength = vec2.length(planetDistance); + const planetDirection = vec2.normalize(planetDistance, planetDistance); + const planetAngle = Math.atan2(planetDirection.y, planetDirection.x); + + this.renderer.addDrawable(topPlanet); + this.renderer.addDrawable(bottomPlanet); + this.renderer.addDrawable( + new CircleLight( + this.calculateLightPosition( + planetAngle, + planetDistanceLength * 1.2, + -time / 3000, + ), + hsl(25, 75, 60), + 0.75, + ), + ); + + this.renderer.addDrawable( + new CircleLight( + this.calculateLightPosition( + planetAngle, + planetDistanceLength * 1.2, + time / 2000 + Math.PI, + ), + hsl(249, 79, 70), + 0.25, + ), + ); + + this.renderer.renderDrawables(); + + requestAnimationFrame(this.gameLoop.bind(this)); + } + + private calculateLightPosition(angle: number, length: number, t: number): vec2 { + const lightPosition = vec2.fromValues( + length * Math.sin(t), + length * Math.sin(t) * Math.cos(t), + ); + + const canvasCenter = vec2.scale(vec2.create(), this.renderer.canvasSize, 0.5); + + vec2.add(lightPosition, lightPosition, canvasCenter); + vec2.rotate(lightPosition, lightPosition, canvasCenter, angle); + return lightPosition; + } +} diff --git a/frontend/src/styles/form.scss b/frontend/src/styles/form.scss new file mode 100644 index 0000000..3aa02d7 --- /dev/null +++ b/frontend/src/styles/form.scss @@ -0,0 +1,124 @@ +@import './vars.scss'; + +form { + backdrop-filter: blur(24px); + background-color: rgba(0, 0, 0, 0.15); + padding: $small-padding / 2 $small-padding $small-padding $small-padding; + + input, + label { + font-size: 1.5rem; + } + + fieldset { + border: $border; + padding: 0 $medium-padding $medium-padding $medium-padding; + border-radius: $border-radius; + + div { + // disable margin collapse + overflow: auto; + } + + legend { + font-family: 'Comfortaa', sans-serif; + font-size: 2rem; + } + + .name-group { + position: relative; + padding: 15px 0 0; + margin-top: 10px; + margin: 10px 0 $small-padding 0; + + input { + font-family: inherit; + border: none; + border-bottom: $border; + color: white; + padding: 10px 0 0px 0; + background: transparent; + margin-bottom: $border-width-focused - $border-width; + + &::placeholder { + color: transparent; + } + + &:placeholder-shown ~ label { + font-size: 1.3rem; + cursor: text; + top: 20px; + } + } + + label { + position: absolute; + top: 0; + display: block; + transition: $animation-time; + color: $gray; + } + + input:focus, + input:not(:placeholder-shown) { + outline: none; + margin-bottom: 0; + border-width: $border-width-focused; + border-color: $accent; + + ~ label { + position: absolute; + top: 0; + display: block; + transition: $animation-time; + } + } + } + + input[type='radio'] { + width: 0; + height: 0; + + + label { + display: block; + border-radius: $border-radius; + padding: $small-padding; + border: $border; + cursor: pointer; + margin: $border-width-focused - $border-width; + } + + &:focus { + outline: none; + } + + &:focus + label, + &:checked + label { + border-color: $accent; + border-width: $border-width-focused; + margin: 0; + } + } + } + + button { + border: none; + background: none; + font-size: 2rem; + font-family: 'Comfortaa', sans-serif; + cursor: pointer; + display: block; + margin: auto; + padding-top: $medium-padding; + transition: transform $animation-time; + + &:hover, + &:focus { + outline: none; + color: $accent; + transform: translateY(-8px); + } + } + + border-radius: $border-radius; +} diff --git a/frontend/src/styles/main.scss b/frontend/src/styles/main.scss index 710ef3a..42b56cf 100644 --- a/frontend/src/styles/main.scss +++ b/frontend/src/styles/main.scss @@ -1,27 +1,64 @@ -$breakpoint: 800px; +@import './vars.scss'; +@import './form.scss'; +@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300&family=Open+Sans&display=swap'); * { margin: 0; box-sizing: border-box; color: white; + font-family: 'Open Sans', sans-serif; + + &::selection { + color: white; + background-color: $accent; + } } html { - background-color: linear-gradient(45deg, #103783, #9bafd9); - + font-size: 0.85rem; @media (max-width: $breakpoint) { - font-size: 0.7rem; + font-size: 0.6rem; + } +} + +h1 { + &, + * { + font-family: 'Comfortaa', sans-serif; + } + font-size: 6rem; + text-align: center; + padding-bottom: $medium-padding; +} + +.red { + color: $red; + &::selection { + color: $accent; + background-color: white; } } html, body, -canvas#main { +canvas { height: 100%; width: 100%; + background: black; } body { + #landing-ui { + position: absolute; + width: 100%; + height: 100%; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + #overlay { margin: 0.5rem 1.25rem; position: absolute; diff --git a/frontend/src/styles/vars.scss b/frontend/src/styles/vars.scss new file mode 100644 index 0000000..7d0be82 --- /dev/null +++ b/frontend/src/styles/vars.scss @@ -0,0 +1,12 @@ +$gray: #ccc; +$red: #b33951; +$accent: #b33951; +$border-width: 1.5px; +$border-width-focused: 3px; +$border: $border-width solid white; +$small-padding: 12px; +$medium-padding: 24px; +$large-padding: 64px; +$border-radius: 15px; +$animation-time: 200ms; +$breakpoint: 700px; diff --git a/shared/src/communication/player-information.ts b/shared/src/communication/player-information.ts new file mode 100644 index 0000000..1130b7b --- /dev/null +++ b/shared/src/communication/player-information.ts @@ -0,0 +1,3 @@ +export interface PlayerInformation { + name: string; +} diff --git a/shared/src/communication/server-information.ts b/shared/src/communication/server-information.ts new file mode 100644 index 0000000..fabbbdb --- /dev/null +++ b/shared/src/communication/server-information.ts @@ -0,0 +1,7 @@ +export interface ServerInformation { + playerLimit: number; + playerCount: number; + serverName: string; +} + +export const serverInformationEndpoint = '/stats'; diff --git a/shared/src/main.ts b/shared/src/main.ts index 9b2d076..94325b5 100644 --- a/shared/src/main.ts +++ b/shared/src/main.ts @@ -25,6 +25,8 @@ export * from './helper/rectangle'; export * from './helper/mix'; export * from './helper/random'; export * from './helper/id'; +export * from './communication/server-information'; +export * from './communication/player-information'; export * from './helper/rotate-90-deg'; export * from './helper/rotate-minus-90-deg'; export * from './objects/game-object'; diff --git a/shared/src/objects/types/planet-base.ts b/shared/src/objects/types/planet-base.ts index ee62dfd..205f6c4 100644 --- a/shared/src/objects/types/planet-base.ts +++ b/shared/src/objects/types/planet-base.ts @@ -1,4 +1,6 @@ import { vec2 } from 'gl-matrix'; +import { Random } from '../../helper/random'; +import { settings } from '../../settings'; import { Id } from '../../transport/identity'; import { serializable } from '../../transport/serialization/serializable'; import { GameObject } from '../game-object'; @@ -9,6 +11,30 @@ export class PlanetBase extends GameObject { super(id); } + public static createPlanetVertices( + center: vec2, + width: number, + height: number, + randomness: number, + vertexCount = settings.polygonEdgeCount, + ): Array { + const vertices = []; + + for (let i = 0; i < vertexCount; i++) { + vertices.push( + vec2.fromValues( + center.x + + (width / 2) * Math.cos((i / vertexCount) * -Math.PI * 2) + + Random.getRandomInRange(-randomness, randomness), + center.y + + (height / 2) * Math.sin((i / vertexCount) * -Math.PI * 2) + + Random.getRandomInRange(-randomness, randomness), + ), + ); + } + return vertices; + } + public toArray(): Array { return [this.id, this.vertices]; } diff --git a/shared/tsconfig.json b/shared/tsconfig.json index f85c232..3a55c4e 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -13,5 +13,6 @@ "module": "commonjs", "composite": true, "lib": ["dom", "es2017"] - } + }, + "include": ["src/**/*.ts"] }