Improve the floating message API to better handle multiple concurrent messages
This commit is contained in:
parent
e21042d08b
commit
9a9ec999ff
|
|
@ -1,59 +0,0 @@
|
|||
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
|
||||
74
src/components/FloatingMessages.tsx
Normal file
74
src/components/FloatingMessages.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import React, { useEffect, useState, useImperativeHandle, forwardRef } from 'react'
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
text: string
|
||||
position: { x: number; y: number }
|
||||
}
|
||||
|
||||
export interface FloatingMessagesHandle {
|
||||
addMessage: (text: string, position: { x: number; y: number }) => void
|
||||
}
|
||||
|
||||
const FloatingMessages = forwardRef<FloatingMessagesHandle>((_, ref) => {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
addMessage: (text: string, position: { x: number; y: number }) => {
|
||||
const newMessage: Message = {
|
||||
id: Math.random().toString(36).substring(2),
|
||||
text,
|
||||
position,
|
||||
}
|
||||
setMessages(prev => [...prev, newMessage])
|
||||
}
|
||||
}))
|
||||
|
||||
const handleAnimationEnd = (messageId: string) => {
|
||||
setMessages(prev => prev.filter(msg => msg.id !== messageId))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
@keyframes floatUp {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.floating-message {
|
||||
position: absolute;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
animation: floatUp 1s ease-out forwards;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
{messages.map(message => (
|
||||
<div
|
||||
key={message.id}
|
||||
className="floating-message"
|
||||
style={{
|
||||
left: message.position.x,
|
||||
top: message.position.y,
|
||||
}}
|
||||
onAnimationEnd={() => handleAnimationEnd(message.id)}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
FloatingMessages.displayName = 'FloatingMessages'
|
||||
|
||||
export default FloatingMessages
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import { useGameStore } from '../store/useGameStore'
|
||||
import { MARKET_ITEMS } from '../constants'
|
||||
import FloatingMessage from './FloatingMessage'
|
||||
import FloatingMessage, { FloatingMessagesHandle } from './FloatingMessages'
|
||||
import { styled } from '@linaria/react'
|
||||
|
||||
const MarketContainer = styled.div`
|
||||
|
|
@ -71,17 +71,9 @@ const ActionButton = styled.button<{ disabled?: boolean }>`
|
|||
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 floatingMessagesRef = useRef<FloatingMessagesHandle>(null)
|
||||
|
||||
const handleBuy = (itemId: string, e: React.MouseEvent) => {
|
||||
const item = MARKET_ITEMS[itemId]
|
||||
|
|
@ -92,14 +84,10 @@ const MarketComponent: React.FC = () => {
|
|||
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,
|
||||
},
|
||||
])
|
||||
floatingMessagesRef.current?.addMessage(
|
||||
`+1 ${item.emoji} ${item.name}`,
|
||||
position
|
||||
)
|
||||
}
|
||||
|
||||
const handleSell = (itemId: string, e: React.MouseEvent) => {
|
||||
|
|
@ -117,18 +105,10 @@ const MarketComponent: React.FC = () => {
|
|||
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))
|
||||
floatingMessagesRef.current?.addMessage(
|
||||
`-1 ${item.emoji} ${item.name}`,
|
||||
position
|
||||
)
|
||||
}
|
||||
|
||||
const sellableItems = Object.entries(MARKET_ITEMS)
|
||||
|
|
@ -216,14 +196,7 @@ const MarketComponent: React.FC = () => {
|
|||
</tbody>
|
||||
</MarketTable>
|
||||
|
||||
{floatingMessages.map((msg) => (
|
||||
<FloatingMessage
|
||||
key={msg.id}
|
||||
message={msg.message}
|
||||
startPosition={msg.position}
|
||||
onComplete={() => removeFloatingMessage(msg.id)}
|
||||
/>
|
||||
))}
|
||||
<FloatingMessage ref={floatingMessagesRef} />
|
||||
</MarketContainer>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { CropDefinitions } from '../types'
|
||||
|
||||
export const INITIAL_CASH = 50
|
||||
export const INITIAL_FIELD_SIZE = 4
|
||||
export const INITIAL_FIELD_SIZE = 3
|
||||
export const COOLDOWN_DURATION = 2000 // 2 seconds in milliseconds
|
||||
export const TICK_INTERVAL = 12000 // 12 seconds in milliseconds
|
||||
export const GAME_SPEEDS = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue