Add server screen
This commit is contained in:
parent
89fafeafd3
commit
e2129bbb26
20 changed files with 672 additions and 174 deletions
|
|
@ -15,10 +15,33 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<!--h1>Decla.red</h1>
|
||||
<section id="servers"></section-->
|
||||
<article id="landing-ui">
|
||||
<h1>decla.<span class="red">red</span></h1>
|
||||
<form id="join-game-form">
|
||||
<fieldset class="content">
|
||||
<legend>Join a game</legend>
|
||||
|
||||
<div class="name-group">
|
||||
<input
|
||||
required
|
||||
minlength="1"
|
||||
maxlength="20"
|
||||
placeholder=" "
|
||||
id="playerName"
|
||||
name="playerName"
|
||||
type="text"
|
||||
/>
|
||||
<label for="playerName">Choose a name</label>
|
||||
</div>
|
||||
|
||||
<div id="server-container"></div>
|
||||
<button id="join-game" type="submit">Join</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<noscript>Javascript is required for this website.</noscript>
|
||||
<div id="overlay"></div>
|
||||
<canvas id="main"></canvas>
|
||||
<canvas></canvas>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await Promise.all([this.setupCommunication(), this.setupRenderer()]);
|
||||
await this.promises;
|
||||
requestAnimationFrame(this.gameLoop.bind(this));
|
||||
}
|
||||
|
||||
|
|
|
|||
72
frontend/src/scripts/join-form-handler.ts
Normal file
72
frontend/src/scripts/join-form-handler.ts
Normal file
|
|
@ -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<PlayerDecision>;
|
||||
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<PlayerDecision> {
|
||||
return this.waitingForDecision;
|
||||
}
|
||||
|
||||
private isFirstServer = true;
|
||||
private displayNewServerInfo(content: ServerInformation, url: string) {
|
||||
this.container.innerHTML += `
|
||||
<div>
|
||||
<input required ${
|
||||
this.isFirstServer ? 'checked' : ''
|
||||
} type="radio" id="${url}" name="server" value="${url}" />
|
||||
<label for="${url}">${content.serverName} - ${content.playerCount}/${
|
||||
content.playerLimit
|
||||
} players</label>
|
||||
</div>
|
||||
`;
|
||||
this.isFirstServer = false;
|
||||
}
|
||||
}
|
||||
165
frontend/src/scripts/landing-page-background.ts
Normal file
165
frontend/src/scripts/landing-page-background.ts
Normal file
|
|
@ -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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
124
frontend/src/styles/form.scss
Normal file
124
frontend/src/styles/form.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
12
frontend/src/styles/vars.scss
Normal file
12
frontend/src/styles/vars.scss
Normal file
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue