Compare commits

..

2 commits

Author SHA1 Message Date
Ryan Lanny Jenkins 23c312299c Add "additional land" item to the store. 2025-05-24 18:50:22 -05:00
Ryan Lanny Jenkins ba9b26ff64 Add prayer action to the temple. 2025-05-24 15:50:55 -05:00
6 changed files with 284 additions and 48 deletions

View file

@ -1,6 +1,6 @@
import React, { useRef } from 'react'
import { useGameStore } from '../store/useGameStore'
import { MARKET_ITEMS } from '../constants'
import { MARKET_ITEMS, MarketItem } from '../constants'
import FloatingMessage, { FloatingMessagesHandle } from './FloatingMessages'
import { styled } from '@linaria/react'
@ -72,12 +72,20 @@ const ActionButton = styled.button<{ disabled?: boolean }>`
`
const MarketComponent: React.FC = () => {
const { inventory, cash, buyItem, sellItem } = useGameStore()
const { inventory, cash, buyItem, sellItem, landPurchases } = useGameStore()
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 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)
@ -182,11 +190,13 @@ const MarketComponent: React.FC = () => {
<ItemIcon>{item.emoji}</ItemIcon>
{item.name}
</MarketItemCell>
<MarketItemCell>${item.buyPrice}</MarketItemCell>
<MarketItemCell>${getItemPrice(item)}</MarketItemCell>
<MarketItemCell>
<ActionButton
onClick={(e) => handleBuy(item.id, e)}
disabled={item.buyPrice ? cash < item.buyPrice : true}
disabled={
getItemPrice(item) ? cash < getItemPrice(item)! : true
}
>
Buy
</ActionButton>

View file

@ -2,7 +2,7 @@ import React from 'react'
import { useGameStore } from '../store/useGameStore'
import { UPGRADES } from '../constants'
import { styled } from '@linaria/react'
import type { UpgradeCost } from '../types'
import type { UpgradeCost, Upgrade } from '../types'
const TempleContainer = styled.div`
padding: 1rem;
@ -15,6 +15,46 @@ const Title = styled.h2`
text-align: center;
`
const PrayerSection = styled.div`
background-color: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: center;
`
const PietyDisplay = styled.div`
font-size: 1.25rem;
color: #4b5563;
margin-bottom: 1rem;
`
const PrayerButton = styled.button<{ disabled?: boolean }>`
padding: 1rem 2rem;
font-size: 1.25rem;
border-radius: 0.5rem;
background-color: ${(props) => (props.disabled ? '#9ca3af' : '#8b5cf6')};
color: white;
font-weight: bold;
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
border: none;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0 auto;
&:hover:not(:disabled) {
background-color: #7c3aed;
transform: translateY(-1px);
}
&:active:not(:disabled) {
transform: translateY(0);
}
`
const UpgradesGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
@ -23,7 +63,7 @@ const UpgradesGrid = styled.div`
`
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-radius: 0.5rem;
padding: 1rem;
@ -59,35 +99,91 @@ const CostItem = styled.div`
const PurchaseButton = styled.button<{ disabled?: boolean }>`
padding: 0.5rem 1rem;
border-radius: 0.25rem;
background-color: ${props => props.disabled ? '#9ca3af' : '#3b82f6'};
background-color: ${(props) => (props.disabled ? '#9ca3af' : '#3b82f6')};
color: white;
font-weight: bold;
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
border: none;
margin-top: auto;
`
const TempleComponent: React.FC = () => {
const { inventory, purchasedUpgrades, purchaseUpgrade } = useGameStore()
const {
inventory,
purchasedUpgrades,
purchaseUpgrade,
pray,
piety,
actionCooldown,
} = useGameStore()
const handlePurchase = (upgradeId: string) => {
purchaseUpgrade(upgradeId)
}
const canAffordUpgrade = (costs: UpgradeCost[]) => {
return costs.every(cost =>
return costs.every(
(cost) =>
inventory[cost.itemId] !== undefined &&
inventory[cost.itemId] >= cost.amount
inventory[cost.itemId] >= cost.amount,
)
}
const canPurchaseUpgrade = (upgrade: Upgrade) => {
// Check if already purchased
if (purchasedUpgrades.includes(upgrade.id)) {
return false
}
// Check if can afford
if (!canAffordUpgrade(upgrade.cost)) {
return false
}
// Check requirements
if (
upgrade.id === 'aqueous_vigor_2' &&
!purchasedUpgrades.includes('aqueous_vigor_1')
) {
return false
}
return true
}
const getUpgradeStatus = (upgrade: Upgrade) => {
if (purchasedUpgrades.includes(upgrade.id)) {
return 'Purchased'
}
if (
upgrade.id === 'aqueous_vigor_2' &&
!purchasedUpgrades.includes('aqueous_vigor_1')
) {
return 'Requires Aqueous Vigor I'
}
if (!canAffordUpgrade(upgrade.cost)) {
return 'Cannot Afford'
}
return 'Purchase'
}
return (
<TempleContainer>
<Title>Temple</Title>
<PrayerSection>
<PietyDisplay>Piety Level: {piety}</PietyDisplay>
<PrayerButton onClick={() => pray()} disabled={actionCooldown > 0}>
🙏 Pray to the Gods
{actionCooldown > 0 && ` (${(actionCooldown / 1000).toFixed(1)}s)`}
</PrayerButton>
</PrayerSection>
<UpgradesGrid>
{Object.values(UPGRADES).map((upgrade) => {
const isPurchased = purchasedUpgrades.includes(upgrade.id)
const canAfford = canAffordUpgrade(upgrade.cost)
const canPurchase = canPurchaseUpgrade(upgrade)
const status = getUpgradeStatus(upgrade)
return (
<UpgradeCard key={upgrade.id} purchased={isPurchased}>
@ -97,18 +193,18 @@ const TempleComponent: React.FC = () => {
<span>Cost:</span>
{upgrade.cost.map((cost, index) => (
<CostItem key={index}>
<span>{cost.amount} {cost.itemId}</span>
<span>
(You have: {inventory[cost.itemId] || 0})
{cost.amount} {cost.itemId}
</span>
<span>(You have: {inventory[cost.itemId] || 0})</span>
</CostItem>
))}
</UpgradeCost>
<PurchaseButton
onClick={() => handlePurchase(upgrade.id)}
disabled={isPurchased || !canAfford}
disabled={!canPurchase}
>
{isPurchased ? 'Purchased' : 'Purchase'}
{status}
</PurchaseButton>
</UpgradeCard>
)

View file

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

View file

@ -54,17 +54,46 @@ export const hasSaveInSlot = (slot: number): boolean => {
return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`)
}
const addConsoleMessage = (state: GameState, text: string) => (
state.consoleMessages = [
const addConsoleMessage = (state: GameState, text: string) =>
(state.consoleMessages = [
{
id: Math.random().toString(36).substring(7),
text,
timestamp: Date.now(),
},
...state.consoleMessages,
].slice(0, 50)
)
].slice(0, 50))
const progressRandomImmaturePlot = (
state: GameState,
growthAmount: number,
): boolean => {
// Find all immature plants
const immaturePlots: { row: number; col: number }[] = []
state.plots.forEach((row, rowIndex) => {
row.forEach((plot, colIndex) => {
if (plot.current && !plot.current.mature) {
immaturePlots.push({ row: rowIndex, col: colIndex })
}
})
})
if (immaturePlots.length > 0) {
// Select a random immature plot and progress it
const randomPlot =
immaturePlots[Math.floor(Math.random() * immaturePlots.length)]
const plot = state.plots[randomPlot.row][randomPlot.col]
const crop = CROPS[plot.current!.cropId]
const actualGrowthAmount = crop.growthTicks * growthAmount
plot.current!.progress = Math.min(
crop.growthTicks,
plot.current!.progress + actualGrowthAmount,
)
return true
}
return false
}
export const useGameStore = create<
GameState & {
@ -81,6 +110,7 @@ export const useGameStore = create<
saveToSlot: (slot: number) => void
loadFromSlot: (slot: number) => void
purchaseUpgrade: (upgradeId: string) => void
pray: () => void
}
>((set, get) => ({
cash: INITIAL_CASH,
@ -94,6 +124,8 @@ export const useGameStore = create<
tickCount: 0,
consoleMessages: [],
purchasedUpgrades: [],
piety: 50,
landPurchases: 0,
assignCrop: (row, col, cropId) => {
set(
@ -140,16 +172,16 @@ export const useGameStore = create<
const plot = plots[row][col]
if (plot.current && plot.moisture < 1) {
// Calculate cooldown reduction from upgrades
let cooldownReduction = 0
// Calculate cooldown reduction from upgrades (multiplicative)
let cooldownMultiplier = 1
if (purchasedUpgrades.includes('aqueous_vigor_1')) {
cooldownReduction += 0.25 // 25% reduction
cooldownMultiplier *= 0.75 // 25% reduction
}
if (purchasedUpgrades.includes('aqueous_vigor_2')) {
cooldownMultiplier *= 0.75 // Additional 25% reduction
}
const finalCooldown = Math.max(
0,
COOLDOWN_DURATION * (1 - cooldownReduction)
)
const finalCooldown = Math.max(0, COOLDOWN_DURATION * cooldownMultiplier)
set(
produce((state) => {
@ -290,21 +322,57 @@ export const useGameStore = create<
},
buyItem: (itemId) => {
const { cash } = get()
const { cash, landPurchases, plots, fieldSize } = get()
const item = MARKET_ITEMS[itemId]
if (!item || item.buyPrice === null || item.buyPrice === undefined) {
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
}
set(
produce((state) => {
state.cash -= item.buyPrice!
state.cash -= price
// 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
}
}),
)
},
@ -346,6 +414,7 @@ export const useGameStore = create<
saveToSlot,
loadFromSlot,
purchaseUpgrade,
pray,
...gameState
} = state
saveGame(slot, gameState)
@ -368,7 +437,9 @@ export const useGameStore = create<
// Check if player can afford all costs
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) {
@ -378,14 +449,50 @@ export const useGameStore = create<
set(
produce((state) => {
// Deduct all costs
upgrade.cost.forEach(cost => {
upgrade.cost.forEach((cost) => {
state.inventory[cost.itemId] -= cost.amount
})
state.purchasedUpgrades.push(upgradeId)
addConsoleMessage(state, `Purchased upgrade: ${upgrade.name}`)
}),
)
},
pray: () => {
const { actionCooldown, piety } = get()
if (actionCooldown > 0) {
return
}
set(
produce((state) => {
const roll = Math.random()
const result = roll * piety
if (result < 50 || roll < 0.3) {
state.piety += 1
addConsoleMessage(
state,
`Purchased upgrade: ${upgrade.name}`
'Nothing happens, but you feel the approval of the gods',
)
} else if (result > 100) {
if (progressRandomImmaturePlot(state, 0.3)) {
addConsoleMessage(
state,
'The gods have bestowed a significant blessing on one of your crops',
)
}
} else if (result > 50) {
if (progressRandomImmaturePlot(state, 0.1)) {
addConsoleMessage(
state,
'The gods have bestowed a minor blessing on one of your crops',
)
}
}
state.actionCooldown = 3000
}),
)
},

View file

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