Improve settings

This commit is contained in:
schmelczerandras 2020-10-22 10:53:31 +02:00
parent 2509199abc
commit 81a8834c4d
19 changed files with 439 additions and 123 deletions

View file

@ -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",

View file

@ -46,23 +46,44 @@
</form>
</section>
<img id="open-settings" class="icon" alt="open-settings" src="static/settings.svg" />
<div id="settings-container">
<section id="settings">
<label for="enable-relative-movement">
<input id="enable-relative-movement" type="checkbox" />
<img alt="a dashed circle" src="static/circle.svg" />
</label>
<section id="settings">
<h2>Settings</h2>
<input id="enable-relative-movement" checked type="checkbox" />
<label for="enable-relative-movement">Enable relative movement</label>
<button id="close-settings">Back</button>
</section>
<label for="enable-vibration">
<input id="enable-vibration" type="checkbox" />
<img alt="vibrating mobile" src="static/vibrate.svg" />
</label>
<label for="enable-sounds">
<input id="enable-sounds" type="checkbox" />
<img alt="speaker with sound waves" src="static/volume.svg" />
</label>
<img id="logout" alt="logout" src="static/logout.svg" />
</section>
</div>
<div id="toggle-settings-container">
<img
id="toggle-settings"
class="icon"
alt="toggle-settings"
src="static/settings.svg"
/>
</div>
<img
class="full-screen-controllers icon"
class="full-screen-controllers"
id="minimize"
alt="minimize-application"
src="static/minimize.svg"
/>
<img
class="full-screen-controllers icon"
class="full-screen-controllers"
id="maximize"
alt="maximize-application"
src="static/maximize.svg"

View file

@ -17,12 +17,14 @@ import { JoinFormHandler } from './scripts/join-form-handler';
import { handleFullScreen } from './scripts/handle-full-screen';
import { Game } from './scripts/game';
import { PlayerCharacterView } from './scripts/objects/player-character-view';
import '../static/settings.svg';
import '../static/minimize.svg';
import '../static/maximize.svg';
import { handleInsights } from './scripts/handle-insights';
import { getInsightsFromRenderer } from './scripts/get-insights-from-renderer';
import { Renderer } from 'sdf-2d';
import ResizeObserver from 'resize-observer-polyfill';
import { OptionsHandler } from './scripts/options-handler';
glMatrix.setMatrixArrayType(Array);
@ -32,40 +34,44 @@ overrideDeserialization(PlanetBase, PlanetView);
overrideDeserialization(LampBase, LampView);
overrideDeserialization(ProjectileBase, ProjectileView);
const main = async () => {
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);

View file

@ -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<string> = 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 {

View file

@ -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,
),
);
});
}

View file

@ -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<void> {
public async start(): Promise<void> {
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;
}
}

View file

@ -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);
}
}

View file

@ -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<keyof Options, string>]: HTMLInputElement },
) {
if (localStorage.getItem('options')) {
const stored: Partial<Options> | 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;
}
}

View file

@ -1,3 +0,0 @@
export const options = {
vibrationEnabled: true,
};

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}
}
}

View file

@ -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;

View file

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" stroke-width="1.5" stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M8.56 3.69a9 9 0 0 0 -2.92 1.95" />
<path d="M3.69 8.56a9 9 0 0 0 -.69 3.44" />
<path d="M3.69 15.44a9 9 0 0 0 1.95 2.92" />
<path d="M8.56 20.31a9 9 0 0 0 3.44 .69" />
<path d="M15.44 20.31a9 9 0 0 0 2.92 -1.95" />
<path d="M20.31 15.44a9 9 0 0 0 .69 -3.44" />
<path d="M20.31 8.56a9 9 0 0 0 -1.95 -2.92" />
<path d="M15.44 3.69a9 9 0 0 0 -3.44 -.69" />
</svg>

After

Width:  |  Height:  |  Size: 619 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" stroke-width="1.5" stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2" />
<path d="M7 12h14l-3 -3m0 6l3 -3" />
</svg>

After

Width:  |  Height:  |  Size: 373 B

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" stroke-width="1.5" stroke="#FFFFFF" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<rect x="4" y="4" width="10" height="16" rx="1" />
<line x1="8" y1="5" x2="10" y2="5" />
<line x1="9" y1="17" x2="9" y2="17.01" />
<path d="M20 6l-2 3l2 3l-2 3l2 3" />
</svg>

After

Width:  |  Height:  |  Size: 415 B

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" stroke-width="1.5" stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M15 8a5 5 0 0 1 0 8" />
<path d="M17.7 5a9 9 0 0 1 0 14" />
<path d="M6 15h-2a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1h2l3.5 -4.5a.8 .8 0 0 1 1.5 .5v14a.8 .8 0 0 1 -1.5 .5l-3.5 -4.5" />
</svg>

After

Width:  |  Height:  |  Size: 433 B