diff --git a/app/provider.tsx b/app/provider.tsx
index 1631ea2..3d19e55 100644
--- a/app/provider.tsx
+++ b/app/provider.tsx
@@ -5,7 +5,6 @@ import { ConvexReactClient } from "convex/react";
import { ThemeProvider } from "next-themes";
import PlausibleProvider from "next-plausible";
import { Toaster } from "@/components/ui/toaster";
-import VisualizerProvider from "@/components/VisualizerProvider";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
@@ -18,10 +17,8 @@ export function Providers({ children }: { children: React.ReactNode }) {
>
-
- {children}
-
-
+ {children}
+
diff --git a/components/Visualizer.tsx b/components/Visualizer.tsx
index 0e239d1..0058b20 100644
--- a/components/Visualizer.tsx
+++ b/components/Visualizer.tsx
@@ -2,7 +2,6 @@ import React from "react";
import { Button } from "@/components/ui/button";
import { Renderer } from "@/renderer";
import { ZombieSurvival } from "@/simulators/zombie-survival";
-import { useVisualizer } from "@/components/VisualizerProvider";
const AUTO_REPLAY_SPEED = 1_500;
const REPLAY_SPEED = 600;
@@ -24,7 +23,6 @@ export function Visualizer({
onReset?: () => unknown;
onSimulationEnd?: (isWin: boolean) => unknown;
}) {
- const visualizer = useVisualizer();
const simulator = React.useRef(new ZombieSurvival(map));
const renderer = React.useRef(null);
const interval = React.useRef | null>(null);
@@ -34,20 +32,15 @@ export function Visualizer({
const [running, setRunning] = React.useState(false);
React.useEffect(() => {
- if (
- visualizer.ready &&
- canvas.current !== null &&
- renderer.current === null
- ) {
+ if (canvas.current !== null) {
renderer.current = new Renderer(
- visualizer.getAssets(),
ZombieSurvival.boardHeight(map),
ZombieSurvival.boardWidth(map),
canvas.current,
Number.parseInt(cellSize, 10),
);
}
- }, [canvas, visualizer.ready]);
+ }, [canvas]);
React.useEffect(() => {
if (autoStart) {
diff --git a/components/VisualizerProvider.tsx b/components/VisualizerProvider.tsx
deleted file mode 100644
index 23b8562..0000000
--- a/components/VisualizerProvider.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-"use client";
-
-import React from "react";
-import { type RendererAssets } from "@/renderer";
-
-export type VisualizerContextValue =
- | {
- ready: false;
- }
- | {
- getAssets: () => RendererAssets;
- ready: true;
- };
-
-const VisualizerContext = React.createContext(
- null,
-);
-
-async function loadImage(src: string): Promise {
- return await new Promise((resolve) => {
- const img = new Image();
- img.addEventListener("load", () => resolve(img));
- img.src = src;
- });
-}
-
-export default function VisualizerProvider({
- children,
-}: {
- children: React.ReactNode;
-}): React.ReactNode {
- const [ready, setReady] = React.useState(false);
- const assets = React.useRef(null);
-
- React.useEffect(() => {
- Promise.all([
- loadImage("/map.png"),
- loadImage("/entities/block.svg"),
- loadImage("/entities/player_alive_1.svg"),
- loadImage("/entities/rocks.png"),
- loadImage("/entities/zombie_alive_1.svg"),
- ]).then((result) => {
- setReady(true);
-
- assets.current = {
- bg: result[0],
- box: result[1],
- player: result[2],
- rock: result[3],
- zombie: result[4],
- };
- });
- }, []);
-
- function getAssets(): RendererAssets {
- if (!ready || assets.current === null) {
- throw new Error(
- "Tried accessing visualizer images before they are loaded",
- );
- }
-
- return assets.current;
- }
-
- return (
-
- {children}
-
- );
-}
-
-export function useVisualizer() {
- const context = React.useContext(VisualizerContext);
-
- if (context === null) {
- throw new Error(
- "useVisualizer should be used within the VisualizerProvider",
- );
- }
-
- return context;
-}
diff --git a/renderer/index.ts b/renderer/index.ts
index b44a48f..f32827b 100644
--- a/renderer/index.ts
+++ b/renderer/index.ts
@@ -1,11 +1,78 @@
import { type Entity, EntityType } from "@/simulators/zombie-survival";
export interface RendererAssets {
- bg: HTMLImageElement;
- box: HTMLImageElement;
- player: HTMLImageElement;
- rock: HTMLImageElement;
- zombie: HTMLImageElement;
+ loading: boolean;
+ loaded: boolean;
+ bg: HTMLImageElement | null;
+ box: HTMLImageElement | null;
+ player: HTMLImageElement | null;
+ rock: HTMLImageElement | null;
+ zombie: HTMLImageElement | null;
+}
+
+const assets: RendererAssets = {
+ loading: false,
+ loaded: false,
+ bg: null,
+ box: null,
+ player: null,
+ rock: null,
+ zombie: null,
+};
+
+async function loadAssets() {
+ if (assets.loading || assets.loaded) {
+ return;
+ }
+
+ assets.loading = true;
+
+ const [bg, box, player, rock, zombie] = await Promise.all([
+ loadImage("/map.png"),
+ loadImage("/entities/block.svg"),
+ loadImage("/entities/player_alive_1.svg"),
+ loadImage("/entities/rocks.svg"),
+ loadImage("/entities/zombie_alive_1.svg"),
+ ]);
+
+ assets.loaded = true;
+ assets.bg = bg;
+ assets.box = box;
+ assets.player = player;
+ assets.rock = rock;
+ assets.zombie = zombie;
+}
+
+async function loadImage(src: string): Promise {
+ return await new Promise((resolve) => {
+ const img = new Image();
+ img.addEventListener("load", () => resolve(img));
+ img.src = src;
+ });
+}
+
+function getEntityImage(entity: Entity): HTMLImageElement | null {
+ switch (entity.getType()) {
+ case EntityType.Box: {
+ return assets.box;
+ }
+ case EntityType.Player: {
+ return assets.player;
+ }
+ case EntityType.Rock: {
+ return assets.rock;
+ }
+ case EntityType.Zombie: {
+ return assets.zombie;
+ }
+ }
+}
+
+function getEntityOffset(entity: Entity): { x: number; y: number } {
+ return {
+ x: entity.getType() === EntityType.Zombie ? 16 : 0,
+ y: 0,
+ };
}
export class Renderer {
@@ -17,7 +84,6 @@ export class Renderer {
private ctx: CanvasRenderingContext2D;
public constructor(
- assets: RendererAssets,
boardHeight: number,
boardWidth: number,
canvas: HTMLCanvasElement,
@@ -42,6 +108,7 @@ export class Renderer {
canvas.style.width = `${this.w}px`;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
+ void loadAssets();
}
public render(entities: Entity[]) {
@@ -56,8 +123,12 @@ export class Renderer {
}
private drawBg() {
+ if (assets.bg === null) {
+ return;
+ }
+
const canvasRatio = this.w / this.h;
- const bgRatio = this.assets.bg.width / this.assets.bg.height;
+ const bgRatio = assets.bg.width / assets.bg.height;
let drawWidth, drawHeight, offsetX, offsetY;
@@ -74,14 +145,19 @@ export class Renderer {
}
this.ctx.globalAlpha = 0.5;
- this.ctx.drawImage(this.assets.bg, offsetX, offsetY, drawWidth, drawHeight);
+ this.ctx.drawImage(assets.bg, offsetX, offsetY, drawWidth, drawHeight);
this.ctx.globalAlpha = 1.0;
}
private drawEntity(entity: Entity) {
- const entityImage = this.getEntityImage(entity);
+ const entityImage = getEntityImage(entity);
+
+ if (entityImage === null) {
+ return;
+ }
+
const entityPosition = entity.getPosition();
- const entityOffset = this.getEntityOffset(entity);
+ const entityOffset = getEntityOffset(entity);
this.ctx.globalAlpha =
entity.getType() === EntityType.Zombie && entity.getHealth() === 1
@@ -96,28 +172,4 @@ export class Renderer {
this.cellSize,
);
}
-
- private getEntityImage(entity: Entity): HTMLImageElement {
- switch (entity.getType()) {
- case EntityType.Box: {
- return this.assets.box;
- }
- case EntityType.Player: {
- return this.assets.player;
- }
- case EntityType.Rock: {
- return this.assets.rock;
- }
- case EntityType.Zombie: {
- return this.assets.zombie;
- }
- }
- }
-
- private getEntityOffset(entity: Entity): { x: number; y: number } {
- return {
- x: entity.getType() === EntityType.Zombie ? 16 : 0,
- y: 0,
- };
- }
}