Skip to content

Commit

Permalink
feat: implement basic game menu
Browse files Browse the repository at this point in the history
  • Loading branch information
learosema committed Nov 7, 2023
1 parent 6fe1fce commit cd3972a
Show file tree
Hide file tree
Showing 13 changed files with 436 additions and 308 deletions.
2 changes: 2 additions & 0 deletions src/_includes/game-menu.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<game-menu>
</game-menu>
20 changes: 1 addition & 19 deletions src/_layouts/game.njk
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,7 @@
{{ content | safe }}
</script>
</boulders-game>
<button class="burger" aria-controls="gameMenu" onclick="gameMenu.showModal()">
<svg viewBox="0 0 16 16" fill="currentColor">
<rect x="1" y="1" width="14" height="3" />
<rect x="1" y="6" width="14" height="3" />
<rect x="1" y="11" width="14" height="3" />
</svg>
<span class="v-hidden">
open menu
</span>
</button>
<dialog class="game-menu flow" id="gameMenu">
<h2>Menu</h2>
<form method="dialog">
<button class="button">Return to game</button>
</form>
<a href="{{ meta.url }}{{ page.url }}" class="button">Restart game</a>
<a href="{{ meta.url }}" class="button">Back to main menu</a>
</dialog>

{% include "game-menu.njk" %}
{% endblock %}

