Fly shop apples out from the creature that made them.
This commit is contained in:
@@ -67,6 +67,7 @@ export function CardView({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classes}
|
className={classes}
|
||||||
|
data-card-id={card.id || undefined}
|
||||||
onClick={disabled ? undefined : onClick}
|
onClick={disabled ? undefined : onClick}
|
||||||
role={onClick ? 'button' : undefined}
|
role={onClick ? 'button' : undefined}
|
||||||
onMouseEnter={preview ? undefined : (e) => setHover({ x: e.clientX, y: e.clientY })}
|
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 type { Card, ClientMessage, GameView, PlayerView } from '../types'
|
||||||
import { CardView } from './CardView'
|
import { CardView } from './CardView'
|
||||||
|
|
||||||
|
interface Flyer {
|
||||||
|
key: string
|
||||||
|
emoji: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
view: GameView
|
view: GameView
|
||||||
you: PlayerView
|
you: PlayerView
|
||||||
@@ -23,6 +31,44 @@ export function ShopPhase({ view, you, send }: Props) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [you.deck])
|
}, [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) {
|
function toggle(id: string) {
|
||||||
setSelected((sel) =>
|
setSelected((sel) =>
|
||||||
sel.includes(id) ? sel.filter((s) => s !== id) : [...sel, id],
|
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')
|
selectedCards.every((c) => c.kind === 'pet')
|
||||||
|
|
||||||
function act(msg: ClientMessage) {
|
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)
|
send(msg)
|
||||||
setSelected([])
|
setSelected([])
|
||||||
}
|
}
|
||||||
@@ -193,6 +247,25 @@ export function ShopPhase({ view, you, send }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1048,6 +1048,31 @@ h3 {
|
|||||||
animation-name: summon-from-left;
|
animation-name: summon-from-left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Apple/bee spawned in the shop, flying out from the creature that made it. */
|
||||||
|
@keyframes spawn-fly {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(0.4) rotate(-10deg);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -85%) scale(1.35) rotate(4deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -165%) scale(1) rotate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spawn-flyer {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 60;
|
||||||
|
font-size: 2rem;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: spawn-fly 1000ms ease-out forwards;
|
||||||
|
filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
/* --- foods waiting for a pet --- */
|
/* --- foods waiting for a pet --- */
|
||||||
|
|
||||||
.pending-foods {
|
.pending-foods {
|
||||||
|
|||||||
Reference in New Issue
Block a user