diff --git a/src/App.tsx b/src/App.tsx index 76bcf90..d38abaf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,31 +1,32 @@ import React, { useState, useEffect } from 'react' +import { styled } from '@linaria/react' +import { useGameTick } from './hooks/useGameTick' +import Field from './components/Field' +import Warehouse from './components/Warehouse' +import Market from './components/Market' +import Temple from './components/Temple' +import { StaminaDisplay } from './components/StaminaDisplay' +import { useSaveSystem } from './store/useSaveSystem' +import { Console } from './components/Console' +import { GodModal } from './components/GodModal' import { Tabs, TabsContent, TabsList, TabsTrigger, } from './components/CustomTabs' -import { useGameTick } from './hooks/useGameTick' -import Field from './components/Field' -import Warehouse from './components/Warehouse' -import Market from './components/Market' -import Temple from './components/Temple' -import { ActionCooldown } from './components/ActionCooldown' -import { useSaveSystem } from './store/useSaveSystem' -import { Console } from './components/Console' -import { GodModal } from './components/GodModal' -const appContainerStyle: React.CSSProperties = { - maxWidth: '1200px', - margin: '0 auto', - padding: '2rem', -} +const AppContainer = styled.div` + max-width: 1200px; + margin: 0 auto; + padding: 4rem 2rem 2rem; +` -const tabsListStyles: React.CSSProperties = { - display: 'grid', - width: '100%', - gridTemplateColumns: 'repeat(4, 1fr)', -} +const StyledTabsList = styled(TabsList)` + display: grid; + width: 100%; + grid-template-columns: repeat(4, 1fr); +` function App() { useGameTick() @@ -57,15 +58,15 @@ function App() { return (
- -
+ + - + Fields (1) Warehouse (2) Market (3) Temple (4) - + @@ -80,7 +81,7 @@ function App() { -
+ {isGodModalOpen && setIsGodModalOpen(false)} />}
) diff --git a/src/components/ActionCooldown.tsx b/src/components/ActionCooldown.tsx deleted file mode 100644 index e25432c..0000000 --- a/src/components/ActionCooldown.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useEffect, useState } from 'react' -import { styled } from '@linaria/react' - -import { useGameStore } from '../store/useGameStore' - -const CooldownContainer = styled.div` - position: fixed; - top: 1rem; - left: 1rem; - width: 48px; - height: 48px; - display: flex; - align-items: center; - justify-content: center; -` - -const DialSVG = styled.svg` - width: 100%; - height: 100%; -` - -const DialBackground = styled.circle` - fill: #e5e7eb; -` - -const DialProgress = styled.path` - fill: #3b82f6; -` - -const calculatePieWedge = (progress: number, size: number) => { - // Convert progress to go counter-clockwise from top - const adjustedProgress = 100 - progress - const center = size / 2 - const radius = size / 2 - 1 - const angle = (adjustedProgress / 100) * 360 - - // Always start from top (90 degrees in SVG coordinates) - const startAngle = 90 - const endAngle = startAngle + angle - - // Convert angles to radians - const startAngleRad = (startAngle * Math.PI) / 180 - const endAngleRad = (endAngle * Math.PI) / 180 - - // Calculate points - const startX = center + radius * Math.cos(startAngleRad) - const startY = center - radius * Math.sin(startAngleRad) - const endX = center + radius * Math.cos(endAngleRad) - const endY = center - radius * Math.sin(endAngleRad) - - // Create the arc flag - const largeArcFlag = angle <= 180 ? '0' : '1' - - // Create the SVG path - moving counter-clockwise - return ` - M ${center},${center} - L ${startX},${startY} - A ${radius} ${radius} 0 ${largeArcFlag} 0 ${endX} ${endY} - L ${center},${center} - `.trim() -} - -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]) - - // Don't render if there's no active cooldown - if (actionCooldown <= 0) { - return null - } - - return ( - - - - - - - ) -} diff --git a/src/components/CustomTabs.tsx b/src/components/CustomTabs.tsx index d3c311b..7e85ae5 100644 --- a/src/components/CustomTabs.tsx +++ b/src/components/CustomTabs.tsx @@ -85,8 +85,8 @@ export const Tabs: React.FC = ({ value, onValueChange, children }) => ) } -export const TabsList: React.FC = ({ children, style }) => { - return
{children}
+export const TabsList: React.FC = ({ children, ...props }) => { + return
{children}
} export const TabsTrigger: React.FC = ({ diff --git a/src/components/EquipmentModal.tsx b/src/components/EquipmentModal.tsx index aedfc96..ada1bff 100644 --- a/src/components/EquipmentModal.tsx +++ b/src/components/EquipmentModal.tsx @@ -82,7 +82,7 @@ const RecipeDetails = styled.div` ` const EquipmentModal = ({ equipmentId, onClose }: EquipmentModalProps) => { - const { inventory, configureEquipment, useEquipment, actionCooldown, equipment } = useGameStore() + const { inventory, configureEquipment, useEquipment, stamina, equipment } = useGameStore() const equipmentInstance = equipment[equipmentId] const equipmentDef = EQUIPMENT[equipmentInstance.type] @@ -99,7 +99,7 @@ const EquipmentModal = ({ equipmentId, onClose }: EquipmentModalProps) => { const canUse = !equipmentInstance.isProcessing && - actionCooldown === 0 && + stamina >= 1 && selectedRecipe && (inventory[selectedRecipe.inputItem] || 0) >= selectedRecipe.inputAmount @@ -163,14 +163,14 @@ const EquipmentModal = ({ equipmentId, onClose }: EquipmentModalProps) => { Output: {selectedRecipe.outputAmount} {ITEMS[selectedRecipe.outputItem].name}

Processing time: {selectedRecipe.processTicks} ticks

-

Cooldown: {selectedRecipe.cooldownDuration / 1000} seconds

+

Stamina cost: 1

- {equipmentInstance.isProcessing ? 'Processing...' : 'Use'} + {equipmentInstance.isProcessing ? 'Processing...' : stamina < 1 ? 'No Stamina' : 'Use'} )} diff --git a/src/components/Field.tsx b/src/components/Field.tsx index 1ec2374..c96f4e2 100644 --- a/src/components/Field.tsx +++ b/src/components/Field.tsx @@ -187,7 +187,7 @@ const FieldComponent: React.FC = () => { harvest, remove, inventory, - actionCooldown, + stamina, } = useGameStore() const [selectedSeed, setSelectedSeed] = useState(null) @@ -199,7 +199,7 @@ const FieldComponent: React.FC = () => { } | null>(null) const handlePlotClick = (row: number, col: number) => { - if (actionCooldown > 0) return + if (stamina < 1) return switch (selectedTool) { case 'plant': diff --git a/src/components/StaminaDisplay.tsx b/src/components/StaminaDisplay.tsx new file mode 100644 index 0000000..ef52449 --- /dev/null +++ b/src/components/StaminaDisplay.tsx @@ -0,0 +1,66 @@ +import { styled } from '@linaria/react' +import { useGameStore } from '../store/useGameStore' + +const StaminaContainer = styled.div` + position: fixed; + top: 1rem; + left: 1rem; + width: 200px; + height: 24px; + display: flex; + justify-content: left; + background-color: #4b5563; /* Grey for depleted stamina */ + border: 3px solid #000; + border-radius: 4px; + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.2); + overflow: hidden; + z-index: 2; +` + +const StaminaFill = styled.div<{ width: number }>` + height: 100%; + width: ${props => props.width + '%'}; + background-color: #22c55e; + transition: width 0.2s linear; + position: relative; + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + to bottom, + rgba(255, 255, 255, 0.2) 0%, + rgba(255, 255, 255, 0) 50%, + rgba(0, 0, 0, 0.1) 100% + ); + } +` + +const StaminaText = styled.div` + position: absolute; + width: 100%; + text-align: center; + color: #fff; + font-weight: bold; + text-shadow: 1px 1px 0 #000; + font-size: 0.875rem; + z-index: 1; +` + +export const StaminaDisplay = () => { + const { stamina, maxStamina } = useGameStore() + const staminaPercentage = (stamina / maxStamina) * 100 + + return ( + + + + {stamina}/{maxStamina} + + + ) +} \ No newline at end of file diff --git a/src/components/Temple.tsx b/src/components/Temple.tsx index d53a419..10381f2 100644 --- a/src/components/Temple.tsx +++ b/src/components/Temple.tsx @@ -114,7 +114,7 @@ const TempleComponent: React.FC = () => { purchaseUpgrade, pray, piety, - actionCooldown, + stamina, } = useGameStore() const handlePurchase = (upgradeId: string) => { @@ -186,9 +186,9 @@ const TempleComponent: React.FC = () => { Piety Level: {piety} - pray()} disabled={actionCooldown > 0}> + pray()} disabled={stamina < 1}> 🙏 Pray to the Gods - {actionCooldown > 0 && ` (${(actionCooldown / 1000).toFixed(1)}s)`} + {stamina < 1 && ' (No Stamina)'} diff --git a/src/constants/index.ts b/src/constants/index.ts index 9231542..f4ad4ab 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -3,7 +3,7 @@ import { CropDefinitions, Upgrade, Equipment } from '../types' export const INITIAL_CASH = 50 export const INITIAL_FIELD_SIZE = 3 export const COOLDOWN_DURATION = 2000 // 2 seconds in milliseconds -export const TICK_INTERVAL = 12000 // 12 seconds in milliseconds +export const TICK_INTERVAL = 5000 export const GAME_SPEEDS = { NORMAL: 1, FAST: 5, @@ -68,7 +68,7 @@ export const UPGRADES: Record = { aqueous_vigor_1: { id: 'aqueous_vigor_1', name: 'Aqueous Vigor I', - description: 'Reduces watering cooldown by 25%', + description: 'Reduces watering stamina cost by 25%', cost: [ { itemId: 'celery', @@ -80,7 +80,7 @@ export const UPGRADES: Record = { id: 'aqueous_vigor_2', name: 'Aqueous Vigor II', description: - 'Reduces watering cooldown by an additional 25% (requires Aqueous Vigor I)', + 'Reduces watering stamina cost by an additional 25% (requires Aqueous Vigor I)', cost: [ { itemId: 'celery', diff --git a/src/store/useGameStore.ts b/src/store/useGameStore.ts index d038219..9a1c0d6 100644 --- a/src/store/useGameStore.ts +++ b/src/store/useGameStore.ts @@ -7,7 +7,6 @@ import { INITIAL_INVENTORY, CROPS, INITIAL_GAME_SPEED, - COOLDOWN_DURATION, ITEMS, UPGRADES, EQUIPMENT, @@ -29,27 +28,6 @@ const initializeField = (size: number): PlotState[][] => { const SAVE_SLOT_PREFIX = 'dionysian_idle_save_' -const saveGame = (slot: number, state: GameState) => { - try { - const saveData = JSON.stringify(state) - localStorage.setItem(`${SAVE_SLOT_PREFIX}${slot}`, saveData) - return true - } catch (error) { - console.error('Failed to save game:', error) - return false - } -} - -const loadGame = (slot: number): GameState | null => { - try { - const saveData = localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`) - if (!saveData) return null - return JSON.parse(saveData) - } catch (error) { - console.error('Failed to load game:', error) - return null - } -} export const hasSaveInSlot = (slot: number): boolean => { return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`) @@ -105,7 +83,6 @@ export const useGameStore = create< tick: () => void upgradeField: () => void setGameSpeed: (speed: number) => void - setActionCooldown: (cooldown: number) => void buyItem: (itemId: string) => void sellItem: (itemId: string) => void purchaseUpgrade: (upgradeId: string) => void @@ -130,7 +107,8 @@ export const useGameStore = create< fieldUpgradeCosts: FIELD_UPGRADE_COSTS, plots: initializeField(INITIAL_FIELD_SIZE), gameSpeed: INITIAL_GAME_SPEED, - actionCooldown: 0, + stamina: 10, + maxStamina: 10, tickCount: 0, consoleMessages: [], purchasedUpgrades: [], @@ -140,9 +118,9 @@ export const useGameStore = create< milestones: {}, plant: (row, col, cropId) => { - const { plots, inventory, actionCooldown } = get() + const { plots, inventory, stamina } = get() - if (actionCooldown > 0) { + if (stamina < 1) { return } @@ -160,36 +138,36 @@ export const useGameStore = create< } state.inventory[seedId] = state.inventory[seedId] - 1 - state.actionCooldown = COOLDOWN_DURATION + state.stamina = Math.max(0, state.stamina - 1) }), ) } }, water: (row, col) => { - const { plots, actionCooldown, purchasedUpgrades } = get() - - if (actionCooldown > 0) { - return - } + const { plots, stamina, purchasedUpgrades } = get() const plot = plots[row][col] if (plot.current && plot.moisture < 1) { - // Calculate cooldown reduction from upgrades (multiplicative) - let cooldownMultiplier = 1 + // Calculate stamina cost reduction from upgrades (multiplicative) + let staminaMultiplier = 1 if (purchasedUpgrades.includes('aqueous_vigor_1')) { - cooldownMultiplier *= 0.75 // 25% reduction + staminaMultiplier *= 0.75 // 25% reduction } if (purchasedUpgrades.includes('aqueous_vigor_2')) { - cooldownMultiplier *= 0.75 // Additional 25% reduction + staminaMultiplier *= 0.75 // Additional 25% reduction } - const finalCooldown = Math.max(0, COOLDOWN_DURATION * cooldownMultiplier) + const staminaCost = Math.max(1, Math.floor(staminaMultiplier)) + + if (stamina < staminaCost) { + return + } set( produce((state) => { state.plots[row][col].moisture = 1 - state.actionCooldown = finalCooldown + state.stamina = Math.max(0, state.stamina - staminaCost) // Handle water diffusion to adjacent plots const diffusionAmount = purchasedUpgrades.includes('aqua_diffundere_2') @@ -199,26 +177,24 @@ export const useGameStore = create< : 0 if (diffusionAmount > 0) { - // Check and water adjacent plots (up, right, down, left) - const adjacentPositions = [ - [row - 1, col], // up - [row, col + 1], // right - [row + 1, col], // down - [row, col - 1], // left + // Apply diffusion to adjacent plots + const adjacentCoords = [ + [row - 1, col], + [row + 1, col], + [row, col - 1], + [row, col + 1], ] - adjacentPositions.forEach(([adjRow, adjCol]) => { - // Check if the adjacent position is within bounds + adjacentCoords.forEach(([r, c]) => { if ( - adjRow >= 0 && - adjRow < state.plots.length && - adjCol >= 0 && - adjCol < state.plots[0].length + r >= 0 && + r < state.plots.length && + c >= 0 && + c < state.plots[0].length ) { - const adjPlot = state.plots[adjRow][adjCol] - state.plots[adjRow][adjCol].moisture = Math.min( + state.plots[r][c].moisture = Math.min( 1, - adjPlot.moisture + diffusionAmount + state.plots[r][c].moisture + diffusionAmount, ) } }) @@ -229,9 +205,9 @@ export const useGameStore = create< }, harvest: (row, col) => { - const { plots, actionCooldown } = get() + const { plots, stamina } = get() - if (actionCooldown > 0) { + if (stamina < 1) { return } @@ -261,7 +237,7 @@ export const useGameStore = create< 0, state.plots[row][col].fertility - crop.fertilityDepletion, ) - state.actionCooldown = COOLDOWN_DURATION + state.stamina = Math.max(0, state.stamina - 1) // Track milestone for crop harvest const milestoneKey = `crops_harvested_${crop.id}` @@ -272,9 +248,9 @@ export const useGameStore = create< }, remove: (row, col) => { - const { plots, actionCooldown } = get() + const { plots, stamina } = get() - if (actionCooldown > 0) { + if (stamina < 1) { return } @@ -283,7 +259,7 @@ export const useGameStore = create< set( produce((state) => { state.plots[row][col].current = undefined - state.actionCooldown = COOLDOWN_DURATION + state.stamina = Math.max(0, state.stamina - 1) }), ) } @@ -292,6 +268,18 @@ export const useGameStore = create< tick: () => { set( produce((state) => { + // Regenerate stamina + if (state.stamina < state.maxStamina) { + const oldStamina = state.stamina + state.stamina = Math.min(state.maxStamina, state.stamina + 1) + + // Increment milestone counter for stamina gained + if (state.stamina > oldStamina) { + const staminaGained = state.stamina - oldStamina + state.milestones.stamina_gained = (state.milestones.stamina_gained || 0) + staminaGained + } + } + // Update plots state.plots.forEach((row: PlotState[], rowIndex: number) => { row.forEach((plot: PlotState, colIndex: number) => { @@ -412,14 +400,6 @@ export const useGameStore = create< ) }, - setActionCooldown: (cooldown) => { - set( - produce((state) => { - state.actionCooldown = cooldown - }), - ) - }, - buyItem: (itemId) => { const { cash, landPurchases } = get() const item = ITEMS[itemId] @@ -529,9 +509,9 @@ export const useGameStore = create< }, pray: () => { - const { actionCooldown, piety } = get() + const { stamina, piety } = get() - if (actionCooldown > 0) { + if (stamina < 1) { return } @@ -562,7 +542,7 @@ export const useGameStore = create< } } - state.actionCooldown = 3000 + state.stamina = Math.max(0, state.stamina - 1) }), ) }, @@ -614,13 +594,13 @@ export const useGameStore = create< }, useEquipment: (equipmentId) => { - const { equipment, inventory, actionCooldown } = get() + const { equipment, inventory, stamina } = get() const equipmentInstance = equipment[equipmentId] if ( !equipmentInstance || equipmentInstance.isProcessing || - actionCooldown > 0 || + stamina < 1 || !equipmentInstance.selectedRecipeId ) { return @@ -645,7 +625,7 @@ export const useGameStore = create< // Start processing state.equipment[equipmentId].isProcessing = true state.equipment[equipmentId].progress = 0 - state.actionCooldown = recipe.cooldownDuration + state.stamina = Math.max(0, state.stamina - 1) }), ) }, diff --git a/src/types/index.ts b/src/types/index.ts index 1cf31dd..5841773 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -85,7 +85,8 @@ export interface GameState { fieldUpgradeCosts: number[] plots: PlotState[][] gameSpeed: number - actionCooldown: number + stamina: number + maxStamina: number consoleMessages: ConsoleMessage[] purchasedUpgrades: string[] piety: number