diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx
index 579590b..e14f5d6 100644
--- a/src/components/App/index.tsx
+++ b/src/components/App/index.tsx
@@ -30,19 +30,21 @@ export default function App() {
{gameState === GAME_STATES.GameOver && }
-
+
-
+
diff --git a/src/components/CellarDoor/index.tsx b/src/components/CellarDoor/index.tsx
index 3c37be8..77b3161 100644
--- a/src/components/CellarDoor/index.tsx
+++ b/src/components/CellarDoor/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect, FunctionComponent } from "react";
+import { useEffect, useRef, FC } from "react";
import { TILE_SETS } from "../../constants";
import "./style.css";
@@ -7,63 +7,51 @@ const HEIGHT = 64;
const TILE_X = 992;
const TILE_Y = 160;
+type CellarDoorProps = { top: number; left: number; isOpen?: boolean };
+
/*
* TODO:
- * - useRef instead of getElementById
* - util function for tile set, tiles and animation
- * - create global constants for tile sets and tile size
- * - prefer to return early, flip the if condition
* - track state internally
*/
-const CellarDoor: FunctionComponent<{ isOpen?: boolean }> = ({
- isOpen = false,
-}) => {
+const CellarDoor: FC = ({ isOpen = false, top, left }) => {
+ const canvasRef = useRef(null);
useEffect(() => {
- const canvas = document.getElementById(
- "cellar-door-canvas"
- ) as HTMLCanvasElement | null;
- const ctx = canvas?.getContext("2d");
+ const ctx = canvasRef.current?.getContext("2d");
+
+ if (!canvasRef.current || !ctx) {
+ return;
+ }
- if (canvas && ctx) {
- canvas.style.left = "528px";
- canvas.style.top = "272px";
+ canvasRef.current.style.left = `${left}px`;
+ canvasRef.current.style.top = `${top}px`;
- const tileSet = new Image();
- tileSet.src = TILE_SETS.World;
- tileSet.onload = () => {
- ctx.clearRect(0, 0, WIDTH, HEIGHT);
+ const tileSet = new Image();
+ tileSet.src = TILE_SETS.World;
+ tileSet.onload = () => {
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
- if (isOpen) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- };
- }
- }, [isOpen]);
+ ctx.drawImage(
+ tileSet,
+ isOpen ? TILE_X + WIDTH : TILE_X,
+ TILE_Y,
+ WIDTH,
+ HEIGHT,
+ 0,
+ 0,
+ WIDTH,
+ HEIGHT
+ );
+ };
+ }, [isOpen, left, top]);
return (
-
+
);
};
diff --git a/src/components/Coin/index.tsx b/src/components/Coin/index.tsx
index 52c0673..b2ee5b8 100644
--- a/src/components/Coin/index.tsx
+++ b/src/components/Coin/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect, FunctionComponent } from "react";
+import { useEffect, useRef, FC } from "react";
import { TILE_SIZE, TILE_SETS } from "../../constants";
import "./style.css";
@@ -8,94 +8,63 @@ const TILE_X = 0;
const TILE_Y = 128;
const ANIMATION_LENGTH = 3;
+type CoinProps = { left: number; top: number };
+
/*
* TODO:
- * - useRef instead of getElementById
* - util function for tile set, tiles and animation
- * - create global constants for tile sets and tile size
- * - prefer to return early, flip the if condition
- * - clear interval on component destroy
*/
-const Coin: FunctionComponent<{ left: number; top: number }> = ({
- left,
- top,
-}) => {
+const Coin: FC = ({ left, top }) => {
+ const canvasRef = useRef(null);
+
useEffect(() => {
- const canvas = document.getElementById(
- "coin-canvas"
- ) as HTMLCanvasElement | null;
- const ctx = canvas?.getContext("2d");
+ const ctx = canvasRef.current?.getContext("2d");
+ let intervalId: number;
+
+ if (!canvasRef.current || !ctx) {
+ return;
+ }
- if (canvas && ctx) {
- canvas.style.left = `${left}px`;
- canvas.style.top = `${top}px`;
+ canvasRef.current.style.left = `${left}px`;
+ canvasRef.current.style.top = `${top}px`;
- const tileSet = new Image();
- tileSet.src = TILE_SETS.Objects;
- tileSet.onload = () => {
- let currentFrame = 0;
+ const tileSet = new Image();
+ tileSet.src = TILE_SETS.Objects;
+ tileSet.onload = () => {
+ let currentFrame = 0;
- setInterval(() => {
- ctx.clearRect(0, 0, WIDTH, HEIGHT);
+ intervalId = window.setInterval(() => {
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
- if (currentFrame === 0) {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 1) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 2) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 2,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 3) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 3,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
+ ctx.drawImage(
+ tileSet,
+ TILE_X + WIDTH * currentFrame,
+ TILE_Y,
+ WIDTH,
+ HEIGHT,
+ 0,
+ 0,
+ WIDTH,
+ HEIGHT
+ );
- currentFrame =
- currentFrame === ANIMATION_LENGTH ? 0 : currentFrame + 1;
- }, 100);
- };
- }
+ currentFrame = currentFrame === ANIMATION_LENGTH ? 0 : currentFrame + 1;
+ }, 100);
+ };
+
+ return () => {
+ clearInterval(intervalId);
+ };
}, [left, top]);
- return ;
+ return (
+
+ );
};
export default Coin;
diff --git a/src/components/Fire/index.tsx b/src/components/Fire/index.tsx
index 8a0bdc7..f9cd81d 100644
--- a/src/components/Fire/index.tsx
+++ b/src/components/Fire/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect, FunctionComponent } from "react";
+import { useEffect, useRef, FC } from "react";
import { TILE_SIZE, TILE_SETS } from "../../constants";
import "./style.css";
@@ -8,130 +8,63 @@ const TILE_X = 130;
const TILE_Y = 98;
const ANIMATION_LENGTH = 6;
+type FireProps = { left: number; top: number };
+
/*
* TODO:
- * - useRef instead of getElementById
* - util function for tile set, tiles and animation
- * - create global constants for tile sets and tile size
- * - prefer to return early, flip the if condition
- * - clear interval on component destroy
*/
-const Fire: FunctionComponent<{ left: number; top: number }> = ({
- left,
- top,
-}) => {
+const Fire: FC = ({ left, top }) => {
+ const canvasRef = useRef(null);
+
useEffect(() => {
- const canvas = document.getElementById(
- "fire-canvas"
- ) as HTMLCanvasElement | null;
- const ctx = canvas?.getContext("2d");
+ const ctx = canvasRef.current?.getContext("2d");
+ let intervalId: number;
+
+ if (!canvasRef.current || !ctx) {
+ return;
+ }
- if (canvas && ctx) {
- canvas.style.left = `${left}px`;
- canvas.style.top = `${top}px`;
+ canvasRef.current.style.left = `${left}px`;
+ canvasRef.current.style.top = `${top}px`;
- const tileSet = new Image();
- tileSet.src = TILE_SETS.Objects;
- tileSet.onload = () => {
- let currentFrame = 0;
+ const tileSet = new Image();
+ tileSet.src = TILE_SETS.Objects;
+ tileSet.onload = () => {
+ let currentFrame = 0;
- setInterval(() => {
- ctx.clearRect(0, 0, WIDTH, HEIGHT);
+ intervalId = window.setInterval(() => {
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
- if (currentFrame === 0) {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 1) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 2) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 2,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 3) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 3,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 4) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 4,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 5) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 5,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 6) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 6,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
+ ctx.drawImage(
+ tileSet,
+ TILE_X + WIDTH * currentFrame,
+ TILE_Y,
+ WIDTH,
+ HEIGHT,
+ 0,
+ 0,
+ WIDTH,
+ HEIGHT
+ );
- currentFrame =
- currentFrame === ANIMATION_LENGTH ? 0 : currentFrame + 1;
- }, 125);
- };
- }
+ currentFrame = currentFrame === ANIMATION_LENGTH ? 0 : currentFrame + 1;
+ }, 125);
+ };
+
+ return () => {
+ clearInterval(intervalId);
+ };
}, [left, top]);
- return ;
+ return (
+
+ );
};
export default Fire;
diff --git a/src/components/Heart/index.tsx b/src/components/Heart/index.tsx
index 12e19ac..2d22edb 100644
--- a/src/components/Heart/index.tsx
+++ b/src/components/Heart/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect, FunctionComponent } from "react";
+import { useEffect, useRef, FC } from "react";
import { TILE_SIZE, TILE_SETS } from "../../constants";
import "./style.css";
@@ -8,94 +8,62 @@ const TILE_X = 0;
const TILE_Y = 96;
const ANIMATION_LENGTH = 3;
+type HeartProps = { left: number; top: number };
+
/*
* TODO:
- * - useRef instead of getElementById
* - util function for tile set, tiles and animation
- * - create global constants for tile sets and tile size
- * - prefer to return early, flip the if condition
- * - clear interval on component destroy
*/
-const Heart: FunctionComponent<{ left: number; top: number }> = ({
- left,
- top,
-}) => {
+const Heart: FC = ({ left, top }) => {
+ const canvasRef = useRef(null);
+
useEffect(() => {
- const canvas = document.getElementById(
- "heart-canvas"
- ) as HTMLCanvasElement | null;
- const ctx = canvas?.getContext("2d");
+ const ctx = canvasRef.current?.getContext("2d");
+ let intervalId: number;
- if (canvas && ctx) {
- canvas.style.left = `${left}px`;
- canvas.style.top = `${top}px`;
+ if (!canvasRef.current || !ctx) {
+ return;
+ }
+ canvasRef.current.style.left = `${left}px`;
+ canvasRef.current.style.top = `${top}px`;
- const tileSet = new Image();
- tileSet.src = TILE_SETS.Objects;
- tileSet.onload = () => {
- let currentFrame = 0;
+ const tileSet = new Image();
+ tileSet.src = TILE_SETS.Objects;
+ tileSet.onload = () => {
+ let currentFrame = 0;
- setInterval(() => {
- ctx.clearRect(0, 0, WIDTH, HEIGHT);
+ intervalId = window.setInterval(() => {
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
- if (currentFrame === 0) {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 1) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 2) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 2,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else if (currentFrame === 3) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 3,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
+ ctx.drawImage(
+ tileSet,
+ TILE_X + WIDTH * currentFrame,
+ TILE_Y,
+ WIDTH,
+ HEIGHT,
+ 0,
+ 0,
+ WIDTH,
+ HEIGHT
+ );
- currentFrame =
- currentFrame === ANIMATION_LENGTH ? 0 : currentFrame + 1;
- }, 75);
- };
- }
+ currentFrame = currentFrame === ANIMATION_LENGTH ? 0 : currentFrame + 1;
+ }, 75);
+ };
+
+ return () => {
+ clearInterval(intervalId);
+ };
}, [left, top]);
- return ;
+ return (
+
+ );
};
export default Heart;
diff --git a/src/components/House/index.tsx b/src/components/House/index.tsx
index 55af9d5..0cdacc1 100644
--- a/src/components/House/index.tsx
+++ b/src/components/House/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect, FunctionComponent } from "react";
+import { useEffect, useRef, FC } from "react";
import { TILE_SETS } from "../../constants";
import "./style.css";
@@ -7,43 +7,50 @@ const HEIGHT = 160;
const TILE_X = 198;
const TILE_Y = 0;
+type HouseProps = { left: number; top: number };
+
/*
* TODO:
- * - useRef instead of getElementById
* - util function for tile set, tiles and animation
- * - create global constants for tile sets and tile size
- * - prefer to return early, flip the if condition
*/
-const House: FunctionComponent = () => {
+const House: FC = ({ left, top }) => {
+ const canvasRef = useRef(null);
+
useEffect(() => {
- const canvas = document.getElementById(
- "house-canvas"
- ) as HTMLCanvasElement | null;
- const ctx = canvas?.getContext("2d");
-
- if (canvas && ctx) {
- canvas.style.left = "372px";
- canvas.style.top = "192px";
-
- const tileSet = new Image();
- tileSet.src = TILE_SETS.World;
- tileSet.onload = () => {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- };
+ const ctx = canvasRef.current?.getContext("2d");
+
+ if (!canvasRef.current || !ctx) {
+ return;
}
- }, []);
- return ;
+ canvasRef.current.style.left = `${left}px`;
+ canvasRef.current.style.top = `${top}px`;
+
+ const tileSet = new Image();
+ tileSet.src = TILE_SETS.World;
+ tileSet.onload = () => {
+ ctx.drawImage(
+ tileSet,
+ TILE_X,
+ TILE_Y,
+ WIDTH,
+ HEIGHT,
+ 0,
+ 0,
+ WIDTH,
+ HEIGHT
+ );
+ };
+ }, [left, top]);
+
+ return (
+
+ );
};
export default House;
diff --git a/src/components/Lever/index.tsx b/src/components/Lever/index.tsx
index c69c16c..bf40c47 100644
--- a/src/components/Lever/index.tsx
+++ b/src/components/Lever/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect, FunctionComponent } from "react";
+import { useEffect, useRef, FC } from "react";
import { TILE_SIZE, TILE_SETS } from "../../constants";
import "./style.css";
@@ -7,65 +7,58 @@ const HEIGHT = TILE_SIZE;
const TILE_X = 64;
const TILE_Y = 288;
-/*
- * TODO:
- * - useRef instead of getElementById
- * - util function for tile set, tiles and animation
- * - create global constants for tile sets and tile size
- * - prefer to return early, flip the if condition
- * - clear interval on component destroy
- */
-const Lever: FunctionComponent<{
+type LeverProps = {
left: number;
top: number;
used: boolean;
onInteract: (value: boolean | ((prev: boolean) => boolean)) => void;
-}> = ({ left, top, used, onInteract }) => {
- useEffect(() => {
- const canvas = document.getElementById(
- "lever-canvas"
- ) as HTMLCanvasElement | null;
- const ctx = canvas?.getContext("2d");
+};
- if (canvas && ctx) {
- canvas.style.left = `${left}px`;
- canvas.style.top = `${top}px`;
+/*
+ * TODO:
+ * - util function for tile set, tiles and animation
+ */
+const Lever: FC = ({ left, top, used, onInteract }) => {
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const ctx = canvasRef.current?.getContext("2d");
- const tileSet = new Image();
- tileSet.src = TILE_SETS.Objects;
- tileSet.onload = () => {
- ctx.clearRect(0, 0, WIDTH, HEIGHT);
- onInteract(used);
- if (used) {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- } else {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- };
+ if (!canvasRef.current || !ctx) {
+ return;
}
+
+ canvasRef.current.style.left = `${left}px`;
+ canvasRef.current.style.top = `${top}px`;
+
+ const tileSet = new Image();
+ tileSet.src = TILE_SETS.Objects;
+ tileSet.onload = () => {
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
+ onInteract(used);
+
+ ctx.drawImage(
+ tileSet,
+ used ? TILE_X + WIDTH : TILE_X,
+ TILE_Y,
+ WIDTH,
+ HEIGHT,
+ 0,
+ 0,
+ WIDTH,
+ HEIGHT
+ );
+ };
}, [left, top, used, onInteract]);
- return ;
+ return (
+
+ );
};
export default Lever;
diff --git a/src/components/Npc/index.tsx b/src/components/Npc/index.tsx
index ed03469..c6be0a0 100644
--- a/src/components/Npc/index.tsx
+++ b/src/components/Npc/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect, FunctionComponent } from "react";
+import { useEffect, useRef, FC } from "react";
import { TILE_SETS } from "../../constants";
import "./style.css";
@@ -10,60 +10,72 @@ const HUE_STEP = 10;
let increment = HUE_STEP;
+type NpcProps = { left: number; top: number };
+
/*
* TODO:
- * - useRef instead of getElementById
* - util function for tile set, tiles and animation
- * - create global constants for tile sets and tile size
- * - prefer to return early, flip the if condition
- * - clear interval on component destroy
*/
-const Npc: FunctionComponent<{
- left: number;
- top: number;
-}> = ({ left, top }) => {
+const Npc: FC = ({ left, top }) => {
+ const canvasRef = useRef(null);
+
useEffect(() => {
- const canvas = document.getElementById(
- "npc-canvas"
- ) as HTMLCanvasElement | null;
- const ctx = canvas?.getContext("2d");
+ const ctx = canvasRef.current?.getContext("2d");
+ let intervalId: number;
- if (canvas && ctx) {
- canvas.style.left = `${left}px`;
- canvas.style.top = `${top}px`;
+ if (!canvasRef.current || !ctx) {
+ return;
+ }
- const tileSet = new Image();
- tileSet.src = TILE_SETS.Npc;
- tileSet.onload = () => {
- ctx.clearRect(0, 0, WIDTH, HEIGHT);
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- };
+ canvasRef.current.style.left = `${left}px`;
+ canvasRef.current.style.top = `${top}px`;
- window.setInterval(() => {
- const currentHue = parseInt(
- canvas.style.filter.match(/\d+/)?.[0] || "0"
- );
- if (currentHue === 360) {
- increment = -HUE_STEP;
- } else if (currentHue === 0) {
- increment = HUE_STEP;
- }
- const hue = Math.max(0, Math.min(360, currentHue + increment));
- canvas.style.filter = `hue-rotate(${hue}deg)`;
- }, 100);
- }
+ const tileSet = new Image();
+ tileSet.src = TILE_SETS.Npc;
+ tileSet.onload = () => {
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
+ ctx.drawImage(
+ tileSet,
+ TILE_X,
+ TILE_Y,
+ WIDTH,
+ HEIGHT,
+ 0,
+ 0,
+ WIDTH,
+ HEIGHT
+ );
+ };
+
+ intervalId = window.setInterval(() => {
+ if (!canvasRef.current) {
+ return;
+ }
+
+ const currentHue = parseInt(
+ canvasRef.current.style.filter.match(/\d+/)?.[0] || "0"
+ );
+ if (currentHue === 360) {
+ increment = -HUE_STEP;
+ } else if (currentHue === 0) {
+ increment = HUE_STEP;
+ }
+ const hue = Math.max(0, Math.min(360, currentHue + increment));
+ canvasRef.current.style.filter = `hue-rotate(${hue}deg)`;
+ }, 100);
+
+ return () => {
+ clearInterval(intervalId);
+ };
}, [left, top]);
- return ;
+ return (
+
+ );
};
export default Npc;
diff --git a/src/components/Player/index.tsx b/src/components/Player/index.tsx
index 13b98ae..53ede43 100644
--- a/src/components/Player/index.tsx
+++ b/src/components/Player/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect, FunctionComponent, useContext } from "react";
+import { useEffect, useRef, FC, useContext } from "react";
import { GAME_STATES, TILE_SETS, TILE_SIZE } from "../../constants";
import { GlobalContext } from "../../contexts";
import "./style.css";
@@ -9,29 +9,35 @@ const TILE_X = 0;
const TILE_Y = 8;
const ANIMATION_LENGTH = 3;
+type PlayerProps = {
+ top: number;
+ left: number;
+ health: number;
+ onInteract: (isOpen: boolean | ((wasOpen: boolean) => boolean)) => void;
+ onCollision: (health: number | ((prev: number) => number)) => void;
+};
+
/*
* TODO:
* - 2D Vectors for movement direction
- * - useRef instead of getElementById
* - util function for tile set, tiles and animation
- * - create global constants for tile sets and tile size
- * - prefer to return early, flip the if condition
* - move object specific interactions outside of Player
* - move player controls to global context
* - use input loop to remove keydown delay
* - create util function for collisions
*/
let invulnerable = false;
-const Player: FunctionComponent<{
- health: number;
- onInteract: (isOpen: boolean | ((wasOpen: boolean) => boolean)) => void;
- onCollision: (health: number | ((prev: number) => number)) => void;
-}> = ({ health, onInteract, onCollision }) => {
+const Player: FC = ({
+ health,
+ onInteract,
+ onCollision,
+ top,
+ left,
+}) => {
+ const canvasRef = useRef(null);
const { setGameState } = useContext(GlobalContext);
+
useEffect(() => {
- const canvas = document.getElementById(
- "player-canvas"
- ) as HTMLCanvasElement | null;
const fireCanvas = document.getElementById(
"fire-canvas"
) as HTMLCanvasElement | null;
@@ -67,18 +73,66 @@ const Player: FunctionComponent<{
}
}
- if (canvas) {
- canvas.style.top = canvas.style.top || "328px";
- canvas.style.left = canvas.style.left || "420px";
- const ctx = canvas.getContext("2d");
+ if (!canvasRef.current) {
+ return;
+ }
- if (ctx) {
- const tileSet = new Image();
- tileSet.src = TILE_SETS.Player;
- tileSet.onload = () => {
- let keyPressed = false;
- let direction = "down";
- let currentFrame = 0;
+ canvasRef.current.style.top = canvasRef.current.style.top || `${top}px`;
+ canvasRef.current.style.left = canvasRef.current.style.left || `${left}px`;
+ const ctx = canvasRef.current.getContext("2d");
+
+ if (!ctx) {
+ return;
+ }
+
+ const tileSet = new Image();
+ tileSet.src = TILE_SETS.Player;
+ tileSet.onload = () => {
+ let keyPressed = false;
+ let direction = "down";
+ let currentFrame = 0;
+ ctx.drawImage(
+ tileSet,
+ TILE_X,
+ TILE_Y,
+ WIDTH,
+ HEIGHT,
+ 0,
+ 0,
+ WIDTH,
+ HEIGHT
+ );
+
+ window.onkeyup = () => {
+ currentFrame = 0;
+ keyPressed = false;
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
+
+ if (direction === "up") {
+ ctx.drawImage(
+ tileSet,
+ TILE_X,
+ TILE_Y + TILE_SIZE * 4,
+ WIDTH,
+ HEIGHT,
+ 0,
+ 0,
+ WIDTH,
+ HEIGHT
+ );
+ } else if (direction === "left") {
+ ctx.drawImage(
+ tileSet,
+ TILE_X,
+ TILE_Y + TILE_SIZE * 6,
+ WIDTH,
+ HEIGHT,
+ 0,
+ 0,
+ WIDTH,
+ HEIGHT
+ );
+ } else if (direction === "down") {
ctx.drawImage(
tileSet,
TILE_X,
@@ -90,15 +144,149 @@ const Player: FunctionComponent<{
WIDTH,
HEIGHT
);
+ } else if (direction === "right") {
+ ctx.drawImage(
+ tileSet,
+ TILE_X,
+ TILE_Y + TILE_SIZE * 2,
+ WIDTH,
+ HEIGHT,
+ 0,
+ 0,
+ WIDTH,
+ HEIGHT
+ );
+ }
+ };
+
+ window.onkeydown = (event) => {
+ if (!canvasRef.current) {
+ return;
+ }
+
+ if (health > 0) {
+ if (health < 4) {
+ if (
+ heartCanvas &&
+ parseInt(canvasRef.current.style.left || "0") + 6 <=
+ parseInt(heartCanvas.style.left || "0") + 16 &&
+ parseInt(canvasRef.current.style.left || "0") + 36 >=
+ parseInt(heartCanvas.style.left || "0") &&
+ parseInt(canvasRef.current.style.top || "0") + 36 <=
+ parseInt(heartCanvas.style.top || "0") + 32 &&
+ parseInt(canvasRef.current.style.top || "0") + 36 >=
+ parseInt(heartCanvas.style.top || "0") + 16
+ ) {
+ onCollision((health) => Math.min(4, health + 1));
+ heartCanvas.remove();
+ }
+ }
+
+ if (
+ coinCanvas &&
+ parseInt(canvasRef.current.style.left || "0") + 6 <=
+ parseInt(coinCanvas.style.left || "0") + 16 &&
+ parseInt(canvasRef.current.style.left || "0") + 36 >=
+ parseInt(coinCanvas.style.left || "0") &&
+ parseInt(canvasRef.current.style.top || "0") + 36 <=
+ parseInt(coinCanvas.style.top || "0") + 32 &&
+ parseInt(canvasRef.current.style.top || "0") + 36 >=
+ parseInt(coinCanvas.style.top || "0") + 16
+ ) {
+ coinCanvas.remove();
+ }
- window.onkeyup = () => {
- currentFrame = 0;
- keyPressed = false;
+ if (event.key === " " || event.key === "Enter") {
+ onInteract((wasOpen) => !wasOpen);
+ } else if (event.key === "w" || event.key === "ArrowUp") {
+ direction = "up";
+ canvasRef.current.style.top = `${
+ parseInt(canvasRef.current.style.top || "0") - 4
+ }px`;
+ } else if (event.key === "s" || event.key === "ArrowDown") {
+ direction = "down";
+ canvasRef.current.style.top = `${
+ parseInt(canvasRef.current.style.top || "0") + 4
+ }px`;
+ } else if (event.key === "a" || event.key === "ArrowLeft") {
+ direction = "left";
+ canvasRef.current.style.left = `${
+ parseInt(canvasRef.current.style.left || "0") - 4
+ }px`;
+ } else if (event.key === "d" || event.key === "ArrowRight") {
+ direction = "right";
+ canvasRef.current.style.left = `${
+ parseInt(canvasRef.current.style.left || "0") + 4
+ }px`;
+ }
+
+ if (
+ fireCanvas &&
+ !invulnerable &&
+ parseInt(canvasRef.current.style.left || "0") + 6 <=
+ parseInt(fireCanvas.style.left || "0") + 16 &&
+ parseInt(canvasRef.current.style.left || "0") + 36 >=
+ parseInt(fireCanvas.style.left || "0") &&
+ parseInt(canvasRef.current.style.top || "0") + 36 <=
+ parseInt(fireCanvas.style.top || "0") + 32 &&
+ parseInt(canvasRef.current.style.top || "0") + 36 >=
+ parseInt(fireCanvas.style.top || "0") + 16
+ ) {
+ if (event.key === "w" || event.key === "ArrowUp") {
+ canvasRef.current.style.top = `${Math.min(
+ window.innerHeight,
+ parseInt(canvasRef.current.style.top || "0") + 48
+ )}px`;
+ } else if (event.key === "s" || event.key === "ArrowDown") {
+ canvasRef.current.style.top = `${Math.max(
+ 0,
+ parseInt(canvasRef.current.style.top || "0") - 48
+ )}px`;
+ } else if (event.key === "a" || event.key === "ArrowLeft") {
+ canvasRef.current.style.left = `${Math.min(
+ window.innerWidth,
+ parseInt(canvasRef.current.style.left || "0") + 48
+ )}px`;
+ } else if (event.key === "d" || event.key === "ArrowRight") {
+ canvasRef.current.style.left = `${Math.max(
+ 0,
+ parseInt(canvasRef.current.style.left || "0") - 48
+ )}px`;
+ }
+
+ onCollision((health) => Math.max(0, health - 1));
+ invulnerable = true;
+ canvasRef.current.style.filter = "brightness(6)";
+
+ const interval = setInterval(() => {
+ if (!canvasRef.current) {
+ return;
+ }
+
+ canvasRef.current.style.filter =
+ canvasRef.current.style.filter.includes("1")
+ ? "brightness(6)"
+ : "brightness(1)";
+ }, 100);
+
+ setTimeout(() => {
+ clearInterval(interval);
+ if (!canvasRef.current) {
+ return;
+ }
+ canvasRef.current.style.filter = "brightness(1)";
+ invulnerable = false;
+ }, 1500);
+ }
+
+ if (!keyPressed) {
+ keyPressed = true;
ctx.clearRect(0, 0, WIDTH, HEIGHT);
+
if (direction === "up") {
ctx.drawImage(
tileSet,
- TILE_X,
+ TILE_X + WIDTH * currentFrame,
TILE_Y + TILE_SIZE * 4,
WIDTH,
HEIGHT,
@@ -107,10 +295,11 @@ const Player: FunctionComponent<{
WIDTH,
HEIGHT
);
- } else if (direction === "left") {
+ }
+ if (direction === "left") {
ctx.drawImage(
tileSet,
- TILE_X,
+ TILE_X + WIDTH * currentFrame,
TILE_Y + TILE_SIZE * 6,
WIDTH,
HEIGHT,
@@ -119,10 +308,11 @@ const Player: FunctionComponent<{
WIDTH,
HEIGHT
);
- } else if (direction === "down") {
+ }
+ if (direction === "down") {
ctx.drawImage(
tileSet,
- TILE_X,
+ TILE_X + WIDTH * currentFrame,
TILE_Y,
WIDTH,
HEIGHT,
@@ -131,10 +321,11 @@ const Player: FunctionComponent<{
WIDTH,
HEIGHT
);
- } else if (direction === "right") {
+ }
+ if (direction === "right") {
ctx.drawImage(
tileSet,
- TILE_X,
+ TILE_X + WIDTH * currentFrame,
TILE_Y + TILE_SIZE * 2,
WIDTH,
HEIGHT,
@@ -144,348 +335,28 @@ const Player: FunctionComponent<{
HEIGHT
);
}
- };
-
- window.onkeydown = (event) => {
- if (health > 0) {
- if (health < 4) {
- if (
- heartCanvas &&
- parseInt(canvas.style.left || "0") + 6 <=
- parseInt(heartCanvas.style.left || "0") + 16 &&
- parseInt(canvas.style.left || "0") + 36 >=
- parseInt(heartCanvas.style.left || "0") &&
- parseInt(canvas.style.top || "0") + 36 <=
- parseInt(heartCanvas.style.top || "0") + 32 &&
- parseInt(canvas.style.top || "0") + 36 >=
- parseInt(heartCanvas.style.top || "0") + 16
- ) {
- onCollision((health) => Math.min(4, health + 1));
- heartCanvas.remove();
- }
- }
-
- if (
- coinCanvas &&
- parseInt(canvas.style.left || "0") + 6 <=
- parseInt(coinCanvas.style.left || "0") + 16 &&
- parseInt(canvas.style.left || "0") + 36 >=
- parseInt(coinCanvas.style.left || "0") &&
- parseInt(canvas.style.top || "0") + 36 <=
- parseInt(coinCanvas.style.top || "0") + 32 &&
- parseInt(canvas.style.top || "0") + 36 >=
- parseInt(coinCanvas.style.top || "0") + 16
- ) {
- coinCanvas.remove();
- }
-
- if (event.key === " " || event.key === "Enter") {
- onInteract((wasOpen) => !wasOpen);
- } else if (event.key === "w" || event.key === "ArrowUp") {
- direction = "up";
- canvas.style.top = `${parseInt(canvas.style.top || "0") - 4}px`;
- } else if (event.key === "s" || event.key === "ArrowDown") {
- direction = "down";
- canvas.style.top = `${parseInt(canvas.style.top || "0") + 4}px`;
- } else if (event.key === "a" || event.key === "ArrowLeft") {
- direction = "left";
- canvas.style.left = `${
- parseInt(canvas.style.left || "0") - 4
- }px`;
- } else if (event.key === "d" || event.key === "ArrowRight") {
- direction = "right";
- canvas.style.left = `${
- parseInt(canvas.style.left || "0") + 4
- }px`;
- }
-
- if (
- fireCanvas &&
- !invulnerable &&
- parseInt(canvas.style.left || "0") + 6 <=
- parseInt(fireCanvas.style.left || "0") + 16 &&
- parseInt(canvas.style.left || "0") + 36 >=
- parseInt(fireCanvas.style.left || "0") &&
- parseInt(canvas.style.top || "0") + 36 <=
- parseInt(fireCanvas.style.top || "0") + 32 &&
- parseInt(canvas.style.top || "0") + 36 >=
- parseInt(fireCanvas.style.top || "0") + 16
- ) {
- if (event.key === "w" || event.key === "ArrowUp") {
- canvas.style.top = `${Math.min(
- window.innerHeight,
- parseInt(canvas.style.top || "0") + 48
- )}px`;
- } else if (event.key === "s" || event.key === "ArrowDown") {
- canvas.style.top = `${Math.max(
- 0,
- parseInt(canvas.style.top || "0") - 48
- )}px`;
- } else if (event.key === "a" || event.key === "ArrowLeft") {
- canvas.style.left = `${Math.min(
- window.innerWidth,
- parseInt(canvas.style.left || "0") + 48
- )}px`;
- } else if (event.key === "d" || event.key === "ArrowRight") {
- canvas.style.left = `${Math.max(
- 0,
- parseInt(canvas.style.left || "0") - 48
- )}px`;
- }
-
- onCollision((health) => Math.max(0, health - 1));
- invulnerable = true;
- canvas.style.filter = "brightness(6)";
- const interval = setInterval(() => {
- canvas.style.filter = canvas.style.filter.includes("1")
- ? "brightness(6)"
- : "brightness(1)";
- }, 100);
-
- setTimeout(() => {
- clearInterval(interval);
- canvas.style.filter = "brightness(1)";
- invulnerable = false;
- }, 1500);
- }
-
- if (!keyPressed) {
- keyPressed = true;
- ctx.clearRect(0, 0, WIDTH, HEIGHT);
-
- if (currentFrame === 0) {
- if (direction === "up") {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y + TILE_SIZE * 4,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "left") {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y + TILE_SIZE * 6,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "down") {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "right") {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y + TILE_SIZE * 2,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- } else if (currentFrame === 1) {
- if (direction === "up") {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH,
- TILE_Y + TILE_SIZE * 4,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "left") {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH,
- TILE_Y + TILE_SIZE * 6,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "down") {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "right") {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH,
- TILE_Y + TILE_SIZE * 2,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- } else if (currentFrame === 2) {
- if (direction === "up") {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y + TILE_SIZE * 4,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "left") {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y + TILE_SIZE * 6,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "down") {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "right") {
- ctx.drawImage(
- tileSet,
- TILE_X,
- TILE_Y + TILE_SIZE * 2,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- } else if (currentFrame === 3) {
- if (direction === "up") {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 3,
- TILE_Y + TILE_SIZE * 4,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "left") {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 3,
- TILE_Y + TILE_SIZE * 6,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "down") {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 3,
- TILE_Y,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- if (direction === "right") {
- ctx.drawImage(
- tileSet,
- TILE_X + WIDTH * 3,
- TILE_Y + TILE_SIZE * 2,
- WIDTH,
- HEIGHT,
- 0,
- 0,
- WIDTH,
- HEIGHT
- );
- }
- }
-
- setTimeout(() => {
- keyPressed = false;
- currentFrame =
- currentFrame === ANIMATION_LENGTH ? 0 : currentFrame + 1;
- }, 125);
- }
- } else {
- setGameState(GAME_STATES.GameOver);
- }
- };
- };
- }
- }
- }, [onInteract, onCollision, health, setGameState]);
+ setTimeout(() => {
+ keyPressed = false;
+ currentFrame =
+ currentFrame === ANIMATION_LENGTH ? 0 : currentFrame + 1;
+ }, 125);
+ }
+ } else {
+ setGameState(GAME_STATES.GameOver);
+ }
+ };
+ };
+ }, [onInteract, onCollision, health, setGameState, top, left]);
return (
<>
-
+
>
);
diff --git a/src/components/World/index.tsx b/src/components/World/index.tsx
index 43d6f60..017e052 100644
--- a/src/components/World/index.tsx
+++ b/src/components/World/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
import {
WORLD_WIDTH,
WORLD_HEIGHT,
@@ -11,58 +11,52 @@ import "./style.css";
// Number of tiles in the tilset
const TILES_COUNT = 40;
-/*
- * Refactor:
- * - useRef instead of getElementById
- * - useContext to provide global tile count for rows and columns
- * - create global constants for tile sets and tile size
- * - create util to draw different tiles of a tile set: drawGrass(x, y), drawStone(x, y)
- * - prefer to return early, flip the if condition
- */
export default function World() {
- useEffect(() => {
- const canvas = document.getElementById(
- "world-canvas"
- ) as HTMLCanvasElement | null;
- const ctx = canvas?.getContext("2d");
+ const canvasRef = useRef(null);
- if (canvas && ctx) {
- const tileSet = new Image();
- tileSet.src = TILE_SETS.World;
- tileSet.onload = () => {
- level.layers.forEach(({ chunks }) => {
- let i = 0;
- for (let y = 0; y < level.height; y++) {
- for (let x = 0; x < level.width; x++) {
- // Tiled exports tiles counting from 1 rather than 0
- const tile = chunks[0].data[i++] - 1;
- if (tile < 0) {
- continue;
- }
+ useEffect(() => {
+ const ctx = canvasRef.current?.getContext("2d");
- const tileX = (tile % TILES_COUNT) * TILE_SIZE;
- const tileY = Math.floor(tile / TILES_COUNT) * TILE_SIZE;
+ if (!canvasRef.current || !ctx) {
+ return;
+ }
- ctx.drawImage(
- tileSet,
- tileX,
- tileY,
- TILE_SIZE,
- TILE_SIZE,
- x * TILE_SIZE,
- y * TILE_SIZE,
- TILE_SIZE,
- TILE_SIZE
- );
+ const tileSet = new Image();
+ tileSet.src = TILE_SETS.World;
+ tileSet.onload = () => {
+ level.layers.forEach(({ chunks }) => {
+ let i = 0;
+ for (let y = 0; y < level.height; y++) {
+ for (let x = 0; x < level.width; x++) {
+ // Tiled exports tiles counting from 1 rather than 0
+ const tile = chunks[0].data[i++] - 1;
+ if (tile < 0) {
+ continue;
}
+
+ const tileX = (tile % TILES_COUNT) * TILE_SIZE;
+ const tileY = Math.floor(tile / TILES_COUNT) * TILE_SIZE;
+
+ ctx.drawImage(
+ tileSet,
+ tileX,
+ tileY,
+ TILE_SIZE,
+ TILE_SIZE,
+ x * TILE_SIZE,
+ y * TILE_SIZE,
+ TILE_SIZE,
+ TILE_SIZE
+ );
}
- });
- };
- }
+ }
+ });
+ };
}, []);
return (