Skip to content
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
34 changes: 14 additions & 20 deletions src/components/ChampPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import Image from "next/image";
import React from "react";

import { KEY_CODES } from "@src/constants";
import { getNeighborsDirections } from "@src/core/board";
import { getBoardDimensions, getNeighborsDirections } from "@src/core/board";
import { getChampLevelImage } from "@src/core/champ";
import { useKeyPress, useMoveService } from "@src/hooks";
import { useKeyPress, useWorldActor } from "@src/hooks";
import { RequestMoveEvent } from "@src/machines/world";

import type { MoveMachineEvent } from "@src/machines/move";
import type { ClassroomChamp, Direction } from "@src/types";
import type { Direction } from "@src/types";

/**
* Get offset in increments of the given percentage.
Expand Down Expand Up @@ -42,28 +42,22 @@ function getHesitateMotionDefinition(direction: Direction) {
}
}

interface ChampPlayerProps {
classroomChamp: ClassroomChamp;
}

export default function ChampPlayer({ classroomChamp }: ChampPlayerProps) {
const [state, send] = useMoveService();
export default function ChampPlayer() {
const [state, send] = useWorldActor();
const [isFocused, setIsFocused] = React.useState(false);
const motionControls = useAnimationControls();

const { board, coordinates, dimensions } = state.context;
const { board, player } = state.context;
const dimensions = getBoardDimensions(board);
const widthPercent = 100 / dimensions.width;
const heightPercent = 100 / dimensions.height;
const champImage = getChampLevelImage(
classroomChamp.champ,
classroomChamp.mind.level,
);
const champImage = getChampLevelImage(player.champ, player.mind.level);

const { x, y } = coordinates;
const { x, y } = player.coordinates;

const neighborsDirections = React.useMemo(
() => getNeighborsDirections(coordinates, board),
[coordinates, board],
() => getNeighborsDirections(player.coordinates, board),
[player.coordinates, board],
);

const onCompleteMove = React.useCallback(
Expand All @@ -90,9 +84,9 @@ export default function ChampPlayer({ classroomChamp }: ChampPlayerProps) {
e.preventDefault();
const direction = keyCodesToDirection[key];

const event: MoveMachineEvent = {
const event: RequestMoveEvent = {
type: "REQUEST_MOVE",
value: direction,
direction,
};

if (state.can(event)) {
Expand Down
29 changes: 12 additions & 17 deletions src/components/ChampProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import { useSelector } from "@xstate/react";
import cn from "classnames";
import { animate, motion, useMotionValue } from "framer-motion";
import Image from "next/image";

import HeartProgressBar from "@src/components/HeartProgressBar";
import FeelingsCheckLevelTrigger from "@src/components/Level/FeelingsCheckLevelTrigger";
import * as Section from "@src/components/Section";
import { WorldContext } from "@src/contexts";
import { getChampLevelImage } from "@src/core/champ";
import { useHocusProps } from "@src/hooks";
import { useHocusProps, useRequiredContext } from "@src/hooks";
import CheckInIconSVG from "@src/svgs/check-in-icon.svg";

import type { ClassroomChamp } from "@src/types";
export default function ChampProgressBar() {
const service = useRequiredContext(WorldContext);
const player = useSelector(service, (state) => state.context.player);

interface ChampProgressBarProps {
classroomChamp: ClassroomChamp;
}

export default function ChampProgressBar({
classroomChamp,
}: ChampProgressBarProps) {
const FULL_SPIN = 360;
const spin = useMotionValue(0);
const hocusProps = useHocusProps({
Expand Down Expand Up @@ -49,10 +46,7 @@ export default function ChampProgressBar({
},
});

const champImage = getChampLevelImage(
classroomChamp.champ,
classroomChamp.mind.level,
);
const champImage = getChampLevelImage(player.champ, player.mind.level);

return (
<Section.Root className="fixed bottom-0 z-50 flex w-full items-stretch bg-purple-dark">
Expand All @@ -72,16 +66,16 @@ export default function ChampProgressBar({
</div>
<div className="flex flex-col justify-center">
<h2 className="text-2xl font-medium text-white xl:text-3xl">
{classroomChamp.champ.name}
{player.champ.name}
</h2>
<p className="font-rounded text-purple-light">
Mind Level {classroomChamp.mind.level}
Mind Level {player.mind.level}
</p>
</div>
<div className="flex-wrap max-xl:mx-auto xl:ml-20">
<HeartProgressBar
points={classroomChamp.mind.pointsInLevel}
max={classroomChamp.mind.pointsPerLevel}
points={player.mind.pointsInLevel}
max={player.mind.pointsPerLevel}
/>
</div>
</div>
Expand All @@ -91,6 +85,7 @@ export default function ChampProgressBar({
"group flex flex-col items-center justify-center gap-2 border-l border-purple p-4 lg:px-10",
"outline-none ring-0 ring-inset ring-purple transition-shadow hover:ring-2 focus:ring-4 focus:ring-purple-light",
)}
defaultOpen
{...hocusProps}
>
<motion.span style={{ rotateZ: spin }}>
Expand Down
13 changes: 10 additions & 3 deletions src/components/Level/FeelingsCheckLevelTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,18 @@ function FeelingsCheckLevel() {
);
}

export default function FeelingsCheckLevelTrigger(
props: React.ButtonHTMLAttributes<HTMLButtonElement>,
) {
export interface FeelingsCheckLevelTriggerProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
defaultOpen?: boolean;
}

export default function FeelingsCheckLevelTrigger({
defaultOpen,
...props
}: FeelingsCheckLevelTriggerProps) {
return (
<LevelTrigger
defaultOpen={defaultOpen}
icon={<OmsCalmingCornerWorldIconSVG aria-hidden />}
title="Feelings Check!"
activity={<FeelingsCheckLevel />}
Expand Down
26 changes: 20 additions & 6 deletions src/components/Level/LevelTrigger.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as Dialog from "@radix-ui/react-dialog";
import { useSelector } from "@xstate/react";
import cn from "classnames";
import React, { PropsWithChildren } from "react";
import { useIsomorphicLayoutEffect } from "framer-motion";
import React from "react";

import Button from "@src/components/Button";
import { WorldContext } from "@src/contexts";
Expand Down Expand Up @@ -34,25 +36,37 @@ function LevelBanner({ className, icon, heading, title }: LevelBannerProps) {
);
}

export interface LevelTriggerProps extends PropsWithChildren {
export interface LevelTriggerProps extends React.PropsWithChildren {
icon: React.ReactNode;
title: string;
activity: React.ReactNode;
defaultOpen?: boolean;
}

export default function LevelTrigger({
children,
icon,
title,
activity,
defaultOpen,
}: LevelTriggerProps) {
const world = useRequiredContext(WorldContext);
// Set container for portal in useLayoutEffect to prevent initial hydration error
const [container, setContainer] = React.useState<HTMLElement | null>(null);
useIsomorphicLayoutEffect(() => {
const container = document.querySelector("#portal");
if (container instanceof HTMLElement) {
setContainer(container);
}
}, []);

const service = useRequiredContext(WorldContext);
const worldTitle = useSelector(service, (state) => state.context.title);

return (
<Dialog.Root>
<Dialog.Root defaultOpen={defaultOpen}>
<Dialog.Trigger asChild>{children}</Dialog.Trigger>

<Dialog.Portal>
<Dialog.Portal container={container}>
<Dialog.Overlay />

<Dialog.Content
Expand All @@ -62,7 +76,7 @@ export default function LevelTrigger({
<LevelBanner
className="absolute top-5 left-5 xl:top-10 xl:left-10"
icon={icon}
heading={world.title}
heading={worldTitle}
title={title}
/>

Expand Down
16 changes: 9 additions & 7 deletions src/components/Tile/BaseTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import noop from "lodash/noop";
import React, { PropsWithChildren } from "react";

import {
getBoardDimensions,
getDirectionBetweenCoordinates,
isReachableNeighbor,
} from "@src/core/board";
import { useMoveService } from "@src/hooks";
import { useWorldActor } from "@src/hooks";

import type {
PropsWithClassName,
Expand Down Expand Up @@ -56,30 +57,31 @@ export default function BaseTile({
outline = true,
paths = true,
}: BaseTileProps) {
const [state, send] = useMoveService();
const [state, send] = useWorldActor();

const isCurrentPlayerPosition = isEqual(
state.context.coordinates,
state.context.player.coordinates,
coordinates,
);

const isCurrentPlayerNeighbor = isReachableNeighbor(
state.context.coordinates,
state.context.player.coordinates,
coordinates,
state.context.board,
);

const widthPercent = 100 / state.context.dimensions.width;
const dimensions = getBoardDimensions(state.context.board);
const widthPercent = 100 / dimensions.width;
const neighborsDirections = tile ? tile.directions : [];

function onRequestMove() {
const direction = getDirectionBetweenCoordinates(
state.context.coordinates,
state.context.player.coordinates,
coordinates,
);

if (direction) {
send({ type: "REQUEST_MOVE", value: direction });
send({ type: "REQUEST_MOVE", direction });
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/components/Tile/LevelTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import React from "react";
import mcpuffersonFigure from "@public/champs/mcpufferson-figure.png";
import BoardTooltipsPortal from "@src/components/BoardTooltipsPortal";
import LevelPreview from "@src/components/LevelPreview";
import { BoardContext } from "@src/contexts";
import { WorldContext } from "@src/contexts";
import { useRequiredContext, useTimeoutState } from "@src/hooks";

import type { Coordinates, LevelTile } from "@src/types";
Expand Down Expand Up @@ -41,10 +41,10 @@ export default function LevelTile({
200,
);

const service = useRequiredContext(BoardContext);
const service = useRequiredContext(WorldContext);
const playerCoordinates = useSelector(
service,
(state) => state.context.coordinates,
(state) => state.context.player.coordinates,
);

const isCompleted = tile.completed;
Expand Down
6 changes: 4 additions & 2 deletions src/components/Tile/StartTile.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Tooltip from "@radix-ui/react-tooltip";
import { useSelector } from "@xstate/react";
import { motion, useAnimationControls } from "framer-motion";

import BoardTooltipsPortal from "@src/components/BoardTooltipsPortal";
Expand All @@ -21,7 +22,8 @@ export interface StartTileProps {
* starting the board.
*/
export default function StartTile({ tile, coordinates }: StartTileProps) {
const world = useRequiredContext(WorldContext);
const service = useRequiredContext(WorldContext);
const goal = useSelector(service, (state) => state.context.goal);

const tileMotionControls = useAnimationControls();

Expand Down Expand Up @@ -81,7 +83,7 @@ export default function StartTile({ tile, coordinates }: StartTileProps) {
Start!
</h3>
<p className="font-rounded text-xs text-mint lg:text-lg">
{world.goal}
{goal}
</p>
</div>
</div>
Expand Down
37 changes: 11 additions & 26 deletions src/components/WorldBoard.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,39 @@
import { useInterpret, useSelector } from "@xstate/react";
import { useSelector } from "@xstate/react";
import React from "react";

import ChampPlayer from "@src/components/ChampPlayer";
import * as Section from "@src/components/Section";
import Tile from "@src/components/Tile";
import { BoardContext } from "@src/contexts";
import { WorldContext } from "@src/contexts";
import { getBoardDimensions } from "@src/core/board";
import mcpuffersonClassroomChamp from "@src/data/champs/mcpufferson";
import { moveMachine } from "@src/machines/move";
import { useRequiredContext } from "@src/hooks";

import type { Board, Coordinates } from "@src/types";

export interface WorldBoardProps {
position: Coordinates;
board: Board;
}

export default function WorldBoard({ position, board }: WorldBoardProps) {
export default function WorldBoard() {
const service = useRequiredContext(WorldContext);
const board = useSelector(service, (state) => state.context.board);
const dimensions = getBoardDimensions(board);

const service = useInterpret(moveMachine, {
context: {
board,
coordinates: position,
dimensions,
},
});
const state = useSelector(service, (state) => state.context);

if (state.dimensions.width <= 0 || state.dimensions.height <= 0) {
if (dimensions.width <= 0 || dimensions.height <= 0) {
return null;
}

return (
<BoardContext.Provider value={service}>
<WorldContext.Provider value={service}>
<Section.Root className="relative">
<Section.Heading className="sr-only">Game board</Section.Heading>

<div className="flex flex-wrap">
{state.board.map((row, y) =>
{board.map((row, y) =>
row.map((tile, x) => (
<Tile tile={tile} coordinates={{ x, y }} key={`${x}:${y}`} />
)),
)}
</div>

<ChampPlayer classroomChamp={mcpuffersonClassroomChamp} />
<ChampPlayer />

<div id="board-tooltips" />
</Section.Root>
</BoardContext.Provider>
</WorldContext.Provider>
);
}
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const KEY_CODES = {
ARROW_LEFT: "ArrowLeft",
ARROW_RIGHT: "ArrowRight",
ARROW_DOWN: "ArrowDown",
ENTER: "Enter",
};

export const DIRECTIONS: Direction[] = ["up", "down", "right", "left"];
Loading