From ff146d7de004213a715ce4648c55e46fc987b200 Mon Sep 17 00:00:00 2001 From: Michael Tintiuc Date: Wed, 12 Oct 2022 23:32:50 +0300 Subject: [PATCH] Refactor 7 --- src/components/App/index.tsx | 15 ++- src/components/Coin/index.tsx | 24 ++++- src/components/Coin/style.css | 4 + src/components/Player/index.tsx | 134 ++++++++++++-------------- src/components/Player/utils.ts | 26 +++++ src/components/PlayerHealth/style.css | 5 +- src/components/Ui/index.tsx | 16 +++ src/components/Ui/style.css | 17 ++++ src/contexts/global.ts | 4 + src/hooks/useAnimatedSprite.ts | 55 +++++++---- src/hooks/useSprite.ts | 37 ++++--- src/utils/clampValue.ts | 3 + src/utils/collider.ts | 13 +++ src/utils/index.ts | 1 + 14 files changed, 234 insertions(+), 120 deletions(-) create mode 100644 src/components/Ui/index.tsx create mode 100644 src/components/Ui/style.css create mode 100644 src/utils/clampValue.ts diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 5dde5b2..5c504c2 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -2,7 +2,7 @@ import { MutableRefObject, useState } from "react"; import { GlobalContext } from "../../contexts"; import World from "../World"; import Player from "../Player"; -import PlayerHealth from "../PlayerHealth"; +import Ui from "../Ui"; import Npc from "../Npc"; import Heart from "../Heart"; import Coin from "../Coin"; @@ -11,9 +11,10 @@ import Lever from "../Lever"; import House from "../House"; import Fire from "../Fire"; import GameOver from "../GameOver"; -import { GAME_STATES, MAX_HEALTH } from "../../constants"; +import { GAME_STATES, MAX_HEALTH, MIN_HEALTH } from "../../constants"; import { Collider } from "../../utils"; import "./style.css"; +import { clampValue } from "../../utils/clampValue"; /* * TODO: @@ -26,6 +27,7 @@ export default function App() { const [isCellarDoorOpen, setIsCellarDoorOpen] = useState(false); const [isLeverUsed, setIsLeverUsed] = useState(false); const [playerHealth, setPlayerHealth] = useState(MAX_HEALTH); + const [score, setScore] = useState(0); return (
@@ -34,15 +36,18 @@ export default function App() { gameState, setGameState, playerHealth, - setPlayerHealth, + setPlayerHealth: (health: number) => + setPlayerHealth(clampValue(health, MIN_HEALTH, MAX_HEALTH)), colliders, setColliders, + score, + setScore: (value: number) => setScore((oldScore) => oldScore + value), }} > {gameState === GAME_STATES.GameOver && } - - + + = ({ left, top }) => { + const { setScore } = useContext(GlobalContext); + const [isHidden, setIsHidden] = useState(false); const canvasRef = useRef(null); + const onCollision = (c: Collider) => { + setScore(POINTS); + setIsHidden(true); + collider.current.hide(); + + setTimeout(() => { + collider.current.show(); + setIsHidden(false); + }, TIMEOUT); + }; const collider = useRef( - new Collider(new Rect(left, top, WIDTH, HEIGHT), ColliderType.Bonus) + new Collider( + new Rect(left, top, WIDTH, HEIGHT), + ColliderType.Bonus, + onCollision + ) ); useColliders(collider); @@ -34,6 +53,7 @@ const Coin: FC = ({ left, top }) => { return ( boolean)) => void; }; -/* - * TODO: - * - move player controls to global context - * - use input loop to remove keydown delay - */ -let invulnerable = false; const Player: FC = ({ onInteract, top, left }) => { const canvasRef = useRef(null); const playerRect = useRef(new Rect(left, top, WIDTH, HEIGHT)); + const invulnerable = useRef(false); + const keyPressed = useRef(false); + const direction = useRef(Vector.Down); + const currentFrame = useRef(0); const { setGameState, playerHealth, setPlayerHealth, colliders } = useContext(GlobalContext); @@ -35,22 +41,50 @@ const Player: FC = ({ onInteract, top, left }) => { return; } - canvasRef.current.style.top = canvasRef.current.style.top || `${top}px`; - canvasRef.current.style.left = canvasRef.current.style.left || `${left}px`; + moveTo(new Vector(left, top), canvasRef.current); + + const checkCollisions = () => { + colliders.forEach((collider) => { + if (!collider.current.rect.overlaps(playerRect.current)) { + return; + } + + if ( + collider.current.is(ColliderType.Health) && + playerHealth < MAX_HEALTH + ) { + collider.current.onCollision(); + setPlayerHealth(playerHealth + 1); + return; + } + + if (collider.current.is(ColliderType.Bonus)) { + collider.current.onCollision(); + return; + } + + if (collider.current.is(ColliderType.Damage) && !invulnerable.current) { + collider.current.onCollision(); + + const velocity = knockback(direction.current, canvasRef.current!); + playerRect.current.moveBy(velocity.x, velocity.y); + + setPlayerHealth(playerHealth - 1); + invulnerable.current = true; + blink(canvasRef.current!, () => (invulnerable.current = false)); + } + }); + }; const tileSet = new Image(); tileSet.src = TILE_SETS.Player; tileSet.onload = () => { - let keyPressed = false; - let direction = Vector.Down; - let currentFrame = 0; - - drawFrame(ctx, tileSet, direction, currentFrame); + drawFrame(ctx, tileSet, direction.current, currentFrame.current); window.onkeyup = () => { - currentFrame = 0; - keyPressed = false; - drawFrame(ctx, tileSet, direction, currentFrame); + currentFrame.current = 0; + keyPressed.current = false; + drawFrame(ctx, tileSet, direction.current, currentFrame.current); }; window.onkeydown = (event) => { @@ -58,54 +92,7 @@ const Player: FC = ({ onInteract, top, left }) => { return; } - colliders.forEach((collider) => { - if (!collider.current.rect.overlaps(playerRect.current)) { - return; - } - - if ( - collider.current.type === ColliderType.Health && - playerHealth < MAX_HEALTH - ) { - collider.current.onCollision(); - setPlayerHealth(Math.min(MAX_HEALTH, playerHealth + 1)); - } else if (collider.current.type === ColliderType.Bonus) { - collider.current.onCollision(); - // TODO - } else if ( - collider.current.type === ColliderType.Damage && - !invulnerable - ) { - collider.current.onCollision(); - - const velocity = knockback(direction, canvasRef.current!); - playerRect.current.moveBy(velocity.x, velocity.y); - - setPlayerHealth(Math.max(MIN_HEALTH, playerHealth - 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); - } - }); + checkCollisions(); if (playerHealth <= MIN_HEALTH) { setGameState(GAME_STATES.GameOver); @@ -116,18 +103,17 @@ const Player: FC = ({ onInteract, top, left }) => { onInteract((wasOpen) => !wasOpen); } - direction = getInputVector(event.key); - const velocity = walk(direction, canvasRef.current); + direction.current = getInputVector(event.key); + const velocity = walk(direction.current, canvasRef.current); playerRect.current.moveBy(velocity.x, velocity.y); - if (!keyPressed) { - keyPressed = true; - drawFrame(ctx, tileSet, direction, currentFrame); + if (!keyPressed.current) { + keyPressed.current = true; + drawFrame(ctx, tileSet, direction.current, currentFrame.current); setTimeout(() => { - keyPressed = false; - currentFrame = - currentFrame === ANIMATION_LENGTH ? 0 : currentFrame + 1; + keyPressed.current = false; + currentFrame.current = getNextFrame(currentFrame.current); }, 125); } }; diff --git a/src/components/Player/utils.ts b/src/components/Player/utils.ts index 439c516..bdbede9 100644 --- a/src/components/Player/utils.ts +++ b/src/components/Player/utils.ts @@ -8,6 +8,7 @@ import { TILE_Y, SPEED, KNOCKBACK, + ANIMATION_LENGTH, } from "./constants"; export const getSpritePos = (direction: Vector, currentFrame: number) => { @@ -58,6 +59,11 @@ const move = (velocity: Vector, canvas: HTMLCanvasElement) => { canvas.style.left = `${parseInt(canvas.style.left || "0") + velocity.x}px`; }; +export const moveTo = ({ x, y }: Vector, canvas: HTMLCanvasElement) => { + canvas.style.left = canvas.style.left || `${x}px`; + canvas.style.top = canvas.style.top || `${y}px`; +}; + export const walk = (direction: Vector, canvas: HTMLCanvasElement) => { if (direction.eq(Vector.Zero)) { return Vector.Zero; @@ -76,3 +82,23 @@ export const knockback = (direction: Vector, canvas: HTMLCanvasElement) => { return velocity; }; + +export const blink = (canvas: HTMLCanvasElement, cb: () => void) => { + 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)"; + cb(); + }, 1500); +}; + +export const getNextFrame = (currentFrame: number) => { + return currentFrame === ANIMATION_LENGTH ? 0 : currentFrame + 1; +}; diff --git a/src/components/PlayerHealth/style.css b/src/components/PlayerHealth/style.css index 1e5b27d..6abae48 100644 --- a/src/components/PlayerHealth/style.css +++ b/src/components/PlayerHealth/style.css @@ -1,5 +1,4 @@ #health-canvas { - z-index: 99; - top: calc((1536px - 100vh) / 2 + 32px); - left: calc((2048px - 100vw) / 2 + 8px); + top: 8px; + left: 8px; } diff --git a/src/components/Ui/index.tsx b/src/components/Ui/index.tsx new file mode 100644 index 0000000..c34628c --- /dev/null +++ b/src/components/Ui/index.tsx @@ -0,0 +1,16 @@ +import { useContext, FC } from "react"; +import { GlobalContext } from "../../contexts"; +import PlayerHealth from "../PlayerHealth"; +import "./style.css"; + +const Ui: FC = () => { + const { score } = useContext(GlobalContext); + return ( +
+ +
SCORE: {score}
+
+ ); +}; + +export default Ui; diff --git a/src/components/Ui/style.css b/src/components/Ui/style.css new file mode 100644 index 0000000..20d7abc --- /dev/null +++ b/src/components/Ui/style.css @@ -0,0 +1,17 @@ +.ui { + z-index: 99; + position: absolute; + width: 100%; + height: 100vh; + top: calc((1536px - 100vh) / 2 + 24px); + left: calc((2048px - 100vw) / 2); +} +.score { + top: 10px; + left: 48px; + position: absolute; + color: white; + font-size: 1.5rem; + font-weight: bold; + text-shadow: 2px 2px 2px black; +} diff --git a/src/contexts/global.ts b/src/contexts/global.ts index 8686d53..04d6147 100644 --- a/src/contexts/global.ts +++ b/src/contexts/global.ts @@ -13,6 +13,8 @@ export type GlobalContextType = { prevValue: MutableRefObject[] ) => MutableRefObject[] ) => void; + readonly score: number; + setScore: (value: number) => void; }; export const GlobalContext = createContext({ @@ -22,4 +24,6 @@ export const GlobalContext = createContext({ setPlayerHealth: noop, colliders: [], setColliders: noop, + score: 0, + setScore: noop, }); diff --git a/src/hooks/useAnimatedSprite.ts b/src/hooks/useAnimatedSprite.ts index 3d88a67..67da2ba 100644 --- a/src/hooks/useAnimatedSprite.ts +++ b/src/hooks/useAnimatedSprite.ts @@ -6,45 +6,66 @@ type AnimatedSpriteProps = SpriteProps & { animationSpeed: number; }; -export const useAnimatedSprite = (props: AnimatedSpriteProps) => { +export const useAnimatedSprite = ({ + canvasRef, + tileSet, + animationSpeed, + animationLength, + width, + height, + tileX, + tileY, + left, + top, +}: AnimatedSpriteProps) => { useEffect(() => { - const ctx = props.canvasRef.current?.getContext("2d"); + const ctx = canvasRef.current?.getContext("2d"); let intervalId: number; - if (!props.canvasRef.current || !ctx) { + if (!canvasRef.current || !ctx) { return; } - props.left && (props.canvasRef.current.style.left = `${props.left}px`); - props.top && (props.canvasRef.current.style.top = `${props.top}px`); + left && (canvasRef.current.style.left = `${left}px`); + top && (canvasRef.current.style.top = `${top}px`); const sprite = new Image(); - sprite.src = props.tileSet; + sprite.src = tileSet; sprite.onload = () => { let currentFrame = 0; intervalId = window.setInterval(() => { - ctx.clearRect(0, 0, props.width, props.height); + ctx.clearRect(0, 0, width, height); ctx.drawImage( sprite, - props.tileX + props.width * currentFrame, - props.tileY, - props.width, - props.height, + tileX + width * currentFrame, + tileY, + width, + height, 0, 0, - props.width, - props.height + width, + height ); - currentFrame = - currentFrame === props.animationLength ? 0 : currentFrame + 1; - }, props.animationSpeed); + currentFrame = currentFrame === animationLength ? 0 : currentFrame + 1; + }, animationSpeed); }; return () => { clearInterval(intervalId); }; - }, [props]); + }, [ + canvasRef, + tileSet, + animationSpeed, + animationLength, + width, + height, + tileX, + tileY, + left, + top, + ]); }; diff --git a/src/hooks/useSprite.ts b/src/hooks/useSprite.ts index f4a0790..153cbd1 100644 --- a/src/hooks/useSprite.ts +++ b/src/hooks/useSprite.ts @@ -12,33 +12,32 @@ export type SpriteProps = { top?: number; }; -export const useSprite = (props: SpriteProps) => { +export const useSprite = ({ + canvasRef, + tileSet, + width, + height, + tileX, + tileY, + left, + top, +}: SpriteProps) => { useEffect(() => { - const ctx = props.canvasRef.current?.getContext("2d"); + const ctx = canvasRef.current?.getContext("2d"); - if (!props.canvasRef.current || !ctx) { + if (!canvasRef.current || !ctx) { return; } - props.left && (props.canvasRef.current.style.left = `${props.left}px`); - props.top && (props.canvasRef.current.style.top = `${props.top}px`); + left && (canvasRef.current.style.left = `${left}px`); + top && (canvasRef.current.style.top = `${top}px`); const sprite = new Image(); - sprite.src = props.tileSet; + sprite.src = tileSet; sprite.onload = () => { - ctx.clearRect(0, 0, props.width, props.height); + ctx.clearRect(0, 0, width, height); - ctx.drawImage( - sprite, - props.tileX, - props.tileY, - props.width, - props.height, - 0, - 0, - props.width, - props.height - ); + ctx.drawImage(sprite, tileX, tileY, width, height, 0, 0, width, height); }; - }, [props]); + }, [canvasRef, tileSet, width, height, tileX, tileY, left, top]); }; diff --git a/src/utils/clampValue.ts b/src/utils/clampValue.ts new file mode 100644 index 0000000..7bdc51d --- /dev/null +++ b/src/utils/clampValue.ts @@ -0,0 +1,3 @@ +export const clampValue = (value: number, min: number, max: number) => { + return Math.max(min, Math.min(max, value)); +}; diff --git a/src/utils/collider.ts b/src/utils/collider.ts index 60b9105..2556ed9 100644 --- a/src/utils/collider.ts +++ b/src/utils/collider.ts @@ -10,6 +10,7 @@ export class Collider { public readonly rect: Rect; public readonly type: ColliderType; public readonly onCollision: () => void; + private ignoreCollisions = false; constructor( rect: Rect, @@ -20,4 +21,16 @@ export class Collider { this.type = type; this.onCollision = () => (onCollision ? onCollision(this) : noop()); } + + public is(type: ColliderType) { + return !this.ignoreCollisions && this.type === type; + } + + public hide() { + this.ignoreCollisions = true; + } + + public show() { + this.ignoreCollisions = false; + } } diff --git a/src/utils/index.ts b/src/utils/index.ts index e64a3ad..faa25e5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,3 +3,4 @@ export * from "./rect"; export * from "./collider"; export * from "./noop"; export * from "./getRandomPosition"; +export * from "./clampValue";