Skip to content

Commit

Permalink
add particles & sound effects, & some refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
Vulae committed Apr 26, 2024
1 parent 8ba188d commit f018b9c
Show file tree
Hide file tree
Showing 26 changed files with 956 additions and 222 deletions.
14 changes: 7 additions & 7 deletions TODO
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@

* [ ] Actual README.md

* [X] Custom visuals
* [ ] Reveal visual particle
* [ ] Explosion particles
* [ ] Custom sound effects
* [ ] Flag sound effect
* [ ] Reveal sound effect
* [ ] Bomb sound effect
* [X] Particles
* [ ] Option to disable particle layer
* [ ] Better sound effects
* [ ] Option to change volume of sound effects
* [ ] Favicon
* [ ] Theme picker
* [ ] Modern theme (Flat style graphics)
* [ ] SeaBear1015 theme (https://github.com/Vulae/infinite-minesweeper/issues/4)

* [ ] Extreme zoom out
* (Zoom out very far, stop rendering individual tiles and just render biome colors)
Expand Down
26 changes: 23 additions & 3 deletions src/components/Game.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,36 @@
import { World } from "$lib/game/World";
import { onMount, onDestroy } from "svelte";
import Renderer from "./Renderer.svelte";
import type { Theme } from "$lib/game/theme/Theme";
import { ThemeRetro } from "$lib/game/theme/retro";
import { volume } from "../store";
export let saveSlot: string;
let world: World;
let theme: Theme = new ThemeRetro();
$: theme.volume = $volume;
onMount(async () => {
await theme.init();
world = load(saveSlot);
world.addEventListener('sound_unflag', () => {
theme.playSound('unflag');
});
world.addEventListener('sound_reveal', ({ data: count }) => {
theme.playSound('reveal', count / 10 + 0.9);
});
world.addEventListener('sound_explosion', () => {
theme.playSound('explosion');
});
});
onDestroy(() => {
save(saveSlot, world);
// FIXME: Fix onDestroy renderer error.
// world.destroyDispatcher();
// save(saveSlot, world);
location.reload();
});
</script>
Expand All @@ -23,8 +43,8 @@
}}
/>

