Change gameplay

This commit is contained in:
schmelczerandras 2020-10-24 22:24:27 +02:00
parent d79900e3ea
commit 34dae300da
56 changed files with 906 additions and 400 deletions

View file

@ -108,7 +108,12 @@ const applyServerContainerShadows = () => {
const main = async () => {
try {
let game: Game;
SoundHandler.initialize();
const firstClickListener = () => {
SoundHandler.initialize();
document.removeEventListener('click', firstClickListener);
};
document.addEventListener('click', firstClickListener);
handleFullScreen(minimize, maximize);
toggleSettingsButton.addEventListener('click', toggleSettings);
@ -134,7 +139,7 @@ const main = async () => {
for (;;) {
show(spinner);
hide(logoutButton);
hide(logoutButton, true);
show(landingUI, true, 'flex');
const background = new LandingPageBackground(canvas);
@ -156,7 +161,7 @@ const main = async () => {
await game.started;
isInGame = true;
hide(spinner);
show(logoutButton);
show(logoutButton, true, 'block');
await gameOver;
isInGame = false;
}

View file

@ -37,7 +37,9 @@ h1 {
font-size: 6rem;
text-align: center;
padding-bottom: $medium-padding;
@media (max-width: $height-breakpoint) {
font-size: 4.5rem;
}
@media (max-height: $height-breakpoint) {
display: none;
}
@ -163,11 +165,23 @@ body {
font-size: 0;
transform: translateX(-50%) translateY(-50%);
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2);
@include square(50px);
border-radius: 1000px;
mask-image: url('../static/mask.svg');
}
.falling-point {
font-size: 1.2em;
&.decla {
color: $bright-decla;
}
&.red {
color: $bright-red;
}
}
.other-player-arrow {
@include square($large-icon);
@media (max-width: $breakpoint) {
@ -205,18 +219,29 @@ body {
}
.announcement {
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
top: 25%;
transform: translateX(calc(-50% + 50vw)) translateY(-50%);
font-size: 3rem;
@include background;
z-index: 1000;
padding: $medium-padding;
border-radius: 16px;
&:empty {
display: none;
}
.decla {
color: $bright-decla;
}
.red {
color: $bright-red;
}
}
.planet-progress {
position: absolute;
top: $small-padding;
left: 50%;
transform: translateX(-50%);
@ -225,26 +250,50 @@ body {
$height: 8px;
height: $height;
justify-content: space-between;
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2);
border-radius: 4px;
z-index: 100;
div {
height: $height;
&::before {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
background-color: #888;
height: 24px;
width: 4px;
border-radius: 1000px;
}
border-radius: 100px;
overflow: hidden;
&::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(50%);
@include square(24px);
background-image: url('../static/flag.svg');
background-size: contain;
}
div {
height: $height;
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2);
}
div:nth-child(1) {
background: $bright-decla;
border-radius: 100px 0 0 100px;
}
div:nth-child(2) {
background: $bright-neutral;
}
div:nth-child(3) {
background: $bright-red;
border-radius: 0 100px 100px 0;
}
}
}

View file

@ -1,156 +1,95 @@
import { vec2 } from 'gl-matrix';
import {
CircleLight,
ColorfulCircle,
FilteringOptions,
Flashlight,
Renderer,
renderNoise,
runAnimation,
WrapOptions,
} from 'sdf-2d';
import { Renderer, renderNoise } from 'sdf-2d';
import {
broadcastCommands,
deserialize,
serialize,
settings,
TransportEvents,
SetAspectRatioActionCommand,
PlayerInformation,
PlayerDiedCommand,
UpdateGameState,
UpdateOtherPlayerDirections,
clamp,
UpdateGameState,
GameEnd,
CharacterTeam,
ServerAnnouncement,
GameStart,
CommandReceiver,
CommandExecutors,
Command,
} from 'shared';
import io from 'socket.io-client';
import { KeyboardListener } from './commands/generators/keyboard-listener';
import { MouseListener } from './commands/generators/mouse-listener';
import { TouchJoystickListener } from './commands/generators/touch-joystick-listener';
import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket';
import { startAnimation } from './start-animation';
import { PlayerDecision } from './join-form-handler';
import { GameObjectContainer } from './objects/game-object-container';
import { OptionsHandler } from './options-handler';
import { BlobShape } from './shapes/blob-shape';
import { PlanetShape } from './shapes/planet-shape';
export class Game {
public readonly gameObjects = new GameObjectContainer(this);
export class Game extends CommandReceiver {
public gameObjects = new GameObjectContainer(this);
public renderer?: Renderer;
private socket!: SocketIOClient.Socket;
private deadTimeout = 0;
private isBetweenGames = false;
public started: Promise<void>;
private resolveStarted!: () => unknown;
private declaPlanetCountElement = document.createElement('div');
private redPlanetCountElement = document.createElement('div');
private neutralPlanetCountElement = document.createElement('div');
private announcementText = document.createElement('h2');
private progressBar = document.createElement('div');
private arrowElements: Array<HTMLElement> = [];
constructor(
private readonly playerDecision: PlayerDecision,
private readonly canvas: HTMLCanvasElement,
private readonly overlay: HTMLElement,
) {
super();
this.started = new Promise((r) => (this.resolveStarted = r));
this.announcementText.className = 'announcement';
const progressBar = document.createElement('div');
progressBar.className = 'planet-progress';
overlay.appendChild(progressBar);
progressBar.appendChild(this.declaPlanetCountElement);
progressBar.appendChild(this.neutralPlanetCountElement);
progressBar.appendChild(this.redPlanetCountElement);
this.progressBar.className = 'planet-progress';
this.progressBar.appendChild(this.declaPlanetCountElement);
this.progressBar.appendChild(this.redPlanetCountElement);
}
private arrowElements: Array<HTMLElement> = [];
private async setupCommunication(serverUrl: string): Promise<void> {
this.socket = io(serverUrl, {
private initialize() {
this.isBetweenGames = true;
this.socket?.close();
this.gameObjects = new GameObjectContainer(this);
this.overlay.innerHTML = '';
this.overlay.appendChild(this.progressBar);
this.announcementText.innerText = '';
this.overlay.appendChild(this.announcementText);
this.socket = io(this.playerDecision.server, {
reconnectionDelayMax: 10000,
transports: ['websocket'],
forceNew: true,
});
this.socket.on('reconnect_attempt', () => {
this.socket.io.opts.transports = ['polling', 'websocket'];
});
this.socket.on('disconnect', this.destroy.bind(this));
this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) => {
const command = deserialize(serialized);
if (command instanceof PlayerDiedCommand) {
this.deadTimeout = command.timeout;
if (OptionsHandler.options.vibrationEnabled) {
navigator.vibrate(150);
}
this.overlay.appendChild(this.announcementText);
} else if (command instanceof UpdateGameState) {
const all = command.declaCount + command.redCount + command.neutralCount;
this.declaPlanetCountElement.style.width = (command.declaCount / all) * 100 + '%';
this.neutralPlanetCountElement.style.width =
(command.neutralCount / all) * 100 + '%';
this.redPlanetCountElement.style.width = (command.redCount / all) * 100 + '%';
if (command.declaCount > all * 0.5) {
this.overlay.appendChild(this.announcementText);
this.announcementText.innerText = 'Decla team won 🎉';
}
if (command.redCount > all * 0.5) {
this.overlay.appendChild(this.announcementText);
this.announcementText.innerText = 'Red team won 🎉';
}
this.arrowElements
.splice(command.otherPlayerDirections.length, this.arrowElements.length)
.forEach((e) => e.parentElement?.removeChild(e));
for (
let i = this.arrowElements.length;
i < command.otherPlayerDirections.length;
i++
) {
const element = document.createElement('div');
this.arrowElements.push(element);
this.overlay.appendChild(element);
}
this.arrowElements.forEach((e, i) => {
const direction = command.otherPlayerDirections[i].direction;
const team = command.otherPlayerDirections[i].team;
const angle = Math.atan2(direction.y, direction.x);
e.className = 'other-player-arrow ' + team;
const { width, height } = this.overlay.getBoundingClientRect();
const aspectRatio = width / height;
const directionRatio = direction.x / direction.y;
let deltaX: number, deltaY: number;
if (aspectRatio < Math.abs(directionRatio)) {
deltaX = (width / 2) * Math.sign(direction.x);
deltaY = deltaX / directionRatio;
} else {
deltaY = (height / 2) * Math.sign(direction.y);
deltaX = deltaY * directionRatio;
}
const delta = vec2.fromValues(deltaX, deltaY);
const center = vec2.fromValues(width / 2, height / 2);
const p = vec2.add(center, center, delta);
const arrowPadding = 24;
vec2.set(
p,
clamp(p.x, arrowPadding, width - arrowPadding),
clamp(height - p.y, arrowPadding, height - arrowPadding),
);
e.style.transform = `translateX(${p.x}px) translateY(${
p.y
}px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `;
});
} else this.gameObjects.sendCommand(command);
this.socket.on('disconnect', () => {
if (!this.isBetweenGames) {
this.destroy();
}
});
this.socket.on(TransportEvents.Ping, () => {
this.socket.emit(TransportEvents.Pong);
});
this.socket.emit(TransportEvents.PlayerJoining, {
name: this.playerDecision.playerName,
} as PlayerInformation);
this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) =>
this.sendCommand(deserialize(serialized)),
);
broadcastCommands(
[
@ -160,57 +99,94 @@ export class Game {
],
[this.gameObjects, new CommandReceiverSocket(this.socket)],
);
this.isBetweenGames = false;
this.socket.emit(TransportEvents.PlayerJoining, {
name: this.playerDecision.playerName,
} as PlayerInformation);
}
protected defaultCommandExecutor(c: Command) {
this.gameObjects.sendCommand(c);
}
protected commandExecutors: CommandExecutors = {
[ServerAnnouncement.type]: (c: ServerAnnouncement) =>
(this.announcementText.innerText = c.text),
[PlayerDiedCommand.type]: (c: PlayerDiedCommand) => {
if (OptionsHandler.options.vibrationEnabled) {
navigator.vibrate(150);
}
},
[UpdateGameState.type]: (c: UpdateGameState) => {
this.declaPlanetCountElement.style.width = (c.declaCount / c.limit) * 50 + '%';
this.redPlanetCountElement.style.width = (c.redCount / c.limit) * 50 + '%';
},
[GameEnd.type]: (c: GameEnd) => {
const team =
c.winningTeam === CharacterTeam.decla
? '<span class="decla">decla</span>'
: '<span class="red">red</span>';
this.announcementText.innerHTML = `Team ${team} won 🎉`;
},
[UpdateOtherPlayerDirections.type]: this.handleOtherPlayerDirections.bind(this),
[GameStart.type]: this.initialize.bind(this),
};
private handleOtherPlayerDirections(command: UpdateOtherPlayerDirections) {
this.arrowElements
.splice(command.otherPlayerDirections.length, this.arrowElements.length)
.forEach((e) => e.parentElement?.removeChild(e));
for (
let i = this.arrowElements.length;
i < command.otherPlayerDirections.length;
i++
) {
const element = document.createElement('div');
this.arrowElements.push(element);
this.overlay.appendChild(element);
}
this.arrowElements.forEach((e, i) => {
const direction = command.otherPlayerDirections[i].direction;
const team = command.otherPlayerDirections[i].team;
const angle = Math.atan2(direction.y, direction.x);
e.className = 'other-player-arrow ' + team;
const { width, height } = this.overlay.getBoundingClientRect();
const aspectRatio = width / height;
const directionRatio = direction.x / direction.y;
let deltaX: number, deltaY: number;
if (aspectRatio < Math.abs(directionRatio)) {
deltaX = (width / 2) * Math.sign(direction.x);
deltaY = deltaX / directionRatio;
} else {
deltaY = (height / 2) * Math.sign(direction.y);
deltaX = deltaY * directionRatio;
}
const delta = vec2.fromValues(deltaX, deltaY);
const center = vec2.fromValues(width / 2, height / 2);
const p = vec2.add(center, center, delta);
const arrowPadding = 24;
vec2.set(
p,
clamp(p.x, arrowPadding, width - arrowPadding),
clamp(height - p.y, arrowPadding, height - arrowPadding),
);
e.style.transform = `translateX(${p.x}px) translateY(${
p.y
}px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `;
});
}
public async start(): Promise<void> {
const noiseTexture = await renderNoise([256, 256], 2, 1);
this.setupCommunication(this.playerDecision.server);
await runAnimation(
this.canvas,
[
{
...PlanetShape.descriptor,
shaderCombinationSteps: [0, 1, 2, 3],
},
{
...BlobShape.descriptor,
shaderCombinationSteps: [0, 1, 2, 8],
},
{
...ColorfulCircle.descriptor,
shaderCombinationSteps: [0, 2, 16],
},
{
...CircleLight.descriptor,
shaderCombinationSteps: [0, 1, 2, 4, 8, 16],
},
{
...Flashlight.descriptor,
shaderCombinationSteps: [0],
},
],
this.gameLoop.bind(this),
{
shadowTraceCount: 16,
paletteSize: settings.palette.length,
//enableStopwatch: true,
},
{
colorPalette: settings.palette,
enableHighDpiRendering: true,
lightCutoffDistance: settings.lightCutoffDistance,
textures: {
noiseTexture: {
source: noiseTexture,
overrides: {
maxFilter: FilteringOptions.LINEAR,
wrapS: WrapOptions.MIRRORED_REPEAT,
wrapT: WrapOptions.MIRRORED_REPEAT,
},
},
},
},
);
this.initialize();
await startAnimation(this.canvas, this.gameLoop.bind(this), noiseTexture);
this.socket.close();
this.overlay.innerHTML = '';
}
@ -237,17 +213,13 @@ export class Game {
private gameLoop(
renderer: Renderer,
currentTime: DOMHighResTimeStamp,
_: DOMHighResTimeStamp,
deltaTime: DOMHighResTimeStamp,
): boolean {
this.resolveStarted();
deltaTime /= 1000;
this.renderer = renderer;
if ((this.deadTimeout -= deltaTime / 1000) > 0) {
this.announcementText.innerText = `Reviving in ${Math.floor(this.deadTimeout)}`;
} else {
this.announcementText.parentElement?.removeChild(this.announcementText);
}
this.gameObjects.stepObjects(deltaTime);
this.gameObjects.drawObjects(this.renderer, this.overlay);

View file

@ -95,6 +95,9 @@ class ServerChooserOption {
private divElement = document.createElement('div');
private inputElement = document.createElement('input');
private labelElement = document.createElement('label');
private serverNameElement = document.createElement('span');
private completionElement = document.createElement('span');
private socket: SocketIOClient.Socket;
constructor(
@ -111,7 +114,11 @@ class ServerChooserOption {
this.labelElement.htmlFor = url;
this.divElement.appendChild(this.inputElement);
this.divElement.appendChild(this.labelElement);
this.setPlayerLabelText();
this.labelElement.appendChild(this.serverNameElement);
this.labelElement.appendChild(document.createElement('br'));
this.labelElement.appendChild(this.completionElement);
this.completionElement.className = 'completion';
this.setServerInfoLabelText();
this.socket = io(url, {
reconnection: false,
@ -121,11 +128,15 @@ class ServerChooserOption {
this.socket.on('connect_error', this.destroy.bind(this));
this.socket.on('connect_timeout', this.destroy.bind(this));
this.socket.on('disconnect', this.destroy.bind(this));
this.socket.emit(TransportEvents.SubscribeForPlayerCount);
this.socket.on(TransportEvents.PlayerCountUpdate, (v: number) => {
this.content.playerCount = v;
this.setPlayerLabelText();
});
this.socket.emit(TransportEvents.SubscribeForServerInfoUpdates);
this.socket.on(
TransportEvents.ServerInfoUpdate,
([playerCount, gameState]: [number, number]) => {
this.content.playerCount = playerCount;
this.content.gameStatePercent = gameState;
this.setServerInfoLabelText();
},
);
}
public destroy() {
@ -134,8 +145,25 @@ class ServerChooserOption {
this.onDestroy(this);
}
private setPlayerLabelText() {
this.labelElement.innerText = `${this.content.serverName} - ${this.content.playerCount}/${this.content.playerLimit} players`;
private getRoundCompletionText(percent: number): string {
const texts = [
'Just started',
'Just started',
'Ongoing',
'Halfway through',
'Nearly over',
'About to finish',
'Game is over',
];
return texts[Math.floor((percent / 100) * (texts.length - 1))];
}
private setServerInfoLabelText() {
this.serverNameElement.innerText = `${this.content.serverName} - ${this.content.playerCount}/${this.content.playerLimit} players`;
this.completionElement.innerText = this.getRoundCompletionText(
this.content.gameStatePercent,
);
}
public get element(): HTMLElement {

View file

@ -16,7 +16,7 @@ export class Camera extends GameObject implements ViewObject {
public beforeDestroy(): void {}
public step(deltaTimeInMilliseconds: number): void {}
public step(deltaTimeInSeconds: number): void {}
public draw(renderer: Renderer, overlay: HTMLElement) {
const canvasAspectRatio = renderer.canvasSize.x / renderer.canvasSize.y;

View file

@ -33,7 +33,7 @@ export class CharacterView extends CharacterBase implements ViewObject {
public beforeDestroy(): void {}
public step(deltaTimeInMilliseconds: number): void {}
public step(deltaTimeInSeconds: number): void {}
public draw(renderer: Renderer, overlay: HTMLElement): void {
this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]);

View file

@ -5,9 +5,8 @@ import {
CreateObjectsCommand,
CreatePlayerCommand,
DeleteObjectsCommand,
GameObject,
Id,
UpdateObjectsCommand,
RemoteCallsForObjects,
} from 'shared';
import { Game } from '../game';
import { Camera } from './camera';
@ -24,8 +23,11 @@ export class GameObjectContainer extends CommandReceiver {
if (this.camera) {
this.deleteObject(this.camera.id);
}
this.player = c.character as PlayerCharacterView;
this.camera = new Camera(this.game);
this.addObject(this.player);
this.addObject(this.camera);
},
@ -33,24 +35,25 @@ export class GameObjectContainer extends CommandReceiver {
[CreateObjectsCommand.type]: (c: CreateObjectsCommand) =>
c.objects.forEach((o) => this.addObject(o as ViewObject)),
[RemoteCallsForObjects.type]: (c: RemoteCallsForObjects) =>
c.callsForObjects.forEach((c) =>
this.objects.get(c.id)?.processRemoteCalls(c.calls),
),
[DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) =>
c.ids.forEach((id: Id) => this.deleteObject(id)),
[UpdateObjectsCommand.type]: (c: UpdateObjectsCommand) => {
c.updates.forEach((u) => this.objects.get(u.id)?.update(u.updates));
},
};
constructor(private game: Game) {
super();
}
public stepObjects(delta: number) {
public stepObjects(deltaTimeInSeconds: number) {
if (this.player) {
this.camera.center = this.player.position;
}
this.objects.forEach((o) => o.step(delta));
this.objects.forEach((o) => o.step(deltaTimeInSeconds));
}
public drawObjects(renderer: Renderer, overlay: HTMLElement) {

View file

@ -16,7 +16,7 @@ export class LampView extends LampBase implements ViewObject {
this.light = new CircleLight(center, color, lightness);
}
public step(deltaTimeInMilliseconds: number): void {}
public step(deltaTimeInSeconds: number): void {}
public beforeDestroy(): void {}

View file

@ -5,6 +5,14 @@ import { RenderCommand } from '../commands/types/render';
import { PlanetShape } from '../shapes/planet-shape';
import { ViewObject } from './view-object';
type FallingPoint = {
velocity: vec2;
position: vec2;
element: HTMLElement;
addedToOverlay: boolean;
timeToLive: number;
};
export class PlanetView extends PlanetBase implements ViewObject {
private shape: PlanetShape;
private ownershipProgess: HTMLElement;
@ -22,9 +30,48 @@ export class PlanetView extends PlanetBase implements ViewObject {
this.ownershipProgess.className = 'ownership';
}
public step(deltaTimeInMilliseconds: number): void {
this.shape.randomOffset += deltaTimeInMilliseconds / 4000;
public step(deltaTimeInSeconds: number): void {
this.shape.randomOffset += deltaTimeInSeconds / 4;
this.shape.colorMixQ = this.ownership;
this.generatedPointElements.forEach((p) => {
vec2.add(
p.velocity,
p.velocity,
vec2.scale(vec2.create(), vec2.fromValues(0, 50), deltaTimeInSeconds),
);
vec2.add(
p.position,
p.position,
vec2.scale(vec2.create(), p.velocity, deltaTimeInSeconds),
);
if ((p.timeToLive -= deltaTimeInSeconds) <= 0) {
p.element.parentElement?.removeChild(p.element);
} else {
p.element.style.opacity = Math.min(1, p.timeToLive).toString();
}
});
this.generatedPointElements = this.generatedPointElements.filter(
(p) => p.timeToLive > 0,
);
}
private generatedPointElements: Array<FallingPoint> = [];
public generatedPoints(value: number) {
const element = document.createElement('div');
element.className = 'falling-point ' + (this.ownership < 0.5 ? 'decla' : 'red');
element.innerText = '+' + value;
this.generatedPointElements.push({
element,
addedToOverlay: false,
timeToLive: Random.getRandomInRange(2, 3),
position: vec2.create(),
velocity: vec2.fromValues(Random.getRandomInRange(-30, 30), 0),
});
}
private getGradient(): string {
@ -34,18 +81,21 @@ export class PlanetView extends PlanetBase implements ViewObject {
? `conic-gradient(
var(--bright-decla) ${sidePercent}%,
var(--bright-decla) ${sidePercent}%,
var(--bright-neutral) ${sidePercent}%,
var(--bright-neutral) 100%
rgba(0, 0, 0, 0) ${sidePercent}%,
rgba(0, 0, 0, 0) 100%
)`
: `conic-gradient(
var(--bright-neutral) 0%,
var(--bright-neutral) ${100 - sidePercent}%,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) ${100 - sidePercent}%,
var(--bright-red) ${100 - sidePercent}%,
var(--bright-red) 100%
)`;
}
public beforeDestroy(): void {
this.ownershipProgess.parentElement?.removeChild(this.ownershipProgess);
this.generatedPointElements.forEach((p) =>
p.element.parentElement?.removeChild(p.element),
);
}
public draw(renderer: Renderer, overlay: HTMLElement): void {
@ -54,6 +104,16 @@ export class PlanetView extends PlanetBase implements ViewObject {
}
const screenPosition = renderer.worldToDisplayCoordinates(this.center);
this.generatedPointElements.forEach((p) => {
if (!p.addedToOverlay) {
overlay.appendChild(p.element);
}
p.element.style.left = screenPosition.x + p.position.x + 'px';
p.element.style.top = screenPosition.y + p.position.y + 'px';
});
this.ownershipProgess.style.left = screenPosition.x + 'px';
this.ownershipProgess.style.top = screenPosition.y + 'px';
this.ownershipProgess.style.background = this.getGradient();

View file

@ -4,6 +4,7 @@ import { Circle, Id, PlayerCharacterBase, CharacterTeam, settings } from 'shared
import { OptionsHandler } from '../options-handler';
import { BlobShape } from '../shapes/blob-shape';
import { SoundHandler, Sounds } from '../sound-handler';
import { ViewObject } from './view-object';
export class PlayerCharacterView extends PlayerCharacterBase implements ViewObject {
@ -39,8 +40,17 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
return this.head!.center;
}
public step(deltaTimeInMilliseconds: number): void {
this.timeSinceLastNameElementUpdate += deltaTimeInMilliseconds;
public setHealth(health: number) {
const previousHealth = this.health;
super.setHealth(health);
SoundHandler.play(
Sounds.hit,
(0.4 * 2 * (previousHealth - health)) / settings.playerMaxStrength,
);
}
public step(deltaTimeInSeconds: number): void {
this.timeSinceLastNameElementUpdate += deltaTimeInSeconds;
this.healthElement.style.width = (50 * this.health) / settings.playerMaxHealth + 'px';
this.statsElement.innerText = this.getStatsText();
if (this.previousHealth > this.health) {
@ -52,6 +62,10 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
this.previousHealth = this.health;
}
public onShoot(strength: number) {
SoundHandler.play(Sounds.shoot, (0.3 * 2 * strength) / settings.playerMaxStrength);
}
public beforeDestroy(): void {
this.nameElement.parentElement?.removeChild(this.nameElement);
}

View file

@ -23,7 +23,9 @@ export class ProjectileView extends ProjectileBase implements ViewObject {
);
}
public step(deltaTimeInMilliseconds: number): void {
public step(deltaTimeInSeconds: number): void {
super.step(deltaTimeInSeconds);
this.circle.center = this.center;
this.light.center = this.center;
this.light.intensity = (0.15 * this.strength) / settings.projectileMaxStrength;

View file

@ -29,11 +29,7 @@ export abstract class OptionsHandler {
};
if (this._options.musicEnabled) {
const firstClickListener = () => {
document.removeEventListener('click', firstClickListener);
SoundHandler.playAmbient();
};
document.addEventListener('click', firstClickListener);
SoundHandler.playAmbient();
}
}

View file

@ -8,37 +8,71 @@ export enum Sounds {
hit = 'hit',
shoot = 'shoot',
click = 'click',
ambient = 'ambient',
}
const concurrencyScale = 5;
export abstract class SoundHandler {
private static sounds: { [key in Sounds]: HTMLAudioElement };
private static isAmbientPlaying = false;
public static initialize() {
private static ambientSound = new Audio(ambientSound);
private static initialized = false;
public static async initialize() {
this.sounds = {
[Sounds.hit]: new Audio(hitSound),
[Sounds.shoot]: new Audio(shootSound),
[Sounds.click]: new Audio(clickSound),
[Sounds.ambient]: new Audio(ambientSound),
[Sounds.hit]: await this.initializeSound(hitSound),
[Sounds.shoot]: await this.initializeSound(shootSound),
[Sounds.click]: await this.initializeSound(clickSound),
};
this.sounds.ambient.volume = 0.5;
this.ambientSound.play();
this.ambientSound.muted = true;
this.initialized = true;
setTimeout(() => {
this.ambientSound.muted = false;
this.ambientSound.volume = 0.5;
if (!this.isAmbientPlaying) {
this.ambientSound.pause();
}
}, 100);
}
public static play(sound: Sounds) {
if (OptionsHandler.options.soundsEnabled) {
if (this.sounds[sound].currentTime > 0) {
(this.sounds[sound].cloneNode() as HTMLAudioElement).play();
} else {
this.sounds[sound].play();
}
private static async initializeSound(hitSound: string): Promise<HTMLAudioElement> {
const sound = new Audio(hitSound);
sound.muted = true;
await sound.play();
sound.pause();
sound.muted = false;
sound.currentTime = 0;
return sound;
}
public static play(sound: Sounds, volume: number = 1) {
if (!this.initialized || !OptionsHandler.options.soundsEnabled) {
return;
}
const audio =
this.sounds[sound].currentTime > 0
? (this.sounds[sound].cloneNode(true) as HTMLAudioElement)
: this.sounds[sound];
audio.volume = volume;
audio.play();
}
public static playAmbient() {
this.sounds.ambient.play();
this.isAmbientPlaying = true;
if (this.initialized) {
this.ambientSound.play();
}
}
public static stopAmbient() {
this.sounds.ambient.pause();
this.isAmbientPlaying = false;
if (this.initialized) {
this.ambientSound.pause();
}
}
}

View file

@ -0,0 +1,64 @@
import {
CircleLight,
ColorfulCircle,
FilteringOptions,
Flashlight,
Renderer,
runAnimation,
WrapOptions,
} from 'sdf-2d';
import { settings } from 'shared';
import { BlobShape } from './shapes/blob-shape';
import { PlanetShape } from './shapes/planet-shape';
export const startAnimation = async (
canvas: HTMLCanvasElement,
draw: (r: Renderer, current: number, delta: number) => boolean,
noiseTexture: TexImageSource,
): Promise<void> =>
await runAnimation(
canvas,
[
{
...PlanetShape.descriptor,
shaderCombinationSteps: [0, 1, 2, 3],
},
{
...BlobShape.descriptor,
shaderCombinationSteps: [0, 1, 2, 8],
},
{
...ColorfulCircle.descriptor,
shaderCombinationSteps: [0, 2, 16],
},
{
...CircleLight.descriptor,
shaderCombinationSteps: [0, 1, 2, 4, 8, 16],
},
{
...Flashlight.descriptor,
shaderCombinationSteps: [0],
},
],
draw,
{
shadowTraceCount: 16,
paletteSize: settings.palette.length,
//enableStopwatch: true,
},
{
colorPalette: settings.palette,
enableHighDpiRendering: true,
lightCutoffDistance: settings.lightCutoffDistance,
textures: {
noiseTexture: {
source: noiseTexture,
overrides: {
maxFilter: FilteringOptions.LINEAR,
wrapS: WrapOptions.MIRRORED_REPEAT,
wrapT: WrapOptions.MIRRORED_REPEAT,
},
},
},
},
);

View file

@ -13,7 +13,7 @@ $large-icon: 48px;
$small-icon: 32px;
$bright-decla: #4069a5;
$bright-red: #d15652;
$bright-neutral: #88888877;
$bright-neutral: #ccc;
:root {
--bright-decla: #{$bright-decla};

View file

@ -96,9 +96,15 @@ form {
border-radius: $border-radius;
padding: $small-padding;
border: $border;
cursor: pointer;
margin: $border-width-focused - $border-width $border-width-focused -
$border-width + $small-padding / 2;
.completion {
font-size: 0.7em;
color: $bright-neutral;
}
}
&:focus {

View file

@ -50,13 +50,18 @@
#settings {
display: flex;
flex-direction: column;
padding: 0 $medium-padding $medium-padding $medium-padding;
@include background;
border-radius: 12px;
z-index: 100;
margin-right: $medium-padding - $small-padding;
padding: $small-padding;
transform: translateY(-100%);
@media (max-width: $breakpoint) {
flex-direction: row-reverse;
transform: translateX(100%);
padding: $medium-padding 0 $medium-padding $medium-padding;
margin-top: $medium-padding - $small-padding;
margin-right: 0;
}
opacity: 0;
@ -72,13 +77,9 @@
svg {
cursor: pointer;
transition: opacity $animation-time;
margin-bottom: $small-padding;
margin-left: 0;
@include square($large-icon);
@media (max-width: $breakpoint) {
@include square($small-icon);
margin-bottom: 0;
margin-left: $small-padding;
}
}
@ -130,4 +131,33 @@
}
}
}
img,
svg {
margin-top: $small-padding;
@media (max-width: $breakpoint) {
margin-top: 0;
margin-right: $small-padding;
}
}
label:first-child {
img,
svg {
margin-top: 0;
margin-right: 0;
}
}
label:not(:first-child) {
input[type='checkbox']:after {
$height: 4px;
top: $height / 2 + $small-padding;
@media (max-width: $breakpoint) {
right: $small-padding;
top: $height / 2;
}
}
}
}

7
frontend/static/flag.svg Normal file
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"/>
<line x1="5" y1="5" x2="5" y2="21" />
<line x1="19" y1="5" x2="19" y2="14" />
<path d="M5 5a5 5 0 0 1 7 0a5 5 0 0 0 7 0" />
<path d="M5 14a5 5 0 0 1 7 0a5 5 0 0 0 7 0" />
</svg>

After

Width:  |  Height:  |  Size: 418 B

BIN
frontend/static/shoot2.mp3 Normal file

Binary file not shown.

BIN
frontend/static/shoot3.mp3 Normal file

Binary file not shown.

BIN
frontend/static/shoot4.mp3 Normal file

Binary file not shown.

BIN
frontend/static/shoot5.mp3 Normal file

Binary file not shown.

BIN
frontend/static/shoot6.mp3 Normal file

Binary file not shown.