Compare commits

..

No commits in common. "b1bc592a28472ef35d313e9be6f1f70ad2faf2d8" and "453910282a039b0d8c393ea3cdd30fdf1a363374" have entirely different histories.

7 changed files with 141 additions and 165 deletions

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { styled } from '@linaria/react' import { styled } from '@linaria/react'
import { useGameTick } from './hooks/useGameTick' import { useGameTick } from './hooks/useGameTick'
import Field from './components/Field' import Field from './components/Field'

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, ITEMS, secondsToTicks } 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'
@ -105,15 +105,17 @@ const getMaturityProgressContainerStyle = (): React.CSSProperties => ({
const getMaturityProgressBarStyle = ( const getMaturityProgressBarStyle = (
progress: number, progress: number,
cropId: string, total: number,
): React.CSSProperties => ({ ): React.CSSProperties => ({
width: `${Math.min( position: 'absolute',
100, bottom: 0,
(progress / secondsToTicks(CROPS[cropId].growthTimeSeconds)) * 100, left: 0,
)}%`, width: `${Math.min((progress / total) * 100, 100)}%`,
height: '100%', height: '10px',
backgroundColor: '#22c55e', backgroundColor: '#ff5722',
transition: 'width 0.5s ease-in-out', transition: 'width 0.3s',
zIndex: 6,
boxShadow: '0 0 3px rgba(0, 0, 0, 0.5)',
}) })
const plotInfoStyle: React.CSSProperties = { const plotInfoStyle: React.CSSProperties = {
@ -166,7 +168,7 @@ const PlotInfoModal: React.FC<PlotInfoModalProps> = ({ plot, onClose }) => {
<span> <span>
{formatPercentage( {formatPercentage(
plot.current.progress / plot.current.progress /
secondsToTicks(CROPS[plot.current.cropId].growthTimeSeconds), CROPS[plot.current.cropId].growthTicks,
)} )}
</span> </span>
</div> </div>
@ -322,7 +324,7 @@ const FieldComponent: React.FC = () => {
<div <div
style={getMaturityProgressBarStyle( style={getMaturityProgressBarStyle(
plot.current.progress, plot.current.progress,
plot.current.cropId, CROPS[plot.current.cropId].growthTicks,
)} )}
/> />
</div> </div>

View file

@ -1,4 +1,4 @@
import { import React, {
useState, useState,
useImperativeHandle, useImperativeHandle,
forwardRef, forwardRef,

View file

@ -115,7 +115,6 @@ const TempleComponent: React.FC = () => {
pray, pray,
piety, piety,
stamina, stamina,
milestones,
} = useGameStore() } = useGameStore()
const handlePurchase = (upgradeId: string) => { const handlePurchase = (upgradeId: string) => {
@ -137,13 +136,44 @@ const TempleComponent: React.FC = () => {
} }
// Check if can afford // Check if can afford
return canAffordUpgrade(upgrade.cost) if (!canAffordUpgrade(upgrade.cost)) {
return false
}
// Check requirements
if (
upgrade.id === 'aqueous_vigor_2' &&
!purchasedUpgrades.includes('aqueous_vigor_1')
) {
return false
}
if (
upgrade.id === 'aqua_diffundere_2' &&
!purchasedUpgrades.includes('aqua_diffundere_1')
) {
return false
}
return true
} }
const getUpgradeStatus = (upgrade: Upgrade) => { const getUpgradeStatus = (upgrade: Upgrade) => {
if (purchasedUpgrades.includes(upgrade.id)) { if (purchasedUpgrades.includes(upgrade.id)) {
return 'Purchased' return 'Purchased'
} }
if (
upgrade.id === 'aqueous_vigor_2' &&
!purchasedUpgrades.includes('aqueous_vigor_1')
) {
return 'Requires Aqueous Vigor I'
}
if (
upgrade.id === 'aqua_diffundere_2' &&
!purchasedUpgrades.includes('aqua_diffundere_1')
) {
return 'Requires Aqua Diffundere I'
}
if (!canAffordUpgrade(upgrade.cost)) { if (!canAffordUpgrade(upgrade.cost)) {
return 'Cannot Afford' return 'Cannot Afford'
} }
@ -163,37 +193,35 @@ const TempleComponent: React.FC = () => {
</PrayerSection> </PrayerSection>
<UpgradesGrid> <UpgradesGrid>
{Object.values(UPGRADES) {Object.values(UPGRADES).map((upgrade) => {
.filter(upgrade => !upgrade.isAvailable || upgrade.isAvailable({ purchasedUpgrades, milestones })) const isPurchased = purchasedUpgrades.includes(upgrade.id)
.map((upgrade) => { const canPurchase = canPurchaseUpgrade(upgrade)
const isPurchased = purchasedUpgrades.includes(upgrade.id) const status = getUpgradeStatus(upgrade)
const canPurchase = canPurchaseUpgrade(upgrade)
const status = getUpgradeStatus(upgrade)
return ( return (
<UpgradeCard key={upgrade.id} purchased={isPurchased}> <UpgradeCard key={upgrade.id} purchased={isPurchased}>
<UpgradeName>{upgrade.name}</UpgradeName> <UpgradeName>{upgrade.name}</UpgradeName>
<UpgradeDescription>{upgrade.description}</UpgradeDescription> <UpgradeDescription>{upgrade.description}</UpgradeDescription>
<UpgradeCost> <UpgradeCost>
<span>Cost:</span> <span>Cost:</span>
{upgrade.cost.map((cost, index) => ( {upgrade.cost.map((cost, index) => (
<CostItem key={index}> <CostItem key={index}>
<span> <span>
{cost.amount} {cost.itemId} {cost.amount} {cost.itemId}
</span> </span>
<span>(You have: {inventory[cost.itemId] || 0})</span> <span>(You have: {inventory[cost.itemId] || 0})</span>
</CostItem> </CostItem>
))} ))}
</UpgradeCost> </UpgradeCost>
<PurchaseButton <PurchaseButton
onClick={() => handlePurchase(upgrade.id)} onClick={() => handlePurchase(upgrade.id)}
disabled={!canPurchase} disabled={!canPurchase}
> >
{status} {status}
</PurchaseButton> </PurchaseButton>
</UpgradeCard> </UpgradeCard>
) )
})} })}
</UpgradesGrid> </UpgradesGrid>
</TempleContainer> </TempleContainer>
) )

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 = 1000 // 1 second in milliseconds export const TICK_INTERVAL = 5000
export const GAME_SPEEDS = { export const GAME_SPEEDS = {
NORMAL: 1, NORMAL: 1,
FAST: 5, FAST: 5,
@ -13,17 +13,12 @@ export const INITIAL_GAME_SPEED = GAME_SPEEDS.NORMAL
export const FIELD_UPGRADE_COSTS = [100, 1000, 10000, 100000] export const FIELD_UPGRADE_COSTS = [100, 1000, 10000, 100000]
// Helper function to convert seconds to ticks based on current tick interval
export const secondsToTicks = (seconds: number): number => {
return Math.ceil((seconds * 1000) / TICK_INTERVAL)
}
export const CROPS: CropDefinitions = { export const CROPS: CropDefinitions = {
celery: { celery: {
id: 'celery', id: 'celery',
name: 'Celery', name: 'Celery',
growthTimeSeconds: 10, growthTicks: 36,
waterPerSecond: 0.20, // 1/5 water per tick = 0.04 water per second waterPerTick: 1 / 20,
yield: 1, yield: 1,
yieldType: 'celery', yieldType: 'celery',
fertilityDepletion: 0.1, fertilityDepletion: 0.1,
@ -32,8 +27,8 @@ export const CROPS: CropDefinitions = {
corn: { corn: {
id: 'corn', id: 'corn',
name: 'Corn', name: 'Corn',
growthTimeSeconds: 600, growthTicks: 120,
waterPerSecond: 0.005, // 1/40 water per tick = 0.005 water per second waterPerTick: 1 / 40,
yield: 5, yield: 5,
yieldType: 'corn', yieldType: 'corn',
fertilityDepletion: 0.3, fertilityDepletion: 0.3,
@ -42,26 +37,26 @@ export const CROPS: CropDefinitions = {
olive: { olive: {
id: 'olive', id: 'olive',
name: 'Olive Tree', name: 'Olive Tree',
growthTimeSeconds: 1000, growthTicks: 200,
waterPerSecond: 0.004, // 1/50 water per tick = 0.004 water per second waterPerTick: 1 / 50,
yield: 5, yield: 5,
yieldType: 'olive', yieldType: 'olive',
fertilityDepletion: 0.1, fertilityDepletion: 0.1,
seedType: 'olive', // Uses olives as seeds seedType: 'olive', // Uses olives as seeds
isPerennial: true, isPerennial: true,
regrowthTimeSeconds: 750, // 150 ticks = 750 seconds regrowthProgress: 150, // Resets to 150 ticks after harvest
}, },
grape_vine: { grape_vine: {
id: 'grape_vine', id: 'grape_vine',
name: 'Grape Vine', name: 'Grape Vine',
growthTimeSeconds: 1250, growthTicks: 250,
waterPerSecond: 0.00444, // 1/45 water per tick = 0.00444 water per second waterPerTick: 1 / 45,
yield: 2, yield: 2,
yieldType: 'grape', yieldType: 'grape',
fertilityDepletion: 0.15, fertilityDepletion: 0.15,
seedType: 'grape_seed', seedType: 'grape_seed',
isPerennial: true, isPerennial: true,
regrowthTimeSeconds: 1000, // 200 ticks = 1000 seconds regrowthProgress: 200, // Resets to 200 ticks after harvest
}, },
} }
@ -70,50 +65,6 @@ export const INITIAL_INVENTORY = {
} }
export const UPGRADES: Record<string, Upgrade> = { export const UPGRADES: Record<string, Upgrade> = {
stamina_1: {
id: 'stamina_1',
name: 'Stamina I',
description: 'Increases maximum stamina by 5',
cost: [
{
itemId: 'celery',
amount: 20,
},
],
},
stamina_2: {
id: 'stamina_2',
name: 'Stamina II',
description: 'Increases maximum stamina by 8',
cost: [
{
itemId: 'corn',
amount: 20,
},
],
},
stamina_3: {
id: 'stamina_3',
name: 'Stamina III',
description: 'Increases maximum stamina by 12',
cost: [
{
itemId: 'olives',
amount: 100,
},
],
},
stamina_4: {
id: 'stamina_4',
name: 'Stamina IV',
description: 'Increases maximum stamina by 15',
cost: [
{
itemId: 'grapes',
amount: 100,
},
],
},
aqueous_vigor_1: { aqueous_vigor_1: {
id: 'aqueous_vigor_1', id: 'aqueous_vigor_1',
name: 'Aqueous Vigor I', name: 'Aqueous Vigor I',

View file

@ -10,8 +10,6 @@ import {
ITEMS, ITEMS,
UPGRADES, UPGRADES,
EQUIPMENT, EQUIPMENT,
TICK_INTERVAL,
secondsToTicks,
} from '../constants' } from '../constants'
import { GameState, PlotState, Equipment } from '../types' import { GameState, PlotState, Equipment } from '../types'
@ -66,10 +64,9 @@ const progressRandomImmaturePlot = (
const plot = state.plots[randomPlot.row][randomPlot.col] const plot = state.plots[randomPlot.row][randomPlot.col]
const crop = CROPS[plot.current!.cropId] const crop = CROPS[plot.current!.cropId]
const growthTicks = secondsToTicks(crop.growthTimeSeconds) const actualGrowthAmount = crop.growthTicks * growthAmount
const actualGrowthAmount = growthTicks * growthAmount
plot.current!.progress = Math.min( plot.current!.progress = Math.min(
growthTicks, crop.growthTicks,
plot.current!.progress + actualGrowthAmount, plot.current!.progress + actualGrowthAmount,
) )
return true return true
@ -225,10 +222,9 @@ export const useGameStore = create<
produce((state) => { produce((state) => {
if (crop.isPerennial) { if (crop.isPerennial) {
// For perennial crops, reset progress instead of clearing // For perennial crops, reset progress instead of clearing
const regrowthTicks = secondsToTicks(crop.regrowthTimeSeconds!)
state.plots[row][col].current = { state.plots[row][col].current = {
cropId: plot.current!.cropId, cropId: plot.current!.cropId,
progress: regrowthTicks, progress: crop.regrowthProgress!,
mature: false, mature: false,
} }
} else { } else {
@ -274,52 +270,67 @@ export const useGameStore = create<
set( set(
produce((state) => { produce((state) => {
// Regenerate stamina // Regenerate stamina
state.stamina = Math.min( if (state.stamina < state.maxStamina) {
state.maxStamina, const oldStamina = state.stamina
state.stamina + 0.5, state.stamina = Math.min(state.maxStamina, state.stamina + 1)
)
// Increment milestone counter for stamina gained
// Process each plot 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) => { state.plots.forEach((row: PlotState[], rowIndex: number) => {
row.forEach((plot: PlotState, colIndex: number) => { row.forEach((plot: PlotState, colIndex: number) => {
// Regenerate fertility every 100 ticks // Regenerate fertility every 100 ticks
if (state.tickCount % 100 === 0) { if (state.tickCount % 100 === 0 && plot.fertility < 1) {
state.plots[rowIndex][colIndex].fertility = Math.min( state.plots[rowIndex][colIndex].fertility = Math.min(
1, 1,
plot.fertility + 0.01, plot.fertility + 0.1,
) )
} }
if (plot.current && !plot.current.mature) { if (!plot.current || plot.current.mature) {
const crop = CROPS[plot.current.cropId] return
const growthTicks = secondsToTicks(crop.growthTimeSeconds) }
const waterNeeded = crop.waterPerSecond * (TICK_INTERVAL / 1000)
// Only grow if fertility is above 0.2 const crop = CROPS[plot.current.cropId]
if (plot.moisture >= waterNeeded && plot.fertility >= 0.2) { const waterNeeded = crop.waterPerTick
let growthRate = 1
// Half growth rate if fertility is between 0.2 and 0.5
if (plot.fertility < 0.5) {
growthRate = 0.5
}
const newProgress = plot.current.progress + growthRate // Check if water is running low (less than 25% of what's needed)
const mature = newProgress >= growthTicks if (plot.moisture < waterNeeded * 0.25 && plot.moisture > 0) {
addConsoleMessage(
state,
`Plot (${rowIndex + 1},${colIndex + 1}) ${crop.name} is running low on water!`,
)
}
state.plots[rowIndex][colIndex].moisture = // Only grow if fertility is above 0.2
plot.moisture - waterNeeded if (plot.moisture >= waterNeeded && plot.fertility >= 0.2) {
state.plots[rowIndex][colIndex].current.progress = newProgress let growthRate = 1
// Half growth rate if fertility is between 0.2 and 0.5
// If the plot just became mature, add a message if (plot.fertility < 0.5) {
if (mature && !plot.current.mature) { growthRate = 0.5
addConsoleMessage(
state,
`Plot (${rowIndex + 1},${colIndex + 1}) ${crop.name} is ready to harvest!`,
)
}
state.plots[rowIndex][colIndex].current.mature = mature
} }
const newProgress = plot.current.progress + growthRate
const mature = newProgress >= crop.growthTicks
state.plots[rowIndex][colIndex].moisture =
plot.moisture - waterNeeded
state.plots[rowIndex][colIndex].current.progress = newProgress
// If the plot just became mature, add a message
if (mature && !plot.current.mature) {
addConsoleMessage(
state,
`Plot (${rowIndex + 1},${colIndex + 1}) ${crop.name} is ready to harvest!`,
)
}
state.plots[rowIndex][colIndex].current.mature = mature
} }
}) })
}) })
@ -494,21 +505,6 @@ export const useGameStore = create<
}) })
state.purchasedUpgrades.push(upgradeId) state.purchasedUpgrades.push(upgradeId)
addConsoleMessage(state, `Purchased upgrade: ${upgrade.name}`) addConsoleMessage(state, `Purchased upgrade: ${upgrade.name}`)
// Handle stamina upgrades
if (upgradeId === 'stamina_1') {
state.maxStamina += 5
state.stamina = Math.min(state.maxStamina, state.stamina + 5)
} else if (upgradeId === 'stamina_2') {
state.maxStamina += 8
state.stamina = Math.min(state.maxStamina, state.stamina + 8)
} else if (upgradeId === 'stamina_3') {
state.maxStamina += 12
state.stamina = Math.min(state.maxStamina, state.stamina + 12)
} else if (upgradeId === 'stamina_4') {
state.maxStamina += 15
state.stamina = Math.min(state.maxStamina, state.stamina + 15)
}
}), }),
) )
}, },

View file

@ -1,14 +1,14 @@
export interface Crop { export interface Crop {
id: string id: string
name: string name: string
growthTimeSeconds: number growthTicks: number
waterPerSecond: number waterPerTick: number
yield: number yield: number
yieldType: string yieldType: string
fertilityDepletion: number fertilityDepletion: number
seedType: string seedType: string
isPerennial?: boolean isPerennial?: boolean
regrowthTimeSeconds?: number regrowthProgress?: number
} }
export interface CropDefinitions { export interface CropDefinitions {
@ -48,7 +48,6 @@ export interface Upgrade {
name: string name: string
description: string description: string
cost: UpgradeCost[] cost: UpgradeCost[]
isAvailable?: (state: Pick<GameState, 'purchasedUpgrades' | 'milestones'>) => boolean
} }
export interface Recipe { export interface Recipe {