Compare commits
No commits in common. "7c015787dd6d04e47de60d376e2b6480f0f59d9d" and "5eda5c158b3a7e44391f6977047b9cc796b8f71e" have entirely different histories.
7c015787dd
...
5eda5c158b
16
index.html
|
|
@ -2,21 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<!-- Favicon -->
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<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>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 736 B |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
|
@ -1 +0,0 @@
|
||||||
{"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"}
|
|
||||||
1
public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
71
src/App.tsx
|
|
@ -1,55 +1,42 @@
|
||||||
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 AppContainer = styled.div`
|
const appContainerStyle: React.CSSProperties = {
|
||||||
max-width: 1200px;
|
maxWidth: '1200px',
|
||||||
margin: 0 auto;
|
margin: '0 auto',
|
||||||
padding: 4rem 2rem 2rem;
|
padding: '2rem',
|
||||||
`
|
}
|
||||||
|
|
||||||
const StyledTabsList = styled(TabsList)`
|
const tabsListStyles: React.CSSProperties = {
|
||||||
display: grid;
|
display: 'grid',
|
||||||
width: 100%;
|
width: '100%',
|
||||||
grid-template-columns: repeat(4, 1fr);
|
gridTemplateColumns: '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)
|
||||||
|
|
@ -58,15 +45,15 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<StaminaDisplay />
|
<ActionCooldown />
|
||||||
<AppContainer>
|
<div style={appContainerStyle}>
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
<Tabs defaultValue="fields">
|
||||||
<StyledTabsList>
|
<TabsList style={tabsListStyles}>
|
||||||
<TabsTrigger value="fields">Fields (1)</TabsTrigger>
|
<TabsTrigger value="fields">Fields</TabsTrigger>
|
||||||
<TabsTrigger value="warehouse">Warehouse (2)</TabsTrigger>
|
<TabsTrigger value="warehouse">Warehouse</TabsTrigger>
|
||||||
<TabsTrigger value="market">Market (3)</TabsTrigger>
|
<TabsTrigger value="market">Market</TabsTrigger>
|
||||||
<TabsTrigger value="temple">Temple (4)</TabsTrigger>
|
<TabsTrigger value="temple">Temple</TabsTrigger>
|
||||||
</StyledTabsList>
|
</TabsList>
|
||||||
<TabsContent value="fields">
|
<TabsContent value="fields">
|
||||||
<Field />
|
<Field />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
@ -81,7 +68,7 @@ function App() {
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Console />
|
<Console />
|
||||||
</AppContainer>
|
</div>
|
||||||
{isGodModalOpen && <GodModal onClose={() => setIsGodModalOpen(false)} />}
|
{isGodModalOpen && <GodModal onClose={() => setIsGodModalOpen(false)} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
110
src/components/ActionCooldown.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
export interface TabProps {
|
export interface TabProps {
|
||||||
value: string
|
value: string
|
||||||
|
|
@ -6,8 +6,7 @@ export interface TabProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsProps {
|
export interface TabsProps {
|
||||||
value: string
|
defaultValue: string
|
||||||
onValueChange: (value: string) => void
|
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,16 +76,18 @@ const TabsContext = React.createContext<{
|
||||||
setValue: () => {},
|
setValue: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Tabs: React.FC<TabsProps> = ({ value, onValueChange, children }) => {
|
export const Tabs: React.FC<TabsProps> = ({ defaultValue, children }) => {
|
||||||
|
const [value, setValue] = useState(defaultValue)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContext.Provider value={{ value, setValue: onValueChange }}>
|
<TabsContext.Provider value={{ value, setValue }}>
|
||||||
<div style={tabsContainerStyle}>{children}</div>
|
<div style={tabsContainerStyle}>{children}</div>
|
||||||
</TabsContext.Provider>
|
</TabsContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TabsList: React.FC<TabsListProps> = ({ children, ...props }) => {
|
export const TabsList: React.FC<TabsListProps> = ({ children, style }) => {
|
||||||
return <div style={{ ...tabsListContainerStyle }} {...props}>{children}</div>
|
return <div style={{ ...tabsListContainerStyle, ...style }}>{children}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TabsTrigger: React.FC<TabsTriggerProps> = ({
|
export const TabsTrigger: React.FC<TabsTriggerProps> = ({
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ const RecipeDetails = styled.div`
|
||||||
`
|
`
|
||||||
|
|
||||||
const EquipmentModal = ({ equipmentId, onClose }: EquipmentModalProps) => {
|
const EquipmentModal = ({ equipmentId, onClose }: EquipmentModalProps) => {
|
||||||
const { inventory, configureEquipment, useEquipment, stamina, equipment } = useGameStore()
|
const { inventory, configureEquipment, useEquipment, actionCooldown, 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 &&
|
||||||
stamina >= 1 &&
|
actionCooldown === 0 &&
|
||||||
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>Stamina cost: 1</p>
|
<p>Cooldown: {selectedRecipe.cooldownDuration / 1000} seconds</p>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
disabled={!canUse}
|
disabled={!canUse}
|
||||||
onClick={handleUse}
|
onClick={handleUse}
|
||||||
>
|
>
|
||||||
{equipmentInstance.isProcessing ? 'Processing...' : stamina < 1 ? 'No Stamina' : 'Use'}
|
{equipmentInstance.isProcessing ? 'Processing...' : 'Use'}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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, ITEMS } from '../constants'
|
import { CROPS } from '../constants'
|
||||||
import { FieldTool, PlotState } from '../types'
|
import { FieldTool, PlotState } from '../types'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
|
|
||||||
|
|
@ -154,6 +154,10 @@ 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>
|
||||||
|
|
@ -186,12 +190,12 @@ const FieldComponent: React.FC = () => {
|
||||||
water,
|
water,
|
||||||
harvest,
|
harvest,
|
||||||
remove,
|
remove,
|
||||||
inventory,
|
assignCrop,
|
||||||
stamina,
|
actionCooldown,
|
||||||
} = useGameStore()
|
} = useGameStore()
|
||||||
|
|
||||||
const [selectedSeed, setSelectedSeed] = useState<string | null>(null)
|
const [selectedCrop, setSelectedCrop] = useState<string | null>(null)
|
||||||
const [selectedTool, setSelectedTool] = useState<FieldTool>('plant')
|
const [selectedTool, setSelectedTool] = useState<FieldTool>('mark')
|
||||||
const [inspectedPlot, setInspectedPlot] = useState<{
|
const [inspectedPlot, setInspectedPlot] = useState<{
|
||||||
plot: PlotState
|
plot: PlotState
|
||||||
row: number
|
row: number
|
||||||
|
|
@ -199,17 +203,17 @@ const FieldComponent: React.FC = () => {
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
const handlePlotClick = (row: number, col: number) => {
|
const handlePlotClick = (row: number, col: number) => {
|
||||||
if (stamina < 1) return
|
if (actionCooldown > 0) return
|
||||||
|
|
||||||
switch (selectedTool) {
|
switch (selectedTool) {
|
||||||
case 'plant':
|
case 'mark':
|
||||||
if (selectedSeed) {
|
if (selectedCrop) {
|
||||||
const crop = Object.values(CROPS).find(c => c.seedType === selectedSeed)
|
assignCrop(row, col, selectedCrop)
|
||||||
if (crop && inventory[selectedSeed] > 0) {
|
|
||||||
plant(row, col, crop.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case 'plant':
|
||||||
|
plant(row, col)
|
||||||
|
break
|
||||||
case 'water':
|
case 'water':
|
||||||
water(row, col)
|
water(row, col)
|
||||||
break
|
break
|
||||||
|
|
@ -242,6 +246,7 @@ 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: '✂️' },
|
||||||
|
|
@ -249,14 +254,6 @@ 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>
|
||||||
|
|
@ -274,28 +271,18 @@ const FieldComponent: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedTool === 'plant' && (
|
{selectedTool === 'mark' && (
|
||||||
<div style={cropSelectionContainerStyle}>
|
<div style={cropSelectionContainerStyle}>
|
||||||
<p style={cropSelectionLabelStyle}>Select a seed to plant:</p>
|
<p style={cropSelectionLabelStyle}>Select a crop to mark:</p>
|
||||||
{availableSeeds.length > 0 ? (
|
{Object.values(CROPS).map((crop) => (
|
||||||
availableSeeds.map((seedId) => {
|
<button
|
||||||
const crop = Object.values(CROPS).find(c => c.seedType === seedId)
|
key={crop.id}
|
||||||
if (!crop) return null
|
style={getCropButtonStyle(selectedCrop === crop.id)}
|
||||||
return (
|
onClick={() => setSelectedCrop(crop.id)}
|
||||||
<button
|
>
|
||||||
key={seedId}
|
{crop.name}
|
||||||
style={getCropButtonStyle(selectedSeed === seedId)}
|
</button>
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -312,6 +299,9 @@ 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 ? '🌿' : '🌱'}{' '}
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -114,7 +114,7 @@ const TempleComponent: React.FC = () => {
|
||||||
purchaseUpgrade,
|
purchaseUpgrade,
|
||||||
pray,
|
pray,
|
||||||
piety,
|
piety,
|
||||||
stamina,
|
actionCooldown,
|
||||||
} = 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={stamina < 1}>
|
<PrayerButton onClick={() => pray()} disabled={actionCooldown > 0}>
|
||||||
🙏 Pray to the Gods
|
🙏 Pray to the Gods
|
||||||
{stamina < 1 && ' (No Stamina)'}
|
{actionCooldown > 0 && ` (${(actionCooldown / 1000).toFixed(1)}s)`}
|
||||||
</PrayerButton>
|
</PrayerButton>
|
||||||
</PrayerSection>
|
</PrayerSection>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = 5000
|
export const TICK_INTERVAL = 12000 // 12 seconds in milliseconds
|
||||||
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 stamina cost by 25%',
|
description: 'Reduces watering cooldown 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 stamina cost by an additional 25% (requires Aqueous Vigor I)',
|
'Reduces watering cooldown by an additional 25% (requires Aqueous Vigor I)',
|
||||||
cost: [
|
cost: [
|
||||||
{
|
{
|
||||||
itemId: 'celery',
|
itemId: 'celery',
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +1,51 @@
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useGameStore } from './useGameStore'
|
import { hasSaveInSlot, 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(() => {
|
||||||
// Auto-save every 30 seconds
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
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(() => {
|
||||||
window.saves.save(0) // Save to autosave slot
|
saveToSlot(0)
|
||||||
}, 30000)
|
}, 30000)
|
||||||
|
|
||||||
console.log('Initialized save system')
|
console.log('Initalized 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(() => {
|
||||||
window.saves.load(0)
|
loadFromSlot(0)
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export interface CropDefinitions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlotState {
|
export interface PlotState {
|
||||||
|
intended?: string
|
||||||
current?: {
|
current?: {
|
||||||
cropId: string
|
cropId: string
|
||||||
progress: number
|
progress: number
|
||||||
|
|
@ -30,7 +31,7 @@ export interface InventoryItem {
|
||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FieldTool = 'plant' | 'water' | 'harvest' | 'inspect' | 'remove'
|
export type FieldTool = 'mark' | 'plant' | 'water' | 'harvest' | 'inspect' | 'remove'
|
||||||
|
|
||||||
export interface ConsoleMessage {
|
export interface ConsoleMessage {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -85,8 +86,7 @@ export interface GameState {
|
||||||
fieldUpgradeCosts: number[]
|
fieldUpgradeCosts: number[]
|
||||||
plots: PlotState[][]
|
plots: PlotState[][]
|
||||||
gameSpeed: number
|
gameSpeed: number
|
||||||
stamina: number
|
actionCooldown: number
|
||||||
maxStamina: number
|
|
||||||
consoleMessages: ConsoleMessage[]
|
consoleMessages: ConsoleMessage[]
|
||||||
purchasedUpgrades: string[]
|
purchasedUpgrades: string[]
|
||||||
piety: number
|
piety: number
|
||||||
|
|
|
||||||