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;
+ };
+}