From 90a50af66ae67f15b312a0973fdcc7c9621c8eb9 Mon Sep 17 00:00:00 2001
From: Michael Tintiuc <contact@michaeltintiuc.com>
Date: Tue, 11 Oct 2022 18:42:57 +0300
Subject: [PATCH] Refactor 4

---
 src/components/App/index.tsx          | 11 ++++---
 src/components/Player/index.tsx       | 47 ++++-----------------------
 src/components/Player/style.css       |  6 ----
 src/components/PlayerHealth/index.tsx | 34 +++++++++++++++++++
 src/components/PlayerHealth/style.css |  5 +++
 src/constants.ts                      |  3 ++
 src/contexts/global.ts                |  4 ++-
 src/hooks/useAnimatedSprite.ts        |  4 +--
 src/hooks/useSprite.ts                |  8 ++---
 9 files changed, 65 insertions(+), 57 deletions(-)
 create mode 100644 src/components/PlayerHealth/index.tsx
 create mode 100644 src/components/PlayerHealth/style.css

diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx
index e14f5d6..88e26f2 100644
--- a/src/components/App/index.tsx
+++ b/src/components/App/index.tsx
@@ -2,6 +2,7 @@ import { useState } from "react";
 import { GlobalContext } from "../../contexts";
 import World from "../World";
 import Player from "../Player";
+import PlayerHealth from "../PlayerHealth";
 import Npc from "../Npc";
 import Heart from "../Heart";
 import Coin from "../Coin";
@@ -10,7 +11,7 @@ import Lever from "../Lever";
 import House from "../House";
 import Fire from "../Fire";
 import GameOver from "../GameOver";
-import { GAME_STATES } from "../../constants";
+import { GAME_STATES, MAX_HEALTH } from "../../constants";
 import "./style.css";
 
 /*
@@ -22,17 +23,19 @@ export default function App() {
   const [gameState, setGameState] = useState<GAME_STATES>(GAME_STATES.Game);
   const [isCellarDoorOpen, setIsCellarDoorOpen] = useState(false);
   const [isLeverUsed, setIsLeverUsed] = useState(false);
-  const [playerHealth, setPlayerHealth] = useState(4);
+  const [playerHealth, setPlayerHealth] = useState(MAX_HEALTH);
 
   return (
     <div className="App">
-      <GlobalContext.Provider value={{ gameState: gameState, setGameState }}>
+      <GlobalContext.Provider
+        value={{ gameState: gameState, setGameState, playerHealth }}
+      >
         {gameState === GAME_STATES.GameOver && <GameOver />}
         <World />
+        <PlayerHealth />
         <Player
           top={328}
           left={420}
-          health={playerHealth}
           onInteract={setIsLeverUsed}
           onCollision={setPlayerHealth}
         />
diff --git a/src/components/Player/index.tsx b/src/components/Player/index.tsx
index 53ede43..4545e01 100644
--- a/src/components/Player/index.tsx
+++ b/src/components/Player/index.tsx
@@ -12,7 +12,6 @@ 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;
 };
@@ -27,23 +26,14 @@ type PlayerProps = {
  * - create util function for collisions
  */
 let invulnerable = false;
