Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor 7 #25

Open
wants to merge 1 commit into
base: refactor-6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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:
Expand All @@ -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 (
<div className="App">
Expand All @@ -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 && <GameOver />}
<World />
<PlayerHealth />
<Player top={328} left={420} onInteract={setIsLeverUsed} />
<Ui />
<Player top={332} left={428} onInteract={setIsLeverUsed} />
<Npc left={1608} top={224} />
<CellarDoor isOpen={isCellarDoorOpen} left={528} top={272} />
<Lever
Expand Down
24 changes: 22 additions & 2 deletions src/components/Coin/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRef, FC } from "react";
import { useRef, FC, useContext, useState } from "react";
import { TILE_SIZE, TILE_SETS } from "../../constants";
import { GlobalContext } from "../../contexts";
import { useAnimatedSprite, useColliders } from "../../hooks";
import { Collider, ColliderType, Rect } from "../../utils";
import "./style.css";
Expand All @@ -8,13 +9,31 @@ const WIDTH = TILE_SIZE;
const HEIGHT = TILE_SIZE;
const TILE_X = 0;
const TILE_Y = 128;
const POINTS = 10;
const TIMEOUT = 2000;

type CoinProps = { left: number; top: number };

const Coin: FC<CoinProps> = ({ left, top }) => {
const { setScore } = useContext(GlobalContext);
const [isHidden, setIsHidden] = useState(false);
const canvasRef = useRef<HTMLCanvasElement>(null);
const onCollision = (c: Collider) => {
setScore(POINTS);
setIsHidden(true);
collider.current.hide();

setTimeout(() => {
collider.current.show();
setIsHidden(false);
}, TIMEOUT);
};
const collider = useRef<Collider>(
new Collider(new Rect(left, top, WIDTH, HEIGHT), ColliderType.Bonus)
new Collider(
new Rect(left, top, WIDTH, HEIGHT),
ColliderType.Bonus,
onCollision
)
);

useColliders(collider);
Expand All @@ -34,6 +53,7 @@ const Coin: FC<CoinProps> = ({ left, top }) => {

return (
<canvas
className={isHidden ? "hidden" : ""}
ref={canvasRef}
id="coin-canvas"
width={WIDTH}
Expand Down
4 changes: 4 additions & 0 deletions src/components/Coin/style.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#coin-canvas {
z-index: 2;
}

.hidden {
display: none;
}
134 changes: 60 additions & 74 deletions src/components/Player/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ import {
} from "../../constants";
import { GlobalContext } from "../../contexts";
import { ColliderType, Rect, Vector } from "../../utils";
import { ANIMATION_LENGTH, HEIGHT, Input, WIDTH } from "./constants";
import { drawFrame, getInputVector, walk, knockback } from "./utils";
import { HEIGHT, Input, WIDTH } from "./constants";
import {
drawFrame,
getInputVector,
walk,
knockback,
blink,
getNextFrame,
moveTo,
} from "./utils";
import "./style.css";

type PlayerProps = {
Expand All @@ -17,15 +25,13 @@ type PlayerProps = {
onInteract: (isOpen: boolean | ((wasOpen: boolean) => boolean)) => void;
};

/*
* TODO:
* - move player controls to global context
* - use input loop to remove keydown delay
*/
let invulnerable = false;
const Player: FC<PlayerProps> = ({ onInteract, top, left }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const playerRect = useRef<Rect>(new Rect(left, top, WIDTH, HEIGHT));
const invulnerable = useRef<boolean>(false);
const keyPressed = useRef<boolean>(false);
const direction = useRef<Vector>(Vector.Down);
const currentFrame = useRef<number>(0);
const { setGameState, playerHealth, setPlayerHealth, colliders } =
useContext(GlobalContext);

Expand All @@ -35,77 +41,58 @@ const Player: FC<PlayerProps> = ({ 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) => {
if (!canvasRef.current) {
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);
Expand All @@ -116,18 +103,17 @@ const Player: FC<PlayerProps> = ({ 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);
}
};
Expand Down
26 changes: 26 additions & 0 deletions src/components/Player/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
TILE_Y,
SPEED,
KNOCKBACK,
ANIMATION_LENGTH,
} from "./constants";

export const getSpritePos = (direction: Vector, currentFrame: number) => {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
};
5 changes: 2 additions & 3 deletions src/components/PlayerHealth/style.css
Original file line number Diff line number Diff line change
@@ -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;
}
16 changes: 16 additions & 0 deletions src/components/Ui/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="ui">
<PlayerHealth />
<div className="score">SCORE: {score}</div>
</div>
);
};

export default Ui;
17 changes: 17 additions & 0 deletions src/components/Ui/style.css
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions src/contexts/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type GlobalContextType = {
prevValue: MutableRefObject<Collider>[]
) => MutableRefObject<Collider>[]
) => void;
readonly score: number;
setScore: (value: number) => void;
};

export const GlobalContext = createContext<GlobalContextType>({
Expand All @@ -22,4 +24,6 @@ export const GlobalContext = createContext<GlobalContextType>({
setPlayerHealth: noop,
colliders: [],
setColliders: noop,
score: 0,
setScore: noop,
});
Loading