Skip to content

Commit

Permalink
Merge pull request #2 from DVDAGames/feature/easter-egg
Browse files Browse the repository at this point in the history
Feature/easter egg
  • Loading branch information
ericrallen authored May 2, 2024
2 parents fba5d09 + f71313d commit 43d5f99
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 7 deletions.
67 changes: 66 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
"start": "next start"
},
"dependencies": {
"@uidotdev/usehooks": "^2.4.1",
"classnames": "^2.5.1",
"date-fns": "^3.3.1",
"framer-motion": "^11.1.7",
"gray-matter": "^4.0.3",
"next": "14.1.0",
"react": "^18",
"react-dom": "^18",
"react-konami-code": "^2.3.0",
"react-marquee-text": "^1.0.3",
"rehype-highlight": "^7.0.0",
"rehype-remark": "^10.0.0",
Expand Down
Binary file added public/assets/images/pulsar-ship.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions src/app/_components/easter-egg/game.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useMouse } from "@uidotdev/usehooks";

import Ship from "./ship";

export interface GameProps {
isPlayable?: boolean;
quit?: () => void;
}

export function Game({ isPlayable = false, quit }: GameProps): React.ReactElement {
const [mouse, ref] = useMouse();

if (!isPlayable) {
return <></>;
}

const classes = ["fixed", "top-0", "left-0", "w-full", "h-full", "justify-center", "items-center"];

const shipPosition = {
x: mouse.elementPositionX,
y: mouse.elementPositionY,
};

// calculate rotation angle of the ship to point the nose of the ship towards mouse x and y
const rotation = Math.atan2(shipPosition.y - mouse.y, shipPosition.x - mouse.x) * (180 / Math.PI);

// get distance between ship and mouse
const dx = mouse.x - shipPosition.x;
const dy = mouse.y - shipPosition.y;

const distance = Math.sqrt(dx * dx + dy * dy);

const position = {
x: shipPosition.x,
y: shipPosition.y,
};

if (Math.abs(distance) >= 32) {
position.x = shipPosition.x + dx;
position.y = shipPosition.y + dy;
}

return (
<section className={classes.join(" ")}>
<Ship shipRef={ref} rotate={rotation - 90} x={position.x} y={position.y} onClickShip={quit} />
</section>
);
}

export default Game;
21 changes: 21 additions & 0 deletions src/app/_components/easter-egg/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";

import { useState } from "react";
import { useKonami } from "react-konami-code";

import Game from "./game";

export function EasterEgg(): React.ReactElement {
const [isPlayable, setIsPlayable] = useState(false);

const togglePlayable = () => {
console.log("KONAMI!");
setIsPlayable((prev) => !prev);
};

useKonami(togglePlayable);

return <Game isPlayable={isPlayable} quit={togglePlayable} />;
}

export default EasterEgg;
75 changes: 75 additions & 0 deletions src/app/_components/easter-egg/ship.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use client";

import { useEffect, useRef } from "react";
import { useAnimate, AnimationPlaybackControls } from "framer-motion";

export interface ShipProps {
x: number;
y: number;
rotate: number;
shipRef: React.MutableRefObject<Element>;
onClickShip?: () => void;
}

export function Ship({ shipRef, x, y, rotate, onClickShip }: ShipProps): React.ReactElement {
// TODO: make ship fire bullets at the mouse position and trigger fire sound effect
// TODO: garbage collect bullets that go off screen
// TODO: make mouse take damage when hit by bullets
// TODO: Game over screen after 5 hits
const [scope, animate] = useAnimate();
const animationPromises: React.MutableRefObject<AnimationPlaybackControls[]> = useRef([]);

const quit = () => {
animationPromises?.current?.forEach((promise) => {
promise?.stop();
promise?.cancel();
});

onClickShip?.();
};

useEffect(() => {
const animation = async () => {
if (
typeof scope.current !== "undefined" &&
scope.current !== null &&
typeof shipRef.current !== "undefined" &&
shipRef.current !== null
) {
animationPromises.current.push(animate(scope.current, { rotate }, { duration: 0.5, ease: "circOut" }));
animationPromises.current.push(animate(scope.current, { x, y }, { duration: 5, ease: "circOut", delay: 0.25 }));

await Promise.all(animationPromises.current);
}
};

if (
typeof scope.current !== "undefined" &&
scope.current !== null &&
typeof shipRef.current !== "undefined" &&
shipRef.current !== null
) {
animation?.()
.then(() => {
animationPromises.current = [];
})
.catch((error) => {
console.error(error);
});
}
}, [rotate, x, y]);

return (
<button onClick={quit} ref={scope} title="Click to dismiss" className="h-[32px] w-[32px] appearance-none">
<img
ref={shipRef as React.MutableRefObject<HTMLImageElement>}
src="/assets/images/pulsar-ship.gif"
alt=""
width="32"
height="32"
/>
</button>
);
}

export default Ship;
3 changes: 0 additions & 3 deletions src/app/_components/player/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,13 @@ export function Player({ tracks = [], autoplay = false, loop = false }: PlayerPr

const togglePlay = (): void => {
setIsPlaying((prev) => !prev);
console.log("togglePlay", isPlaying);
};

const togglePlaylist = (): void => {
setShowPlaylist((prev) => !prev);
console.log("togglePlayList", showPlaylist);
};

useEffect(() => {
console.log(currentTrack);
if (playerRef.current && currentTrack) {
if (isPlaying) {
playerRef.current.play();
Expand Down
9 changes: 6 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import Footer from "@/app/_components/footer";
import { HOME_OG_IMAGE_URL } from "@/lib/constants";
import type { Metadata } from "next";
import { IBM_Plex_Mono } from "next/font/google";

import EasterEgg from "@/app/_components/easter-egg";
import Footer from "@/app/_components/footer";
import { HOME_OG_IMAGE_URL } from "@/lib/constants";

import "./globals.css";

const ibm = IBM_Plex_Mono({
Expand All @@ -12,7 +14,7 @@ const ibm = IBM_Plex_Mono({

export const metadata: Metadata = {
title: `Dead Villager Dead Adventurer Games`,
description: `A wannabe indie game developer.`,
description: `A wannabe indie game studio and open source tool developer.`,
openGraph: {
images: [HOME_OG_IMAGE_URL],
},
Expand Down Expand Up @@ -41,6 +43,7 @@ export default function RootLayout({
<body className={ibm.className}>
<div className="min-h-screen">{children}</div>
<Footer />
<EasterEgg />
</body>
</html>
);
Expand Down

0 comments on commit 43d5f99

Please sign in to comment.