-const Player: FC<PlayerProps> = ({
-  health,
-  onInteract,
-  onCollision,
-  top,
-  left,
-}) => {
+const Player: FC<PlayerProps> = ({ onInteract, onCollision, top, left }) => {
   const canvasRef = useRef<HTMLCanvasElement>(null);
-  const { setGameState } = useContext(GlobalContext);
+  const { setGameState, playerHealth } = useContext(GlobalContext);
 
   useEffect(() => {
     const fireCanvas = document.getElementById(
       "fire-canvas"
     ) as HTMLCanvasElement | null;
-    const healthCanvas = document.getElementById(
-      "health-canvas"
-    ) as HTMLCanvasElement | null;
     const heartCanvas = document.getElementById(
       "heart-canvas"
     ) as HTMLCanvasElement | null;
@@ -51,28 +41,6 @@ const Player: FC<PlayerProps> = ({
       "coin-canvas"
     ) as HTMLCanvasElement | null;
 
-    if (healthCanvas) {
-      const ctx = healthCanvas.getContext("2d");
-      if (ctx) {
-        const tileSet = new Image();
-        tileSet.src = TILE_SETS.Objects;
-        tileSet.onload = () => {
-          ctx.clearRect(0, 0, 30, 26);
-          if (health === 4) {
-            ctx.drawImage(tileSet, 128, 4, 30, 26, 0, 0, 30, 26);
-          } else if (health === 3) {
-            ctx.drawImage(tileSet, 160, 4, 30, 26, 0, 0, 30, 26);
-          } else if (health === 2) {
-            ctx.drawImage(tileSet, 192, 4, 30, 26, 0, 0, 30, 26);
-          } else if (health === 1) {
-            ctx.drawImage(tileSet, 224, 4, 30, 26, 0, 0, 30, 26);
-          } else if (health === 0) {
-            ctx.drawImage(tileSet, 256, 4, 30, 26, 0, 0, 30, 26);
-          }
-        };
-      }
-    }
-
     if (!canvasRef.current) {
       return;
     }
@@ -164,8 +132,8 @@ const Player: FC<PlayerProps> = ({
           return;
         }
 
-        if (health > 0) {
-          if (health < 4) {
+        if (playerHealth > 0) {
+          if (playerHealth < 4) {
             if (
               heartCanvas &&
               parseInt(canvasRef.current.style.left || "0") + 6 <=
@@ -177,7 +145,7 @@ const Player: FC<PlayerProps> = ({
               parseInt(canvasRef.current.style.top || "0") + 36 >=
                 parseInt(heartCanvas.style.top || "0") + 16
             ) {
-              onCollision((health) => Math.min(4, health + 1));
+              onCollision((playerHealth) => Math.min(4, playerHealth + 1));
               heartCanvas.remove();
             }
           }
@@ -254,7 +222,7 @@ const Player: FC<PlayerProps> = ({
               )}px`;
             }
 
-            onCollision((health) => Math.max(0, health - 1));
+            onCollision((playerHealth) => Math.max(0, playerHealth - 1));
             invulnerable = true;
             canvasRef.current.style.filter = "brightness(6)";
 
@@ -347,7 +315,7 @@ const Player: FC<PlayerProps> = ({
         }
       };
     };
-  }, [onInteract, onCollision, health, setGameState, top, left]);
+  }, [onInteract, onCollision, playerHealth, setGameState, top, left]);
 
   return (
     <>
@@ -357,7 +325,6 @@ const Player: FC<PlayerProps> = ({
         width={WIDTH}
         height={HEIGHT}
       ></canvas>
-      <canvas id="health-canvas" width="30" height="26"></canvas>
     </>
   );
 };
diff --git a/src/components/Player/style.css b/src/components/Player/style.css
index 51d62d6..b09f201 100644
--- a/src/components/Player/style.css
+++ b/src/components/Player/style.css
@@ -1,9 +1,3 @@
 #player-canvas {
   z-index: 99;
 }
-
-#health-canvas {
-  z-index: 99;
-  top: calc((1536px - 100vh) / 2 + 8px);
-  left: calc((2048px - 100vw) / 2 + 8px);
-}
diff --git a/src/components/PlayerHealth/index.tsx b/src/components/PlayerHealth/index.tsx
new file mode 100644
index 0000000..e669ca4
--- /dev/null
+++ b/src/components/PlayerHealth/index.tsx
@@ -0,0 +1,34 @@
+import { useRef, useContext, FC } from "react";
+import { TILE_SIZE, TILE_SETS, MAX_HEALTH } from "../../constants";
+import { GlobalContext } from "../../contexts";
+import { useSprite } from "../../hooks";
+import "./style.css";
+
+const WIDTH = TILE_SIZE;
+const HEIGHT = TILE_SIZE;
+const TILE_X = 128;
+
+const PlayerHealth: FC = () => {
+  const canvasRef = useRef<HTMLCanvasElement>(null);
+  const { playerHealth } = useContext(GlobalContext);
+
+  useSprite({
+    canvasRef,
+    tileSet: TILE_SETS.Objects,
+    width: WIDTH,
+    height: HEIGHT,
+    tileX: TILE_X + WIDTH * (MAX_HEALTH - playerHealth),
+    tileY: 0,
+  });
+
+  return (
+    <canvas
+      ref={canvasRef}
+      id="health-canvas"
+      width={WIDTH}
+      height={HEIGHT}
+    ></canvas>
+  );
+};
+
+export default PlayerHealth;
diff --git a/src/components/PlayerHealth/style.css b/src/components/PlayerHealth/style.css
new file mode 100644
index 0000000..1e5b27d
--- /dev/null
+++ b/src/components/PlayerHealth/style.css
@@ -0,0 +1,5 @@
+#health-canvas {
+  z-index: 99;
+  top: calc((1536px - 100vh) / 2 + 32px);
+  left: calc((2048px - 100vw) / 2 + 8px);
+}
diff --git a/src/constants.ts b/src/constants.ts
index 5073bea..75f666c 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -13,3 +13,6 @@ export enum TILE_SETS {
 export const TILE_SIZE = 32;
 export const WORLD_WIDTH = 2048;
 export const WORLD_HEIGHT = 1536;
+
+export const MAX_HEALTH = 4;
+export const MIN_HEALTH = 0;
diff --git a/src/contexts/global.ts b/src/contexts/global.ts
index 4c5a3a0..3c7aa8f 100644
--- a/src/contexts/global.ts
+++ b/src/contexts/global.ts
@@ -1,12 +1,14 @@
 import { createContext } from "react";
-import { GAME_STATES } from "../constants";
+import { GAME_STATES, MAX_HEALTH } from "../constants";
 
 export type GlobalContextType = {
   readonly gameState: GAME_STATES;
   setGameState(newState: GAME_STATES): void;
+  playerHealth: number;
 };
 
 export const GlobalContext = createContext<GlobalContextType>({
   gameState: GAME_STATES.Game,
   setGameState: () => {},
+  playerHealth: MAX_HEALTH,
 });
diff --git a/src/hooks/useAnimatedSprite.ts b/src/hooks/useAnimatedSprite.ts
index 8538c2b..3d88a67 100644
--- a/src/hooks/useAnimatedSprite.ts
+++ b/src/hooks/useAnimatedSprite.ts
@@ -15,8 +15,8 @@ export const useAnimatedSprite = (props: AnimatedSpriteProps) => {
       return;
     }
 
-    props.canvasRef.current.style.left = `${props.left}px`;
-    props.canvasRef.current.style.top = `${props.top}px`;
+    props.left && (props.canvasRef.current.style.left = `${props.left}px`);
+    props.top && (props.canvasRef.current.style.top = `${props.top}px`);
 
     const sprite = new Image();
     sprite.src = props.tileSet;
diff --git a/src/hooks/useSprite.ts b/src/hooks/useSprite.ts
index d1a6397..f4a0790 100644
--- a/src/hooks/useSprite.ts
+++ b/src/hooks/useSprite.ts
@@ -8,8 +8,8 @@ export type SpriteProps = {
   height: number;
   tileX: number;
   tileY: number;
-  left: number;
-  top: number;
+  left?: number;
+  top?: number;
 };
 
 export const useSprite = (props: SpriteProps) => {
@@ -20,8 +20,8 @@ export const useSprite = (props: SpriteProps) => {
       return;
     }
 
-    props.canvasRef.current.style.left = `${props.left}px`;
-    props.canvasRef.current.style.top = `${props.top}px`;
+    props.left && (props.canvasRef.current.style.left = `${props.left}px`);
+    props.top && (props.canvasRef.current.style.top = `${props.top}px`);
 
     const sprite = new Image();
     sprite.src = props.tileSet;