// A crow on screen. This interface mirrors CrowModel in RIVE_CONTRACT.md, // so a Rive-backed view can replace the SVG stand-in without touching main.ts. export type Role = "crow" | "sentry" | "elder"; export type Pose = "perched" | "flying" | "waiting" | "grounded"; export type Reaction = "cheer" | "sulk" | "levelUp" | "levelDown" | "prank"; export interface CrowView { readonly el: HTMLElement; setPose(pose: Pose): void; setTrust(level: number): void; fire(reaction: Reaction): void; } const NS = "http://www.w3.org/2000/svg"; // Smokey's crow head: disc, upper beak, head (with eye cutout), jaw. const ART = ` `; // Trust badge: a pen-and-ink star. Outline at "regular", inked in at "trusted". const STAR = ``; const REACTION_MS: Record = { cheer: 800, sulk: 800, levelUp: 2000, levelDown: 1000, prank: 2500 }; // A still crow for message avatars. The live, animated crows live in the roster. export function crowAvatar(role: Role): SVGSVGElement { const svg = document.createElementNS(NS, "svg"); svg.setAttribute("viewBox", "0 0 352.23 324"); svg.setAttribute("class", `avatar crow-art role-${role}`); svg.setAttribute("aria-hidden", "true"); svg.innerHTML = ART; return svg; } export function svgCrow(name: string, role: Role): CrowView { const el = document.createElement("figure"); el.className = `crow role-${role} pose-perched`; el.dataset.trust = "0"; const svg = document.createElementNS(NS, "svg"); svg.setAttribute("viewBox", "0 0 352.23 324"); svg.setAttribute("aria-hidden", "true"); svg.innerHTML = ART; // static art only; no server data goes through innerHTML const body = document.createElement("div"); body.className = "crow-body"; body.append(svg); const badge = document.createElementNS(NS, "svg"); badge.setAttribute("class", "badge"); badge.setAttribute("viewBox", "0 0 24 24"); badge.setAttribute("aria-hidden", "true"); badge.innerHTML = STAR; const cap = document.createElement("figcaption"); cap.textContent = name; el.append(body, badge, cap); let timer = 0; return { el, setPose(pose) { el.classList.remove("pose-perched", "pose-flying", "pose-waiting", "pose-grounded"); el.classList.add(`pose-${pose}`); el.setAttribute("aria-label", `${name}, ${pose}`); }, setTrust(level) { el.dataset.trust = String(Math.max(0, Math.min(2, level))); }, fire(reaction) { clearTimeout(timer); el.classList.remove(...Object.keys(REACTION_MS).map((r) => `r-${r}`)); void el.offsetWidth; // restart the animation if the same reaction fires twice el.classList.add(`r-${reaction}`); timer = window.setTimeout(() => el.classList.remove(`r-${reaction}`), REACTION_MS[reaction]); }, }; }