diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 5c504c2..deaf77c 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -1,4 +1,4 @@ -import { MutableRefObject, useState } from "react"; +import { MutableRefObject, useRef, useState } from "react"; import { GlobalContext } from "../../contexts"; import World from "../World"; import Player from "../Player"; @@ -11,23 +11,27 @@ import Lever from "../Lever"; import House from "../House"; import Fire from "../Fire"; import GameOver from "../GameOver"; -import { GAME_STATES, MAX_HEALTH, MIN_HEALTH } from "../../constants"; +import { EVENTS, GAME_STATES, MAX_HEALTH, MIN_HEALTH } from "../../constants"; +import { AnyFunction, Events } from "../../types"; import { Collider } from "../../utils"; -import "./style.css"; import { clampValue } from "../../utils/clampValue"; +import "./style.css"; -/* - * TODO: - * - Move component actions and state inside components - * - Use context to connect components - */ export default function App() { const [gameState, setGameState] = useState(GAME_STATES.Game); const [colliders, setColliders] = useState[]>([]); - const [isCellarDoorOpen, setIsCellarDoorOpen] = useState(false); - const [isLeverUsed, setIsLeverUsed] = useState(false); const [playerHealth, setPlayerHealth] = useState(MAX_HEALTH); const [score, setScore] = useState(0); + const events = useRef({}); + const setEvent = (event: EVENTS, cb: AnyFunction) => { + events.current = { + ...events.current, + [event]: [...(events.current[event] || []), cb], + }; + }; + const callEvent = (event: EVENTS) => { + events.current[event]?.forEach((cb) => cb()); + }; return (
@@ -35,6 +39,8 @@ export default function App() { value={{ gameState, setGameState, + callEvent, + setEvent, playerHealth, setPlayerHealth: (health: number) => setPlayerHealth(clampValue(health, MIN_HEALTH, MAX_HEALTH)), @@ -47,15 +53,10 @@ export default function App() { {gameState === GAME_STATES.GameOver && } - + - - + + diff --git a/src/components/CellarDoor/index.tsx b/src/components/CellarDoor/index.tsx index 081a415..efe06b2 100644 --- a/src/components/CellarDoor/index.tsx +++ b/src/components/CellarDoor/index.tsx @@ -1,5 +1,6 @@ -import { useRef, FC } from "react"; -import { TILE_SETS } from "../../constants"; +import { useRef, FC, useContext, useState, useEffect } from "react"; +import { EVENTS, TILE_SETS } from "../../constants"; +import { GlobalContext } from "../../contexts"; import { useSprite } from "../../hooks"; import "./style.css"; @@ -7,16 +8,22 @@ const WIDTH = 64; const HEIGHT = 64; const TILE_X = 992; -type CellarDoorProps = { top: number; left: number; isOpen?: boolean }; +type CellarDoorProps = { top: number; left: number }; -/* - * TODO: - * - util function for tile set, tiles and animation - * - track state internally - */ -const CellarDoor: FC = ({ isOpen = false, top, left }) => { +const CellarDoor: FC = ({ top, left }) => { + const { setEvent } = useContext(GlobalContext); + const [isOpen, setIsOpen] = useState(false); const canvasRef = useRef(null); + useEffect(() => { + setEvent(EVENTS.LEVER_ON, () => { + setIsOpen(true); + }); + setEvent(EVENTS.LEVER_OFF, () => { + setIsOpen(false); + }); + }, [setEvent]); + useSprite({ canvasRef, left, diff --git a/src/components/Coin/index.tsx b/src/components/Coin/index.tsx index 0e7caf7..666a824 100644 --- a/src/components/Coin/index.tsx +++ b/src/components/Coin/index.tsx @@ -21,14 +21,14 @@ const Coin: FC = ({ left, top }) => { const onCollision = (c: Collider) => { setScore(POINTS); setIsHidden(true); - collider.current.hide(); + c.hide(); setTimeout(() => { - collider.current.show(); + c.show(); setIsHidden(false); }, TIMEOUT); }; - const collider = useRef( + const colliderRef = useRef( new Collider( new Rect(left, top, WIDTH, HEIGHT), ColliderType.Bonus, @@ -36,7 +36,7 @@ const Coin: FC = ({ left, top }) => { ) ); - useColliders(collider); + useColliders(colliderRef); useAnimatedSprite({ canvasRef, diff --git a/src/components/Fire/index.tsx b/src/components/Fire/index.tsx index 21c7d60..953ccab 100644 --- a/src/components/Fire/index.tsx +++ b/src/components/Fire/index.tsx @@ -13,11 +13,11 @@ type FireProps = { left: number; top: number }; const Fire: FC = ({ left, top }) => { const canvasRef = useRef(null); - const collider = useRef( + const colliderRef = useRef( new Collider(new Rect(left, top, WIDTH, HEIGHT), ColliderType.Damage) ); - useColliders(collider); + useColliders(colliderRef); useAnimatedSprite({ canvasRef, diff --git a/src/components/Heart/index.tsx b/src/components/Heart/index.tsx index cefdd07..fdb87ee 100644 --- a/src/components/Heart/index.tsx +++ b/src/components/Heart/index.tsx @@ -20,16 +20,19 @@ type HeartProps = { left: number; top: number }; const Heart: FC = ({ left, top }) => { const [position, setPosition] = useState(new Vector(left, top)); const canvasRef = useRef(null); + const updatePosition = (c: Collider) => { const newPosition = getRandomPosition(WIDTH, HEIGHT); setPosition(newPosition); c.rect.moveTo(newPosition.x, newPosition.y); }; + const collider = new Collider( new Rect(position.x, position.y, WIDTH, HEIGHT), ColliderType.Health, updatePosition ); + const colliderRef = useRef(collider); useColliders(colliderRef); diff --git a/src/components/Lever/index.tsx b/src/components/Lever/index.tsx index b2428fa..c6964e4 100644 --- a/src/components/Lever/index.tsx +++ b/src/components/Lever/index.tsx @@ -1,22 +1,41 @@ -import { useRef, FC } from "react"; -import { TILE_SIZE, TILE_SETS } from "../../constants"; -import { useSprite } from "../../hooks"; +import { useRef, FC, useState, useContext } from "react"; +import { TILE_SIZE, TILE_SETS, EVENTS } from "../../constants"; +import { GlobalContext } from "../../contexts"; +import { useChangeEffect, useColliders, useSprite } from "../../hooks"; +import { Collider, ColliderType, Rect } from "../../utils"; import "./style.css"; const WIDTH = TILE_SIZE; const HEIGHT = TILE_SIZE; +const INTERACTION_RANGE = TILE_SIZE / 2; const TILE_X = 64; type LeverProps = { left: number; top: number; - used: boolean; - onInteract: (value: boolean | ((prev: boolean) => boolean)) => void; }; -const Lever: FC = ({ left, top, used, onInteract }) => { +const Lever: FC = ({ left, top }) => { + const [isOn, setIsOn] = useState(false); + const { callEvent } = useContext(GlobalContext); const canvasRef = useRef(null); + useChangeEffect( + () => callEvent(isOn ? EVENTS.LEVER_ON : EVENTS.LEVER_OFF), + [isOn] + ); + + const onCollision = () => setIsOn((v) => !v); + const colliderRef = useRef( + new Collider( + new Rect(left, top, WIDTH, HEIGHT, INTERACTION_RANGE, INTERACTION_RANGE), + ColliderType.Object, + onCollision + ) + ); + + useColliders(colliderRef); + useSprite({ canvasRef, left, @@ -24,12 +43,10 @@ const Lever: FC = ({ left, top, used, onInteract }) => { tileSet: TILE_SETS.Objects, width: WIDTH, height: HEIGHT, - tileX: used ? TILE_X + WIDTH : TILE_X, + tileX: isOn ? TILE_X + WIDTH : TILE_X, tileY: 288, }); - onInteract(used); - return ( boolean)) => void; }; -const Player: FC = ({ onInteract, top, left }) => { +const Player: FC = ({ top, left }) => { const canvasRef = useRef(null); const playerRect = useRef(new Rect(left, top, WIDTH, HEIGHT)); const invulnerable = useRef(false); @@ -43,12 +42,20 @@ const Player: FC = ({ onInteract, top, left }) => { moveTo(new Vector(left, top), canvasRef.current); - const checkCollisions = () => { + const checkCollisions = (isInteraction: boolean) => { colliders.forEach((collider) => { if (!collider.current.rect.overlaps(playerRect.current)) { return; } + if (isInteraction) { + if (collider.current.is(ColliderType.Object)) { + collider.current.onCollision(); + } + + return; + } + if ( collider.current.is(ColliderType.Health) && playerHealth < MAX_HEALTH @@ -92,15 +99,16 @@ const Player: FC = ({ onInteract, top, left }) => { return; } - checkCollisions(); + const isInteraction = Input.Interact.includes(event.key); + checkCollisions(isInteraction); if (playerHealth <= MIN_HEALTH) { setGameState(GAME_STATES.GameOver); return; } - if (Input.Interact.includes(event.key)) { - onInteract((wasOpen) => !wasOpen); + if (isInteraction) { + return; } direction.current = getInputVector(event.key); @@ -118,15 +126,7 @@ const Player: FC = ({ onInteract, top, left }) => { } }; }; - }, [ - onInteract, - setPlayerHealth, - playerHealth, - setGameState, - top, - left, - colliders, - ]); + }, [setPlayerHealth, playerHealth, setGameState, top, left, colliders]); return ( <> diff --git a/src/components/Ui/style.css b/src/components/Ui/style.css index 20d7abc..cc2c0d6 100644 --- a/src/components/Ui/style.css +++ b/src/components/Ui/style.css @@ -3,7 +3,7 @@ position: absolute; width: 100%; height: 100vh; - top: calc((1536px - 100vh) / 2 + 24px); + top: calc((1536px - 100vh) / 2); left: calc((2048px - 100vw) / 2); } .score { diff --git a/src/constants.ts b/src/constants.ts index 75f666c..bb9c3af 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -10,6 +10,11 @@ export enum TILE_SETS { World = "assets/overworld.png", } +export enum EVENTS { + LEVER_ON = "LEVER_ON", + LEVER_OFF = "LEVER_OFF", +} + export const TILE_SIZE = 32; export const WORLD_WIDTH = 2048; export const WORLD_HEIGHT = 1536; diff --git a/src/contexts/global.ts b/src/contexts/global.ts index 04d6147..a91c709 100644 --- a/src/contexts/global.ts +++ b/src/contexts/global.ts @@ -1,5 +1,6 @@ import { createContext, MutableRefObject } from "react"; -import { GAME_STATES, MAX_HEALTH } from "../constants"; +import { EVENTS, GAME_STATES, MAX_HEALTH } from "../constants"; +import { AnyFunction } from "../types"; import { Collider, noop } from "../utils"; export type GlobalContextType = { @@ -15,6 +16,8 @@ export type GlobalContextType = { ) => void; readonly score: number; setScore: (value: number) => void; + callEvent: (event: EVENTS) => void; + setEvent: (event: EVENTS, cb: AnyFunction) => void; }; export const GlobalContext = createContext({ @@ -26,4 +29,6 @@ export const GlobalContext = createContext({ setColliders: noop, score: 0, setScore: noop, + setEvent: noop, + callEvent: noop, }); diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 1f08d62..364af04 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,3 +1,4 @@ export * from "./useSprite"; export * from "./useAnimatedSprite"; export * from "./useColliders"; +export * from "./useChangeEffect"; diff --git a/src/hooks/useChangeEffect.ts b/src/hooks/useChangeEffect.ts new file mode 100644 index 0000000..4a4547c --- /dev/null +++ b/src/hooks/useChangeEffect.ts @@ -0,0 +1,14 @@ +import { useEffect, useRef } from "react"; +import { AnyFunction } from "../types"; + +export const useChangeEffect = (fn: AnyFunction, inputs: unknown[]) => { + const didMount = useRef(false); + useEffect(() => { + if (didMount.current) { + fn(); + return; + } + + didMount.current = true; + }, [inputs, fn]); +}; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..b644e0b --- /dev/null +++ b/src/types.ts @@ -0,0 +1,4 @@ +import { EVENTS } from "./constants"; + +export type AnyFunction = () => void; +export type Events = Partial>; diff --git a/src/utils/collider.ts b/src/utils/collider.ts index 2556ed9..b6a3f41 100644 --- a/src/utils/collider.ts +++ b/src/utils/collider.ts @@ -1,15 +1,17 @@ import { Rect, noop } from "./"; +import { AnyFunction } from "../types"; export enum ColliderType { Health, Bonus, Damage, + Object, } export class Collider { public readonly rect: Rect; public readonly type: ColliderType; - public readonly onCollision: () => void; + public readonly onCollision: AnyFunction; private ignoreCollisions = false; constructor( diff --git a/src/utils/rect.ts b/src/utils/rect.ts index fb6c0bf..83bb70d 100644 --- a/src/utils/rect.ts +++ b/src/utils/rect.ts @@ -6,11 +6,18 @@ export class Rect { public width: number; public height: number; - constructor(x: number, y: number, width: number, height: number) { - this.x1 = x; - this.y1 = y; - this.x2 = x + width; - this.y2 = y + height; + constructor( + x: number, + y: number, + width: number, + height: number, + scaleX = 0, + scaleY = 0 + ) { + this.x1 = x - scaleX; + this.y1 = y - scaleY; + this.x2 = x + width + scaleX; + this.y2 = y + height + scaleY; this.width = width; this.height = height; } @@ -34,4 +41,11 @@ export class Rect { this.x2 = this.x1 + this.width; this.y2 = this.y1 + this.height; } + + public scale(scaleX: number, scaleY: number) { + this.x1 -= scaleX; + this.y1 -= scaleY; + this.x2 = this.x1 + this.width + scaleX; + this.y2 = this.y1 + this.height + scaleY; + } }