// 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]);
},
};
}