Add a basic implementation of the market tab.
This commit is contained in:
parent
345bb0f44f
commit
d4dda5b3e4
|
|
@ -3,6 +3,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/CustomTab
|
||||||
import { useGameTick } from './hooks/useGameTick';
|
import { useGameTick } from './hooks/useGameTick';
|
||||||
import Field from './components/Field';
|
import Field from './components/Field';
|
||||||
import Warehouse from './components/Warehouse';
|
import Warehouse from './components/Warehouse';
|
||||||
|
import Market from './components/Market';
|
||||||
import { ActionCooldown } from './components/ActionCooldown';
|
import { ActionCooldown } from './components/ActionCooldown';
|
||||||
|
|
||||||
const appContainerStyle: React.CSSProperties = {
|
const appContainerStyle: React.CSSProperties = {
|
||||||
|
|
@ -28,7 +29,7 @@ function App() {
|
||||||
<TabsList style={tabsListStyles}>
|
<TabsList style={tabsListStyles}>
|
||||||
<TabsTrigger value="fields">Fields</TabsTrigger>
|
<TabsTrigger value="fields">Fields</TabsTrigger>
|
||||||
<TabsTrigger value="warehouse">Warehouse</TabsTrigger>
|
<TabsTrigger value="warehouse">Warehouse</TabsTrigger>
|
||||||
<TabsTrigger value="market" disabled>Market</TabsTrigger>
|
<TabsTrigger value="market">Market</TabsTrigger>
|
||||||
<TabsTrigger value="temple" disabled>Temple</TabsTrigger>
|
<TabsTrigger value="temple" disabled>Temple</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="fields">
|
<TabsContent value="fields">
|
||||||
|
|
@ -37,6 +38,9 @@ function App() {
|
||||||
<TabsContent value="warehouse">
|
<TabsContent value="warehouse">
|
||||||
<Warehouse />
|
<Warehouse />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="market">
|
||||||
|
<Market />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
53
src/components/FloatingMessage.tsx
Normal file
53
src/components/FloatingMessage.tsx
Normal file
|
|
@ -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<FloatingMessageProps> = ({ 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 (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: startPosition.x,
|
||||||
|
top: position.y,
|
||||||
|
opacity,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: 100
|
||||||
|
}}>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FloatingMessage;
|
||||||
220
src/components/Market.tsx
Normal file
220
src/components/Market.tsx
Normal file
|
|
@ -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<FloatingMessageData[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<MarketContainer>
|
||||||
|
<Title>Market</Title>
|
||||||
|
|
||||||
|
<CashDisplay>
|
||||||
|
<CashAmount>Cash: ${cash.toFixed(2)}</CashAmount>
|
||||||
|
</CashDisplay>
|
||||||
|
|
||||||
|
{sellableItems.length > 0 && (
|
||||||
|
<>
|
||||||
|
<SectionTitle>Sell Items</SectionTitle>
|
||||||
|
<MarketTable>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<TableHeader>Item</TableHeader>
|
||||||
|
<TableHeader>Sell Price</TableHeader>
|
||||||
|
<TableHeader>Quantity</TableHeader>
|
||||||
|
<TableHeader>Action</TableHeader>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sellableItems.map(item => (
|
||||||
|
<MarketItemRow key={`sell-${item.id}`}>
|
||||||
|
<MarketItemCell>
|
||||||
|
<ItemIcon>{item.emoji}</ItemIcon>
|
||||||
|
{item.name}
|
||||||
|
</MarketItemCell>
|
||||||
|
<MarketItemCell>${item.sellPrice}</MarketItemCell>
|
||||||
|
<MarketItemCell>{inventory[item.id] || 0}</MarketItemCell>
|
||||||
|
<MarketItemCell>
|
||||||
|
<ActionButton
|
||||||
|
onClick={(e) => handleSell(item.id, e)}
|
||||||
|
disabled={!inventory[item.id] || inventory[item.id] <= 0}
|
||||||
|
>
|
||||||
|
Sell
|
||||||
|
</ActionButton>
|
||||||
|
</MarketItemCell>
|
||||||
|
</MarketItemRow>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</MarketTable>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SectionTitle>Buy Items</SectionTitle>
|
||||||
|
<MarketTable>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<TableHeader>Item</TableHeader>
|
||||||
|
<TableHeader>Buy Price</TableHeader>
|
||||||
|
<TableHeader>Action</TableHeader>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{buyableItems.map(item => (
|
||||||
|
<MarketItemRow key={`buy-${item.id}`}>
|
||||||
|
<MarketItemCell>
|
||||||
|
<ItemIcon>{item.emoji}</ItemIcon>
|
||||||
|
{item.name}
|
||||||
|
</MarketItemCell>
|
||||||
|
<MarketItemCell>${item.buyPrice}</MarketItemCell>
|
||||||
|
<MarketItemCell>
|
||||||
|
<ActionButton
|
||||||
|
onClick={(e) => handleBuy(item.id, e)}
|
||||||
|
disabled={item.buyPrice ? cash < item.buyPrice : true}
|
||||||
|
>
|
||||||
|
Buy
|
||||||
|
</ActionButton>
|
||||||
|
</MarketItemCell>
|
||||||
|
</MarketItemRow>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</MarketTable>
|
||||||
|
|
||||||
|
{floatingMessages.map(msg => (
|
||||||
|
<FloatingMessage
|
||||||
|
key={msg.id}
|
||||||
|
message={msg.message}
|
||||||
|
startPosition={msg.position}
|
||||||
|
onComplete={() => removeFloatingMessage(msg.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</MarketContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarketComponent;
|
||||||
|
|
@ -33,3 +33,18 @@ export const CROPS: CropDefinitions = {
|
||||||
export const INITIAL_INVENTORY = {
|
export const INITIAL_INVENTORY = {
|
||||||
'celery_seed': 8
|
'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<string, MarketItem> = {
|
||||||
|
'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 },
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ import {
|
||||||
INITIAL_INVENTORY,
|
INITIAL_INVENTORY,
|
||||||
CROPS,
|
CROPS,
|
||||||
INITIAL_GAME_SPEED,
|
INITIAL_GAME_SPEED,
|
||||||
COOLDOWN_DURATION
|
COOLDOWN_DURATION,
|
||||||
|
MARKET_ITEMS
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { GameState, PlotState } from '../types';
|
import { GameState, PlotState } from '../types';
|
||||||
|
|
||||||
|
|
@ -28,6 +29,8 @@ export const useGameStore = create<GameState & {
|
||||||
upgradeField: () => void;
|
upgradeField: () => void;
|
||||||
setGameSpeed: (speed: number) => void;
|
setGameSpeed: (speed: number) => void;
|
||||||
setActionCooldown: (cooldown: number) => void;
|
setActionCooldown: (cooldown: number) => void;
|
||||||
|
buyItem: (itemId: string) => void;
|
||||||
|
sellItem: (itemId: string) => void;
|
||||||
}>((set, get) => ({
|
}>((set, get) => ({
|
||||||
cash: INITIAL_CASH,
|
cash: INITIAL_CASH,
|
||||||
inventory: INITIAL_INVENTORY,
|
inventory: INITIAL_INVENTORY,
|
||||||
|
|
@ -193,5 +196,37 @@ export const useGameStore = create<GameState & {
|
||||||
set(produce(state => {
|
set(produce(state => {
|
||||||
state.actionCooldown = cooldown;
|
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;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -36,3 +36,13 @@ export interface GameState {
|
||||||
gameSpeed: number;
|
gameSpeed: number;
|
||||||
actionCooldown: number;
|
actionCooldown: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MarketTransaction {
|
||||||
|
itemId: string;
|
||||||
|
amount: number;
|
||||||
|
type: 'buy' | 'sell';
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue