diff --git a/src/App.tsx b/src/App.tsx index 068a1ac..e5dd764 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/CustomTab import { useGameTick } from './hooks/useGameTick'; import Field from './components/Field'; import Warehouse from './components/Warehouse'; +import Market from './components/Market'; import { ActionCooldown } from './components/ActionCooldown'; const appContainerStyle: React.CSSProperties = { @@ -28,7 +29,7 @@ function App() { Fields Warehouse - Market + Market Temple @@ -37,6 +38,9 @@ function App() { + + + diff --git a/src/components/FloatingMessage.tsx b/src/components/FloatingMessage.tsx new file mode 100644 index 0000000..fb56d1a --- /dev/null +++ b/src/components/FloatingMessage.tsx @@ -0,0 +1,53 @@ +import React, { useEffect, useState } from 'react'; + +interface FloatingMessageProps { + message: string; + startPosition: { x: number, y: number }; + onComplete: () => void; +} + +const FloatingMessage: React.FC = ({ message, startPosition, onComplete }) => { + const [position, setPosition] = useState({ y: startPosition.y }); + const [opacity, setOpacity] = useState(1); + + useEffect(() => { + const startTime = Date.now(); + const duration = 1000; // 1 second animation + + const animate = () => { + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + + setPosition({ y: startPosition.y - progress * 50 }); + setOpacity(1 - progress); + + if (progress < 1) { + requestAnimationFrame(animate); + } else { + onComplete(); + } + }; + + const animationFrame = requestAnimationFrame(animate); + + return () => { + cancelAnimationFrame(animationFrame); + }; + }, [startPosition, onComplete]); + + return ( +
+ {message} +
+ ); +}; + +export default FloatingMessage; diff --git a/src/components/Market.tsx b/src/components/Market.tsx new file mode 100644 index 0000000..6dc3e1a --- /dev/null +++ b/src/components/Market.tsx @@ -0,0 +1,220 @@ +import React, { useState } from 'react'; +import { useGameStore } from '../store/useGameStore'; +import { MARKET_ITEMS } from '../constants'; +import FloatingMessage from './FloatingMessage'; +import { styled } from '@linaria/react'; + +const MarketContainer = styled.div` + padding: 1rem; +`; + +const Title = styled.h2` + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1rem; + text-align: center; +`; + +const CashDisplay = styled.div` + text-align: center; + margin-bottom: 1.5rem; + padding: 1rem; + background-color: #dcfce7; + border-radius: 0.375rem; +`; + +const CashAmount = styled.h3` + font-size: 1.25rem; + font-weight: 500; +`; + +const SectionTitle = styled.h3` + font-size: 1.25rem; + font-weight: 500; + margin-bottom: 0.5rem; + margin-top: 1.5rem; +`; + +const MarketTable = styled.table` + width: 100%; + border-collapse: separate; + border-spacing: 0 0.5rem; +`; + +const MarketItemRow = styled.tr` + background-color: #f3f4f6; + border-radius: 0.5rem; +`; + +const MarketItemCell = styled.td` + padding: 0.75rem; + text-align: center; +`; + +const TableHeader = styled.th` + padding: 0.75rem; + text-align: center; +`; + +const ItemIcon = styled.span` + font-size: 1.5rem; + margin-right: 0.5rem; +`; + +const ActionButton = styled.button<{ disabled?: boolean }>` + padding: 0.5rem 1rem; + border-radius: 0.25rem; + background-color: ${props => props.disabled ? '#9ca3af' : '#3b82f6'}; + color: white; + font-weight: bold; + cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'}; + border: none; +`; + +interface FloatingMessageData { + id: string; + message: string; + position: { x: number, y: number }; +} + +const MarketComponent: React.FC = () => { + const { inventory, cash, buyItem, sellItem } = useGameStore(); + const [floatingMessages, setFloatingMessages] = useState([]); + + const handleBuy = (itemId: string, e: React.MouseEvent) => { + const item = MARKET_ITEMS[itemId]; + if (!item || item.buyPrice === null || cash < item.buyPrice) return; + + buyItem(itemId); + + const rect = (e.target as HTMLElement).getBoundingClientRect(); + const position = { x: rect.left + rect.width / 2, y: rect.top }; + + setFloatingMessages([ + ...floatingMessages, + { + id: `buy-${itemId}-${Date.now()}`, + message: `+1 ${item.emoji} ${item.name}`, + position + } + ]); + }; + + const handleSell = (itemId: string, e: React.MouseEvent) => { + const item = MARKET_ITEMS[itemId]; + if (!item || item.sellPrice === null || !inventory[itemId] || inventory[itemId] <= 0) return; + + sellItem(itemId); + + const rect = (e.target as HTMLElement).getBoundingClientRect(); + const position = { x: rect.left + rect.width / 2, y: rect.top }; + + setFloatingMessages([ + ...floatingMessages, + { + id: `sell-${itemId}-${Date.now()}`, + message: `-1 ${item.emoji} ${item.name}`, + position + } + ]); + }; + + const removeFloatingMessage = (messageId: string) => { + setFloatingMessages(floatingMessages.filter(msg => msg.id !== messageId)); + }; + + const sellableItems = Object.entries(MARKET_ITEMS) + .filter(([_, item]) => item.sellPrice !== null && inventory[item.id] && inventory[item.id] > 0) + .map(([_, item]) => item); + + const buyableItems = Object.entries(MARKET_ITEMS) + .filter(([_, item]) => item.buyPrice !== null) + .map(([_, item]) => item); + + return ( + + Market + + + Cash: ${cash.toFixed(2)} + + + {sellableItems.length > 0 && ( + <> + Sell Items + + + + Item + Sell Price + Quantity + Action + + + + {sellableItems.map(item => ( + + + {item.emoji} + {item.name} + + ${item.sellPrice} + {inventory[item.id] || 0} + + handleSell(item.id, e)} + disabled={!inventory[item.id] || inventory[item.id] <= 0} + > + Sell + + + + ))} + + + + )} + + Buy Items + + + + Item + Buy Price + Action + + + + {buyableItems.map(item => ( + + + {item.emoji} + {item.name} + + ${item.buyPrice} + + handleBuy(item.id, e)} + disabled={item.buyPrice ? cash < item.buyPrice : true} + > + Buy + + + + ))} + + + + {floatingMessages.map(msg => ( + removeFloatingMessage(msg.id)} + /> + ))} + + ); +}; + +export default MarketComponent; diff --git a/src/constants/index.ts b/src/constants/index.ts index 59fbeaa..5b9fd6c 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -33,3 +33,18 @@ export const CROPS: CropDefinitions = { export const INITIAL_INVENTORY = { 'celery_seed': 8 }; + +export interface MarketItem { + id: string; + name: string; + emoji: string; + buyPrice: number | null; // null means not buyable + sellPrice: number | null; // null means not sellable +} + +export const MARKET_ITEMS: Record = { + 'celery_seed': { id: 'celery_seed', name: 'Celery Seeds', emoji: '🌰', buyPrice: 1, sellPrice: null }, + 'corn_seed': { id: 'corn_seed', name: 'Corn Seeds', emoji: '🌰', buyPrice: 5, sellPrice: null }, + 'celery': { id: 'celery', name: 'Celery', emoji: '🥬', buyPrice: null, sellPrice: 5 }, + 'corn': { id: 'corn', name: 'Corn', emoji: '🌽', buyPrice: null, sellPrice: 8 }, +}; diff --git a/src/store/useGameStore.ts b/src/store/useGameStore.ts index b158875..13358a3 100644 --- a/src/store/useGameStore.ts +++ b/src/store/useGameStore.ts @@ -7,7 +7,8 @@ import { INITIAL_INVENTORY, CROPS, INITIAL_GAME_SPEED, - COOLDOWN_DURATION + COOLDOWN_DURATION, + MARKET_ITEMS } from '../constants'; import { GameState, PlotState } from '../types'; @@ -28,6 +29,8 @@ export const useGameStore = create void; setGameSpeed: (speed: number) => void; setActionCooldown: (cooldown: number) => void; + buyItem: (itemId: string) => void; + sellItem: (itemId: string) => void; }>((set, get) => ({ cash: INITIAL_CASH, inventory: INITIAL_INVENTORY, @@ -193,5 +196,37 @@ export const useGameStore = create { state.actionCooldown = cooldown; })); + }, + + buyItem: (itemId) => { + const { cash } = get(); + const item = MARKET_ITEMS[itemId]; + + if (!item || item.buyPrice === null) { + return; + } + + if (cash < item.buyPrice) { + return; + } + + set(produce(state => { + state.cash -= item.buyPrice; + state.inventory[itemId] = (state.inventory[itemId] || 0) + 1; + })); + }, + + sellItem: (itemId) => { + const { inventory } = get(); + const item = MARKET_ITEMS[itemId]; + + if (!item || item.sellPrice === null || !inventory[itemId] || inventory[itemId] <= 0) { + return; + } + + set(produce(state => { + state.cash += item.sellPrice; + state.inventory[itemId] -= 1; + })); } })); diff --git a/src/types/index.ts b/src/types/index.ts index 3e2efe5..fb12142 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -36,3 +36,13 @@ export interface GameState { gameSpeed: number; actionCooldown: number; } + +export interface MarketTransaction { + itemId: string; + amount: number; + type: 'buy' | 'sell'; + position: { + x: number; + y: number; + }; +}