Get Linaria actually working, add global cooldown for actions.

This commit is contained in:
Ryan Lanny Jenkins 2025-05-18 04:57:38 -05:00
parent 974d851767
commit 6408fcd199
9 changed files with 210 additions and 117 deletions

71
package-lock.json generated
View file

@ -1,11 +1,11 @@
{
"name": "idle-farm-game",
"name": "dionysian-idle",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "idle-farm-game",
"name": "dionysian-idle",
"version": "0.0.0",
"dependencies": {
"@hookform/resolvers": "^5.0.1",
@ -70,6 +70,7 @@
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"@wyw-in-js/vite": "^0.6.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.15.0",
"eslint-plugin-react-hooks": "^5.0.0",
@ -4863,6 +4864,72 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@wyw-in-js/transform": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@wyw-in-js/transform/-/transform-0.6.0.tgz",
"integrity": "sha512-/Rxw8e3KcmSjnqWs/6flmpcNTa4jLw8NViIyNwqem498AfSWXrPnYf3RyjWed9zDVL72q7Xjei6TcRqMupl3Eg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.23.5",
"@babel/generator": "^7.23.5",
"@babel/helper-module-imports": "^7.22.15",
"@babel/plugin-transform-modules-commonjs": "^7.23.3",
"@babel/template": "^7.22.15",
"@babel/traverse": "^7.23.5",
"@babel/types": "^7.23.5",
"@wyw-in-js/processor-utils": "0.6.0",
"@wyw-in-js/shared": "0.6.0",
"babel-merge": "^3.0.0",
"cosmiconfig": "^8.0.0",
"happy-dom": "^15.11.0",
"source-map": "^0.7.4",
"stylis": "^4.3.0",
"ts-invariant": "^0.10.3"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@wyw-in-js/transform/node_modules/happy-dom": {
"version": "15.11.7",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.11.7.tgz",
"integrity": "sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"entities": "^4.5.0",
"webidl-conversions": "^7.0.0",
"whatwg-mimetype": "^3.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@wyw-in-js/transform/node_modules/stylis": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@wyw-in-js/vite": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@wyw-in-js/vite/-/vite-0.6.0.tgz",
"integrity": "sha512-nGeH/dFrjecHRJfxd5/71cJu4V2YWakUmnEoAUpS0iTnVVC4GWjKqt1dLtsJvtKZIm2OeNoTzzdFrP4bxyXYxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@wyw-in-js/shared": "0.6.0",
"@wyw-in-js/transform": "0.6.0"
},
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"vite": ">=3.2.7"
}
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",

View file

@ -72,6 +72,7 @@
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"@wyw-in-js/vite": "^0.6.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.15.0",
"eslint-plugin-react-hooks": "^5.0.0",

View file