{#if world}
<Renderer {world} on:action={ev => {
{#if world && theme}
<Renderer {world} {theme} on:action={ev => {
if(ev.detail.type == 'reveal') {
world.reveal(ev.detail.pos.x, ev.detail.pos.y);
} else if(ev.detail.type == 'flag') {
Expand Down
1 change: 1 addition & 0 deletions src/components/InfoModalBiomes.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
}
.biome-backdrop {
// FIXME: overflow-visible on image is a very jank way to do this, please use another way.
@apply blur-sm w-full h-full object-cover overflow-visible;
animation: backdrop-intro 250ms ease-out;
Expand Down
109 changes: 68 additions & 41 deletions src/components/Renderer.svelte
Original file line number Diff line number Diff line change
@@ -1,74 +1,94 @@
<script lang="ts">
import { resize } from "$lib/actions/Resize";
import { WorldRenderer } from "$lib/game/Renderer";
import { WorldRenderer } from "$lib/game/WorldRenderer";
import { Viewport } from "$lib/game/Viewport";
import type { World } from "$lib/game/World";
import { createEventDispatcher, onDestroy, onMount } from "svelte";
import { ParticleRenderer } from "$lib/game/ParticleRenderer";
import type { Theme } from "$lib/game/theme/Theme";
const dispatcher = createEventDispatcher();
let container: HTMLDivElement;
export let world: World;
let renderer: WorldRenderer;
let canvas: HTMLCanvasElement;
export let theme: Theme;
const viewport: Viewport = new Viewport(world);
// We separate world & particles to improve performance, not needing to render both at the same time.
let worldRenderer: WorldRenderer;
let worldCanvas: HTMLCanvasElement;
let particleRenderer: ParticleRenderer;
let particleCanvas: HTMLCanvasElement;
let firstCanvasResize: boolean = true;
let needsRerender: boolean = false;
let worldNeedsRerender: boolean = false;
let animFrame: number = -1;
const render = () => {
cancelAnimationFrame(animFrame);
animFrame = requestAnimationFrame(render);
if(needsRerender) {
needsRerender = false;
renderer.render();
if(worldNeedsRerender) {
worldNeedsRerender = false;
worldRenderer.render();
}
particleRenderer.render();
}
let keys: Set<string> = new Set();
let keysInterval: number = -1;
onMount(async () => {
renderer = new WorldRenderer(world, canvas);
worldRenderer = new WorldRenderer(world, theme, worldCanvas, viewport);
particleRenderer = new ParticleRenderer(world, theme, particleCanvas, viewport);
// TODO: Clean Up!
clearInterval(keysInterval);
keysInterval = setInterval(() => {
let change: boolean = false;
if(keys.has('[')) {
if(renderer.cameraZoom != renderer.cameraScale(1.04)) {
if(viewport.cameraZoom != viewport.cameraScale(1.04)) {
change = true;
}
}
if(keys.has(']')) {
if(renderer.cameraZoom != renderer.cameraScale(0.96)) {
if(viewport.cameraZoom != viewport.cameraScale(0.96)) {
change = true;
}
}
if(keys.has('ArrowUp')) { renderer.cameraTranslate(0, 10); change = true; }
if(keys.has('ArrowDown')) { renderer.cameraTranslate(0, -10); change = true; }
if(keys.has('ArrowLeft')) { renderer.cameraTranslate(10, 0); change = true; }
if(keys.has('ArrowRight')) { renderer.cameraTranslate(-10, 0); change = true; }
if(keys.has('ArrowUp')) { viewport.cameraTranslate(0, 10); change = true; }
if(keys.has('ArrowDown')) { viewport.cameraTranslate(0, -10); change = true; }
if(keys.has('ArrowLeft')) { viewport.cameraTranslate(10, 0); change = true; }
if(keys.has('ArrowRight')) { viewport.cameraTranslate(-10, 0); change = true; }
if(keys.has('s')) {
// DEBUG: Zoom to nearest power of 2, for a crisp screenshot.
renderer.cameraZoom = Math.pow(2, Math.ceil(Math.log(renderer.cameraZoom) / Math.log(2)));
renderer.cameraScale(1);
viewport.cameraZoom = Math.pow(2, Math.ceil(Math.log(viewport.cameraZoom) / Math.log(2)));
viewport.cameraScale(1);
change = true;
}
if(change) {
needsRerender = true;
worldNeedsRerender = true;
}
}, 1000 / 60);
// FIXME: Why is this not always accurate.
// Sometimes renderer.init() does not load theme fully before returning.
await renderer.init();
await worldRenderer.init();
await particleRenderer.init();
setTimeout(() => {
viewport.setSize(worldCanvas.width, worldCanvas.height);
worldNeedsRerender = true;
render();
}, 100);
});
onDestroy(() => {
worldRenderer.destroy();
particleRenderer.destroy();
cancelAnimationFrame(animFrame);
clearInterval(keysInterval);
});
Expand All @@ -84,59 +104,66 @@
}}
/>

<canvas
class="w-full h-full cursor-pointer"
bind:this={canvas}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="w-full h-full force-overlap cursor-pointer"
bind:this={container}
use:resize={(width, height) => {
canvas.width = width;
canvas.height = height;
renderer.cameraScale(1);
worldCanvas.width = width;
worldCanvas.height = height;
particleCanvas.width = width;
particleCanvas.height = height;
viewport.setSize(width, height);
viewport.cameraScale(1);
if(firstCanvasResize) {
renderer.cameraTranslate(canvas.width / 2, canvas.height / 2);
viewport.cameraTranslate(width / 2, height / 2);
firstCanvasResize = false;
}
needsRerender = true;
worldNeedsRerender = true;
}}
on:mousedown={ev => {
if(document.pointerLockElement == canvas) return;
if(document.pointerLockElement == container) return;
if(ev.button == 1) {
canvas.requestPointerLock();
container.requestPointerLock();
ev.preventDefault();
} else if(ev.button == 0) {
ev.preventDefault();
const pos = renderer.cameraPos(ev.offsetX, ev.offsetY);
const pos = viewport.cameraPos(ev.offsetX, ev.offsetY);
dispatcher('action', { type: 'reveal', pos });
needsRerender = true;
worldNeedsRerender = true;
} else if(ev.button == 2) {
ev.preventDefault();
const pos = renderer.cameraPos(ev.offsetX, ev.offsetY);
const pos = viewport.cameraPos(ev.offsetX, ev.offsetY);
dispatcher('action', { type: 'flag', pos });
needsRerender = true;
worldNeedsRerender = true;
} else if(ev.button == 3) {
// DEBUG: Reset tile
ev.preventDefault();
const pos = renderer.cameraPos(ev.offsetX, ev.offsetY);
const pos = viewport.cameraPos(ev.offsetX, ev.offsetY);
dispatcher('action', { type: 'reset', pos });
needsRerender = true;
worldNeedsRerender = true;
}
}}
on:mouseup={ev => {
if(document.pointerLockElement != canvas) return;
if(document.pointerLockElement != container) return;
if(ev.button != 1) return;
document.exitPointerLock();
}}
on:mousemove={ev => {
if(document.pointerLockElement != canvas) return;
renderer.cameraTranslate(ev.movementX, ev.movementY);
needsRerender = true;
if(document.pointerLockElement != container) return;
viewport.cameraTranslate(ev.movementX, ev.movementY);
worldNeedsRerender = true;
}}
on:wheel|passive={ev => {
const scale = ev.deltaY > 0 ? 0.9 : 1.1;
if(renderer.cameraZoom != renderer.cameraScale(scale)) {
needsRerender = true;
if(viewport.cameraZoom != viewport.cameraScale(scale)) {
worldNeedsRerender = true;
}
}}
on:contextmenu={ev => {
ev.preventDefault();
}}
/>
>
<canvas bind:this={worldCanvas} />
<canvas bind:this={particleCanvas} />
</div>
Loading

0 comments on commit f018b9c

Please sign in to comment.