// IF YOU VALUE THIS WORK, PLEASE LEAVE ATTRIBUTION INTACT //
// //
// _|_| //
// _| _| _|_|_|_| _|_| _|_|_| _|_| //
// _| _| _| _| _| _| _| _|_|_|_| //
// _| _| _| _| _| _| _| _| //
// _|_| _|_|_|_| _|_| _| _| _|_|_| //
// MiniVerse //
//............................................................//
//.....................▒▓▓▒▒▒▒▓▓▓▓▓▓▒▒▒▓▓░....................//
//.................._▒▓▒.Ozone.MiniVerse.▒▓▒_.................//
//................_▒▓▓▓▒____PRESENTS____▒▓▓▓▓▓▒_..............//
//..............░▓▓▓▓▓▓▒___I.M.A.G.E.___▒▓▓▓▓▓▓▓░.............//
//.............▒▓▓▓▓▓▓▓▒__IMAGE_MATRIX__▒▓▓▓▓▓▓▓▓▒............//
//............▒▓▒▓▓▓▓▒_ACTION_GAME_ENGINE_▒▓▓▓▓▒▓▓▒...........//
//..........▒▓▓▓▓░...▒▓▓▒_By_Spax_Orion_▒▓▓▒...░▓▓▓▓▒.........//
//.........▒▓▓▓▓░......▒▓_& Dirty Helga_▓▓▒░.....░▒▓▓▓........//
//.........▓▓▓▓▒░.._......░▒▓▓▓▓▓▓▓▓▓▓▓▒░......_..░▒▓▓▓.......//
//.........▓▓▓▓░.._▓░▓░_....▓▓▓▓▓▓▓▓▓▓...._▒▓▒▓_..░▓▓▓▓.......//
//.........▓▓▓▓.._▓█░▒█▒.....▒▓▓▓▓▓▓▒.....▒█▒░█▒_..▓▓▓▓.......//
//........▒▓▓▓▓▓▒_..▒._░_▒▓▓▒░.▒▒.░▒▓▓▒_░_.▒.._▒▓▓▓▓▓▓▒.......//
//.......▒▓▓▓▓▓▓▓▓▓_...▒▓▓▓▓▓░..▒▒..░▓▓▓▓▓▒..._▓▓▓▓▓▓▓▓▒......//
//......▓▓▓▓▓▓▓▓▓▓▓_.▓▓▓▓▓▒.._▓▓▓█_..▒▓▓▓▓▓._▓▓▓▓▓▓▓▓▓▓▓......//
//......▓▓▓▓▓▒▒▓▓▓▒▓▓▓▓▓▓▒░.▒▓▓▓▓▓█▒.░▒▓▓▓▓▓▒▒▓▓▓▒▓▓▓▓▓▓......//
//......▓▓▓▓▓░.▓▓▓.▒▓▓▓▓▓░..▒▓▓▓▓▓▓▒..░▓▓▓▓▓░.▓▓▓.░▓▓▓▓▓......//
//........▒▓▓█░.▓▓▓.▒▓▓▓▓▓__▓_..▒▒.._▓__▓▓▓▓▓░.▓▓▓.▒▓▓▓▒......//
//.........░▒░░▒▓▒░▒▓▓▓▓▓▓▒▒▓▒▒▒▓▓▒▒▒▓▒▒▓▓▓▓▓▒░▒▓▒░░▒░░.......//
//.........░▒.░▓▓░.▓▓▓▓█▒▒▒█▒▒▒█▒▒█▒▒▒█▒▒▒█▓▓▓▓.░▓▓░.▒░.......//
//.............░▓▓░_▓▓▓▓█................█▓▓▓▓_░▓▓░...........//
//..............░▒▓▓▓█▓._▒_.█░....█░._█_.▓█▓▓▓▒░..............//
//.................░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░................//
//.................░▒▓▓▓▒▒▓▒▒▓▓▓▓▒▒▓▓▓▓▒▒▓▓▒░.................//
//..................▒▓▓░░▓░░▓▓▓▓░░▓▓▓▓░░▓▓▒...................//
//.................▒▓░..█__▓▓▓▓__▓▓▓▓__█..░▓▒.................//
//...................░...░..░▓▓░..░▓▓░..░...░.................//
//.................... ..░...░▒░....░▒░...░...................//
//............................................................//
// Common sense not included and must be supplied by end user //
// Read ENTIRE script first and LEARN: KNOW what you're using //
// ===========================================================//
// IMAGE: IMAGE Matrix Action Game Engine //
// OpenSim 0.9.0x / Unix Mono / XEngine / OSSL enabled //
// ===========================================================//
// CC BY-NC
// https://creativecommons.org/licenses/by-nc/4.0/
// ===========================================================
// CONFIGURATION (OWNER EDIT)
// ===========================================================
integer USE_IASR = TRUE; // TRUE: broadcast to IASR
integer IASR_LOCAL = FALSE; // TRUE: ALSO play locally on board (risk: double audio)
integer CH_IASR_CMD = -733102; // MUST match IASR
integer DEBUG = FALSE;
integer EXPO_CH = -777777; // must match ExpoFlare listener channel
// Default NPC count (overridden by game modes)
integer GRUG_COUNT = 3;
// "How far apart" the Grugs try to stay from each other (meters)
float GRUG_SPACING = 4.0;
// Base distance from the prey (agent) to keep while chasing (meters)
float GRUG_RING_RADIUS = 2.8;
// How often Grugs update their follow target (seconds). Larger = clunkier/slower.
float FOLLOW_UPDATE_SEC = 1.6;
// "Clunkiness": 0.2..0.9. Lower = more sluggish/laggy pursuit.
float CLUNKINESS = 0.9;
// Chase/attack ranges
float ATTACK_SENSOR_RANGE = 96.0;
float BITE_DISTANCE = 3.0;
float MOVE_DISTANCE = 1.0;
// Damage / healing
float BITE_DAMAGE = 10.0;
float LOW_HEALTH_THRESHOLD = 45.0;
float HEAL_ON_TELEPORT = 100.0;
// NPC "dies" when it has taken this many damage steps
integer NPC_DEATH_POINTS = 1;
// Sounds / Anims (must exist in inventory / anim set)
string DeathAnim = "a.die";
string ZombieWalk = "a.walk";
string IdleAnim = "a.idle";
string AttractAnim = "a.dance";
string SOUND_ALARM = "Alarm";
string SOUND_GROWL = "Growl";
string SOUND_SCREAM2 = "Die2";
string SOUND_SCREAM1 = "Die1";
// Music loops (combat modes)
string MUSIC_FAMINE = "Famine";
string MUSIC_HUNGER = "Hunger";
string MUSIC_FEAST = "Feast";
string MUSIC_ATTRACT = "Attract";
string gMusicNow = "";
// Notecard (NPC appearance) name
string NPC_NOTECARD = ".grug";
// Teleport destination / landing
string Destination;
vector LandingPoint = <0,0,0>; //set this or else
vector LookAt = <1,1,1>;
// SCOREBOARD LINKSET INTEGRATION
string SCORE_PRIM_NAME = "ga-score"; // child prim name
integer SCORE_LINKNUM = -1; // resolved at runtime
integer SCORE_MSG_NUM = 9001; // arbitrary internal msg number
// ARENA + SPECTATOR BOUNDS
// If BOTH ArenaNE and ArenaSW are <0,0,0>, arena is fallback radius around controller.
vector ArenaNE = <0,0,0>;
vector ArenaSW = <0,0,0>;
// Spectator bounds (used for spectator safety; combat AI will not chase/bite in spectator zone)
vector ArenaSpectatorNE = <0,0,0>;
vector ArenaSpectatorSW = <0,0,0>;
// Kill attribution (slug -> controller HIT feed)
integer CH_CAGE_HIT = -733101; // MUST match slug script channel
integer HIT_WINDOW_SEC = 10; // last-shooter attribution window
// =========================
// ATTRACT MODE (AUTO)
// =========================
integer ATTRACT_IDLE_SEC = 40; // spectator idle time to start show
float ATTRACT_IDLE_EPS = 0.50; // "idle" movement threshold (meters)
float ATTRACT_RING_RADIUS = 5.0; // ring radius around ArenaDROP
integer ATTRACT_SHOW_COUNT = 5; // number of show grugs (fixed)
// Anchor prim: phantom child prim used as show center.
// If missing, fallback position is used.
string ARENA_DROP_PRIM = "ArenaDROP";
vector ArenaDropFallback = <0,0,0>; // set to your provided coords
// ===========================================================
// CONSTANTS / TABLES
// ===========================================================
list Sayings = ["Chomps","Slurrps","Crunches","Nom Noms","Tastes","Bites","Licks","Gnaws","Chews","Grinds","Sucks","Nibbles"];
list Bits = ["Nose","Hand","Arm","Leg","Ass","Head","Foot","Ear","Chest","Stomach","Thighs"];
list TauntsTouch = [
"GRUG SAY NO MENU FOR YOU! KEEP RUNNING!",
"TOUCH ALL YOU WANT... GRUG STILL HUNGRY!",
"GRUG LIKE WHEN PREY PANIC!",
"STOP POKING THING. START BLEEDING INSTEAD!"
];
list TauntsRandom = [
"GRUG MUST STOP PERSONAL PRIVACY!",
"GRUG SAVORS AUTO ID ON HUMANS. YUM!",
"'Dr. FESTER' SAYS 'NO ID? GET GRUGS!'",
"GRUG FEEDS ON THE FAMOUS... OBEY!",
"GRUG KICK FIRST, KISS AFTER OBEDIENCE",
"GRUG EQUIPPED AUTO-ID, OR DIE, PREY!"
];
list LastNames = ["...","....","....."];
// ===========================================================
// RUNTIME VARIABLES
// ===========================================================
// Active game state
integer gRunning = FALSE;
integer gGameSecondsTotal = 0;
integer gGameSecondsLeft = 0;
integer gWarned30 = FALSE;
integer gFeastMode = FALSE;
key gTarget = NULL_KEY;
// NPC tracking
list gNpcs; // keys
list gNpcLastNames; // strings
list gNpcDeathInc; // integers
list gNpcPrevHealth; // floats
list gNpcAngles; // floats
// Kill attribution per NPC (parallel arrays)
list gNpcLastShooter; // keys
list gNpcLastHitTime; // integers (unix time)
// Dialog / listeners
integer gDialogChan = 0;
integer gListenHandle = 0;
integer gHitListenHandle = 0;
// =========================
// ATTRACT RUNTIME
// =========================
integer gAttractMode = FALSE;
key gSpecKey = NULL_KEY; // tracked spectator for idle detection
vector gSpecLastPos = ZERO_VECTOR;
integer gSpecStillSince = 0; // unix time when spectator became "still"
integer gAttractEmptySince = 0; // optional: stop show if nobody watching
integer gArenaDropLink = -1; // cached link number of ArenaDROP
integer gAttractCooldownUntil = 0; // unix time; do not re-arm attract before this
// ===========================================================
// HELPERS (DEBUG / UTILITY)
// ===========================================================
integer iasrSend(string msg)
{
if (!USE_IASR) return FALSE;
llRegionSay(CH_IASR_CMD, msg);
return TRUE;
}
integer iasrLoop(string snd, float vol)
{
// snd may be missing locally (board), but IASR has the inventory.
if (!USE_IASR) return FALSE;
if (vol < 0.0) vol = 0.0;
if (vol > 1.0) vol = 1.0;
return iasrSend("LOOP|" + snd + "|" + (string)vol);
}
integer iasrStop()
{
if (!USE_IASR) return FALSE;
return iasrSend("STOP");
}
integer iasrTrig(string snd, float vol)
{
if (!USE_IASR) return FALSE;
if (vol < 0.0) vol = 0.0;
if (vol > 1.0) vol = 1.0;
return iasrSend("TRIG|" + snd + "|" + (string)vol);
}
// Use this instead of llTriggerSound so IASR mirrors one-shots.
integer trigSound(string snd, float vol)
{
if (USE_IASR) iasrTrig(snd, vol);
if (IASR_LOCAL) llTriggerSound(snd, vol);
return TRUE;
}
integer debug(string s)
{
if (DEBUG) llOwnerSay(s);
return TRUE;
}
integer ExpoFlareSignal(integer on)
{
// External module toggle: START when any mode active, STOP when idle.
if (on) llRegionSay(EXPO_CH, "START");
else llRegionSay(EXPO_CH, "STOP");
return TRUE;
}
// ===========================================================
// BOUNDS HELPERS
// ===========================================================
integer vecIsZero(vector v) { return (v.x == 0.0 && v.y == 0.0 && v.z == 0.0); }
integer pointInRectXY(vector p, vector ne, vector sw)
{
float minX;
float maxX;
float minY;
float maxY;
if (ne.x < sw.x) { minX = ne.x; maxX = sw.x; }
else { minX = sw.x; maxX = ne.x; }
if (ne.y < sw.y) { minY = ne.y; maxY = sw.y; }
else { minY = sw.y; maxY = ne.y; }
if (p.x < minX || p.x > maxX) return FALSE;
if (p.y < minY || p.y > maxY) return FALSE;
return TRUE;
}
integer inArena(vector p)
{
// If BOTH are set, use rectangle. If either is zero, use fallback radius.
if (!vecIsZero(ArenaNE) && !vecIsZero(ArenaSW))
return pointInRectXY(p, ArenaNE, ArenaSW);
// Fallback arena = ATTACK_SENSOR_RANGE radius around controller (XY only)
vector c = llGetPos();
return (llVecDist(
, ) <= ATTACK_SENSOR_RANGE);
}
integer inSpectator(vector p)
{
if (vecIsZero(ArenaSpectatorNE) || vecIsZero(ArenaSpectatorSW)) return FALSE;
return pointInRectXY(p, ArenaSpectatorNE, ArenaSpectatorSW);
}
// ===========================================================
// ACCESS CONTROL (ARENA vs SPECTATOR)
// ===========================================================
integer canLaunchFromPos(vector p)
{
// Only allow launch from inside arena AND not in spectator zone.
if (!inArena(p)) return FALSE;
if (inSpectator(p)) return FALSE;
return TRUE;
}
integer canLaunchKey(key av)
{
list od = llGetObjectDetails(av, [OBJECT_POS]);
vector p = llList2Vector(od, 0);
if (p == ZERO_VECTOR) return FALSE;
return canLaunchFromPos(p);
}
// ===========================================================
// ARENA OCCUPANCY (FEAST end logic)
// ===========================================================
// Uses region-wide avatar list so FEAST doesn't depend on sensor range / sensor events.
integer anyAvatarInArena()
{
// osGetAvatarList() returns a flat list: [key1, name1, pos1, vel1, key2, name2, pos2, vel2, ...]
// We'll step by 4 and read the POS slot.
list av = osGetAvatarList();
integer i;
integer n = llGetListLength(av);
for (i = 0; i + 2 < n; i += 4)
{
vector p = llList2Vector(av, i + 2);
// Count only true arena occupants, not spectators.
if (inArena(p) && !inSpectator(p)) return TRUE;
}
return FALSE;
}
// ===========================================================
// NEAREST PREY PICKER (MULTI-PLAYER ARENA)
// ===========================================================
// Picks the nearest detected avatar (from current sensor pass)
// that is inside arena AND not in spectator zone.
key nearestDetectedCombatant(integer num, vector fromPos)
{
key best = NULL_KEY;
float bestD = 999999.0;
integer k;
for (k = 0; k < num; k++)
{
vector p = llDetectedPos(k);
if (!inArena(p)) jump cont;
if (inSpectator(p)) jump cont;
float d = llVecDist(fromPos, p);
if (d < bestD)
{
bestD = d;
best = llDetectedKey(k);
}
@cont;
}
return best;
}
// ===========================================================
// COUNTS / PICKERS
// ===========================================================
integer clampNpcCount(integer c)
{
if (c < 2) return 2;
if (c > 5) return 5; // FEAST uses 5.
return c;
}
string pickUniqueLastName(integer idx)
{
return llList2String(LastNames, idx % llGetListLength(LastNames));
}
key randomNpcKey()
{
integer n = llGetListLength(gNpcs);
if (n <= 0) return NULL_KEY;
return llList2Key(gNpcs, (integer)llFrand((float)n));
}
// ===========================================================
// SOUND + MUSIC
// ===========================================================
integer playAlarm()
{
// One-shot: mirror to IASR; optionally play local.
if (USE_IASR) iasrTrig(SOUND_ALARM, 1.0);
if (IASR_LOCAL && llGetInventoryType(SOUND_ALARM) == INVENTORY_SOUND)
{
llTriggerSound(SOUND_ALARM, 1.0);
return TRUE;
}
// If local audio is off, still return TRUE because IASR may have played it.
return USE_IASR;
}
integer playMusic(string snd, float vol)
{
// IASR is the authoritative player when enabled.
if (USE_IASR) iasrLoop(snd, vol);
// Optional: also loop locally on the board.
if (IASR_LOCAL && llGetInventoryType(snd) == INVENTORY_SOUND)
{
osLoopSound(LINK_THIS, snd, vol);
return TRUE;
}
return USE_IASR;
}
integer stopMusic()
{
if (USE_IASR) iasrStop();
// Local stop is harmless even if nothing playing.
osStopSound(LINK_THIS);
llStopSound();
return TRUE;
}
// ===========================================================
// ATTRACT: IDLE SHOW (NO SPECTATOR INVOLVEMENT)
// ===========================================================
float gAttractPhase = 0.0;
float ATTRACT_STEP_RAD = 0.20; // clockwise speed per tick
integer resolveArenaDropLink()
{
integer i;
integer n = llGetNumberOfPrims();
for (i = 1; i <= n; i++)
if (llGetLinkName(i) == ARENA_DROP_PRIM) return i;
return -1;
}
vector linkWorldPos(integer linknum)
{
if (linknum <= 1) return llGetPos();
vector local = llList2Vector(llGetLinkPrimitiveParams(linknum, [PRIM_POS_LOCAL]), 0);
return llGetPos() + (local * llGetRot());
}
vector arenaDropPos()
{
if (gArenaDropLink < 0) gArenaDropLink = resolveArenaDropLink();
if (gArenaDropLink > 0) return linkWorldPos(gArenaDropLink);
return ArenaDropFallback;
}
// Show spawn: no clamp to 2..5
integer spawnNpcShow(integer i, vector basePos, integer showCount)
{
string last = pickUniqueLastName(i);
key npc = osNpcCreate("Grug", last, basePos, NPC_NOTECARD, OS_NPC_OBJECT_GROUP);
gNpcs += [npc];
gNpcLastNames += [last];
gNpcDeathInc += [0];
gNpcPrevHealth += [osGetHealth(npc)];
float baseAng = (TWO_PI / (float)showCount) * (float)i;
gNpcAngles += [baseAng];
gNpcLastShooter += [NULL_KEY];
gNpcLastHitTime += [0];
osNpcStand(npc);
osNpcPlayAnimation(npc, AttractAnim);
return TRUE;
}
integer updateAttractCircle()
{
vector c = arenaDropPos();
integer i;
integer n = llGetListLength(gNpcs);
for (i = 0; i < n; i++)
{
key npc = llList2Key(gNpcs, i);
if (npc == NULL_KEY) jump cont;
float ang = ((TWO_PI / (float)ATTRACT_SHOW_COUNT) * (float)i) + gAttractPhase;
vector p = ;
if (!inArena(p))
{
p = ;
}
osNpcMoveToTarget(npc, p, OS_NPC_NO_FLY);
@cont;
}
gAttractPhase += ATTRACT_STEP_RAD;
if (gAttractPhase > TWO_PI) gAttractPhase -= TWO_PI;
return TRUE;
}
integer startAttract()
{
if (gRunning) return FALSE;
if (gAttractMode) return TRUE;
if (gArenaDropLink < 0) gArenaDropLink = resolveArenaDropLink();
if (gArenaDropLink < 0 && vecIsZero(ArenaDropFallback))
{
llOwnerSay("IMAGE: Attract refused. ArenaDROP missing and fallback coords are <0,0,0>.");
return FALSE;
}
gAttractMode = TRUE;
gAttractPhase = 0.0;
ExpoFlareSignal(TRUE);
playAlarm();
llSleep(0.8);
stopMusic();
playMusic(MUSIC_ATTRACT, 1.0);
removeAllNpcs();
vector c = arenaDropPos();
integer i;
for (i = 0; i < ATTRACT_SHOW_COUNT; i++)
{
float ang = (TWO_PI / (float)ATTRACT_SHOW_COUNT) * (float)i;
vector p = ;
spawnNpcShow(i, p, ATTRACT_SHOW_COUNT);
llSleep(0.08);
}
// drive rotation (timer is also used by game; see timer edit)
llSetTimerEvent(1.0);
return TRUE;
}
integer stopAttract(string why)
{
why = why; // lint
if (!gAttractMode) return TRUE;
gAttractMode = FALSE;
// Cooldown so staging/teleports don't immediately re-arm attract.
// Uses ATTRACT_IDLE_SEC as the base unit so you don't need yet another knob.
gAttractCooldownUntil = llGetUnixTime() + (ATTRACT_IDLE_SEC * 3);
// Reset arming timer so we don't instantly trigger again after cooldown.
gSpecStillSince = 0;
stopMusic();
removeAllNpcs();
ExpoFlareSignal(FALSE);
if (!gRunning) llSetTimerEvent(0.0);
return TRUE;
}
// ===========================================================
// NPC LIFECYCLE
// ===========================================================
integer removeAllNpcs()
{
integer i;
integer n = llGetListLength(gNpcs);
for (i = 0; i < n; i++)
{
key npc = llList2Key(gNpcs, i);
if (npc != NULL_KEY)
{
osNpcStopMoveToTarget(npc);
osNpcStand(npc);
osNpcRemove(npc);
}
}
gNpcs = [];
gNpcLastNames = [];
gNpcDeathInc = [];
gNpcPrevHealth = [];
gNpcAngles = [];
gNpcLastShooter = [];
gNpcLastHitTime = [];
return TRUE;
}
integer spawnNpc(integer i, vector basePos)
{
// Combat spawns clamp to 2..5 (FEAST uses 5)
GRUG_COUNT = clampNpcCount(GRUG_COUNT);
string last = pickUniqueLastName(i);
key npc = osNpcCreate("Grug", last, basePos, NPC_NOTECARD, OS_NPC_OBJECT_GROUP);
gNpcs += [npc];
gNpcLastNames += [last];
gNpcDeathInc += [0];
gNpcPrevHealth += [osGetHealth(npc)];
float baseAng = (TWO_PI / (float)GRUG_COUNT) * (float)i;
baseAng += 0.35;
gNpcAngles += [baseAng];
// shooter tracking init
gNpcLastShooter += [NULL_KEY];
gNpcLastHitTime += [0];
osNpcStand(npc);
osNpcPlayAnimation(npc, ZombieWalk);
return TRUE;
}
integer announceFromNpc(key npc, string msg)
{
if (npc != NULL_KEY) osNpcSay(npc, msg);
else llSay(0, msg);
return TRUE;
}
// ===========================================================
// CHASE GEOMETRY
// ===========================================================
vector desiredOffset(integer i, rotation agentRot)
{
float ang = llList2Float(gNpcAngles, i);
vector local = ;
local += <0.0, -1.2, 0.0>;
vector world = local * agentRot;
world.z = 0.0;
return world;
}
vector applySeparation(vector candidatePos, integer selfIdx)
{
integer n = llGetListLength(gNpcs);
integer j;
for (j = 0; j < n; j++)
{
if (j == selfIdx) jump cont;
key other = llList2Key(gNpcs, j);
if (other == NULL_KEY) jump cont;
vector otherPos = osNpcGetPos(other);
float d = llVecDist(candidatePos, otherPos);
if (d > 0.001 && d < GRUG_SPACING)
{
vector away = llVecNorm(candidatePos - otherPos);
float push = (GRUG_SPACING - d);
candidatePos += away * push;
}
@cont;
}
return candidatePos;
}
// ===========================================================
// SCOREBOARD LINKING
// ===========================================================
integer resolveScoreLink()
{
integer i;
integer n = llGetNumberOfPrims();
for (i = 2; i <= n; i++)
{
if (llGetLinkName(i) == SCORE_PRIM_NAME)
return i;
}
return -1;
}
integer scoreAgentKill(key avatar)
{
if (avatar == NULL_KEY) return FALSE;
if (SCORE_LINKNUM < 0) SCORE_LINKNUM = resolveScoreLink();
string nm = llKey2Name(avatar);
string payload = "KILL|" + (string)avatar + "|" + nm;
if (SCORE_LINKNUM > 0)
llMessageLinked(SCORE_LINKNUM, SCORE_MSG_NUM, payload, avatar);
else
llMessageLinked(LINK_SET, SCORE_MSG_NUM, payload, avatar);
return TRUE;
}
integer scoreGrugKill(integer points)
{
if (points < 0) points = 0;
if (SCORE_LINKNUM < 0) SCORE_LINKNUM = resolveScoreLink();
string payload = "GRUG|" + (string)points;
if (SCORE_LINKNUM > 0)
llMessageLinked(SCORE_LINKNUM, SCORE_MSG_NUM, payload, NULL_KEY);
else
llMessageLinked(LINK_SET, SCORE_MSG_NUM, payload, NULL_KEY);
return TRUE;
}
// ===========================================================
// KILL ATTRIBUTION (slug -> controller HIT feed)
// ===========================================================
integer npcIndex(key npc)
{
integer i;
integer n = llGetListLength(gNpcs);
for (i = 0; i < n; i++)
if (llList2Key(gNpcs, i) == npc)
return i;
return -1;
}
integer noteNpcHit(key shooter, key victimNpc)
{
integer idx = npcIndex(victimNpc);
if (idx < 0) return FALSE;
if (!osIsNpc(victimNpc)) return FALSE;
integer now = llGetUnixTime();
gNpcLastShooter = llListReplaceList(gNpcLastShooter, [shooter], idx, idx);
gNpcLastHitTime = llListReplaceList(gNpcLastHitTime, [now], idx, idx);
return TRUE;
}
key getKillAttribution(integer idx)
{
integer now = llGetUnixTime();
key shooter = llList2Key(gNpcLastShooter, idx);
integer t = llList2Integer(gNpcLastHitTime, idx);
if (shooter != NULL_KEY && (now - t) <= HIT_WINDOW_SEC)
return shooter;
return gTarget; // fallback
}
// ===========================================================
// GAME START / END / NPC KILLED
// ===========================================================
integer startGame(integer minutes, key target)
{
// Hard gate: no starts from spectator/out-of-arena.
list _od0 = llGetObjectDetails(target, [OBJECT_POS]);
vector tpos0 = llList2Vector(_od0, 0);
if (tpos0 == ZERO_VECTOR) return FALSE;
if (!canLaunchFromPos(tpos0))
{
llInstantMessage(target, "GRUG SAY: YOU NOT IN ARENA. COME FEAST WITH GRUG.");
return FALSE;
}
gRunning = TRUE;
if (gAttractMode) stopAttract("START GAME");
ExpoFlareSignal(TRUE);
gTarget = target;
gFeastMode = (minutes <= 0);
gGameSecondsTotal = minutes * 60;
gGameSecondsLeft = gGameSecondsTotal;
gWarned30 = FALSE;
// start alarm
playAlarm();
removeAllNpcs();
vector tpos = tpos0;
vector base = tpos + <0.0, 0.0, 1.5>; //GRUG SPAWN CONTROL
integer i;
for (i = 0; i < GRUG_COUNT; i++)
{
vector p = base + <(float)i * (GRUG_SPACING * 0.7), 0.0, 0.0>;
spawnNpc(i, p);
llSleep(0.25);
}
key speaker = randomNpcKey();
if (gFeastMode)
{
announceFromNpc(speaker, "FEAST MODE! ENDLESS BLOODBATH! RUN PREY!");
playMusic(MUSIC_FEAST, 1.0);
}
else
{
announceFromNpc(speaker, "GRUG ASSAULT START! " + (string)minutes + " MINUTES! RUN PREY!");
if (minutes <= 3) playMusic(MUSIC_FAMINE, 1.0);
else playMusic(MUSIC_HUNGER, 1.0);
}
llSensorRemove();
llSensorRepeat("", "", AGENT, ATTACK_SENSOR_RANGE, PI, FOLLOW_UPDATE_SEC);
llSetTimerEvent(1.0);
return TRUE;
}
integer endGame(string reason)
{
// end alarm
playAlarm();
key speaker = randomNpcKey();
announceFromNpc(speaker, "GRUG ASSAULT OVER! " + reason);
// Return to idle behavior: keep sensor + timer alive for auto-attract.
llSetTimerEvent(1.0);
llSensorRemove();
llSensorRepeat("", "", AGENT, ATTACK_SENSOR_RANGE, PI, 2.0);
// reset idle-attract arming timestamp
gSpecStillSince = 0;
removeAllNpcs();
gRunning = FALSE;
gTarget = NULL_KEY;
ExpoFlareSignal(FALSE);
gFeastMode = FALSE;
stopMusic();
return TRUE;
}
integer npcKilled(integer i)
{
// attribute kill to last shooter (10s) else gTarget
key scorer = getKillAttribution(i);
if (scorer != NULL_KEY) scoreAgentKill(scorer);
key npc = llList2Key(gNpcs, i);
if (npc != NULL_KEY)
{
osNpcStopMoveToTarget(npc);
osNpcStand(npc);
osNpcPlayAnimation(npc, DeathAnim);
trigSound(SOUND_SCREAM1, 1.0);
llSleep(0.8);
osNpcRemove(npc);
}
gNpcs = llDeleteSubList(gNpcs, i, i);
gNpcLastNames = llDeleteSubList(gNpcLastNames, i, i);
gNpcDeathInc = llDeleteSubList(gNpcDeathInc, i, i);
gNpcPrevHealth = llDeleteSubList(gNpcPrevHealth, i, i);
gNpcAngles = llDeleteSubList(gNpcAngles, i, i);
gNpcLastShooter = llDeleteSubList(gNpcLastShooter, i, i);
gNpcLastHitTime = llDeleteSubList(gNpcLastHitTime, i, i);
if (gRunning && gTarget != NULL_KEY)
{
list _od = llGetObjectDetails(gTarget, [OBJECT_POS]);
vector tpos = llList2Vector(_od, 0);
vector p = tpos + ;
integer newIdx = llGetListLength(gNpcs);
spawnNpc(newIdx, p);
key speaker = randomNpcKey();
announceFromNpc(speaker, "GRUG FALL DOWN... NEW GRUG COME UP, HA HA!");
}
return TRUE;
}
// ===========================================================
// UI MENU (GAME MODE SELECT)
// ===========================================================
integer showMenu(key who)
{
list buttons = ["FAMINE", "HUNGER", "FEAST"];
llDialog(
who,
"\nFIGHT TYRANNY! SAVE OPENSIM!\nThe craven Dr. Fester deploys his Army of Grugs to punish your right to privacy in the metaverse.\n \nConfirm that you can fire your weapon when in mouselook mode (m) using left mouse button.\n \nGrug Defaults: 2 Shots needed to kill Grug. The good news is that players earn ONE point per KILL. The bad news is that Grugs score more points because clever humans are harder to KILL!\n \nSelect Game Mode and HAPPY SCREAMING, PREY!\n \n",buttons,
gDialogChan
);
return TRUE;
}
// ===========================================================
// STATE: DEFAULT (GAME CORE)
// ===========================================================
default
{
state_entry()
{
Destination = llGetRegionName();
SCORE_LINKNUM = resolveScoreLink();
vector pos = llGetPos();
gDialogChan = (integer)(-pos.x * pos.y);
if (gDialogChan == 0) gDialogChan = -500123;
if (gListenHandle) llListenRemove(gListenHandle);
gListenHandle = llListen(gDialogChan, "", NULL_KEY, "");
// slug HIT feed
if (gHitListenHandle) llListenRemove(gHitListenHandle);
gHitListenHandle = llListen(CH_CAGE_HIT, "", NULL_KEY, "");
llPreloadSound(SOUND_ALARM);
llPreloadSound(SOUND_GROWL);
llPreloadSound(SOUND_SCREAM2);
llPreloadSound(SOUND_SCREAM1);
gRunning = FALSE;
gTarget = NULL_KEY;
gFeastMode = FALSE;
// Idle systems: keep sensor + timer alive so auto-attract can work.
llSetTimerEvent(1.0);
llSensorRemove();
llSensorRepeat("", "", AGENT, ATTACK_SENSOR_RANGE, PI, 2.0);
removeAllNpcs();
stopMusic();
ExpoFlareSignal(FALSE);
// idle-attract arming timestamp
gSpecStillSince = 0;
}
changed(integer change)
{
if (change & CHANGED_REGION_START) llResetScript();
if (change & CHANGED_INVENTORY) llResetScript();
if (change & CHANGED_LINK) llResetScript();
}
touch_start(integer total_number)
{
total_number = total_number; // lint
key toucher = llDetectedKey(0);
// Idle: open game menu (combatants only)
if (!gRunning)
{
vector tp = llDetectedPos(0);
// No menu for spectators, and no menu outside arena.
if (!canLaunchFromPos(tp))
{
// Keep it simple: no dialog, no state changes.
// Optional: swap llInstantMessage for announceFromNpc if you want Grug to mock them.
llInstantMessage(toucher, "GRUG SAY: SPECTATORS NO PUSH BUTTONS. GO TO ARENA. FIGHT OR BE FOOD.");
return;
}
showMenu(toucher);
return;
}
// Running: taunt
if (gRunning)
{
key speaker = randomNpcKey();
string t = llList2String(TauntsTouch, (integer)llFrand((float)llGetListLength(TauntsTouch)));
announceFromNpc(speaker, t);
}
}
listen(integer channel, string name, key id, string message)
{
name = name; // lint
// HIT feed (slug -> controller)
if (channel == CH_CAGE_HIT)
{
// Expected: HIT||
list parts = llParseString2List(message, ["|"], []);
if (llGetListLength(parts) == 3 && llList2String(parts, 0) == "HIT")
{
key shooter = (key)llList2String(parts, 1);
key victim = (key)llList2String(parts, 2);
if (shooter != NULL_KEY && victim != NULL_KEY)
noteNpcHit(shooter, victim);
}
return;
}
// Dialog selection
if (channel != gDialogChan) return;
if (gRunning) return; // block while running
// Failsafe: even if someone got a dialog, do not allow start from spectator/outside arena.
if (!canLaunchKey(id))
{
llInstantMessage(id, "GRUG SAY: NO START FROM SPECTATOR ZONE. GO TO ARENA. BE MY NEXT MEAL.");
return;
}
if (message == "FAMINE")
{
GRUG_COUNT = 2;
startGame(3, id);
return;
}
if (message == "HUNGER")
{
GRUG_COUNT = 3;
startGame(5, id);
return;
}
if (message == "FEAST")
{
GRUG_COUNT = 5;
startGame(0, id); // 0 => feast mode (continuous)
return;
}
}
sensor(integer num)
{
// LSL: declarations must come before statements in the same block
list _od;
// ATTRACT running: stop if combatant enters arena
if (gAttractMode)
{
integer k;
for (k = 0; k < num; k++)
{
vector ap = llDetectedPos(k);
if (inArena(ap) && !inSpectator(ap))
{
stopAttract("COMBATANT");
return;
}
}
return;
}
// IDLE: if anyone is around (any detection), start attract after ATTRACT_IDLE_SEC
if (!gRunning)
{
// Cooldown: do not re-arm attract
if (llGetUnixTime() < gAttractCooldownUntil)
{
gSpecStillSince = 0;
return;
}
// Use first detected agent as "presence"
if (num > 0)
{
if (gSpecStillSince == 0) gSpecStillSince = llGetUnixTime();
if ((llGetUnixTime() - gSpecStillSince) >= ATTRACT_IDLE_SEC)
{
startAttract();
return;
}
}
else
{
gSpecStillSince = 0;
}
return;
}
// ---- COMBAT continues ----
if (gTarget == NULL_KEY) return;
// Do we have ANY valid combatant detected this tick?
integer hasCombatant = FALSE;
integer k2;
for (k2 = 0; k2 < num; k2++)
{
vector ap2 = llDetectedPos(k2);
if (inArena(ap2) && !inSpectator(ap2))
{
hasCombatant = TRUE;
}
}
// FEAST ends when arena has no *non-spectator* avatars.
// Timed modes simply idle if nobody is detected.
if (!hasCombatant)
{
if (gFeastMode)
{
endGame("NO PREY LEFT!");
}
return;
}
integer i;
integer n = llGetListLength(gNpcs);
// LSL: declare locals before executable statements -compiler goof
list od;
vector tpos;
rotation trot;
key prey;
vector npcPos;
float preyHealth;
for (i = 0; i < n; i++)
{
key npc = llList2Key(gNpcs, i);
if (npc == NULL_KEY) jump nextNpc;
npcPos = osNpcGetPos(npc);
// Pick nearest detected prey to THIS NPC.
prey = nearestDetectedCombatant(num, npcPos);
if (prey == NULL_KEY)
{
osNpcStopMoveToTarget(npc);
osNpcStand(npc);
osNpcPlayAnimation(npc, IdleAnim);
jump nextNpc;
}
// Pull prey pos/rot in one go.
od = llGetObjectDetails(prey, [OBJECT_POS, OBJECT_ROT]);
tpos = llList2Vector(od, 0);
trot = llList2Rot(od, 1);
if (tpos == ZERO_VECTOR)
{
// Prey vanished this tick.
osNpcStopMoveToTarget(npc);
osNpcStand(npc);
osNpcPlayAnimation(npc, IdleAnim);
jump nextNpc;
}
// Arena enforcement + spectator safety per prey.
if (!inArena(tpos) || inSpectator(tpos))
{
osNpcStopMoveToTarget(npc);
osNpcStand(npc);
osNpcPlayAnimation(npc, IdleAnim);
jump nextNpc;
}
preyHealth = osGetHealth(prey);
float curNpcHealth = osGetHealth(npc);
float prevNpcHealth = llList2Float(gNpcPrevHealth, i);
if (curNpcHealth < prevNpcHealth - 0.1)
{
integer di = llList2Integer(gNpcDeathInc, i) + 1;
gNpcDeathInc = llListReplaceList(gNpcDeathInc, [di], i, i);
if (di <= NPC_DEATH_POINTS)
{
trigSound(SOUND_SCREAM2, 1.0);
osNpcPlayAnimation(npc, IdleAnim);
llSleep(0.2);
osNpcPlayAnimation(npc, ZombieWalk);
}
if (di >= NPC_DEATH_POINTS)
{
npcKilled(i);
n = llGetListLength(gNpcs);
i = -1;
jump loopContinue;
}
}
gNpcPrevHealth = llListReplaceList(gNpcPrevHealth, [curNpcHealth], i, i);
vector want = tpos + desiredOffset(i, trot);
want = applySeparation(want, i);
want.z = tpos.z;
vector step = npcPos + ((want - npcPos) * CLUNKINESS);
float dist = llVecDist(npcPos, tpos);
if (dist > MOVE_DISTANCE)
{
osNpcMoveToTarget(npc, step, OS_NPC_NO_FLY);
}
float biteDist = llVecDist(npcPos, tpos);
if (biteDist < BITE_DISTANCE)
{
if (preyHealth <= LOW_HEALTH_THRESHOLD)
{
//NEXT LINE AFFECTS GRUGS SCORE
integer grugPts = 1 + (integer)llFrand(3.0); // 1..2
scoreGrugKill(grugPts);
announceFromNpc(npc, "SCORE ONE FOR GRUGS! COME BACK FOR REVENGE, PREY!");
osTeleportAgent(prey, Destination, LandingPoint, LookAt);
osCauseHealing(prey, HEAL_ON_TELEPORT);
preyHealth = osGetHealth(prey);
}
string s1 = llList2String(Sayings, (integer)llFrand((float)llGetListLength(Sayings)));
string s2 = llList2String(Bits, (integer)llFrand((float)llGetListLength(Bits)));
announceFromNpc(npc, s1 + " " + llKey2Name(prey) + "'s " + s2);
osCauseDamage(prey, BITE_DAMAGE);
llSleep(1.3);
}
@nextNpc;
@loopContinue;
}
if (llFrand(2.0) >= 1.2)
trigSound(SOUND_GROWL, 1.0);
}
no_sensor()
{
// FEAST must end even when sensors detect nothing (sensor() won't fire).
if (gRunning && gFeastMode)
{
if (!anyAvatarInArena())
{
endGame("NO PREY LEFT!");
return;
}
}
}
timer()
{
if (gAttractMode)
{
updateAttractCircle();
return;
}
// IDLE: arm attract, then exit. Do NOT run combat countdown here.
if (!gRunning)
{
// Cooldown: prevents re-start while players are staging/teleporting.
if (llGetUnixTime() < gAttractCooldownUntil) return;
if (!gAttractMode)
{
if (gSpecStillSince == 0) gSpecStillSince = llGetUnixTime();
if ((llGetUnixTime() - gSpecStillSince) >= ATTRACT_IDLE_SEC)
{
startAttract();
}
}
return;
}
// ---- COMBAT TIMER ----
// Timed modes only; FEAST has no countdown end.
if (!gFeastMode)
{
gGameSecondsLeft--;
if (!gWarned30 && gGameSecondsLeft == 30)
{
gWarned30 = TRUE;
key speaker = randomNpcKey();
announceFromNpc(speaker, "THIRTY SECONDS LEFT, PREY! GRUG HUNGRY!");
}
if (gGameSecondsLeft <= 0)
{
endGame("TIME UP!");
return;
}
}
if (llFrand(1.0) < 0.10)
{
key speaker2 = randomNpcKey();
string t = llList2String(TauntsRandom, (integer)llFrand((float)llGetListLength(TauntsRandom)));
announceFromNpc(speaker2, t);
}
}
}