Add "additional land" item to the store.

This commit is contained in:
Ryan Lanny Jenkins 2025-05-24 18:50:22 -05:00
parent ba9b26ff64
commit 23c312299c
6 changed files with 134 additions and 64 deletions

View file

@ -40,4 +40,4 @@ export const Console: React.FC = () => {
)} )}
</div> </div>
) )
} }

View file

@ -1,6 +1,6 @@
import React, { useRef } from 'react' import React, { useRef } from 'react'
import { useGameStore } from '../store/useGameStore' import { useGameStore } from '../store/useGameStore'
import { MARKET_ITEMS } from '../constants' import { MARKET_ITEMS, MarketItem } from '../constants'
import FloatingMessage, { FloatingMessagesHandle } from './FloatingMessages' import FloatingMessage, { FloatingMessagesHandle } from './FloatingMessages'
import { styled } from '@linaria/react' import { styled } from '@linaria/react'
@ -72,12 +72,20 @@ const ActionButton = styled.button<{ disabled?: boolean }>`
` `
const MarketComponent: React.FC = () => { const MarketComponent: React.FC = () => {
const { inventory, cash, buyItem, sellItem } = useGameStore() const { inventory, cash, buyItem, sellItem, landPurchases } = useGameStore()
const floatingMessagesRef = useRef<FloatingMessagesHandle>(null) const floatingMessagesRef = useRef<FloatingMessagesHandle>(null)
const getItemPrice = (item: MarketItem) => {
if (item.id === 'additional_land') {
return Math.floor(100 * Math.pow(1.5, landPurchases))
}
return item.buyPrice
}
const handleBuy = (itemId: string, e: React.MouseEvent) => { const handleBuy = (itemId: string, e: React.MouseEvent) => {
const item = MARKET_ITEMS[itemId] const item = MARKET_ITEMS[itemId]
if (!item || item.buyPrice === null || cash < item.buyPrice) return const price = getItemPrice(item)
if (!item || price === null || cash < price) return
buyItem(itemId) buyItem(itemId)
@ -182,11 +190,13 @@ const MarketComponent: React.FC = () => {
<ItemIcon>{item.emoji}</ItemIcon> <ItemIcon>{item.emoji}</ItemIcon>
{item.name} {item.name}
</MarketItemCell> </MarketItemCell>
<MarketItemCell>${item.buyPrice}</MarketItemCell> <MarketItemCell>${getItemPrice(item)}</MarketItemCell>
<MarketItemCell> <MarketItemCell>
<ActionButton <ActionButton
onClick={(e) => handleBuy(item.id, e)} onClick={(e) => handleBuy(item.id, e)}
disabled={item.buyPrice ? cash < item.buyPrice : true} disabled={
getItemPrice(item) ? cash < getItemPrice(item)! : true
}
> >
Buy Buy
</ActionButton> </ActionButton>

View file

@ -34,10 +34,10 @@ const PrayerButton = styled.button<{ disabled?: boolean }>`
padding: 1rem 2rem; padding: 1rem 2rem;
font-size: 1.25rem; font-size: 1.25rem;
border-radius: 0.5rem; border-radius: 0.5rem;
background-color: ${props => props.disabled ? '#9ca3af' : '#8b5cf6'}; background-color: ${(props) => (props.disabled ? '#9ca3af' : '#8b5cf6')};
color: white; color: white;
font-weight: bold; font-weight: bold;
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'}; cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
border: none; border: none;
transition: all 0.2s; transition: all 0.2s;
display: flex; display: flex;
@ -63,7 +63,7 @@ const UpgradesGrid = styled.div`
` `
const UpgradeCard = styled.div<{ purchased?: boolean }>` const UpgradeCard = styled.div<{ purchased?: boolean }>`
background-color: ${props => props.purchased ? '#e5e7eb' : '#f3f4f6'}; background-color: ${(props) => (props.purchased ? '#e5e7eb' : '#f3f4f6')};
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 1rem; padding: 1rem;
@ -99,25 +99,33 @@ const CostItem = styled.div`
const PurchaseButton = styled.button<{ disabled?: boolean }>` const PurchaseButton = styled.button<{ disabled?: boolean }>`
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 0.25rem; border-radius: 0.25rem;
background-color: ${props => props.disabled ? '#9ca3af' : '#3b82f6'}; background-color: ${(props) => (props.disabled ? '#9ca3af' : '#3b82f6')};
color: white; color: white;
font-weight: bold; font-weight: bold;
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'}; cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
border: none; border: none;
margin-top: auto; margin-top: auto;
` `
const TempleComponent: React.FC = () => { const TempleComponent: React.FC = () => {
const { inventory, purchasedUpgrades, purchaseUpgrade, pray, piety, actionCooldown } = useGameStore() const {
inventory,
purchasedUpgrades,
purchaseUpgrade,
pray,
piety,
actionCooldown,
} = useGameStore()
const handlePurchase = (upgradeId: string) => { const handlePurchase = (upgradeId: string) => {
purchaseUpgrade(upgradeId) purchaseUpgrade(upgradeId)
} }
const canAffordUpgrade = (costs: UpgradeCost[]) => { const canAffordUpgrade = (costs: UpgradeCost[]) => {
return costs.every(cost => return costs.every(
inventory[cost.itemId] !== undefined && (cost) =>
inventory[cost.itemId] >= cost.amount inventory[cost.itemId] !== undefined &&
inventory[cost.itemId] >= cost.amount,
) )
} }
@ -133,7 +141,10 @@ const TempleComponent: React.FC = () => {
} }
// Check requirements // Check requirements
if (upgrade.id === 'aqueous_vigor_2' && !purchasedUpgrades.includes('aqueous_vigor_1')) { if (
upgrade.id === 'aqueous_vigor_2' &&
!purchasedUpgrades.includes('aqueous_vigor_1')
) {
return false return false
} }
@ -144,7 +155,10 @@ const TempleComponent: React.FC = () => {
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')) { if (
upgrade.id === 'aqueous_vigor_2' &&
!purchasedUpgrades.includes('aqueous_vigor_1')
) {
return 'Requires Aqueous Vigor I' return 'Requires Aqueous Vigor I'
} }
if (!canAffordUpgrade(upgrade.cost)) { if (!canAffordUpgrade(upgrade.cost)) {
@ -156,13 +170,10 @@ const TempleComponent: React.FC = () => {
return ( return (
<TempleContainer> <TempleContainer>
<Title>Temple</Title> <Title>Temple</Title>
<PrayerSection> <PrayerSection>
<PietyDisplay>Piety Level: {piety}</PietyDisplay> <PietyDisplay>Piety Level: {piety}</PietyDisplay>
<PrayerButton <PrayerButton onClick={() => pray()} disabled={actionCooldown > 0}>
onClick={() => pray()}
disabled={actionCooldown > 0}
>
🙏 Pray to the Gods 🙏 Pray to the Gods
{actionCooldown > 0 && ` (${(actionCooldown / 1000).toFixed(1)}s)`} {actionCooldown > 0 && ` (${(actionCooldown / 1000).toFixed(1)}s)`}
</PrayerButton> </PrayerButton>
@ -182,10 +193,10 @@ const TempleComponent: React.FC = () => {
<span>Cost:</span> <span>Cost:</span>
{upgrade.cost.map((cost, index) => ( {upgrade.cost.map((cost, index) => (
<CostItem key={index}> <CostItem key={index}>
<span>{cost.amount} {cost.itemId}</span>
<span> <span>
(You have: {inventory[cost.itemId] || 0}) {cost.amount} {cost.itemId}
</span> </span>
<span>(You have: {inventory[cost.itemId] || 0})</span>
</CostItem> </CostItem>
))} ))}
</UpgradeCost> </UpgradeCost>
@ -203,4 +214,4 @@ const TempleComponent: React.FC = () => {
) )
} }
export default TempleComponent export default TempleComponent

View file

@ -43,20 +43,25 @@ export const UPGRADES: Record<string, Upgrade> = {
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 cooldown by 25%',
cost: [{ cost: [
itemId: 'celery', {
amount: 10 itemId: 'celery',
}] amount: 10,
},
],
}, },
aqueous_vigor_2: { aqueous_vigor_2: {
id: 'aqueous_vigor_2', id: 'aqueous_vigor_2',
name: 'Aqueous Vigor II', name: 'Aqueous Vigor II',
description: 'Reduces watering cooldown by an additional 25% (requires Aqueous Vigor I)', description:
cost: [{ 'Reduces watering cooldown by an additional 25% (requires Aqueous Vigor I)',
itemId: 'celery', cost: [
amount: 25 {
}] itemId: 'celery',
} amount: 25,
},
],
},
} }
export interface MarketItem { export interface MarketItem {
@ -68,6 +73,13 @@ export interface MarketItem {
} }
export const MARKET_ITEMS: Record<string, MarketItem> = { export const MARKET_ITEMS: Record<string, MarketItem> = {
additional_land: {
id: 'additional_land',
name: 'Additional Land',
emoji: '🌱',
buyPrice: 100,
sellPrice: null,
},
celery_seed: { celery_seed: {
id: 'celery_seed', id: 'celery_seed',
name: 'Celery Seeds', name: 'Celery Seeds',

View file

@ -54,18 +54,20 @@ export const hasSaveInSlot = (slot: number): boolean => {
return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`) return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`)
} }
const addConsoleMessage = (state: GameState, text: string) => ( const addConsoleMessage = (state: GameState, text: string) =>
state.consoleMessages = [ (state.consoleMessages = [
{ {
id: Math.random().toString(36).substring(7), id: Math.random().toString(36).substring(7),
text, text,
timestamp: Date.now(), timestamp: Date.now(),
}, },
...state.consoleMessages, ...state.consoleMessages,
].slice(0, 50) ].slice(0, 50))
)
const progressRandomImmaturePlot = (state: GameState, growthAmount: number): boolean => { const progressRandomImmaturePlot = (
state: GameState,
growthAmount: number,
): boolean => {
// Find all immature plants // Find all immature plants
const immaturePlots: { row: number; col: number }[] = [] const immaturePlots: { row: number; col: number }[] = []
state.plots.forEach((row, rowIndex) => { state.plots.forEach((row, rowIndex) => {
@ -78,14 +80,15 @@ const progressRandomImmaturePlot = (state: GameState, growthAmount: number): boo
if (immaturePlots.length > 0) { if (immaturePlots.length > 0) {
// Select a random immature plot and progress it // Select a random immature plot and progress it
const randomPlot = immaturePlots[Math.floor(Math.random() * immaturePlots.length)] const randomPlot =
immaturePlots[Math.floor(Math.random() * immaturePlots.length)]
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 actualGrowthAmount = crop.growthTicks * growthAmount const actualGrowthAmount = crop.growthTicks * growthAmount
plot.current!.progress = Math.min( plot.current!.progress = Math.min(
crop.growthTicks, crop.growthTicks,
plot.current!.progress + actualGrowthAmount plot.current!.progress + actualGrowthAmount,
) )
return true return true
} }
@ -122,6 +125,7 @@ export const useGameStore = create<
consoleMessages: [], consoleMessages: [],
purchasedUpgrades: [], purchasedUpgrades: [],
piety: 50, piety: 50,
landPurchases: 0,
assignCrop: (row, col, cropId) => { assignCrop: (row, col, cropId) => {
set( set(
@ -177,10 +181,7 @@ export const useGameStore = create<
cooldownMultiplier *= 0.75 // Additional 25% reduction cooldownMultiplier *= 0.75 // Additional 25% reduction
} }
const finalCooldown = Math.max( const finalCooldown = Math.max(0, COOLDOWN_DURATION * cooldownMultiplier)
0,
COOLDOWN_DURATION * cooldownMultiplier
)
set( set(
produce((state) => { produce((state) => {
@ -262,7 +263,7 @@ export const useGameStore = create<
state.plots[rowIndex][colIndex].moisture = state.plots[rowIndex][colIndex].moisture =
plot.moisture - waterNeeded plot.moisture - waterNeeded
state.plots[rowIndex][colIndex].current.progress = newProgress state.plots[rowIndex][colIndex].current.progress = newProgress
// If the plot just became mature, add a message // If the plot just became mature, add a message
if (mature && !plot.current.mature) { if (mature && !plot.current.mature) {
addConsoleMessage( addConsoleMessage(
@ -270,7 +271,7 @@ export const useGameStore = create<
`Plot (${rowIndex + 1},${colIndex + 1}) ${crop.name} is ready to harvest!`, `Plot (${rowIndex + 1},${colIndex + 1}) ${crop.name} is ready to harvest!`,
) )
} }
state.plots[rowIndex][colIndex].current.mature = mature state.plots[rowIndex][colIndex].current.mature = mature
} }
}) })
@ -321,21 +322,57 @@ export const useGameStore = create<
}, },
buyItem: (itemId) => { buyItem: (itemId) => {
const { cash } = get() const { cash, landPurchases, plots, fieldSize } = get()
const item = MARKET_ITEMS[itemId] const item = MARKET_ITEMS[itemId]
if (!item || item.buyPrice === null || item.buyPrice === undefined) { if (!item || item.buyPrice === null || item.buyPrice === undefined) {
return return
} }
if (cash < item.buyPrice) { // Calculate price for Additional Land based on number of purchases
let price = item.buyPrice
if (itemId === 'additional_land') {
price = Math.floor(100 * Math.pow(1.5, landPurchases))
}
if (cash < price) {
return return
} }
set( set(
produce((state) => { produce((state) => {
state.cash -= item.buyPrice! state.cash -= price
state.inventory[itemId] = (state.inventory[itemId] || 0) + 1
// Special handling for Additional Land
if (itemId === 'additional_land') {
state.landPurchases += 1
if (state.fieldSize < state.maxFieldSize) {
const newSize = state.fieldSize + 1
// Add a new row
state.plots.push(
Array(newSize)
.fill(0)
.map(() => ({
moisture: 0,
fertility: 1,
})),
)
// Add a new column to each existing row
state.plots.forEach((row: PlotState[]) => {
row.push({
moisture: 0,
fertility: 1,
})
})
state.fieldSize = newSize
addConsoleMessage(state, 'Field size increased!')
}
} else {
state.inventory[itemId] = (state.inventory[itemId] || 0) + 1
}
}), }),
) )
}, },
@ -400,7 +437,9 @@ export const useGameStore = create<
// Check if player can afford all costs // Check if player can afford all costs
const canAfford = upgrade.cost.every( const canAfford = upgrade.cost.every(
cost => inventory[cost.itemId] !== undefined && inventory[cost.itemId] >= cost.amount (cost) =>
inventory[cost.itemId] !== undefined &&
inventory[cost.itemId] >= cost.amount,
) )
if (!canAfford) { if (!canAfford) {
@ -410,20 +449,17 @@ export const useGameStore = create<
set( set(
produce((state) => { produce((state) => {
// Deduct all costs // Deduct all costs
upgrade.cost.forEach(cost => { upgrade.cost.forEach((cost) => {
state.inventory[cost.itemId] -= cost.amount state.inventory[cost.itemId] -= cost.amount
}) })
state.purchasedUpgrades.push(upgradeId) state.purchasedUpgrades.push(upgradeId)
addConsoleMessage( addConsoleMessage(state, `Purchased upgrade: ${upgrade.name}`)
state,
`Purchased upgrade: ${upgrade.name}`
)
}), }),
) )
}, },
pray: () => { pray: () => {
const { actionCooldown, piety, plots } = get() const { actionCooldown, piety } = get()
if (actionCooldown > 0) { if (actionCooldown > 0) {
return return
@ -438,26 +474,26 @@ export const useGameStore = create<
state.piety += 1 state.piety += 1
addConsoleMessage( addConsoleMessage(
state, state,
"Nothing happens, but you feel the approval of the gods" 'Nothing happens, but you feel the approval of the gods',
) )
} else if (result > 100) { } else if (result > 100) {
if (progressRandomImmaturePlot(state, 0.3)) { if (progressRandomImmaturePlot(state, 0.3)) {
addConsoleMessage( addConsoleMessage(
state, state,
"The gods have bestowed a significant blessing on one of your crops" 'The gods have bestowed a significant blessing on one of your crops',
) )
} }
} else if (result > 50) { } else if (result > 50) {
if (progressRandomImmaturePlot(state, 0.1)) { if (progressRandomImmaturePlot(state, 0.1)) {
addConsoleMessage( addConsoleMessage(
state, state,
"The gods have bestowed a minor blessing on one of your crops" 'The gods have bestowed a minor blessing on one of your crops',
) )
} }
} }
state.actionCooldown = 3000 state.actionCooldown = 3000
}) }),
) )
}, },
})) }))

View file

@ -60,6 +60,7 @@ export interface GameState {
consoleMessages: ConsoleMessage[] consoleMessages: ConsoleMessage[]
purchasedUpgrades: string[] purchasedUpgrades: string[]
piety: number piety: number
landPurchases: number
} }
export interface MarketTransaction { export interface MarketTransaction {