{% block scripts %}
Expand Down
3 changes: 2 additions & 1 deletion src/css/components/_burger.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
.burger {
position: absolute;
position: fixed;
display: block;
top: 16px;
right: 16px;
padding: 8px;
margin: 0;
color: var(--fg);
z-index: 10;

> svg {
display: block;
height: 32px;
Expand Down
14 changes: 0 additions & 14 deletions src/css/components/_button.css

This file was deleted.

23 changes: 23 additions & 0 deletions src/css/components/_game-menu.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,28 @@
&::backdrop {
backdrop-filter: blur(4px);
}

> .field label {
display: block;
}

& .button,
& .select {
display: block;
border: none;
border-radius: 2px;
text-align: center;
background: var(--button-bg);
color: var(--button-fg);
text-decoration: none;
padding: .5em 1em;
width: 100%;
user-select: none;

&:active {
background: var(--button-bg--active);
}
}

}

5 changes: 5 additions & 0 deletions src/css/config/_reset.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ button, *:where([tabindex="0"]) {
color: currentColor;
text-decoration: none;
}

select {
font-family: inherit;
font-size: inherit;
}
2 changes: 1 addition & 1 deletion src/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@import 'components/_boulders-game.css';
@import 'components/_game-menu.css';
@import 'components/_burger.css';
@import 'components/_button.css';


@import 'utils/_v-hidden.css';
@import 'utils/_flow.css';
270 changes: 270 additions & 0 deletions src/game/components/boulders-game.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { SoundMachine } from "../audio/sound-machine";
import { IRenderer } from "../interfaces/irenderer";
import { SupportedEngines, rendererFactory } from "../renderer/engines";
import { AnimationLoop } from "../utils/animation-interval";
import { Level, LevelCallbackFunction } from "../utils/level";
import { loadImage } from "../utils/load-image";

export class BouldersGame extends HTMLElement {

canvas: HTMLCanvasElement | null = null;
renderer: IRenderer | null = null;
level: Level;
timer = NaN;
initialized = false;
sprites: HTMLImageElement|null = null;
animationLoop: AnimationLoop|null = null;
inputQueue: string[] = [];
framecycles = 0;
soundMachine = new SoundMachine();

constructor() {
super();
this.level = Level.parse(this.querySelector('script')?.textContent || '');
}

static observedAttributes = ['autofocus', 'engine'];

static register() {
customElements.define('boulders-game', BouldersGame);
}

/* attributes */

get autofocus(): boolean {
return this.hasAttribute('autofocus');
}

get engine(): SupportedEngines {
const engine = this.getAttribute('engine');
if (! engine) {
return 'canvas2d';
}
if (engine !== 'webgpu' && engine !== 'canvas2d' && engine !== 'webgl' && engine !== 'noop') {
throw new Error('Unsupported Engine');
}
return engine;
}

/* lifecycle callbacks */

async connectedCallback() {
await this.setup();
}

disconnectedCallback() {
this.dispose();
}

async attributeChangedCallback(name: string, oldValue: string, newValue: string) {
console.log(name, oldValue, newValue);
if (name === 'engine') {
this.createRenderer();
}
}

/* private methods */

private async createRenderer() {
if (!this.level) {
throw new Error('no level');
}
if (! this.sprites) {
this.sprites = await loadImage('/gfx/sprites.png');
}
this.createCanvas();
if (this.renderer) {
this.renderer.dispose();
this.renderer = null;
}
this.renderer = rendererFactory(this.engine, this.canvas!, this.sprites, this.level);
await this.renderer.setup();
if (this.level) {
this.renderer.setSize();
this.renderer.frame();
}
if (this.autofocus) {
setTimeout(() => this.canvas?.focus(), 0);
}
}

private createCanvas(): void {
this.destroyCanvas();
this.canvas = document.createElement('canvas');
this.canvas.setAttribute('tabindex', '0');
this.appendChild(this.canvas);
if (! this.canvas) {
throw Error('Canvas creation failed.');
}
this.canvas.addEventListener('focus', this.onFocus, false);
this.canvas.addEventListener('blur', this.onBlur, false);
this.canvas.addEventListener('keydown', this.onKeyDown, false);
window.addEventListener('resize', this.onResize, false);
}

private destroyCanvas(): void {
if (this.canvas) {
window.removeEventListener('resize', this.onResize, false);
this.canvas.removeEventListener('focus', this.onFocus, false);
this.canvas.removeEventListener('blur', this.onBlur, false);
this.canvas.removeEventListener('keydown', this.onKeyDown, false);
this.canvas.remove();
this.canvas = null;
}
}

private async setup() {
this.soundMachine.setup();
if (! this.renderer) {
await this.createRenderer();
}
if (! this.animationLoop) {
this.animationLoop = new AnimationLoop();
this.animationLoop.add(this.renderLoop, 1000 / 25);
this.animationLoop.add(this.inputLoop, 50);
this.animationLoop.add(this.stoneLoop, 200);
this.animationLoop.add(this.ghostLoop, 300);
}
this.level.subscribe(this.onGameEvent);

this.initialized = true;
}

dispose() {
this.animationLoop?.dispose();
this.renderer?.dispose();
this.soundMachine.dispose();
this.destroyCanvas();
this.level?.unsubscribe();
this.renderer = null;
this.initialized = false;
}

onGameEvent: LevelCallbackFunction = (eventName: string) => {
if (eventName === 'gem') {
this.soundMachine.bling();
}
if (eventName === 'push') {
this.soundMachine.push();
}
if (eventName === 'ground') {
this.soundMachine.rock();
}
if (eventName === 'gameover') {
this.soundMachine.gameover();
}
}

onKeyDown = (e: KeyboardEvent) => {
if (e.code === 'Escape') {
const gameMenu: HTMLDialogElement = document.getElementById('gameMenu') as HTMLDialogElement;
window.setTimeout(() => gameMenu.showModal(), 0);
}
if (!this.level?.playerAlive) {
return;
}
if (e.code === 'ArrowUp' || e.code === 'KeyW') {
this.inputQueue.push('up');
}
if (e.code === 'ArrowDown' || e.code === 'KeyS') {
this.inputQueue.push('down');
}
if (e.code === 'ArrowLeft' || e.code === 'KeyA') {
this.inputQueue.push('left');
}
if (e.code === 'ArrowRight' || e.code === 'KeyD') {
this.inputQueue.push('right');
}

};

onPointerDown = (e: PointerEvent) => {
const thirdX = this.clientWidth / 3;
const thirdY = this.clientHeight / 3;
const [X, Y] = [(e.clientX / thirdX)|0, (e.clientY / thirdY)|0];

if (!this.level.playerAlive) {
return;
}
if (X === 1 && Y === 0) {
this.inputQueue.push('up');
}
if (X === 0 && Y === 1) {
this.inputQueue.push('left');
}
if (X === 2 && Y === 1) {
this.inputQueue.push('right');
}
if (X === 1 && Y === 2) {
this.inputQueue.push('down');
}
}

onFocus = () => {
this.animationLoop?.run();
window.setTimeout(() =>
this.addEventListener('pointerdown', this.onPointerDown, false), 100
);
}

onBlur = () => {
this.animationLoop?.stop();
this.removeEventListener('pointerdown', this.onPointerDown, false)
}

onResize = () => {
if (!this.renderer) {
return;
}

this.renderer.setSize();
if (this.level) {
this.renderer.frame();
}
}

renderLoop = (t: DOMHighResTimeStamp) => {
if (!this.level || !this.renderer) {
return;
}
this.renderer.frame();
}

stoneLoop = () => {
if (this.level) {
this.level.stoneFall();
this.renderer?.frame();
}
}

ghostLoop = () => {
if (this.level) {
this.level.moveGhosts();
this.renderer?.frame();
}
}

inputLoop = () => {
const code = this.inputQueue.pop();
const { level } = this;
if (! level || !level.playerAlive) {
return;
}
if (code === 'up') {
level.move(0, -1);
this.renderer?.frame();
}
if (code === 'down') {
level.move(0, 1);
this.renderer?.frame();
}
if (code === 'left') {
level.move(-1, 0);
this.renderer?.frame();
}
if (code === 'right') {
level.move(1, 0);
this.renderer?.frame();
}
}
}
Loading

0 comments on commit cd3972a

Please sign in to comment.