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