qFelix's picture
Brad Did Something - Gradio/FastAPI Space on HF
78c0f6e
Raw
History Blame Contribute Delete
3.25 kB
// the five underlings: floor sprites, mood-driven idles, bubbles, prompts
import { NPC_SPOTS } from "./map.js";
import { makeChar } from "./sprites.js";
import { zzz } from "./particles.js";
// mood (server npc_moods string) → topSprite pose
const MOOD_POSE = {
normal: "idle", happy: "idle", energized: "idle", grateful: "idle",
smug: "lean", suspicious: "lean", confused: "idle",
sad: "slump", tired: "slump", devastated: "headdown", sheepish: "slump",
angry: "shake", hiding: "slump", crying: "slump", sleeping: "headdown",
};
export function createNpcs(worldEl) {
const npcs = {};
for (const [id, spot] of Object.entries(NPC_SPOTS)) {
// desks/cubicles are part of the canvas scene now (map.js)
const opts = { facing: "down", pose: "idle" };
npcs[id] = makeChar(worldEl, id, spot.x, spot.y, opts);
npcs[id].bubble = null;
npcs[id].mood = "normal";
}
// Derek's default idle is one static frame. Intentional. Slightly unsettling.
npcs.derek.setPose("still");
// occasional ZZZ when Derek is left alone long enough
setInterval(() => {
if (npcs.derek.mood === "normal" && Math.random() < 0.3)
zzz(NPC_SPOTS.derek.x + 8, NPC_SPOTS.derek.y - 36);
}, 6000);
return {
npcs,
applyMoods(moods) {
for (const [id, mood] of Object.entries(moods || {})) {
const npc = npcs[id];
if (!npc || npc.mood === mood) continue;
npc.mood = mood;
if (id === "derek" && (mood === "normal" || mood === "happy"))
npc.setPose("still");
else npc.setPose(MOOD_POSE[mood] || "idle");
}
},
showBubble(id, kind = "danger") {
this.clearBubbles();
const npc = npcs[id];
const el = document.createElement("div");
el.className = "bubble" + (kind === "amber" ? " amber" : "") +
(kind === "heart" ? " heart" : "");
el.textContent = kind === "amber" ? "?" : kind === "heart" ? "♥" : "!";
el.style.left = `${npc.x}px`;
el.style.top = `${npc.y - 52}px`;
npc.root.parentElement.appendChild(el);
npc.bubble = el;
if (kind !== "heart") npc.setPose("shake");
setTimeout(() => { if (npc.bubble === el) npc.setPose(
MOOD_POSE[npc.mood] || "idle"); }, 600);
},
clearBubbles() {
for (const npc of Object.values(npcs))
if (npc.bubble) { npc.bubble.remove(); npc.bubble = null; }
},
freezeAll(v) {
for (const npc of Object.values(npcs)) npc.setDim(false), npc.root
.style.filter = v ? "brightness(.8)" : "";
},
// ambient speech bubble above an NPC; queued so eavesdrop alternates
sayBubble(id, text, ms = 4200) {
const npc = npcs[id];
if (!npc) return;
const el = document.createElement("div");
el.className = "say-bubble";
el.textContent = text;
el.style.left = `${npc.x}px`;
el.style.top = `${npc.y - 58}px`;
npc.root.parentElement.appendChild(el);
setTimeout(() => el.classList.add("fading"), ms - 600);
setTimeout(() => el.remove(), ms);
},
saySequence(lines, gap = 3400) {
lines.forEach((entry, i) => {
setTimeout(() => this.sayBubble(entry.speaker, entry.line, gap - 200),
i * gap);
});
},
};
}