Compare commits

..

5 commits

Author SHA1 Message Date
Ryan Lanny Jenkins 7c015787dd Add favico that isn't vite. 2025-06-04 22:10:34 -05:00
Ryan Lanny Jenkins 54b9ec842a Remove the action cooldown system, implement a stamina regen system. 2025-06-04 21:59:31 -05:00
Ryan Lanny Jenkins d5b950b541 Got rid of the mark/plant distinction, player can plant directly. 2025-06-04 20:54:09 -05:00
Ryan Lanny Jenkins b2820bbb47 Allow pressing 1-4 to select a tab. 2025-06-04 20:44:28 -05:00
Ryan Lanny Jenkins 26f4a6120b Move save states to a browser console interface 2025-06-04 20:40:05 -05:00
21 changed files with 734 additions and 749 deletions

View file

@ -2,7 +2,21 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<!-- Android Chrome Icons -->
<link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="/android-chrome-512x512.png" />
<!-- Web Manifest -->
<link rel="manifest" href="/site.webmanifest" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dionysian Idle</title> <title>Dionysian Idle</title>
</head> </head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
public/site.webmanifest Normal file
View file

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,42 +1,55 @@
import React, { useState, useEffect } from 'react' 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 { import {
Tabs, Tabs,
TabsContent, TabsContent,
TabsList, TabsList,
TabsTrigger, TabsTrigger,
} from './components/CustomTabs' } 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 = { const AppContainer = styled.div`
maxWidth: '1200px', max-width: 1200px;
margin: '0 auto', margin: 0 auto;
padding: '2rem', padding: 4rem 2rem 2rem;
} `
const tabsListStyles: React.CSSProperties = { const StyledTabsList = styled(TabsList)`
display: 'grid', display: grid;
width: '100%', width: 100%;
gridTemplateColumns: 'repeat(4, 1fr)', grid-template-columns: repeat(4, 1fr);
} `
function App() { function App() {
useGameTick() useGameTick()
useSaveSystem() useSaveSystem()
const [isGodModalOpen, setIsGodModalOpen] = useState(false) const [isGodModalOpen, setIsGodModalOpen] = useState(false)
const [activeTab, setActiveTab] = useState('fields')
useEffect(() => { useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => { const handleKeyPress = (event: KeyboardEvent) => {
if (event.key.toLowerCase() === 'g') { if (event.key.toLowerCase() === 'g') {
setIsGodModalOpen(prev => !prev) setIsGodModalOpen(prev => !prev)
} }
// Handle number keys 1-4 for tab selection
const tabMap: { [key: string]: string } = {
'1': 'fields',
'2': 'warehouse',
'3': 'market',
'4': 'temple'
}
const tabValue = tabMap[event.key]
if (tabValue) {
setActiveTab(tabValue)
}
} }
window.addEventListener('keydown', handleKeyPress) window.addEventListener('keydown', handleKeyPress)
@ -45,15 +58,15 @@ function App() {
return ( return (
<div> <div>
<ActionCooldown /> <StaminaDisplay />
<div style={appContainerStyle}> <AppContainer>
<Tabs defaultValue="fields"> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList style={tabsListStyles}> <StyledTabsList>
<TabsTrigger value="fields">Fields</TabsTrigger> <TabsTrigger value="fields">Fields (1)</TabsTrigger>
<TabsTrigger value="warehouse">Warehouse</TabsTrigger> <TabsTrigger value="warehouse">Warehouse (2)</TabsTrigger>
<TabsTrigger value="market">Market</TabsTrigger> <TabsTrigger value="market">Market (3)</TabsTrigger>
<TabsTrigger value="temple">Temple</TabsTrigger> <TabsTrigger value="temple">Temple (4)</TabsTrigger>
</TabsList> </StyledTabsList>
<TabsContent value="fields"> <TabsContent value="fields">
<Field /> <Field />
</TabsContent> </TabsContent>
@ -68,7 +81,7 @@ function App() {
</TabsContent> </TabsContent>
</Tabs> </Tabs>
<Console /> <Console />
</div> </AppContainer>
{isGodModalOpen && <GodModal onClose={() => setIsGodModalOpen(false)} />} {isGodModalOpen && <GodModal onClose={() => setIsGodModalOpen(false)} />}
</div> </div>
) )

View file

@ -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 (
<CooldownContainer>
<DialSVG viewBox="0 0 32 32">
<DialBackground cx="16" cy="16" r="15" />
<DialProgress d={calculatePieWedge(progress, 32)} />
</DialSVG>
</CooldownContainer>
)
}

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react' import React from 'react'
export interface TabProps { export interface TabProps {
value: string value: string
@ -6,7 +6,8 @@ export interface TabProps {
} }
export interface TabsProps { export interface TabsProps {
defaultValue: string value: string
onValueChange: (value: string) => void
children: React.ReactNode children: React.ReactNode
} }
@ -76,18 +77,16 @@ const TabsContext = React.createContext<{
setValue: () => {}, setValue: () => {},
}) })
export const Tabs: React.FC<TabsProps> = ({ defaultValue, children }) => { export const Tabs: React.FC<TabsProps> = ({ value, onValueChange, children }) => {
const [value, setValue] = useState(defaultValue)
return ( return (
<TabsContext.Provider value={{ value, setValue }}> <TabsContext.Provider value={{ value, setValue: onValueChange }}>
<div style={tabsContainerStyle}>{children}</div> <div style={tabsContainerStyle}>{children}</div>
</TabsContext.Provider> </TabsContext.Provider>
) )
} }
export const TabsList: React.FC<TabsListProps> = ({ children, style }) => { export const TabsList: React.FC<TabsListProps> = ({ children, ...props }) => {
return <div style={{ ...tabsListContainerStyle, ...style }}>{children}</div> return <div style={{ ...tabsListContainerStyle }} {...props}>{children}</div>
} }
export const TabsTrigger: React.FC<TabsTriggerProps> = ({ export const TabsTrigger: React.FC<TabsTriggerProps> = ({

View file

@ -82,7 +82,7 @@ const RecipeDetails = styled.div`
` `
const EquipmentModal = ({ equipmentId, onClose }: EquipmentModalProps) => { const EquipmentModal = ({ equipmentId, onClose }: EquipmentModalProps) => {
const { inventory, configureEquipment, useEquipment, actionCooldown, equipment } = useGameStore() const { inventory, configureEquipment, useEquipment, stamina, equipment } = useGameStore()
const equipmentInstance = equipment[equipmentId] const equipmentInstance = equipment[equipmentId]
const equipmentDef = EQUIPMENT[equipmentInstance.type] const equipmentDef = EQUIPMENT[equipmentInstance.type]
@ -99,7 +99,7 @@ const EquipmentModal = ({ equipmentId, onClose }: EquipmentModalProps) => {
const canUse = const canUse =
!equipmentInstance.isProcessing && !equipmentInstance.isProcessing &&
actionCooldown === 0 && stamina >= 1 &&
selectedRecipe && selectedRecipe &&
(inventory[selectedRecipe.inputItem] || 0) >= selectedRecipe.inputAmount (inventory[selectedRecipe.inputItem] || 0) >= selectedRecipe.inputAmount
@ -163,14 +163,14 @@ const EquipmentModal = ({ equipmentId, onClose }: EquipmentModalProps) => {
Output: {selectedRecipe.outputAmount} {ITEMS[selectedRecipe.outputItem].name} Output: {selectedRecipe.outputAmount} {ITEMS[selectedRecipe.outputItem].name}
</p> </p>
<p>Processing time: {selectedRecipe.processTicks} ticks</p> <p>Processing time: {selectedRecipe.processTicks} ticks</p>
<p>Cooldown: {selectedRecipe.cooldownDuration / 1000} seconds</p> <p>Stamina cost: 1</p>
</Section> </Section>
<ActionButton <ActionButton
disabled={!canUse} disabled={!canUse}
onClick={handleUse} onClick={handleUse}
> >
{equipmentInstance.isProcessing ? 'Processing...' : 'Use'} {equipmentInstance.isProcessing ? 'Processing...' : stamina < 1 ? 'No Stamina' : 'Use'}
</ActionButton> </ActionButton>
</> </>
)} )}

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { useGameStore } from '../store/useGameStore' import { useGameStore } from '../store/useGameStore'
import { CROPS } from '../constants' import { CROPS, ITEMS } from '../constants'
import { FieldTool, PlotState } from '../types' import { FieldTool, PlotState } from '../types'
import Modal from './Modal' import Modal from './Modal'
@ -154,10 +154,6 @@ const PlotInfoModal: React.FC<PlotInfoModalProps> = ({ plot, onClose }) => {
: 'None'} : 'None'}
</span> </span>
</div> </div>
<div style={plotInfoItemStyle}>
<span>Intended Crop:</span>
<span>{plot.intended ? CROPS[plot.intended].name : 'None'}</span>
</div>
<div style={plotInfoItemStyle}> <div style={plotInfoItemStyle}>
<span>Water Level:</span> <span>Water Level:</span>
<span>{formatPercentage(plot.moisture)}</span> <span>{formatPercentage(plot.moisture)}</span>
@ -190,12 +186,12 @@ const FieldComponent: React.FC = () => {
water, water,
harvest, harvest,
remove, remove,
assignCrop, inventory,
actionCooldown, stamina,
} = useGameStore() } = useGameStore()
const [selectedCrop, setSelectedCrop] = useState<string | null>(null) const [selectedSeed, setSelectedSeed] = useState<string | null>(null)
const [selectedTool, setSelectedTool] = useState<FieldTool>('mark') const [selectedTool, setSelectedTool] = useState<FieldTool>('plant')
const [inspectedPlot, setInspectedPlot] = useState<{ const [inspectedPlot, setInspectedPlot] = useState<{
plot: PlotState plot: PlotState
row: number row: number
@ -203,16 +199,16 @@ const FieldComponent: React.FC = () => {
} | null>(null) } | null>(null)
const handlePlotClick = (row: number, col: number) => { const handlePlotClick = (row: number, col: number) => {
if (actionCooldown > 0) return if (stamina < 1) return
switch (selectedTool) { switch (selectedTool) {
case 'mark':
if (selectedCrop) {
assignCrop(row, col, selectedCrop)
}
break
case 'plant': case 'plant':
plant(row, col) if (selectedSeed) {
const crop = Object.values(CROPS).find(c => c.seedType === selectedSeed)
if (crop && inventory[selectedSeed] > 0) {
plant(row, col, crop.id)
}
}
break break
case 'water': case 'water':
water(row, col) water(row, col)
@ -246,7 +242,6 @@ const FieldComponent: React.FC = () => {
} }
const tools: { id: FieldTool; label: string; icon: string }[] = [ const tools: { id: FieldTool; label: string; icon: string }[] = [
{ id: 'mark', label: 'Mark', icon: '🎯' },
{ id: 'plant', label: 'Plant', icon: '🌱' }, { id: 'plant', label: 'Plant', icon: '🌱' },
{ id: 'water', label: 'Water', icon: '💧' }, { id: 'water', label: 'Water', icon: '💧' },
{ id: 'harvest', label: 'Harvest', icon: '✂️' }, { id: 'harvest', label: 'Harvest', icon: '✂️' },
@ -254,6 +249,14 @@ const FieldComponent: React.FC = () => {
{ id: 'inspect', label: 'Inspect', icon: '🔍' }, { id: 'inspect', label: 'Inspect', icon: '🔍' },
] ]
// Get available seeds from inventory
const availableSeeds = Object.entries(inventory)
.filter(([itemId, count]) => {
const crop = Object.values(CROPS).find(c => c.seedType === itemId)
return crop && count > 0
})
.map(([itemId]) => itemId)
return ( return (
<div style={fieldContainerStyle}> <div style={fieldContainerStyle}>
<h2 style={titleStyle}>Fields</h2> <h2 style={titleStyle}>Fields</h2>
@ -271,18 +274,28 @@ const FieldComponent: React.FC = () => {
))} ))}
</div> </div>
{selectedTool === 'mark' && ( {selectedTool === 'plant' && (
<div style={cropSelectionContainerStyle}> <div style={cropSelectionContainerStyle}>
<p style={cropSelectionLabelStyle}>Select a crop to mark:</p> <p style={cropSelectionLabelStyle}>Select a seed to plant:</p>
{Object.values(CROPS).map((crop) => ( {availableSeeds.length > 0 ? (
<button availableSeeds.map((seedId) => {
key={crop.id} const crop = Object.values(CROPS).find(c => c.seedType === seedId)
style={getCropButtonStyle(selectedCrop === crop.id)} if (!crop) return null
onClick={() => setSelectedCrop(crop.id)} return (
> <button
{crop.name} key={seedId}
</button> style={getCropButtonStyle(selectedSeed === seedId)}
))} onClick={() => setSelectedSeed(seedId)}
>
{ITEMS[seedId].emoji} {crop.name} ({inventory[seedId]})
</button>
)
})
) : (
<p style={{ width: '100%', textAlign: 'center', color: '#666' }}>
No seeds available in inventory
</p>
)}
</div> </div>
)} )}
@ -299,9 +312,6 @@ const FieldComponent: React.FC = () => {
style={getPlotStyle(bgColor)} style={getPlotStyle(bgColor)}
onClick={() => handlePlotClick(rowIndex, colIndex)} onClick={() => handlePlotClick(rowIndex, colIndex)}
> >
{plot.intended && !plot.current && (
<div>🌱 {CROPS[plot.intended]?.name}</div>
)}
{plot.current && ( {plot.current && (
<div> <div>
{plot.current.mature ? '🌿' : '🌱'}{' '} {plot.current.mature ? '🌿' : '🌱'}{' '}

View file

@ -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 (
<StaminaContainer>
<StaminaFill width={staminaPercentage} />
<StaminaText>
{stamina}/{maxStamina}
</StaminaText>
</StaminaContainer>
)
}

View file

@ -114,7 +114,7 @@ const TempleComponent: React.FC = () => {
purchaseUpgrade, purchaseUpgrade,
pray, pray,
piety, piety,
actionCooldown, stamina,
} = useGameStore() } = useGameStore()
const handlePurchase = (upgradeId: string) => { const handlePurchase = (upgradeId: string) => {
@ -186,9 +186,9 @@ const TempleComponent: React.FC = () => {
<PrayerSection> <PrayerSection>
<PietyDisplay>Piety Level: {piety}</PietyDisplay> <PietyDisplay>Piety Level: {piety}</PietyDisplay>
<PrayerButton onClick={() => pray()} disabled={actionCooldown > 0}> <PrayerButton onClick={() => pray()} disabled={stamina < 1}>
🙏 Pray to the Gods 🙏 Pray to the Gods
{actionCooldown > 0 && ` (${(actionCooldown / 1000).toFixed(1)}s)`} {stamina < 1 && ' (No Stamina)'}
</PrayerButton> </PrayerButton>
</PrayerSection> </PrayerSection>

View file

@ -3,7 +3,7 @@ import { CropDefinitions, Upgrade, Equipment } from '../types'
export const INITIAL_CASH = 50 export const INITIAL_CASH = 50
export const INITIAL_FIELD_SIZE = 3 export const INITIAL_FIELD_SIZE = 3
export const COOLDOWN_DURATION = 2000 // 2 seconds in milliseconds 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 = { export const GAME_SPEEDS = {
NORMAL: 1, NORMAL: 1,
FAST: 5, FAST: 5,
@ -68,7 +68,7 @@ export const UPGRADES: Record<string, Upgrade> = {
aqueous_vigor_1: { aqueous_vigor_1: {
id: 'aqueous_vigor_1', id: 'aqueous_vigor_1',
name: 'Aqueous Vigor I', name: 'Aqueous Vigor I',
description: 'Reduces watering cooldown by 25%', description: 'Reduces watering stamina cost by 25%',
cost: [ cost: [
{ {
itemId: 'celery', itemId: 'celery',
@ -80,7 +80,7 @@ export const UPGRADES: Record<string, Upgrade> = {
id: 'aqueous_vigor_2', id: 'aqueous_vigor_2',
name: 'Aqueous Vigor II', name: 'Aqueous Vigor II',
description: 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: [ cost: [
{ {
itemId: 'celery', itemId: 'celery',

59
src/store/saves.ts Normal file
View file

@ -0,0 +1,59 @@
import { GameState } from '../types'
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}`)
}
// Create the global saves object
declare global {
interface Window {
saves: {
save: (slot: number) => void
load: (slot: number) => void
hasSave: (slot: number) => boolean
}
}
}
// Initialize the global saves object
window.saves = {
save: (slot: number) => {
const state = (window as any).gameStore?.getState()
if (state) {
saveGame(slot, state)
console.log(`Game saved to slot ${slot}`)
}
},
load: (slot: number) => {
const savedState = loadGame(slot)
if (savedState && (window as any).gameStore?.setState) {
(window as any).gameStore.setState(savedState)
console.log(`Game loaded from slot ${slot}`)
}
},
hasSave: hasSaveInSlot
}

File diff suppressed because it is too large Load diff

View file

@ -1,51 +1,23 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { hasSaveInSlot, useGameStore } from './useGameStore' import { useGameStore } from './useGameStore'
import './saves' // Import the saves module to initialize the global saves object
export const useSaveSystem = () => { export const useSaveSystem = () => {
const { saveToSlot, loadFromSlot } = useGameStore()
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { // Auto-save every 30 seconds
if (e.key === 'r' && e.ctrlKey) {
localStorage.clear()
}
const slot =
{
'1': 1,
'2': 2,
'3': 3,
'!': 1,
'@': 2,
'#': 3,
}[e.key] ?? null
if (slot !== null) {
if (e.shiftKey) {
saveToSlot(slot)
console.log(`Game saved to slot ${slot}`)
} else if (hasSaveInSlot(slot)) {
loadFromSlot(slot)
console.log(`Game loaded from slot ${slot}`)
}
}
}
window.addEventListener('keydown', handleKeyDown)
const interval = setInterval(() => { const interval = setInterval(() => {
saveToSlot(0) window.saves.save(0) // Save to autosave slot
}, 30000) }, 30000)
console.log('Initalized save system') console.log('Initialized save system')
return () => { return () => {
window.removeEventListener('keydown', handleKeyDown)
clearInterval(interval) clearInterval(interval)
} }
}, [saveToSlot, loadFromSlot]) }, [])
// When starting the game, load from the autosave slot // When starting the game, load from the autosave slot
useEffect(() => { useEffect(() => {
loadFromSlot(0) window.saves.load(0)
}, []) }, [])
} }

View file

@ -16,7 +16,6 @@ export interface CropDefinitions {
} }
export interface PlotState { export interface PlotState {
intended?: string
current?: { current?: {
cropId: string cropId: string
progress: number progress: number
@ -31,7 +30,7 @@ export interface InventoryItem {
count: number count: number
} }
export type FieldTool = 'mark' | 'plant' | 'water' | 'harvest' | 'inspect' | 'remove' export type FieldTool = 'plant' | 'water' | 'harvest' | 'inspect' | 'remove'
export interface ConsoleMessage { export interface ConsoleMessage {
id: string id: string
@ -86,7 +85,8 @@ export interface GameState {
fieldUpgradeCosts: number[] fieldUpgradeCosts: number[]
plots: PlotState[][] plots: PlotState[][]
gameSpeed: number gameSpeed: number
actionCooldown: number stamina: number
maxStamina: number
consoleMessages: ConsoleMessage[] consoleMessages: ConsoleMessage[]
purchasedUpgrades: string[] purchasedUpgrades: string[]
piety: number piety: number