@ -3,6 +3,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/CustomTab
import { useGameTick } from './hooks/useGameTick';
import Field from './components/Field';
import Warehouse from './components/Warehouse';
import { ActionCooldown } from './components/ActionCooldown';
const appContainerStyle: React.CSSProperties = {
maxWidth: '1200px',
@ -10,21 +11,6 @@ const appContainerStyle: React.CSSProperties = {
padding: '2rem'
};
const headerStyle: React.CSSProperties = {
textAlign: 'center',
marginBottom: '2rem'
};
const titleStyle: React.CSSProperties = {
fontSize: '2.25rem',
fontWeight: 'bold',
marginBottom: '0.5rem'
};
const subtitleStyle: React.CSSProperties = {
color: '#6b7280'
};
const tabsListStyles: React.CSSProperties = {
display: 'grid',
width: '100%',
@ -35,26 +21,24 @@ function App() {
useGameTick();
return (
<div style={appContainerStyle}>
<header style={headerStyle}>
<h1 style={titleStyle}>Idle Farm Game</h1>
<p style={subtitleStyle}>Grow crops, harvest, and expand your farm!</p>
</header>
<Tabs defaultValue="fields">
<TabsList style={tabsListStyles}>
<TabsTrigger value="fields">Fields</TabsTrigger>
<TabsTrigger value="warehouse">Warehouse</TabsTrigger>
<TabsTrigger value="market" disabled>Market</TabsTrigger>
<TabsTrigger value="temple" disabled>Temple</TabsTrigger>
</TabsList>
<TabsContent value="fields">
<Field />
</TabsContent>
<TabsContent value="warehouse">
<Warehouse />
</TabsContent>
</Tabs>
<div>
<ActionCooldown />
<div style={appContainerStyle}>
<Tabs defaultValue="fields">
<TabsList style={tabsListStyles}>
<TabsTrigger value="fields">Fields</TabsTrigger>
<TabsTrigger value="warehouse">Warehouse</TabsTrigger>
<TabsTrigger value="market" disabled>Market</TabsTrigger>
<TabsTrigger value="temple" disabled>Temple</TabsTrigger>
</TabsList>
<TabsContent value="fields">
<Field />
</TabsContent>
<TabsContent value="warehouse">
<Warehouse />
</TabsContent>
</Tabs>
</div>
</div>
);
}

View file

@ -0,0 +1,59 @@
import { useEffect, useState } from 'react';
import { styled } from '@linaria/react';
import { useGameStore } from '../store/useGameStore';
const CooldownContainer = styled.div`
position: relative;
height: 1rem;
width: 100%;
background-color: #e5e7eb;
overflow: hidden;
`;
const ProgressBar = styled.div<{ progress: string }>`
height: 100%;
background-color: #3b82f6;
width: ${props => props.progress};
`;
export const ActionCooldown = () => {
const { actionCooldown, setActionCooldown } = useGameStore();
const [progress, setProgress] = useState(0);
useEffect(() => {
if (actionCooldown <= 0) {
setProgress(0);
return;
}
const startTime = Date.now();
const duration = actionCooldown;
const updateProgress = () => {
const elapsed = Date.now() - startTime;
const newProgress = Math.min(100, (elapsed / duration) * 100);
if (newProgress >= 100) {
setProgress(0);
setActionCooldown(0);
return;
}
setProgress(newProgress);
requestAnimationFrame(updateProgress);
};
const animationFrame = requestAnimationFrame(updateProgress);
return () => {
cancelAnimationFrame(animationFrame);
};
}, [actionCooldown, setActionCooldown]);
return (
<CooldownContainer>
<ProgressBar progress={`${progress}%`} />
</CooldownContainer>
);
};

View file

@ -148,32 +148,12 @@ const FieldComponent: React.FC = () => {
water,
harvest,
assignCrop,
lastPlantTime,
lastWaterTime,
lastHarvestTime,
gameSpeed,
setGameSpeed
setGameSpeed,
actionCooldown
} = useGameStore();
const [selectedCrop, setSelectedCrop] = useState<string | null>(null);
const [cooldowns, setCooldowns] = useState({
plant: false,
water: false,
harvest: false
});
useEffect(() => {
const interval = setInterval(() => {
const now = Date.now();
setCooldowns({
plant: now - lastPlantTime < 2000,
water: now - lastWaterTime < 2000,
harvest: now - lastHarvestTime < 2000
});
}, 100);
return () => clearInterval(interval);
}, [lastPlantTime, lastWaterTime, lastHarvestTime]);
const handlePlotClick = (row: number, col: number) => {
if (selectedCrop) {
@ -187,6 +167,8 @@ const FieldComponent: React.FC = () => {
return "#92400e";
};
const availableSpeeds = [1, 2, 4, 8, 16, 32, 64]
return (
<div style={fieldContainerStyle}>
<h2 style={titleStyle}>Fields</h2>
@ -194,24 +176,15 @@ const FieldComponent: React.FC = () => {
<div style={speedSelectorContainerStyle}>
<p style={speedSelectorLabelStyle}>Game Speed:</p>
<div style={speedButtonsContainerStyle}>
<button
style={getSpeedButtonStyle(gameSpeed === 1)}
onClick={() => setGameSpeed(1)}
>
1x
</button>
<button
style={getSpeedButtonStyle(gameSpeed === 5)}
onClick={() => setGameSpeed(5)}
>
5x
</button>
<button
style={getSpeedButtonStyle(gameSpeed === 10)}
onClick={() => setGameSpeed(10)}
>
10x
</button>
{availableSpeeds.map(speed => (
<button
style={getSpeedButtonStyle(gameSpeed === speed)}
onClick={() => setGameSpeed(speed)}
key={speed}
>
{speed}x
</button>
))}
</div>
</div>
@ -230,23 +203,23 @@ const FieldComponent: React.FC = () => {
<div style={actionsContainerStyle}>
<button
style={getActionButtonStyle(cooldowns.plant)}
style={getActionButtonStyle(actionCooldown > 0)}
onClick={plant}
disabled={cooldowns.plant}
disabled={actionCooldown > 0}
>
Plant Crop
</button>
<button
style={getActionButtonStyle(cooldowns.water)}
style={getActionButtonStyle(actionCooldown > 0)}
onClick={water}
disabled={cooldowns.water}
disabled={actionCooldown > 0}
>
Water Crop
</button>
<button
style={getActionButtonStyle(cooldowns.harvest)}
style={getActionButtonStyle(actionCooldown > 0)}
onClick={harvest}
disabled={cooldowns.harvest}
disabled={actionCooldown > 0}
>
Harvest Crop
</button>

View file

@ -1,26 +1,26 @@
import { useEffect, useRef } from 'react';
import { useGameStore } from '../store/useGameStore';
import { TICK_INTERVAL } from '../constants';
import { useEffect, useRef } from 'react'
import { useGameStore } from '../store/useGameStore'
import { TICK_INTERVAL } from '../constants'
export const useGameTick = () => {
const tick = useGameStore(state => state.tick);
const gameSpeed = useGameStore(state => state.gameSpeed);
const intervalRef = useRef<number | null>(null);
const tick = useGameStore(state => state.tick)
const gameSpeed = useGameStore(state => state.gameSpeed)
const intervalRef = useRef<number | null>(null)
useEffect(() => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
clearInterval(intervalRef.current)
}
const adjustedInterval = TICK_INTERVAL / gameSpeed;
intervalRef.current = window.setInterval(() => {
tick();
}, adjustedInterval);
tick()
}, adjustedInterval)
return () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
clearInterval(intervalRef.current)
}
};
}, [tick, gameSpeed]); // Add gameSpeed as dependency
}
}, [tick, gameSpeed])
};

