// PuckSprite โ the flying creature + its speech bubble.
// Forms are swappable; the sprite layer mounts standalone in overlay mode.
import * as React from "react";
import type { ReactionKind, StyleTag, Tier } from "../engine";
export type SpriteForm = "mossling" | "wisp" | "gremlin" | "moth";
export type BubbleStyle = "voice" | "plain" | "hand";
// the creature body in a given form (also reused in panels/toasts at small size)
export function SpriteBody({ form = "mossling" }: { form?: SpriteForm }) {
if (form === "wisp") {
return (
);
}
if (form === "gremlin") {
return (
);
}
if (form === "moth") {
return (
);
}
// mossling (default)
return (
);
}
export interface SpriteState {
x: number;
y: number;
flying: boolean;
resting: boolean;
facing: "left" | "right";
}
export function PuckSprite({
sprite,
form,
speaking,
alert = false,
reaction = null,
shout,
reactKey = 0,
muted = false,
camo = false,
onPoke,
onMenu,
}: {
sprite: SpriteState;
form: SpriteForm;
speaking: boolean;
/** Something is waiting (bubble/toast/interrupt) โ pulse to draw the eye,
* the overlay's answer to a sprite lost on a busy desktop. */
alert?: boolean;
/** Muted: watching but silent โ calm the glow and show a ๐ badge. */
muted?: boolean;
/** Camo: blended into the desktop โ glassy/transparent skin, faint eyes. */
camo?: boolean;
/** One-shot personality gesture (null between reactions). */
reaction?: ReactionKind | null;
/** Optional exclamation that floats over the sprite during the reaction. */
shout?: string;
/** Monotonic counter โ bumping it remounts the animated layers so the SAME
* reaction restarts its CSS animation (back-to-back pushes still pop). */
reactKey?: number;
onPoke: () => void;
/** Right-click โ the overlay has no menu bar, so the sprite IS the menu. */
onMenu?: () => void;
}) {
const cls = ["puck"];
if (sprite.flying) cls.push("flying", "snappy");
if (sprite.resting) cls.push("resting");
if (sprite.facing === "left") cls.push("face-left");
if (speaking) cls.push("speaking");
if (alert) cls.push("alert");
if (reaction) cls.push(`react-${reaction}`);
if (muted) cls.push("muted");
if (camo) cls.push("camo");
return (
{shout && (
// sibling of .puck-bob so the body's scale/spin doesn't warp the text
{shout}
)}
{muted &&
๐
}
{
if (!onMenu) return;
e.preventDefault();
onMenu();
}}
title="poke Puck ยท right-click for menu"
/>