From 54b9ec842a5289300aef571e73edc8b4d2b07c1e Mon Sep 17 00:00:00 2001
From: Ryan Lanny Jenkins
Date: Wed, 4 Jun 2025 21:59:31 -0500
Subject: [PATCH] Remove the action cooldown system, implement a stamina regen
system.
---
src/App.tsx | 49 ++++++------
src/components/ActionCooldown.tsx | 110 --------------------------
src/components/CustomTabs.tsx | 4 +-
src/components/EquipmentModal.tsx | 8 +-
src/components/Field.tsx | 4 +-
src/components/StaminaDisplay.tsx | 66 ++++++++++++++++
src/components/Temple.tsx | 6 +-
src/constants/index.ts | 6 +-
src/store/useGameStore.ts | 126 +++++++++++++-----------------
src/types/index.ts | 3 +-
10 files changed, 160 insertions(+), 222 deletions(-)
delete mode 100644 src/components/ActionCooldown.tsx
create mode 100644 src/components/StaminaDisplay.tsx
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