View file

@ -6,7 +6,8 @@ import {
FIELD_UPGRADE_COSTS,
INITIAL_INVENTORY,
CROPS,
INITIAL_GAME_SPEED
INITIAL_GAME_SPEED,
COOLDOWN_DURATION
} from '../constants';
import { GameState, PlotState } from '../types';
@ -26,6 +27,7 @@ export const useGameStore = create<GameState & {
assignCrop: (row: number, col: number, cropId: string) => void;
upgradeField: () => void;
setGameSpeed: (speed: number) => void;
setActionCooldown: (cooldown: number) => void;
}>((set, get) => ({
cash: INITIAL_CASH,
inventory: INITIAL_INVENTORY,
@ -33,10 +35,8 @@ export const useGameStore = create<GameState & {
maxFieldSize: INITIAL_FIELD_SIZE + FIELD_UPGRADE_COSTS.length,
fieldUpgradeCosts: FIELD_UPGRADE_COSTS,
plots: initializeField(INITIAL_FIELD_SIZE),
lastPlantTime: 0,
lastWaterTime: 0,
lastHarvestTime: 0,
gameSpeed: INITIAL_GAME_SPEED,
actionCooldown: 0,
assignCrop: (row, col, cropId) => {
set(produce(state => {
@ -45,10 +45,9 @@ export const useGameStore = create<GameState & {
},
plant: () => {
const now = Date.now();
const { lastPlantTime, plots, inventory } = get();
const { plots, inventory, actionCooldown } = get();
if (now - lastPlantTime < 2000) {
if (actionCooldown > 0) {
return;
}
@ -68,7 +67,7 @@ export const useGameStore = create<GameState & {
};
state.inventory[seedId] = state.inventory[seedId] - 1;
state.lastPlantTime = now;
state.actionCooldown = COOLDOWN_DURATION;
}));
return;
}
@ -78,10 +77,9 @@ export const useGameStore = create<GameState & {
},
water: () => {
const now = Date.now();
const { lastWaterTime, plots } = get();
const { plots, actionCooldown } = get();
if (now - lastWaterTime < 2000) {
if (actionCooldown > 0) {
return;
}
@ -102,16 +100,15 @@ export const useGameStore = create<GameState & {
if (driestRow >= 0 && driestCol >= 0) {
set(produce(state => {
state.plots[driestRow][driestCol].moisture = 1;
state.lastWaterTime = now;
state.actionCooldown = COOLDOWN_DURATION;
}));
}
},
harvest: () => {
const now = Date.now();
const { lastHarvestTime, plots } = get();
const { plots, actionCooldown } = get();
if (now - lastHarvestTime < 2000) {
if (actionCooldown > 0) {
return;
}
@ -127,7 +124,7 @@ export const useGameStore = create<GameState & {
set(produce(state => {
state.plots[row][col].current = undefined;
state.inventory[yieldItem] = (state.inventory[yieldItem] || 0) + yieldAmount;
state.lastHarvestTime = now;
state.actionCooldown = COOLDOWN_DURATION;
}));
return;
}
@ -136,7 +133,15 @@ export const useGameStore = create<GameState & {
},
tick: () => {
const { actionCooldown } = get();
set(produce(state => {
// Update cooldown
if (state.actionCooldown > 0) {
state.actionCooldown = Math.max(0, state.actionCooldown - (COOLDOWN_DURATION / 20));
}
// Update plots
state.plots.forEach((row: PlotState[], rowIndex: number) => {
row.forEach((plot: PlotState, colIndex: number) => {
if (!plot.current || plot.current.mature) {
@ -184,5 +189,11 @@ export const useGameStore = create<GameState & {
set(produce(state => {
state.gameSpeed = speed;
}));
},
setActionCooldown: (cooldown) => {
set(produce(state => {
state.actionCooldown = cooldown;
}));
}
}));

View file

@ -33,8 +33,6 @@ export interface GameState {
maxFieldSize: number;
fieldUpgradeCosts: number[];
plots: PlotState[][];
lastPlantTime: number;
lastWaterTime: number;
lastHarvestTime: number;
gameSpeed: number;
actionCooldown: number;
}

View file

@ -1,12 +1,12 @@
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
import linaria from "@linaria/vite"
import react from "@vitejs/plugin-react"
import wyw from '@wyw-in-js/vite'
export default defineConfig({
plugins: [
react(),
linaria({
wyw({
include: ['**/*.{ts,tsx}'],
babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],