Skip to content
Draft
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
56 changes: 56 additions & 0 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/history": "1.161.4",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Factory/Sidebar/GameSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BOTTOM_FOOTER_HEIGHT, TOP_NAV_HEIGHT } from "@/utils/constants";

import Buildings from "./Buildings";
import GlobalResources from "./GlobalResources";
import Time from "./Time";
import { Time } from "./Time";

const MIN_WIDTH = 220;
const MAX_WIDTH = 400;
Expand Down
190 changes: 180 additions & 10 deletions src/components/Factory/Sidebar/Time.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,193 @@
import { Button } from "@/components/ui/button";
import { useEffect, useRef, useState } from "react";

import TooltipButton from "@/components/shared/Buttons/TooltipButton";
import { Icon } from "@/components/ui/icon";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Progress } from "@/components/ui/progress";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Text } from "@/components/ui/typography";
import { cn } from "@/lib/utils";

const TICK_INTERVAL = 40; // ~25fps
const MAX_PROGRESS = 1000;
const BASE_DAY_DURATION = 5000; // ms
const BASE_INCREMENT = MAX_PROGRESS / (BASE_DAY_DURATION / TICK_INTERVAL);
const DAY_TRANSITION_PAUSE = 200; // ms pause between days

const GAME_SPEED_MULTIPLIER = {
slow: 1,
medium: 2,
fast: 5,
};

type GameSpeed = keyof typeof GAME_SPEED_MULTIPLIER;

interface TimeProps {
day: number;
onAdvanceDay: () => void;
}

const Time = ({ day, onAdvanceDay }: TimeProps) => {
export const Time = ({ day, onAdvanceDay }: TimeProps) => {
const [progress, setProgress] = useState(0);
const [isPaused, setIsPaused] = useState(true);
const [gameSpeed, setGameSpeed] = useState<GameSpeed>("slow");
const [isTransitioning, setIsTransitioning] = useState(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);

const handleTogglePlay = () => {
setIsPaused((prev) => !prev);
};

const handleDaySkip = () => {
if (isTransitioning) return;

setIsTransitioning(true);
setProgress(0);

setTimeout(() => {
onAdvanceDay();
setIsTransitioning(false);
}, DAY_TRANSITION_PAUSE);
};

useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
if (event.code === "Space") {
event.preventDefault();
handleTogglePlay();
}

if (event.key === "1") {
setGameSpeed("slow");
} else if (event.key === "2") {
setGameSpeed("medium");
} else if (event.key === "3") {
setGameSpeed("fast");
}
};

window.addEventListener("keydown", handleKeyPress);
return () => window.removeEventListener("keydown", handleKeyPress);
}, []);

useEffect(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}

if (isPaused || isTransitioning) {
return;
}

intervalRef.current = setInterval(() => {
setProgress((prev) => {
const speedMultiplier = GAME_SPEED_MULTIPLIER[gameSpeed];
const increment = BASE_INCREMENT * speedMultiplier;
const newProgress = prev + increment;

if (newProgress >= MAX_PROGRESS) {
setIsTransitioning(true);

setProgress(0);

setTimeout(() => {
onAdvanceDay();
setIsTransitioning(false);
}, DAY_TRANSITION_PAUSE);

return 0;
}

return newProgress;
});
}, TICK_INTERVAL);

return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [isPaused, gameSpeed, onAdvanceDay, isTransitioning]);

const progressPercent = (progress / MAX_PROGRESS) * 100;

return (
<BlockStack gap="1">
<Text>Day {day}</Text>
<InlineStack className="px-2">
<Button onClick={onAdvanceDay} className="w-full" size="sm">
Next Day
</Button>
</InlineStack>
<BlockStack gap="2">
<div
className="w-full hover:bg-accent rounded-lg p-2 cursor-pointer"
onClick={handleDaySkip}
>
<Text weight="semibold" size="sm">
Day {day}
</Text>
<Progress value={progressPercent} className="h-2" />
</div>

<InlineStack gap="2" blockAlign="center" className="w-full px-2">
<TooltipButton
value="pause"
className={cn(
"h-8 w-8",
isPaused
? "hover:bg-accent hover:text-foreground hover:border hover:border-foreground cursor-pointer"
: "bg-accent hover:text-background border-foreground border text-foreground",
)}
onClick={handleTogglePlay}
tooltip={isPaused ? "Play (Space)" : "Pause (Space)"}
>
<Icon name="Pause" size="sm" />
</TooltipButton>

<ToggleGroup
type="single"
value={gameSpeed}
onValueChange={(value) => {
if (value) setGameSpeed(value as GameSpeed);
}}
>
<ToggleGroupItem
value="slow"
className={cn(
"h-8 w-8",
isPaused && gameSpeed === "slow" && "border border-foreground",
!isPaused &&
gameSpeed === "slow" &&
"bg-foreground! text-background!",
)}
>
<Icon name="Play" size="sm" />
</ToggleGroupItem>
<ToggleGroupItem
value="medium"
className={cn(
"h-8 w-8",
isPaused &&
gameSpeed === "medium" &&
"border border-foreground",
!isPaused &&
gameSpeed === "medium" &&
"bg-foreground! text-background!",
)}
>
<Icon name="FastForward" size="sm" />
</ToggleGroupItem>
<ToggleGroupItem
value="fast"
className={cn(
"h-8 w-8",
isPaused && gameSpeed === "fast" && "border border-foreground",
!isPaused &&
gameSpeed === "fast" &&
"bg-foreground! text-background!",
)}
>
<Icon name="SkipForward" size="sm" />
</ToggleGroupItem>
</ToggleGroup>
</InlineStack>
</BlockStack>
</BlockStack>
);
};

export default Time;
61 changes: 61 additions & 0 deletions src/components/ui/toggle-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client"

import * as React from "react"

Check warning on line 3 in src/components/ui/toggle-group.tsx

View workflow job for this annotation

GitHub Actions / Linting

Run autofix to sort these imports!

Check warning on line 3 in src/components/ui/toggle-group.tsx

View workflow job for this annotation

GitHub Actions / Linting

Run autofix to sort these imports!
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"

const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})

const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))

ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName

const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)

return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})

ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName

export { ToggleGroup, ToggleGroupItem }
43 changes: 43 additions & 0 deletions src/components/ui/toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from "react"

Check warning on line 1 in src/components/ui/toggle.tsx

View workflow job for this annotation

GitHub Actions / Linting

Run autofix to sort these imports!

Check warning on line 1 in src/components/ui/toggle.tsx

View workflow job for this annotation

GitHub Actions / Linting

Run autofix to sort these imports!
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)

const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))

Toggle.displayName = TogglePrimitive.Root.displayName

export { Toggle, toggleVariants }
Loading