Fly shop apples out from the creature that made them.
This commit is contained in:
@@ -67,6 +67,7 @@ export function CardView({
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
data-card-id={card.id || undefined}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
role={onClick ? 'button' : undefined}
|
||||
onMouseEnter={preview ? undefined : (e) => setHover({ x: e.clientX, y: e.clientY })}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import type { Card, ClientMessage, GameView, PlayerView } from '../types'
|
||||
import { CardView } from './CardView'
|
||||
|
||||
interface Flyer {
|
||||
key: string
|
||||
emoji: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
view: GameView
|
||||
you: PlayerView
|
||||
@@ -23,6 +31,44 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [you.deck])
|
||||
|
||||
// --- spawn animation: apples/bees fly out from the creature that made them.
|
||||
// The log tells us which card (source) spawned what; we fly an emoji from
|
||||
// that card's on-screen position. For sells the source is already gone, so
|
||||
// we grab its rect at click time (see act()).
|
||||
const seenSeqRef = useRef<number | null>(null)
|
||||
const capturedRef = useRef<Map<string, DOMRect>>(new Map())
|
||||
const [flyers, setFlyers] = useState<Flyer[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const log = view.log ?? []
|
||||
const last = log.length ? log[log.length - 1].seq : 0
|
||||
// First render just marks where we came in, so we never replay history.
|
||||
if (seenSeqRef.current === null) {
|
||||
seenSeqRef.current = last
|
||||
return
|
||||
}
|
||||
if (last <= seenSeqRef.current) return
|
||||
const fresh = log.filter((e) => e.seq > seenSeqRef.current! && e.spawn && e.source)
|
||||
seenSeqRef.current = last
|
||||
const added: Flyer[] = []
|
||||
for (const e of fresh) {
|
||||
const src = e.source!
|
||||
let rect: DOMRect | undefined
|
||||
const el = document.querySelector(`[data-card-id="${src}"]`)
|
||||
if (el) rect = el.getBoundingClientRect()
|
||||
else if (capturedRef.current.has(src)) rect = capturedRef.current.get(src)
|
||||
capturedRef.current.delete(src)
|
||||
if (!rect) continue
|
||||
added.push({
|
||||
key: `spawn-${e.seq}`,
|
||||
emoji: e.spawn === 'bee' ? '🐝' : '🍎',
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2,
|
||||
})
|
||||
}
|
||||
if (added.length) setFlyers((f) => [...f, ...added])
|
||||
}, [view.log])
|
||||
|
||||
function toggle(id: string) {
|
||||
setSelected((sel) =>
|
||||
sel.includes(id) ? sel.filter((s) => s !== id) : [...sel, id],
|
||||
@@ -43,6 +89,14 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
selectedCards.every((c) => c.kind === 'pet')
|
||||
|
||||
function act(msg: ClientMessage) {
|
||||
// Sold cards vanish before the apple entry arrives, so remember where they
|
||||
// were now, keyed by id, for the spawn animation to fly from.
|
||||
if (msg.type === 'sell') {
|
||||
for (const id of msg.cards) {
|
||||
const el = document.querySelector(`[data-card-id="${id}"]`)
|
||||
if (el) capturedRef.current.set(id, el.getBoundingClientRect())
|
||||
}
|
||||
}
|
||||
send(msg)
|
||||
setSelected([])
|
||||
}
|
||||
@@ -193,6 +247,25 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{flyers.length > 0 &&
|
||||
createPortal(
|
||||
<>
|
||||
{flyers.map((fl) => (
|
||||
<div
|
||||
key={fl.key}
|
||||
className="spawn-flyer"
|
||||
style={{ left: fl.x, top: fl.y }}
|
||||
onAnimationEnd={() =>
|
||||
setFlyers((f) => f.filter((x) => x.key !== fl.key))
|
||||
}
|
||||
>
|
||||
{fl.emoji}
|
||||
</div>
|
||||
))}
|
||||
</>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user