diff --git a/frontend/package.json b/frontend/package.json
index 54c1224..e817939 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -47,6 +47,7 @@
"sass-loader": "^9.0.2",
"sdf-2d": "^0.6.0",
"shared": "0.0.0",
+ "resize-observer-polyfill": "^1.5.1",
"socket.io-client": "^2.3.1",
"source-map-loader": "^1.1.0",
"svg-url-loader": "^6.0.0",
diff --git a/frontend/src/index.html b/frontend/src/index.html
index db4f744..9b8ca6e 100644
--- a/frontend/src/index.html
+++ b/frontend/src/index.html
@@ -46,23 +46,44 @@
-
+
+
+
+

+
{
- try {
- const landingUI = document.querySelector('#landing-ui') as HTMLElement;
- const joinGameForm = document.querySelector('#join-game-form') as HTMLFormElement;
- const serverContainer = document.querySelector('#server-container') as HTMLElement;
- const canvas = document.querySelector('canvas') as HTMLCanvasElement;
- const overlay = document.querySelector('#overlay') as HTMLElement;
- const settings = document.querySelector('#settings') as HTMLElement;
- const openSettings = document.querySelector('#open-settings') as HTMLElement;
- const closeSettings = document.querySelector('#close-settings') as HTMLElement;
- const minimize = document.querySelector('#minimize') as HTMLElement;
- const maximize = document.querySelector('#maximize') as HTMLElement;
- const enableRelativeMovementCheckbox = document.querySelector(
- '#enable-relative-movement',
- ) as HTMLElement;
+const landingUI = document.querySelector('#landing-ui') as HTMLElement;
+const joinGameForm = document.querySelector('#join-game-form') as HTMLFormElement;
+const serverContainer = document.querySelector('#server-container') as HTMLElement;
+const canvas = document.querySelector('canvas') as HTMLCanvasElement;
+const overlay = document.querySelector('#overlay') as HTMLElement;
+const settings = document.querySelector('#settings') as HTMLElement;
+const toggleSettingsButton = document.querySelector(
+ '#toggle-settings-container',
+) as HTMLElement;
+const minimize = document.querySelector('#minimize') as HTMLElement;
+const maximize = document.querySelector('#maximize') as HTMLElement;
+const logoutButton = document.querySelector('#logout') as HTMLElement;
+const enableRelativeMovement = document.querySelector(
+ '#enable-relative-movement',
+) as HTMLInputElement;
+const enableSounds = document.querySelector('#enable-sounds') as HTMLInputElement;
+const enableVibration = document.querySelector('#enable-vibration') as HTMLInputElement;
- openSettings.addEventListener('click', () => (settings.style.visibility = 'visible'));
- closeSettings.addEventListener('click', () => (settings.style.visibility = 'hidden'));
+let isInGame = false;
- const background = new LandingPageBackground(canvas);
- const joinHandler = new JoinFormHandler(joinGameForm, serverContainer);
- handleFullScreen(minimize, maximize);
-
- let backgroundRenderer = await background.renderer;
- let isInGame = false;
- let game: Game;
- const getFrameData = () => {
+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(isInGame ? game.renderer : backgroundRenderer);
+ } = getInsightsFromRenderer(getRenderer());
return {
isInGame,
@@ -75,26 +81,75 @@ const main = async () => {
canvasWidth,
canvasHeight,
};
- };
- const { vendor, renderer } = getInsightsFromRenderer(backgroundRenderer);
- handleInsights(
- {
- vendor,
- renderer,
- referrer: document.referrer,
- connection: (navigator as any)?.connection?.effectiveType,
- devicePixelRatio: devicePixelRatio,
- },
- getFrameData,
- );
+ },
+ );
+};
- const playerDecision = await joinHandler.getPlayerDecision();
- landingUI.style.display = 'none';
+const toggleSettings = () =>
+ (settings.className = settings.className === 'open' ? '' : 'open');
- background.destroy();
+const applyServerContainerShadows = () => {
+ const { scrollHeight, clientHeight, scrollTop } = serverContainer;
+ console.log(scrollHeight, clientHeight, scrollTop);
+ if (scrollHeight > clientHeight) {
+ serverContainer.className = 'scroll';
+ if (scrollTop === 0) {
+ serverContainer.className += ' top';
+ } else if (scrollTop + clientHeight === scrollHeight) {
+ serverContainer.className += ' bottom';
+ }
+ }
+};
- game = new Game(playerDecision, canvas, overlay);
- isInGame = true;
+const main = async () => {
+ try {
+ let game: Game;
+
+ handleFullScreen(minimize, maximize);
+ toggleSettingsButton.addEventListener('click', toggleSettings);
+
+ new ResizeObserver(applyServerContainerShadows).observe(serverContainer);
+ serverContainer.addEventListener('scroll', applyServerContainerShadows);
+
+ OptionsHandler.initialize({
+ relativeMovementEnabled: enableRelativeMovement,
+ soundsEnabled: enableSounds,
+ vibrationEnabled: enableVibration,
+ });
+
+ logoutButton.addEventListener('click', () => {
+ game.destroy();
+ toggleSettings();
+ });
+ window.onpopstate = () => game.destroy();
+
+ let backgroundRenderer: Renderer | undefined;
+ startInsights(() => (isInGame ? game.renderer : backgroundRenderer));
+
+ for (;;) {
+ landingUI.style.visibility = 'inherit';
+ logoutButton.style.visibility = 'hidden';
+
+ const background = new LandingPageBackground(canvas);
+ const joinHandler = new JoinFormHandler(joinGameForm, serverContainer);
+
+ backgroundRenderer = await background.renderer;
+
+ const playerDecision = await joinHandler.getPlayerDecision();
+ if (!history.state) {
+ history.pushState(true, '');
+ }
+
+ landingUI.style.visibility = 'hidden';
+
+ background.destroy();
+
+ logoutButton.style.visibility = 'inherit';
+ game = new Game(playerDecision, canvas, overlay);
+ isInGame = true;
+ await game.start();
+ isInGame = false;
+ }
} catch (e) {
console.error(e);
alert(e);
diff --git a/frontend/src/scripts/commands/generators/keyboard-listener.ts b/frontend/src/scripts/commands/generators/keyboard-listener.ts
index 09f2c70..58fc775 100644
--- a/frontend/src/scripts/commands/generators/keyboard-listener.ts
+++ b/frontend/src/scripts/commands/generators/keyboard-listener.ts
@@ -1,5 +1,6 @@
import { vec2 } from 'gl-matrix';
import { CommandGenerator, MoveActionCommand } from 'shared';
+import { OptionsHandler } from '../../options-handler';
export class KeyboardListener extends CommandGenerator {
private keysDown: Set = new Set();
@@ -34,7 +35,9 @@ export class KeyboardListener extends CommandGenerator {
if (vec2.squaredLength(movement) > 0) {
vec2.normalize(movement, movement);
}
- this.sendCommandToSubcribers(new MoveActionCommand(movement, true));
+ this.sendCommandToSubcribers(
+ new MoveActionCommand(movement, OptionsHandler.options.relativeMovementEnabled),
+ );
}
private normalize(key: string): string {
diff --git a/frontend/src/scripts/commands/generators/touch-listener.ts b/frontend/src/scripts/commands/generators/touch-listener.ts
index 7252046..45bc024 100644
--- a/frontend/src/scripts/commands/generators/touch-listener.ts
+++ b/frontend/src/scripts/commands/generators/touch-listener.ts
@@ -7,6 +7,7 @@ import {
MoveActionCommand,
} from 'shared';
import { Game } from '../../game';
+import { OptionsHandler } from '../../options-handler';
export class TouchListener extends CommandGenerator {
private previousPosition = vec2.create();
@@ -38,7 +39,9 @@ export class TouchListener extends CommandGenerator {
if (vec2.squaredLength(movement) > 0) {
vec2.normalize(movement, movement);
- this.sendCommandToSubcribers(new MoveActionCommand(movement, false));
+ this.sendCommandToSubcribers(
+ new MoveActionCommand(movement, OptionsHandler.options.relativeMovementEnabled),
+ );
}
this.previousPosition = position;
@@ -46,7 +49,12 @@ export class TouchListener extends CommandGenerator {
target.addEventListener('touchend', (event: TouchEvent) => {
event.preventDefault();
- this.sendCommandToSubcribers(new MoveActionCommand(vec2.create(), false));
+ this.sendCommandToSubcribers(
+ new MoveActionCommand(
+ vec2.create(),
+ OptionsHandler.options.relativeMovementEnabled,
+ ),
+ );
});
}
diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts
index 45344c0..face16f 100644
--- a/frontend/src/scripts/game.ts
+++ b/frontend/src/scripts/game.ts
@@ -29,7 +29,7 @@ import { TouchListener } from './commands/generators/touch-listener';
import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket';
import { PlayerDecision } from './join-form-handler';
import { GameObjectContainer } from './objects/game-object-container';
-import { options } from './options';
+import { OptionsHandler } from './options-handler';
import { BlobShape } from './shapes/blob-shape';
import { PlanetShape } from './shapes/planet-shape';
@@ -48,7 +48,6 @@ export class Game {
private readonly canvas: HTMLCanvasElement,
private readonly overlay: HTMLElement,
) {
- this.start();
const progressBar = document.createElement('div');
progressBar.className = 'planet-progress';
overlay.appendChild(progressBar);
@@ -71,7 +70,7 @@ export class Game {
const command = deserialize(serialized);
if (command instanceof PlayerDiedCommand) {
this.deadTimeout = command.timeout;
- if (options.vibrationEnabled) {
+ if (OptionsHandler.options.vibrationEnabled) {
navigator.vibrate(150);
}
this.overlay.appendChild(this.announcmentText);
@@ -112,10 +111,10 @@ export class Game {
);
}
- private async start(): Promise {
+ public async start(): Promise {
const noiseTexture = await renderNoise([256, 256], 2, 1);
this.setupCommunication(this.playerDecision.server);
- runAnimation(
+ await runAnimation(
this.canvas,
[
{
@@ -162,6 +161,8 @@ export class Game {
},
},
);
+ this.socket.close();
+ this.overlay.innerHTML = '';
}
public displayToWorldCoordinates(p: vec2): vec2 {
@@ -179,6 +180,11 @@ export class Game {
);
}
+ private isActive = true;
+ public destroy() {
+ this.isActive = false;
+ }
+
private announcmentText = document.createElement('h2');
private gameLoop(
renderer: Renderer,
@@ -196,6 +202,6 @@ export class Game {
this.gameObjects.stepObjects(deltaTime);
this.gameObjects.drawObjects(this.renderer, this.overlay);
- return true;
+ return this.isActive;
}
}
diff --git a/frontend/src/scripts/objects/player-character-view.ts b/frontend/src/scripts/objects/player-character-view.ts
index 028f8d7..64aa55a 100644
--- a/frontend/src/scripts/objects/player-character-view.ts
+++ b/frontend/src/scripts/objects/player-character-view.ts
@@ -8,7 +8,7 @@ import {
CharacterTeam,
settings,
} from 'shared';
-import { options } from '../options';
+import { OptionsHandler } from '../options-handler';
import { BlobShape } from '../shapes/blob-shape';
import { ViewObject } from './view-object';
@@ -50,7 +50,7 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
this.healthElement.style.width = this.health + '%';
if (this.previousHealth > this.health) {
this.previousHealth = this.health;
- if (options.vibrationEnabled) {
+ if (OptionsHandler.options.vibrationEnabled) {
navigator.vibrate(50);
}
}
diff --git a/frontend/src/scripts/options-handler.ts b/frontend/src/scripts/options-handler.ts
new file mode 100644
index 0000000..1806322
--- /dev/null
+++ b/frontend/src/scripts/options-handler.ts
@@ -0,0 +1,51 @@
+interface Options {
+ vibrationEnabled: boolean;
+ soundsEnabled: boolean;
+ relativeMovementEnabled: boolean;
+}
+
+export abstract class OptionsHandler {
+ private static initialized = false;
+ private static _options: Options = {
+ vibrationEnabled: true,
+ soundsEnabled: true,
+ relativeMovementEnabled: false,
+ };
+
+ public static initialize(
+ inputElements: { [k in Extract]: HTMLInputElement },
+ ) {
+ if (localStorage.getItem('options')) {
+ const stored: Partial | null = JSON.parse(
+ localStorage.getItem('options')!,
+ );
+ this._options = {
+ ...this._options,
+ ...stored,
+ };
+ }
+
+ for (const k in inputElements) {
+ const element = inputElements[k as keyof Options];
+ element.checked = OptionsHandler._options[k as keyof Options];
+ element.addEventListener('change', function () {
+ OptionsHandler._options[k as keyof Options] = this.checked;
+ OptionsHandler.save();
+ });
+ }
+
+ this.initialized = true;
+ }
+
+ private static save() {
+ localStorage.setItem('options', JSON.stringify(this._options));
+ }
+
+ public static get options(): Options {
+ if (!this.initialized) {
+ throw new Error('Uninitialized');
+ }
+
+ return this._options;
+ }
+}
diff --git a/frontend/src/scripts/options.ts b/frontend/src/scripts/options.ts
deleted file mode 100644
index 7c203b1..0000000
--- a/frontend/src/scripts/options.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export const options = {
- vibrationEnabled: true,
-};
diff --git a/frontend/src/styles/button.scss b/frontend/src/styles/button.scss
new file mode 100644
index 0000000..faad3bc
--- /dev/null
+++ b/frontend/src/styles/button.scss
@@ -0,0 +1,26 @@
+@import './vars.scss';
+
+button {
+ border: none;
+ background: none;
+ font-size: 2rem;
+ font-family: 'Comfortaa', sans-serif;
+ cursor: pointer;
+ display: block;
+ margin: auto;
+
+ $ascend: 6px;
+ padding: $medium-padding 0 0 0;
+ margin-bottom: $ascend;
+ transition: transform $animation-time, padding-bottom $animation-time,
+ margin-bottom $animation-time;
+
+ &:hover,
+ &:focus {
+ outline: none;
+ color: $accent;
+ transform: translateY(-$ascend);
+ padding-bottom: $ascend;
+ margin-bottom: 0;
+ }
+}
diff --git a/frontend/src/styles/form.scss b/frontend/src/styles/form.scss
index 154d393..2bf153f 100644
--- a/frontend/src/styles/form.scss
+++ b/frontend/src/styles/form.scss
@@ -96,7 +96,7 @@ form {
padding: $small-padding;
border: $border;
cursor: pointer;
- margin: $border-width-focused - $border-width;
+ margin: $border-width-focused - $border-width $small-padding / 2;
}
&:focus {
@@ -107,27 +107,8 @@ form {
&:checked + label {
border-color: $accent;
border-width: $border-width-focused;
- margin: 0;
+ margin-right: 0 $small-padding / 2;
}
}
}
}
-
-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);
- }
-}
diff --git a/frontend/src/styles/main.scss b/frontend/src/styles/main.scss
index d0a3f2b..c5d7fc9 100644
--- a/frontend/src/styles/main.scss
+++ b/frontend/src/styles/main.scss
@@ -1,8 +1,11 @@
@import './vars.scss';
+@import './button.scss';
@import './form.scss';
@import './mixins.scss';
+@import './settings.scss';
@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300&family=Open+Sans&display=swap');
+
* {
margin: 0;
box-sizing: border-box;
@@ -22,8 +25,11 @@ html {
}
}
-h1,
-h2 {
+img {
+ user-select: none;
+}
+
+h1 {
&,
* {
font-family: 'Comfortaa', sans-serif;
@@ -31,6 +37,10 @@ h2 {
font-size: 6rem;
text-align: center;
padding-bottom: $medium-padding;
+
+ @media (max-height: $height-breakpoint) {
+ display: none;
+ }
}
h2 {
@@ -131,30 +141,29 @@ body {
}
}
- #open-settings {
- position: absolute;
- top: 0;
- right: 0;
+ #server-container {
+ max-height: 30vh;
+ overflow-y: auto;
- animation: spin 32s linear infinite;
- @keyframes spin {
- 100% {
- transform: rotate(360deg);
- }
+ &::-webkit-scrollbar-track,
+ &::-webkit-scrollbar {
+ background-color: transparent;
+ width: 2px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background-color: $accent;
+ border-radius: $border-radius;
}
- }
- .icon {
- box-sizing: content-box;
-
- user-select: none;
- cursor: pointer;
-
- padding: $medium-padding;
- @include square(48px);
- @media (max-width: $breakpoint) {
- @include square(32px);
- padding: $small-padding;
+ &.scroll {
+ box-shadow: inset 0 -8px 8px -8px rgba(0, 0, 0, 0.4),
+ inset 0 8px 8px -8px rgba(0, 0, 0, 0.4);
+ &.top {
+ box-shadow: inset 0 -8px 8px -8px rgba(0, 0, 0, 0.4);
+ }
+ &.bottom {
+ box-shadow: inset 0 8px 8px -8px rgba(0, 0, 0, 0.4);
+ }
}
}
@@ -162,17 +171,21 @@ body {
position: absolute;
bottom: 0;
left: 0;
- }
- #settings {
- @include background;
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
+ box-sizing: content-box;
- padding: $large-padding;
- visibility: hidden;
+ user-select: none;
+ cursor: pointer;
+
+ padding: $medium-padding;
+ @include square($large-icon);
+ @media (max-width: $breakpoint) {
+ @include square($small-icon);
+ padding: $small-padding;
+ }
+
+ &:not(:first-child) {
+ visibility: hidden;
+ }
}
}
diff --git a/frontend/src/styles/mixins.scss b/frontend/src/styles/mixins.scss
index 1afa563..d845573 100644
--- a/frontend/src/styles/mixins.scss
+++ b/frontend/src/styles/mixins.scss
@@ -6,6 +6,6 @@
@mixin background {
backdrop-filter: blur(24px);
@supports not (backdrop-filter: blur(24px)) {
- background-color: rgba(0, 0, 0, 0.15);
+ background-color: rgba(0, 0, 0, 0.5);
}
}
diff --git a/frontend/src/styles/settings.scss b/frontend/src/styles/settings.scss
new file mode 100644
index 0000000..abb02b0
--- /dev/null
+++ b/frontend/src/styles/settings.scss
@@ -0,0 +1,122 @@
+@import './vars.scss';
+@import './mixins.scss';
+
+#toggle-settings-container,
+#settings-container {
+ position: absolute;
+ display: flex;
+ justify-content: center;
+
+ top: 0;
+ right: 0;
+}
+
+#toggle-settings-container {
+ padding: $medium-padding;
+ cursor: pointer;
+
+ #toggle-settings {
+ @include square($large-icon);
+ @media (max-width: $breakpoint) {
+ @include square($small-icon);
+ }
+
+ animation: spin 32s linear infinite;
+ @keyframes spin {
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+ }
+}
+
+#settings-container {
+ top: $medium-padding + $large-icon + $small-padding;
+ @media (max-width: $breakpoint) {
+ top: 0;
+ right: $medium-padding + $small-icon + $small-padding;
+ }
+ overflow: hidden;
+}
+
+#settings {
+ display: flex;
+ flex-direction: column;
+ padding: 0 $medium-padding $medium-padding $medium-padding;
+
+ transform: translateY(-100%);
+ @media (max-width: $breakpoint) {
+ flex-direction: row-reverse;
+ transform: translateX(100%);
+ padding: $medium-padding 0 $medium-padding $medium-padding;
+ }
+
+ opacity: 0;
+ transition: transform $animation-time, opacity $animation-time;
+
+ &.open {
+ transform: none;
+ opacity: 1;
+ }
+
+ img {
+ cursor: pointer;
+ transition: opacity $animation-time;
+ margin-bottom: $small-padding;
+ @include square($large-icon);
+ @media (max-width: $breakpoint) {
+ @include square($small-icon);
+ margin-bottom: 0;
+ margin-left: $small-padding;
+ }
+ }
+
+ label {
+ position: relative;
+ }
+
+ input[type='checkbox'] {
+ width: 0;
+ height: 0;
+ appearance: none;
+ cursor: pointer;
+
+ &:not(:checked) + img {
+ opacity: 0.4;
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &:after {
+ content: ' ';
+ background: white;
+ position: absolute;
+
+ $height: 4px;
+ width: $height;
+ top: $height / 2;
+
+ right: 0;
+ transform-origin: top right;
+ border-radius: 100px;
+ height: 0;
+ transform: rotate(45deg) translateY(17%);
+ transition: height $animation-time;
+
+ @media (max-width: $breakpoint) {
+ $height: 2px;
+ width: $height;
+ top: $height / 2;
+ }
+ }
+
+ &:not(:checked):after {
+ height: $large-icon;
+ @media (max-width: $breakpoint) {
+ height: $small-icon;
+ }
+ }
+ }
+}
diff --git a/frontend/src/styles/vars.scss b/frontend/src/styles/vars.scss
index 7d0be82..7c583fb 100644
--- a/frontend/src/styles/vars.scss
+++ b/frontend/src/styles/vars.scss
@@ -8,5 +8,8 @@ $small-padding: 12px;
$medium-padding: 24px;
$large-padding: 64px;
$border-radius: 15px;
-$animation-time: 200ms;
+$animation-time: 200ms ease;
$breakpoint: 700px;
+$height-breakpoint: 500px;
+$large-icon: 48px;
+$small-icon: 32px;
diff --git a/frontend/static/circle.svg b/frontend/static/circle.svg
new file mode 100644
index 0000000..f4fb483
--- /dev/null
+++ b/frontend/static/circle.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/frontend/static/logout.svg b/frontend/static/logout.svg
new file mode 100644
index 0000000..ab168e8
--- /dev/null
+++ b/frontend/static/logout.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/frontend/static/vibrate.svg b/frontend/static/vibrate.svg
new file mode 100644
index 0000000..ce6d594
--- /dev/null
+++ b/frontend/static/vibrate.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/frontend/static/volume.svg b/frontend/static/volume.svg
new file mode 100644
index 0000000..dedc4d8
--- /dev/null
+++ b/frontend/static/volume.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file