// ===================================================================== // IMAGE Addon — Pigeon Pressure v1.5 // Self-contained companion to IMAGE Matrix Action Game Engine 1.2 // Authors: Spax Orion & Dirty Helga // License: CC BY-NC 4.0 (matches IMAGE) // ===================================================================== // Originally powered by Minstrel+Cadence which has been retired. // ===================================================================== // ROLE: // Stand-alone addon for the IMAGE Grug Assault deployment. Two pigeon // NPCs (Stool and Guano) perch on owner-supplied objects; when IMAGE // broadcasts a game-start announcement on channel 0, the pigeons take // flight, taunt, and execute an opening / closing skit. Pure theatre — // no scoring, no IMAGE coupling beyond passive ch0 listening. // // DEPLOYMENT: // Drop this script into a single phantom prim. Place the prim outside // the bird flight pen (anywhere in the region works). Touch the prim // as owner to summon birds; touch again to dismiss or check status. // The two perch objects must already exist in-region with the UUIDs // listed in the OWNER CONFIG block below. // // REQUIRED INVENTORY (in this prim): // - notecard "guano" : Guano normal appearance // - notecard "guano_bs" : Guano bombing-run appearance // - notecard "stool" : Stool normal appearance // - notecard "stool_bs" : Stool bombing-run appearance // - animation "flight" : looping flight animation // // Operator, this does not include the NPC, you will need to make your // own bird npc, provide a flight animation for it, the sit keys for them // inworld and the bombing appearance where the birds wear a rezzer which // drops temp objects resembling droppings. Any flying creature and temp // prim drop can be used... use your imagination. // // CHANNEL 0 SACRED RULE: // This addon listens to channel 0 but never broadcasts on it. All // pigeon dialog is also on channel 0 because that is how IMAGE // broadcasts and it preserves the show voice for any future listeners. // ===================================================================== // ========================================================= // OWNER CONFIG (set these; everything else is logic) // ========================================================= // PERCH OBJECTS — UUIDs of in-world objects birds will sit on. // Positions are paired so we can teleport directly to perch on return. key GUANO_PERCH_KEY = "46217c96-e627-4c2f-b9ca-c6f18677c1b5"; vector GUANO_PERCH_POS = <269.588, 265.509, 1303.960>; key STOOL_PERCH_KEY = "04743495-6016-418a-903e-76ce30de4573"; vector STOOL_PERCH_POS = <249.290, 250.442, 1303.900>; // FLIGHT PEN — 3D box birds clamp to during ACTIVE. // Z values explicit (not derived from corners) to enforce sign clearance. vector PP_NE = <0,0,0>; vector PP_SW = <0,0,0>; float Z_MIN = 0.00; float Z_MAX = 0.00; // NPC IDENTITY string NPC_FIRST_GUANO = "Guano"; string NPC_FIRST_STOOL = "Stool"; string NPC_LAST = "Pigeon"; // APPEARANCE NOTECARDS (must exist in this prim's inventory) string APPEARANCE_GUANO = "guano"; string APPEARANCE_GUANO_BS = "guano_bs"; string APPEARANCE_STOOL = "stool"; string APPEARANCE_STOOL_BS = "stool_bs"; // ANIMATION (must exist in this prim's inventory) string ANIM_FLIGHT = "flight"; // CADENCE float BEAT_SECONDS = 7.5; // one new flight destination per beat float MIN_TRAVEL = 20.0; // min meters between successive beat destinations float TAUNT_CHANCE = 0.35; // probability of taunt per beat // Trigger-grace: how long the addon waits after Grug speech before deciding // no Grugs are left in arena (used to detect end-of-game cleanly). float NO_GRUG_SECONDS = 5.0; // Owner-only menu channel (random; never collides with IMAGE channels) integer MENU_CHAN = -7755512; // ========================================================= // IMAGE TRIGGER STRINGS (must match IMAGE 1.2 announcements) // ========================================================= list START_PREFIXES = [ "GRUG ASSAULT START!", "FEAST MODE!", "HUNGER MODE!", "FAMINE MODE!" ]; string OVER_PREFIX = "GRUG ASSAULT OVER!"; // ========================================================= // CONSTANTS // ========================================================= integer ST_OFF = 0; integer ST_IDLE = 1; integer ST_ACTIVE = 2; integer ST_ENDING = 3; // ========================================================= // RUNTIME STATE // ========================================================= integer gState = 0; // ST_OFF on script start key gGuano = NULL_KEY; // current Guano NPC key key gStool = NULL_KEY; // current Stool NPC key integer gListenCh0 = 0; // listen handle for ch0 game triggers integer gListenMenu = 0; // listen handle for owner dialog // Skit step machine integer gSkitMode = 0; // 0 none, 1 OPEN running, 2 END running integer gSkitStep = 0; float gSkitNext = 0.0; // llGetTime() at which next skit step fires // Flight beat cadence float gBeatAccum = 0.0; integer gPattern = 0; vector gLastGuano; vector gLastStool; // End-of-game detection float gNoGrugAccum = 0.0; // ========================================================= // HELPERS — string and math // ========================================================= string trim(string s) { return llStringTrim(s, STRING_TRIM); } integer startsWith(string s, string p) { if (llSubStringIndex(s, p) == 0) return TRUE; return FALSE; } integer isStartMsg(string m) { integer i; integer n = llGetListLength(START_PREFIXES); for (i = 0; i < n; i++) if (startsWith(m, llList2String(START_PREFIXES, i))) return TRUE; return FALSE; } integer isOverMsg(string m) { return startsWith(m, OVER_PREFIX); } integer speakerFirstNameIs(key id, string fn) { string nm = trim(llKey2Name(id)); if (nm == "") return FALSE; list parts = llParseString2List(nm, [" "], []); if (llGetListLength(parts) == 0) return FALSE; if (llToLower(llList2String(parts, 0)) == llToLower(fn)) return TRUE; return FALSE; } float clampFloat(float v, float lo, float hi) { if (v < lo) return lo; if (v > hi) return hi; return v; } float randRange(float lo, float hi) { return lo + llFrand(hi - lo); } vector clampToPen(vector p) { float x = clampFloat(p.x, PP_SW.x, PP_NE.x); float y = clampFloat(p.y, PP_SW.y, PP_NE.y); float z = clampFloat(p.z, Z_MIN, Z_MAX); return ; } // ========================================================= // HELPERS — NPC liveness // ========================================================= integer npcAlive(key npc) { if (npc == NULL_KEY) return FALSE; if (osIsNpc(npc)) return TRUE; return FALSE; } // Count Grug NPCs currently in region (for end-of-game backup detection). integer countGrugs() { list av = osGetAvatarList(); integer n = llGetListLength(av); integer i; integer count = 0; for (i = 0; i + 2 < n; i += 3) { string nm = trim(llList2String(av, i + 2)); list parts = llParseString2List(nm, [" "], []); if (llGetListLength(parts) > 0) { string fn = llToLower(llList2String(parts, 0)); if (fn == "grug") count++; } } return count; } // ========================================================= // HELPERS — bird control (the actual OSSL string-pulling) // ========================================================= // Rez a bird at its perch, sit it on the perch object. key rezBirdAtPerch(string firstName, string apprNotecard, vector perchPos, key perchKey) { if (llGetInventoryType(apprNotecard) != INVENTORY_NOTECARD) { llOwnerSay("PIGEON PRESSURE: missing appearance notecard '" + apprNotecard + "'."); return NULL_KEY; } key npc = osNpcCreate(firstName, NPC_LAST, perchPos, apprNotecard, OS_NPC_OBJECT_GROUP); if (npc == NULL_KEY) { llOwnerSay("PIGEON PRESSURE: osNpcCreate failed for " + firstName + "."); return NULL_KEY; } osNpcSit(npc, perchKey, OS_NPC_SIT_NOW); return npc; } // Send a bird to its perch (end of show). Order: stand → swap → teleport → sit. // SCAR-TISSUE RULE: stand before any movement. sendBirdToPerch(key npc, string apprNotecard, vector perchPos, key perchKey) { if (!npcAlive(npc)) return; // Stop flight anim FIRST so the bird is not still flapping during // the appearance swap and seating transition. Order is critical. if (llGetInventoryType(ANIM_FLIGHT) == INVENTORY_ANIMATION) osNpcStopAnimation(npc, ANIM_FLIGHT); osNpcStand(npc); if (llGetInventoryType(apprNotecard) == INVENTORY_NOTECARD) osNpcLoadAppearance(npc, apprNotecard); osTeleportAgent(npc, "", perchPos, ZERO_VECTOR); osNpcSit(npc, perchKey, OS_NPC_SIT_NOW); } // Launch a bird into flight (start of show). Order: stand → swap → anim → teleport. // SCAR-TISSUE RULE: stand before any movement. launchBirdToFlight(key npc, string apprBs, vector firstBeat) { if (!npcAlive(npc)) return; osNpcStand(npc); if (llGetInventoryType(apprBs) == INVENTORY_NOTECARD) osNpcLoadAppearance(npc, apprBs); if (llGetInventoryType(ANIM_FLIGHT) == INVENTORY_ANIMATION) osNpcPlayAnimation(npc, ANIM_FLIGHT); osTeleportAgent(npc, "", firstBeat, ZERO_VECTOR); } // In-pen flight movement (between beats). FLY flag, NPC moves naturally. flyBirdToBeat(key npc, vector beat) { if (!npcAlive(npc)) return; osNpcMoveToTarget(npc, beat, OS_NPC_FLY); } // Bird speaks on channel 0. Uses osNpcSay so the speaker UUID is the NPC. birdSay(key npc, string msg) { if (!npcAlive(npc)) return; osNpcSay(npc, 0, msg); } // Re-rez a bird if it has gone missing (region restart, manual delete, etc). // Show-Critical Asset Rule: theatre layer must self-heal. ensureBird(integer which) { if (which == 0) { if (!npcAlive(gGuano)) gGuano = rezBirdAtPerch(NPC_FIRST_GUANO, APPEARANCE_GUANO, GUANO_PERCH_POS, GUANO_PERCH_KEY); } else { if (!npcAlive(gStool)) gStool = rezBirdAtPerch(NPC_FIRST_STOOL, APPEARANCE_STOOL, STOOL_PERCH_POS, STOOL_PERCH_KEY); } } // ========================================================= // HELPERS — flight beat patterns (lifted/simplified from ppCadence) // ========================================================= vector pickBeatRaw(integer pattern) { float cx = (PP_SW.x + PP_NE.x) * 0.5; float cy = (PP_SW.y + PP_NE.y) * 0.5; float w = PP_NE.x - PP_SW.x; float h = PP_NE.y - PP_SW.y; float ax = w * 0.40; float ay = h * 0.40; vector p = ; if (pattern == 0) { // Long diagonal sweep float sx = randRange(-1.0, 1.0); float sy = randRange(-1.0, 1.0); p = ; } else if (pattern == 1) { // Racetrack edges: push toward X extremes float edge = randRange(0.85, 1.0); if (llFrand(1.0) < 0.5) edge = -edge; float sy = randRange(-0.6, 0.6); p = ; } else { // Center-cross float sx = randRange(-0.7, 0.7); float sy = randRange(-0.7, 0.7); p = ; } return clampToPen(p); } vector pickBeatFar(integer pattern, vector prev) { integer tries = 0; vector cand = prev; while (tries < 10) { cand = pickBeatRaw(pattern); if (llVecDist(cand, prev) >= MIN_TRAVEL) return cand; tries++; } return cand; } // ========================================================= // HELPERS — taunt schedule // ========================================================= list TAUNTS = [ "Now Serving: Pigeon Poo Pie! Ahhhh ha ha ha ha ha!!", "OOOOH I dropped a one in that Grug's mouth! Did you see that?", "I feel sorry for the simpleton who must clean up after us!", "Grugs are too easy to hit, perhaps I should target humans next!", "I am about to drop a sloppy one on this human!", "It's Everyone's favorite SHIT SHOW!", "SPLAT! Right in the old smacker!", "Did anyone tell you they love you today besides me?", "You might as well toss out those clothes. this will not wash out!" ]; maybeTaunt() { if (llFrand(1.0) >= TAUNT_CHANCE) return; integer idx = (integer)llFloor(llFrand((float)llGetListLength(TAUNTS))); string line = llList2String(TAUNTS, idx); if (llFrand(1.0) < 0.5) birdSay(gStool, line); else birdSay(gGuano, line); } // ========================================================= // SKITS — encoded as step-tables (no notecard, no DSL) // ========================================================= // OPENING SKIT — fires on game start. BOTH birds take flight INSTANTLY in // step 0 (stand → swap → fly anim → teleport to first beat). Dialogue and // taunts happen on top of the live flight beat loop, which runs in parallel. // Total skit time ~9s; flight beat loop begins at t=0 and never stops. runSkitOpenStep(integer step) { if (step == 0) { // t=0: BOTH birds — stand, swap, fly anim, teleport into pen. // Beat scheduler picks up from here and drives ongoing flight. if (npcAlive(gGuano)) { osNpcStand(gGuano); if (llGetInventoryType(APPEARANCE_GUANO_BS) == INVENTORY_NOTECARD) osNpcLoadAppearance(gGuano, APPEARANCE_GUANO_BS); if (llGetInventoryType(ANIM_FLIGHT) == INVENTORY_ANIMATION) osNpcPlayAnimation(gGuano, ANIM_FLIGHT); vector firstG = pickBeatRaw(0); osTeleportAgent(gGuano, "", firstG, ZERO_VECTOR); gLastGuano = firstG; } if (npcAlive(gStool)) { osNpcStand(gStool); if (llGetInventoryType(APPEARANCE_STOOL_BS) == INVENTORY_NOTECARD) osNpcLoadAppearance(gStool, APPEARANCE_STOOL_BS); if (llGetInventoryType(ANIM_FLIGHT) == INVENTORY_ANIMATION) osNpcPlayAnimation(gStool, ANIM_FLIGHT); vector firstS = pickBeatRaw(1); osTeleportAgent(gStool, "", firstS, ZERO_VECTOR); gLastStool = firstS; } gSkitNext = llGetTime() + 1.0; } else if (step == 1) { // t=1: Guano shouts the briefing (in flight) birdSay(gGuano, "C.I.C ... Charlie Two Echo Niner .... Engaging Pigeon Pressure as ORDERED ...."); gSkitNext = llGetTime() + 3.0; } else if (step == 2) { // t=4: Stool shouts the order (in flight) birdSay(gStool, "Charlie Two Echo Niner .... Paint Your Targets and OPEN FIRE!"); gSkitNext = llGetTime() + 2.0; } else if (step == 3) { // t=6: Guano cheers (in flight) birdSay(gGuano, "Woooooo Hooooo, Kill Em' ALL!! Queue the METAL Music!!"); gSkitNext = llGetTime() + 2.0; } else if (step == 4) { // t=8: Stool warns spectators (in flight) birdSay(gStool, "PAY ATTENTION FOLKS! That means UMBRELLA TIME!!"); gSkitNext = llGetTime() + 1.0; } else { // step 5+: opening dialogue done. Flight loop continues uninterrupted. gSkitMode = 0; gSkitStep = 0; } } // END SKIT — fires on game over. Birds are flying, end perched. // Order per spec: dialog → swap → seat (swap-then-seat is the protocol). runSkitEndStep(integer step) { if (step == 0) { // t=0: Guano signs off birdSay(gGuano, "Those monsters will rue the day they sent us away!!"); gSkitNext = llGetTime() + 2.0; } else if (step == 1) { // t=2: Stool signs off birdSay(gStool, "Awww!! Game over!! We'll be back... COUNT ON IT!"); gSkitNext = llGetTime() + 2.0; } else if (step == 2) { // t=4: send Guano home — swap then seat sendBirdToPerch(gGuano, APPEARANCE_GUANO, GUANO_PERCH_POS, GUANO_PERCH_KEY); gSkitNext = llGetTime() + 1.0; } else if (step == 3) { // t=5: send Stool home — swap then seat sendBirdToPerch(gStool, APPEARANCE_STOOL, STOOL_PERCH_POS, STOOL_PERCH_KEY); gSkitNext = llGetTime() + 1.0; } else { // step 4+: end skit done, return to IDLE gSkitMode = 0; gSkitStep = 0; gState = ST_IDLE; llSetText("PIGEON PRESSURE: armed", <0.4, 1.0, 0.4>, 1.0); } } // ========================================================= // STATE TRANSITIONS // ========================================================= // ON: rez birds at perches, start listening to ch0. goOn() { ensureBird(0); ensureBird(1); if (gListenCh0 == 0) gListenCh0 = llListen(0, "", NULL_KEY, ""); gState = ST_IDLE; gNoGrugAccum = 0.0; llSetTimerEvent(1.0); llSetText("PIGEON PRESSURE", <0.4, 1.0, 0.4>, 1.0); } // OFF: dismiss birds, drop ch0 listener. goOff() { if (npcAlive(gGuano)) osNpcRemove(gGuano); if (npcAlive(gStool)) osNpcRemove(gStool); gGuano = NULL_KEY; gStool = NULL_KEY; if (gListenCh0 != 0) { llListenRemove(gListenCh0); gListenCh0 = 0; } gState = ST_OFF; gSkitMode = 0; gSkitStep = 0; llSetTimerEvent(0.0); llSetText("PIGEON PRESSURE", <1.0, 0.4, 0.4>, 1.0); } // Game start triggered (from ch0 listen). startBombingRun() { if (gState != ST_IDLE) return; ensureBird(0); ensureBird(1); gState = ST_ACTIVE; gSkitMode = 1; gSkitStep = 0; gSkitNext = llGetTime(); // fire immediately gNoGrugAccum = 0.0; gBeatAccum = 0.0; llSetText("PIGEON PRESSURE: bombing run", <1.0, 0.7, 0.0>, 1.0); } // Game ended (from ch0 listen, or no-grugs timeout). endBombingRun() { if (gState != ST_ACTIVE) return; gState = ST_ENDING; gSkitMode = 2; gSkitStep = 0; gSkitNext = llGetTime(); gBeatAccum = 0.0; llSetText("PIGEON PRESSURE: pigeons win again", <0.6, 0.6, 1.0>, 1.0); } // ========================================================= // OWNER MENU // ========================================================= showMenu(key id) { if (gListenMenu != 0) llListenRemove(gListenMenu); gListenMenu = llListen(MENU_CHAN, "", id, ""); list buttons; string txt; if (gState == ST_OFF) { buttons = ["Rez Birds", "Cancel"]; txt = "Pigeon Pressure is OFF.\nRez and arm the birds?"; } else { string s = "armed"; if (gState == ST_ACTIVE) s = "bombing run"; else if (gState == ST_ENDING) s = "ending skit"; buttons = ["Dismiss Birds", "Status", "Cancel"]; txt = "Pigeon Pressure status: " + s + "\nWhat would you like?"; } llDialog(id, txt, buttons, MENU_CHAN); } // ========================================================= // DEFAULT STATE // ========================================================= default { state_entry() { // Boot quietly in OFF; owner must explicitly summon birds. gState = ST_OFF; gGuano = NULL_KEY; gStool = NULL_KEY; llSetText("PIGEON PRESSURE", <1.0, 0.4, 0.4>, 1.0); } on_rez(integer sp) { llResetScript(); } changed(integer c) { if (c & (CHANGED_OWNER | CHANGED_REGION_START | CHANGED_INVENTORY)) llResetScript(); } touch_start(integer n) { key id = llDetectedKey(0); if (id != llGetOwner()) return; // owner-only; non-owners silently ignored showMenu(id); } listen(integer ch, string name, key id, string msg) { // Owner menu replies arrive on MENU_CHAN. if (ch == MENU_CHAN) { if (id != llGetOwner()) return; string m = trim(msg); if (m == "Rez Birds") { goOn(); return; } if (m == "Dismiss Birds") { goOff(); return; } if (m == "Status") { showMenu(id); return; } // Cancel and anything else: do nothing. return; } // Channel 0 game triggers — only when armed. if (ch != 0) return; if (gState == ST_OFF) return; // Speaker must be a Grug NPC (first name). if (!speakerFirstNameIs(id, "Grug")) return; string m2 = trim(msg); if (m2 == "") return; if (isStartMsg(m2)) { startBombingRun(); return; } if (isOverMsg(m2)) { endBombingRun(); return; } } timer() { // OFF: timer should be inactive, but guard anyway. if (gState == ST_OFF) { llSetTimerEvent(0.0); return; } // Self-heal birds if either has gone missing. ensureBird(0); ensureBird(1); // Drive skit step machine if a skit is running. if (gSkitMode != 0) { if (llGetTime() >= gSkitNext) { if (gSkitMode == 1) runSkitOpenStep(gSkitStep); else if (gSkitMode == 2) runSkitEndStep(gSkitStep); gSkitStep++; } // END skit suppresses flight beats (birds need stable final // position before sendBirdToPerch fires). OPEN skit lets flight // beats run in parallel — dialogue happens while birds fly. if (gSkitMode == 2) return; } // ACTIVE: flight beat scheduler + end-of-game safety net. if (gState == ST_ACTIVE) { // Backup end detection: if no Grugs in region for NO_GRUG_SECONDS, // assume we missed the OVER announcement and end the show. integer grugs = countGrugs(); if (grugs < 1) { gNoGrugAccum += 1.0; if (gNoGrugAccum >= NO_GRUG_SECONDS) { endBombingRun(); return; } } else { gNoGrugAccum = 0.0; } gBeatAccum += 1.0; if (gBeatAccum >= BEAT_SECONDS) { gBeatAccum = 0.0; gPattern++; if (gPattern > 2) gPattern = 0; vector pG = pickBeatFar(gPattern, gLastGuano); vector pS = pickBeatFar((gPattern + 1) % 3, gLastStool); gLastGuano = pG; gLastStool = pS; flyBirdToBeat(gGuano, pG); flyBirdToBeat(gStool, pS); maybeTaunt(); } return; } // IDLE / ENDING: nothing to drive between skits/beats. } }