// Wolf Dogfight 1.0.0 - makes ANY aircraft able to dogfight on Wolf Territories (the wolf-air plugin), 2026-11-01. // Copyright (C) 2026 IntelligentWolf Ltd. Free to use in any aircraft on Wolf Territories. // // Paul 2026-11-01: "need extra scripting so someone can enable any aircraft". Drop this script into the ROOT prim of your // aircraft (bought, home-made, from any creator). It works BESIDE her own flight script, which keeps flying her. // Design: region-terrain-work/aircraft-dogfight-2026-11-01/a-research/DESIGN.md s3.2 (calls), s3.5 (bail out), s4 (this kit), // s2 (the WolfAir1 lines to the WolfViewer dogfight panel). Calls answered by the wolf-air plugin (c-wolf-air/src). // // Touch her (owner) the first time: a short setup walks you through the pilot's seat, which way her nose points, where the // guns are, the fire key and (optionally) her flight script. Then say "dogfight on" (or press DOGFIGHT on the flight deck). // // Chat (channel 0 from the owner or the pilot, or on her channel, 7 unless set): // dogfight on | dogfight off | status | fire (one second of fire) | bail | guns | cannon | all guns | home here | setup // Keys: the fire key you chose in setup (the left mouse button in mouselook by default). Her own flight script still gets // every key: OpenSim sends a control event to EVERY script that took that key (ScenePresence.cs:6534-6590 // SendControlsToScripts loops over all of them), whatever its pass_on. // // Our own Wolf Air planes carry this same script beside their helm (wolf_plane_helm*). The generator sets the linkset data // "wolfair_class" (her class) and the link messages below connect the two (README.md "Link protocol"): // 7300 helm -> kit "fire|1" / "fire|0", "crash|", "bail" // 7301 kit -> helm "factors|||||||" // 7302 kit -> helm "down" / "respawn||" // 7304 kit -> helm "hud|ammo=/|heat=/|dmg=<8 parts %>|fire=<0/1>|smoke=<0/1>|df=|safe=<0/1>" // With a helm aboard the helm speaks "hi" and "st" to the viewer (it knows speed, throttle, gear); this script then speaks // only the "hit", "kill", "down" and "respawn" lines. Without one, it speaks all of them. string VER = "1.0.0"; integer HELLO_CH = 7; // DESIGN s2: the viewer shouts "hud hello 1" on channel 7 until "hi" names her channel string HUD = "WolfAir1|"; float BURST = 0.25; // DESIGN s3.3: a burst is the trigger held for 0.25 s ... float FIRE_TICK = 0.26; // ... and air.fire is called at most 4 times a second (AirRules.MAX_FIRE_PER_S = 4) float FALL_TICK = 0.2; // the fall's keyframes: a 0.2 s tick and 0.3 s frames (hms_wolfy_helm_template.lsl DT/FRAME) float FALL_FRAME = 0.3; float TERMINAL = 70.0; // GAME TUNING: a falling wreck goes no faster than this (m/s) float SPIN = 1.0; // GAME TUNING: rad/s of yaw while she spins down float LEARN_MS = 10.0; // DESIGN s4 step 2: the nose is learned above 10 m/s list SECTIONS = ["engine", "fuel", "cockpit", "fuselage", "wing_left", "wing_right", "tail", "fin"]; // AirRules.SECTIONS // ── settings (linkset data, so they survive a re-rez) ── string gClass; // "" = custom (her own size), else our generator's class (linkset data "wolfair_class") integer gPilotLink; // the pilot seat prim (0 = the first to sit) string gAxis; // "auto" or "0".."3" = +X, -X, +Y, -Y (the nose axis air.dogfight custom takes) string gGuns; // "x,y,z,type;..." in her root's frame (types mg, hmg, cannon) string gFireKey; // "ml" (mouselook left button), "shift" (Shift+Left/Right), "chat" string gScript; // her flight script (stopped when she is shot down, reset at the respawn), "" = every other script here integer gCh; // her channel (7 unless set) string gGroup; // "all", "guns", "cannon" // ── state ── key gOwner; key gPilot = NULL_KEY; integer gHelm; // a Wolf helm is in this prim integer gOn; // dogfight mode wanted string gState = "off"; integer gSeq = -1; string gHealth = ""; string gGunsSt = "0/0/0"; string gCannonSt = "0/0/0"; float gPower = 1.0; float gRoll = 1.0; string gRollS = "1"; // as the plugin wrote it: "L" or "R" after it = she rolls towards that weaker wing (AirEngine.WatchData) float gPitch = 1.0; float gYaw = 1.0; float gLift = 1.0; integer gFire; integer gSafe; integer gSmoke; string gEvent = ""; key gWatchQ = NULL_KEY; float gWatchAt; float gWatchRetry; integer gTrigger; // the fire key is held (or the panel's FIRE) float gTriggerUntil; // chat "fire": until then float gLastWhy; // when the pilot was last told why a burst was refused list gHud; // avatars who said hello (seated) integer gMenuCh; integer gMenuL; integer gZeroL; integer gZeroPilotL; integer gChL; integer gHelloL; string gStep = ""; // the setup step on show list gScriptsShown; vector gHomeP; rotation gHomeR; integer gHaveHome; // the fall (no helm aboard) integer gFalling; vector gFallV; list gStopped; // scripts this kit stopped list gGunLinks; // gun prims whose +Z lies along her nose (they draw particle tracers) list gGlowSave; // [link, glow] while gun prims are shown in setup integer gEvSeq = -1; // the seq whose event was last acted on integer gTracersOn; float gLastPub; float gLastHitTold; // ─────────────────────────────────────────────────────────────── small helpers Tell(key a, string s) { if (a != NULL_KEY) llRegionSayTo(a, 0, s); } TellBoth(string s) { if (gPilot != NULL_KEY && gPilot != gOwner) Tell(gPilot, s); Tell(gOwner, s); } string Why() { string e = wolfGetLastError(); if (llSubStringIndex(e, "no plugin function") != -1) return "this region has no dogfight plugin yet (wolf-air) - dogfights work only where it is loaded"; if (e == "") e = "the guns did not answer"; return e; } string Lsd(string k, string dflt) { string v = llLinksetDataRead(k); if (v == "") return dflt; return v; } LoadSettings() { gClass = llLinksetDataRead("wolfair_class"); gPilotLink = (integer)Lsd("wolfair_pilot", "0"); gAxis = Lsd("wolfair_axis", "auto"); gGuns = llLinksetDataRead("wolfair_guns"); gFireKey = Lsd("wolfair_firekey", "ml"); gScript = llLinksetDataRead("wolfair_script"); gCh = (integer)Lsd("wolfair_ch", "7"); gGroup = Lsd("wolfair_group", "all"); string h = llLinksetDataRead("wolfair_home"); gHaveHome = FALSE; if (h != "") { list p = llParseString2List(h, ["|"], []); gHomeP = (vector)llList2String(p, 0); gHomeR = (rotation)llList2String(p, 1); gHaveHome = TRUE; } } integer Ready() { if (gClass != "") return TRUE; // our own planes: the generator set everything return llLinksetDataRead("wolfair_ready") == "1"; } integer IsHelmHere() { integer n = llGetInventoryNumber(INVENTORY_SCRIPT); integer i; for (i = 0; i < n; ++i) if (llSubStringIndex(llGetInventoryName(INVENTORY_SCRIPT, i), "wolf_plane_helm") == 0) return TRUE; return FALSE; } // The avatars sitting on her: link numbers after the prims. integer Seated(key a) { integer n = llGetNumberOfPrims(); integer l; for (l = llGetObjectPrimCount(llGetKey()) + 1; l <= n; ++l) if (llGetLinkKey(l) == a) return TRUE; return FALSE; } key FindPilot() { if (gPilotLink > 0) { key k = llAvatarOnLinkSitTarget(gPilotLink); if (k != NULL_KEY) return k; return NULL_KEY; } integer first = llGetObjectPrimCount(llGetKey()) + 1; if (llGetNumberOfPrims() >= first) return llGetLinkKey(first); return NULL_KEY; } // The nose in her root's frame (the axis air.dogfight custom is told). vector Nose() { if (gAxis == "1") return <-1.0, 0.0, 0.0>; if (gAxis == "2") return <0.0, 1.0, 0.0>; if (gAxis == "3") return <0.0, -1.0, 0.0>; return <1.0, 0.0, 0.0>; } string F2(float f) { return (string)((float)llRound(f * 100.0) / 100.0); } string V3(vector v) { return F2(v.x) + "," + F2(v.y) + "," + F2(v.z); } // ─────────────────────────────────────────────────────────────── guns // Prims whose name starts "gun": their positions in the root's frame; their description names the type (mg, hmg, cannon; // anything else is mg). Only prims whose own +Z lies along the nose draw particle tracers (a particle source fires along // its prim's +Z: llviewerpartsource.cpp, part_dir_vector (0,0,1) * the prim's rotation). string GunPrims() { string out = ""; integer n = llGetNumberOfPrims(); integer l; integer count = 0; for (l = 2; l <= n && count < 8; ++l) { // AirRules.MAX_CUSTOM_GUNS = 8 string nm = llToLower(llGetLinkName(l)); if (llGetSubString(nm, 0, 2) == "gun") { list p = llGetLinkPrimitiveParams(l, [PRIM_POS_LOCAL, PRIM_DESC]); string t = llToLower(llStringTrim(llList2String(p, 1), STRING_TRIM)); if (t != "hmg" && t != "cannon") t = "mg"; if (out != "") out += ";"; out += V3(llList2Vector(p, 0)) + "," + t; ++count; } } return out; } FindGunLinks() { gGunLinks = []; integer n = llGetNumberOfPrims(); integer l; vector nose = Nose(); for (l = 2; l <= n; ++l) { if (llGetSubString(llToLower(llGetLinkName(l)), 0, 2) == "gun") { rotation r = llList2Rot(llGetLinkPrimitiveParams(l, [PRIM_ROT_LOCAL]), 0); if (<0.0, 0.0, 1.0> * r * nose > 0.94) gGunLinks += [l]; // within 20 degrees of the nose } } } // Presets from her own bounding box (llGetBoundingBox: the linkset's box in the root's frame, LSL_Api.cs:11694-11697). string Preset(string which) { list bb = llGetBoundingBox(llGetKey()); vector mn = llList2Vector(bb, 0); vector mx = llList2Vector(bb, 1); vector nose = Nose(); vector side = <0.0, 0.0, 1.0> % nose; // her left float front = mx * nose; if (mn * nose > front) front = mn * nose; // the far end along the nose (the nose may point down an axis) float half = llFabs((mx - mn) * side) * 0.5; float midz = (mn.z + mx.z) * 0.5; vector c = (mn + mx) * 0.5; vector ahead = nose * front; vector up = <0.0, 0.0, midz>; vector centre = c - nose * (c * nose) - <0.0, 0.0, c.z>; // the centre line across her, at the root's height if (which == "nose2") return V3(centre + ahead * 0.85 + side * 0.25 + up) + ",mg;" + V3(centre + ahead * 0.85 - side * 0.25 + up) + ",mg"; if (which == "wing4") return V3(centre + ahead * 0.2 + side * half * 0.35 + up) + ",mg;" + V3(centre + ahead * 0.2 + side * half * 0.5 + up) + ",mg;" + V3(centre + ahead * 0.2 - side * half * 0.35 + up) + ",mg;" + V3(centre + ahead * 0.2 - side * half * 0.5 + up) + ",mg"; return V3(centre + ahead * 0.95 + up) + ",cannon"; } integer CountGuns(string type) { list g = llParseString2List(gGuns, [";"], []); integer i; integer n = 0; for (i = 0; i < llGetListLength(g); ++i) { string t = llList2String(llParseString2List(llList2String(g, i), [","], []), 3); if ((type == "cannon") == (t == "cannon")) ++n; } return n; } // The setup preview: "Wolf gun marker" objects (temporary red spheres) at each gun if the marker is in her contents, // otherwise the gun prims glow for 20 s. ShowGuns(string guns) { list g = llParseString2List(guns, [";"], []); integer i; integer have = llGetInventoryType("Wolf gun marker") == INVENTORY_OBJECT; for (i = 0; i < llGetListLength(g) && have; ++i) { list p = llParseString2List(llList2String(g, i), [","], []); vector local = <(float)llList2String(p, 0), (float)llList2String(p, 1), (float)llList2String(p, 2)>; vector at = llGetPos() + local * llGetRot(); if (llVecDist(at, llGetPos()) < 10.0) llRezObject("Wolf gun marker", at, ZERO_VECTOR, ZERO_ROTATION, 20); } if (!have) { integer n = llGetNumberOfPrims(); integer l; for (l = 2; l <= n; ++l) if (llGetSubString(llToLower(llGetLinkName(l)), 0, 2) == "gun") { gGlowSave += [l, llList2Float(llGetLinkPrimitiveParams(l, [PRIM_GLOW, 0]), 0)]; llSetLinkPrimitiveParamsFast(l, [PRIM_GLOW, ALL_SIDES, 0.6]); } } } UnshowGuns() { integer i; for (i = 0; i < llGetListLength(gGlowSave); i += 2) llSetLinkPrimitiveParamsFast(llList2Integer(gGlowSave, i), [PRIM_GLOW, ALL_SIDES, llList2Float(gGlowSave, i + 1)]); gGlowSave = []; } // ─────────────────────────────────────────────────────────────── particles Tracers(integer on) { if (on == gTracersOn) return; gTracersOn = on; integer i; for (i = 0; i < llGetListLength(gGunLinks); ++i) { integer l = llList2Integer(gGunLinks, i); if (!on) llLinkParticleSystem(l, []); else llLinkParticleSystem(l, [ PSYS_PART_FLAGS, PSYS_PART_EMISSIVE_MASK | PSYS_PART_FOLLOW_VELOCITY_MASK | PSYS_PART_INTERP_COLOR_MASK, PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_ANGLE_CONE, PSYS_SRC_ANGLE_BEGIN, 0.0, PSYS_SRC_ANGLE_END, 0.01, PSYS_SRC_BURST_SPEED_MIN, 250.0, PSYS_SRC_BURST_SPEED_MAX, 255.0, // llpartdata.cpp:255-256 unpackFixed 8.8: 255 is the most PSYS_SRC_BURST_RATE, 0.08, PSYS_SRC_BURST_PART_COUNT, 1, PSYS_PART_MAX_AGE, 1.2, PSYS_PART_START_SCALE, <0.12, 2.5, 0.0>, PSYS_PART_END_SCALE, <0.08, 2.0, 0.0>, PSYS_PART_START_COLOR, <1.0, 0.85, 0.45>, PSYS_PART_END_COLOR, <1.0, 0.55, 0.2>, PSYS_PART_START_ALPHA, 1.0, PSYS_PART_END_ALPHA, 0.6, PSYS_PART_START_GLOW, 0.4, PSYS_PART_END_GLOW, 0.2]); } } // Smoke from a damaged engine, flames when she burns (on the root; her own particles come back when she is mended only if // her own scripts set them again - README "Limits"). integer gPartsShown = -1; Damage(integer smoke, integer fire) { integer want = smoke + fire * 2; if (want == gPartsShown) return; gPartsShown = want; if (want == 0) { llParticleSystem([]); return; } vector c1 = <0.35, 0.35, 0.35>; vector c2 = <0.15, 0.15, 0.15>; float glow = 0.0; if (fire) { c1 = <1.0, 0.55, 0.15>; c2 = <0.2, 0.2, 0.2>; glow = 0.15; } llParticleSystem([ PSYS_PART_FLAGS, PSYS_PART_INTERP_COLOR_MASK | PSYS_PART_INTERP_SCALE_MASK | PSYS_PART_WIND_MASK, PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_EXPLODE, PSYS_SRC_BURST_SPEED_MIN, 0.2, PSYS_SRC_BURST_SPEED_MAX, 0.8, PSYS_SRC_BURST_RATE, 0.05, PSYS_SRC_BURST_PART_COUNT, 3, PSYS_SRC_BURST_RADIUS, 0.5, PSYS_PART_MAX_AGE, 4.0, PSYS_PART_START_SCALE, <0.8, 0.8, 0.0>, PSYS_PART_END_SCALE, <3.5, 3.5, 0.0>, PSYS_PART_START_COLOR, c1, PSYS_PART_END_COLOR, c2, PSYS_PART_START_ALPHA, 0.8, PSYS_PART_END_ALPHA, 0.0, PSYS_PART_START_GLOW, glow, PSYS_PART_END_GLOW, 0.0]); } MuzzleFlash() { if (gGunLinks != []) return; llParticleSystem([ PSYS_PART_FLAGS, PSYS_PART_EMISSIVE_MASK | PSYS_PART_INTERP_COLOR_MASK, PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_EXPLODE, PSYS_SRC_BURST_SPEED_MIN, 0.5, PSYS_SRC_BURST_SPEED_MAX, 1.5, PSYS_SRC_BURST_RADIUS, 0.2, PSYS_SRC_BURST_RATE, 0.05, PSYS_SRC_BURST_PART_COUNT, 2, PSYS_SRC_MAX_AGE, 0.3, PSYS_PART_MAX_AGE, 0.15, PSYS_PART_START_SCALE, <0.6, 0.6, 0.0>, PSYS_PART_END_SCALE, <0.2, 0.2, 0.0>, PSYS_PART_START_COLOR, <1.0, 0.85, 0.4>, PSYS_PART_END_COLOR, <1.0, 0.45, 0.1>, PSYS_PART_START_GLOW, 0.5, PSYS_PART_END_GLOW, 0.0]); gPartsShown = -1; } // ─────────────────────────────────────────────────────────────── the viewer's dogfight panel (WolfAir1, DESIGN s2) Line(string s) { integer i; for (i = 0; i < llGetListLength(gHud); ++i) llRegionSayTo(llList2Key(gHud, i), 0, HUD + s); } string Pct() { list h = llParseString2List(gHealth, [","], []); list out = []; integer s; for (s = 0; s < llGetListLength(SECTIONS); ++s) { integer pct = 100; integer i; for (i = 0; i < llGetListLength(h); ++i) { list kv = llParseString2List(llList2String(h, i), ["=", "/"], []); if (llList2String(kv, 0) == llList2String(SECTIONS, s)) { float m = (float)llList2String(kv, 2); if (m > 0.0) pct = llRound(100.0 * (float)llList2String(kv, 1) / m); } } out += [pct]; } return llDumpList2String(out, ","); } string Field(string st, integer i) { return llList2String(llParseStringKeepNulls(st, ["/"], []), i); } string HudFields() { return "ammo=" + Field(gGunsSt, 0) + "/" + Field(gCannonSt, 0) + "|heat=" + Field(gGunsSt, 1) + "/" + Field(gCannonSt, 1) + "|dmg=" + Pct() + "|fire=" + (string)gFire + "|smoke=" + (string)gSmoke + "|df=" + gState + "|safe=" + (string)gSafe; } Hi(key a) { if (gHelm) return; // the helm says hi (it knows her flight numbers) string cls = gClass; if (cls == "") cls = "custom"; llRegionSayTo(a, 0, HUD + "hi|ver=1|plane=" + llGetObjectName() + "|class=" + cls + "|ch=" + (string)gCh + "|vs=0|vne=0|gmax=0|guns=" + (string)CountGuns("guns") + "|cannon=" + (string)CountGuns("cannon") + "|wolfair=1"); } Publish() { gLastPub = llGetTime(); gSmoke = (gPower < 1.0) || gState == "down"; llMessageLinked(LINK_THIS, 7301, "factors|" + F2(gPower) + "|" + gRollS + "|" + F2(gPitch) + "|" + F2(gYaw) + "|" + F2(gLift) + "|" + (string)gFire + "|" + gState, NULL_KEY); if (gHelm) llMessageLinked(LINK_THIS, 7304, "hud|" + HudFields(), NULL_KEY); else if (gHud != []) Line("st|" + HudFields() + "|say="); if (gState == "off") Damage(0, 0); else Damage(gSmoke, gFire); } // ─────────────────────────────────────────────────────────────── the plugin (wolf-air) Watch() { if (!gOn && gState == "off") { gWatchQ = NULL_KEY; return; } gWatchQ = wolfCallAsync("air.watch", [gSeq]); gWatchAt = llGetTime(); if (gWatchQ == NULL_KEY) gWatchRetry = llGetTime() + 15.0; } // STATE: seq|state|health|guns|cannon|power|roll|pitch|yaw|lift|fuel_leak|fire|protected s|safe|last event (DESIGN s3.2) Absorb(string data) { list p = llParseStringKeepNulls(data, ["|"], []); if (llGetListLength(p) < 15) return; string was = gState; gSeq = (integer)llList2String(p, 0); gState = llList2String(p, 1); gHealth = llList2String(p, 2); gGunsSt = llList2String(p, 3); gCannonSt = llList2String(p, 4); gPower = (float)llList2String(p, 5); gRollS = llList2String(p, 6); gRoll = (float)gRollS; gPitch = (float)llList2String(p, 7); gYaw = (float)llList2String(p, 8); gLift = (float)llList2String(p, 9); gFire = (integer)llList2String(p, 11); gSafe = (integer)llList2String(p, 13); string ev = llList2String(p, 14); // a new event whenever the seq moves on (two hits can read the same); the answer to "on" carries seq -1 if (gSeq > gEvSeq && ev != "") { gEvSeq = gSeq; gEvent = ev; Happened(ev); } if (gState != was) Changed(was); Publish(); } // The event's own words: ":". Kinds read here: hit, down, respawn (README "Event kinds"). Happened(string ev) { integer c = llSubStringIndex(ev, ":"); string kind = ev; string text = ""; if (c != -1) { kind = llGetSubString(ev, 0, c - 1); text = llGetSubString(ev, c + 1, -1); } if (kind == "hit") { // " hit for by " list w = llParseString2List(text, [" "], []); string by = ""; integer b = llSubStringIndex(text, " by "); if (b != -1) by = llGetSubString(text, b + 4, -1); string dmg = ""; integer f = llListFindList(w, ["for"]); if (f != -1) dmg = llList2String(w, f + 1); Line("hit|by=" + by + "|part=" + llList2String(w, 0) + "|dmg=" + dmg); } if (kind == "kill") { // "you shot down ()" (AirEngine.cs ShotDown). The region's whole kill feed reaches every Wolf viewer as // the plugin's own "WolfAir" message (AirEngine.AddFeed W.Broadcast), so this line is only her own kills. string v = text; if (llGetSubString(v, 0, 13) == "you shot down ") v = llGetSubString(v, 14, -1); string how = ""; integer o = llSubStringIndex(v, " ("); if (o != -1) { how = llGetSubString(v, o + 2, -2); v = llGetSubString(v, 0, o - 1); } Line("kill|killer=" + llGetObjectName() + "|victim=" + v + "|how=" + how); } if (gPilot == NULL_KEY || kind == "fired" || kind == "hit_given") return; if (kind == "hit") { // the dogfight panel shows hits; in chat at most one every 2 s if (gHud != [] || llGetTime() < gLastHitTold + 2.0) return; gLastHitTold = llGetTime(); } Tell(gPilot, llGetObjectName() + ": " + text); } Changed(string was) { if (gState == "down") { // "down:shot down by : " (AirEngine.cs Event(t, "down", ...)) string by = ""; integer b = llSubStringIndex(gEvent, " by "); if (b != -1) { by = llGetSubString(gEvent, b + 4, -1); integer c = llSubStringIndex(by, ": "); if (c != -1) by = llGetSubString(by, 0, c - 1); } Line("down|by=" + by); Trigger(FALSE); if (gHelm) llMessageLinked(LINK_THIS, 7302, "down", NULL_KEY); else FallStart(); } if (llGetSubString(gState, 0, 4) == "wreck") { FallStop(); UnsitAll(); } if (gState == "on" && (llGetSubString(was, 0, 4) == "wreck" || was == "down")) Respawn(); if (gState == "off" && was != "off") { gOn = FALSE; llLinksetDataWrite("wolfair_df", "off"); Trigger(FALSE); } } Dogfight(key a, integer on, integer quiet) { if (a != gOwner && a != gPilot && !quiet) { Tell(a, "Only her owner or her pilot may switch dogfight mode."); return; } list r; if (on) { if (!Ready()) { Tell(a, "She is not set up for dogfights yet - touch her (owner) to set her up."); return; } if (gClass != "") r = wolfCall("air.dogfight", ["on", gClass]); else { if (gAxis == "auto") { Tell(a, "Fly her straight for a few seconds first so I can learn which way her nose points (or pick it in the setup)."); return; } r = wolfCall("air.dogfight", ["on", "custom", (integer)gAxis, gGuns]); } } else r = wolfCall("air.dogfight", ["off"]); if (r == []) { if (!quiet) Tell(a, "Dogfight mode: " + Why() + "."); return; } gOn = on; if (on) llLinksetDataWrite("wolfair_df", "on"); else llLinksetDataWrite("wolfair_df", "off"); if (llGetListLength(r) >= 15) Absorb(llDumpList2String(["-1"] + llList2List(r, 1, -1), "|")); else { gState = "off"; Publish(); } gSeq = -1; Watch(); if (on) { if (!gHaveHome) SetHome(); else wolfCall("air.home", [gHomeP, gHomeR]); FindGunLinks(); if (!quiet) Tell(a, llGetObjectName() + ": DOGFIGHT MODE ON. Any aircraft in dogfight mode may now fire on her, and she on them " + "(never inside safe airspace near an airport). " + FireHelp()); } else if (!quiet) Tell(a, llGetObjectName() + ": dogfight mode goes off in 30 seconds (firing cancels it)."); TakeKeys(); Tick(); } string FireHelp() { if (gFireKey == "ml") return "Fire: the left mouse button in mouselook (M), or say fire."; if (gFireKey == "shift") return "Fire: Shift+Left or Shift+Right, or say fire."; return "Fire: say fire."; } SetHome() { gHomeP = llGetPos(); gHomeR = llGetRot(); gHaveHome = TRUE; llLinksetDataWrite("wolfair_home", (string)gHomeP + "|" + (string)gHomeR); wolfCall("air.home", [gHomeP, gHomeR]); } Burst() { if (gPilot == NULL_KEY || !Seated(gPilot)) { Trigger(FALSE); return; } list r = wolfCall("air.fire", [gGroup, gPilot, BURST]); if (r == []) { if (llGetTime() > gLastWhy + 3.0) { Tell(gPilot, "Guns: " + Why() + "."); gLastWhy = llGetTime(); } Tracers(FALSE); return; } // ["fired", rounds, guns ammo, cannon ammo, guns heat %, cannon heat %] if ((integer)llList2String(r, 1) > 0) { Tracers(TRUE); MuzzleFlash(); } else Tracers(FALSE); gGunsSt = llList2String(r, 2) + "/" + llList2String(r, 4) + "/" + Field(gGunsSt, 2); gCannonSt = llList2String(r, 3) + "/" + llList2String(r, 5) + "/" + Field(gCannonSt, 2); if (llGetTime() > gLastPub + 0.5) Publish(); // the panel's ammo and heat while firing, twice a second } Trigger(integer on) { if (on == gTrigger) return; gTrigger = on; if (!on) { Tracers(FALSE); Publish(); } Tick(); } Bail() { if (gPilot == NULL_KEY) return; if (gState == "off" || llGetSubString(gState, 0, 4) == "wreck") { llUnSit(gPilot); // nothing to fight about: she just steps out return; } if (llGetInventoryType("Wolf Parachute") == INVENTORY_OBJECT) { // above the cockpit, inside llRezObject's 10 m reach llRezObject("Wolf Parachute", llGetPos() + <0.0, 0.0, 3.0>, ZERO_VECTOR, ZERO_ROTATION, 1); return; // object_rez sends air.bailout with its key } BailWith(""); } BailWith(string chute) { list r = wolfCall("air.bailout", [gPilot, chute]); if (r == []) Tell(gPilot, "Bail out: " + Why() + "."); } // ─────────────────────────────────────────────────────────────── shot down, without a Wolf helm aboard StopFlight() { gStopped = []; integer n = llGetInventoryNumber(INVENTORY_SCRIPT); integer i; string me = llGetScriptName(); for (i = 0; i < n; ++i) { string s = llGetInventoryName(INVENTORY_SCRIPT, i); if (s != me && (gScript == "" || s == gScript) && llGetScriptState(s)) { llSetScriptState(s, FALSE); gStopped += [s]; } } } StartFlight() { integer i; for (i = 0; i < llGetListLength(gStopped); ++i) { string s = llList2String(gStopped, i); if (llGetInventoryType(s) == INVENTORY_SCRIPT) { if (s == gScript) llResetOtherScript(s); // her flight script starts afresh, parked llSetScriptState(s, TRUE); } } gStopped = []; } FallStart() { gFallV = llGetVel(); StopFlight(); llSetStatus(STATUS_PHYSICS, FALSE); gFalling = TRUE; Tick(); } FallStop() { if (!gFalling) return; gFalling = FALSE; llSetKeyframedMotion([], []); } FallStep() { vector pos = llGetPos(); gFallV.z -= 9.80665 * FALL_TICK; if (llVecMag(gFallV) > TERMINAL) gFallV = llVecNorm(gFallV) * TERMINAL; vector move = gFallV * FALL_FRAME; vector next = pos + move; vector size = osGetRegionSize(); // OSSL_Api.cs:3663 osGetRegionSize, CheckThreatLevel() with no level: always allowed if (next.x < 2.0 || next.y < 2.0 || next.x > size.x - 2.0 || next.y > size.y - 2.0) { gFallV.x = 0.0; gFallV.y = 0.0; move.x = 0.0; move.y = 0.0; next = pos + move; } float floor = llGround(move); float w = llWater(move); if (w > floor) floor = w; float bottom = llList2Vector(llGetBoundingBox(llGetKey()), 0) * <0.0, 0.0, 1.0>; if (next.z + bottom <= floor) { float impact = llVecMag(gFallV); llSetKeyframedMotion([], []); gFalling = FALSE; llSetRegionPos(); list r = wolfCall("air.crash", [impact]); if (r != []) Absorb(llDumpList2String(["-1"] + llList2List(r, 1, -1), "|")); return; } rotation spin = llEuler2Rot(<0.0, 0.0, SPIN * FALL_FRAME>); llSetKeyframedMotion([move, spin, FALL_FRAME], []); } UnsitAll() { integer n = llGetNumberOfPrims(); integer l; for (l = n; l > llGetObjectPrimCount(llGetKey()); --l) llUnSit(llGetLinkKey(l)); } Respawn() { FallStop(); Damage(0, 0); Line("respawn"); if (gHelm) { llMessageLinked(LINK_THIS, 7302, "respawn|" + (string)gHomeP + "|" + (string)gHomeR, NULL_KEY); return; } if (gHaveHome) { llSetRegionPos(gHomeP); llSetRot(gHomeR); } StartFlight(); } // ─────────────────────────────────────────────────────────────── keys and the timer TakeKeys() { if (gPilot == NULL_KEY || gFireKey == "chat") return; if (llGetPermissionsKey() == gPilot && (llGetPermissions() & PERMISSION_TAKE_CONTROLS)) Grab(); else llRequestPermissions(gPilot, PERMISSION_TAKE_CONTROLS); } Grab() { integer c = CONTROL_ML_LBUTTON; if (gFireKey == "shift") c = CONTROL_LEFT | CONTROL_RIGHT; // accept, do not pass on. NOT pass_on TRUE: with accept and pass_on both TRUE OpenSim CLEARS the avatar's ignored // controls for every key (ScenePresence.cs:6401-6403 IgnoredControls = CONTROL_ZERO), undoing her own flight script's // llTakeControls(..., TRUE, FALSE). Every script that took a key still gets it (SendControlsToScripts). llTakeControls(c, TRUE, FALSE); } Tick() { float t = 0.0; if (gOn || gState != "off") t = 5.0; if (gAxis == "auto" && gClass == "" && gPilot != NULL_KEY) t = 1.0; if ((gTrigger || llGetTime() < gTriggerUntil) && gState == "on") t = FIRE_TICK; if (gFalling) t = FALL_TICK; llSetTimerEvent(t); } Learn() { vector v = llGetVel() / llGetRot(); // her velocity in her own frame if (llVecMag() < LEARN_MS) return; if (llFabs(v.x) >= llFabs(v.y)) { if (v.x > 0.0) gAxis = "0"; else gAxis = "1"; } else { if (v.y > 0.0) gAxis = "2"; else gAxis = "3"; } llLinksetDataWrite("wolfair_axis", gAxis); Tell(gOwner, llGetObjectName() + ": I have learned which way her nose points (" + llList2String(["+X", "-X", "+Y", "-Y"], (integer)gAxis) + "). Dogfight mode can go on now."); Tick(); } Pilot() { key p = FindPilot(); if (p == gPilot) return; if (gZeroPilotL) { llListenRemove(gZeroPilotL); gZeroPilotL = 0; } gPilot = p; Trigger(FALSE); if (p != NULL_KEY) { if (p != gOwner) gZeroPilotL = llListen(0, "", p, ""); TakeKeys(); if (Ready() && gState == "off") Tell(p, llGetObjectName() + " can dogfight: say \"dogfight on\". " + FireHelp()); } Tick(); } // ─────────────────────────────────────────────────────────────── the setup helper (paul-games-need-setup-helper) Menu(string step, string text, list buttons) { gStep = step; if (gMenuL) llListenRemove(gMenuL); gMenuL = llListen(gMenuCh, "", gOwner, ""); llDialog(gOwner, text, buttons, gMenuCh); } SetupStart() { Menu("pilot", "Let's get " + llGetObjectName() + " ready for dogfights!\n\nStep 1 of 5: sit in the PILOT's seat, then press \"Pilot seat\".", ["Pilot seat", "Skip", "Cancel"]); } SetupNose() { Menu("nose", "Step 2 of 5: which way does her nose point?\n\"Auto\" learns it the first time you fly her faster than 10 m/s.", ["Auto", "+X", "-X", "+Y", "-Y", "Back"]); } SetupGuns() { integer prims = llGetListLength(llParseString2List(GunPrims(), [";"], [])); string t = "Step 3 of 5: where are her guns?"; if (prims > 0) t += "\nShe has " + (string)prims + " prims named gun... (type from each one's description: mg, hmg or cannon)."; Menu("guns", t, ["Gun prims", "2 in the nose", "4 in the wings", "Nose cannon", "Back"]); } SetupGunsCheck(string guns) { gGuns = guns; ShowGuns(guns); Menu("gunscheck", "The guns are shown (red markers or glowing prims) for 20 seconds. Is that right?", ["Yes", "Back"]); } SetupFireKey() { Menu("firekey", "Step 4 of 5: the fire key.", ["Mouse (mouselook)", "Shift+arrows", "Chat only", "Back"]); } SetupScript() { gScriptsShown = []; integer n = llGetInventoryNumber(INVENTORY_SCRIPT); integer i; for (i = 0; i < n && llGetListLength(gScriptsShown) < 9; ++i) { string s = llGetInventoryName(INVENTORY_SCRIPT, i); if (s != llGetScriptName()) gScriptsShown += [s]; } list b = ["None", "Back"]; string t = "Step 5 of 5 (optional): which script flies her? It is stopped when she is shot down and started again when she respawns.\n"; for (i = 0; i < llGetListLength(gScriptsShown); ++i) { t += "\n" + (string)(i + 1) + ". " + llList2String(gScriptsShown, i); b += [(string)(i + 1)]; } Menu("script", t, b); } SetupDone() { llLinksetDataWrite("wolfair_ready", "1"); gStep = ""; if (gMenuL) { llListenRemove(gMenuL); gMenuL = 0; } FindGunLinks(); string more = ""; if (gAxis == "auto") more = " First fly her straight for a few seconds so I can learn which way her nose points."; Tell(gOwner, "Your aircraft is ready! Say \"dogfight on\" (or press DOGFIGHT on the flight deck) to fight." + more + " " + FireHelp()); } SetupAnswer(string m) { if (m == "Cancel") { gStep = ""; UnshowGuns(); return; } if (gStep == "pilot") { if (m == "Skip") { SetupNose(); return; } integer n = llGetObjectPrimCount(llGetKey()); integer l; integer found = -1; for (l = 1; l <= n; ++l) if (llAvatarOnLinkSitTarget(l) == gOwner) found = l; if (found == -1 && Seated(gOwner)) found = 0; // seated, but not on a sit target: "the first to sit" if (found == -1) { Menu("pilot", "You are not sitting on her yet. Sit in the pilot's seat, then press \"Pilot seat\".", ["Pilot seat", "Skip", "Cancel"]); return; } gPilotLink = found; llLinksetDataWrite("wolfair_pilot", (string)found); SetupNose(); return; } if (gStep == "nose") { if (m == "Back") { SetupStart(); return; } integer i = llListFindList(["Auto", "+X", "-X", "+Y", "-Y"], [m]); if (i == -1) return; if (i == 0) gAxis = "auto"; else gAxis = (string)(i - 1); llLinksetDataWrite("wolfair_axis", gAxis); SetupGuns(); return; } if (gStep == "guns") { if (m == "Back") { SetupNose(); return; } string g; if (m == "Gun prims") { g = GunPrims(); if (g == "") { Menu("guns", "She has no prims named gun... - pick one of the others.", ["2 in the nose", "4 in the wings", "Nose cannon", "Back"]); return; } } else { if (gAxis == "auto") { Menu("guns", "The places in the nose or the wings need her nose direction: go Back and pick it (or use her gun prims).", ["Gun prims", "Back"]); return; } if (m == "2 in the nose") g = Preset("nose2"); else if (m == "4 in the wings") g = Preset("wing4"); else g = Preset("cannon"); } SetupGunsCheck(g); return; } if (gStep == "gunscheck") { UnshowGuns(); if (m == "Back") { SetupGuns(); return; } llLinksetDataWrite("wolfair_guns", gGuns); SetupFireKey(); return; } if (gStep == "firekey") { if (m == "Back") { SetupGuns(); return; } if (m == "Mouse (mouselook)") gFireKey = "ml"; else if (m == "Shift+arrows") gFireKey = "shift"; else gFireKey = "chat"; llLinksetDataWrite("wolfair_firekey", gFireKey); SetupScript(); return; } if (gStep == "script") { if (m == "Back") { SetupFireKey(); return; } if (m == "None") gScript = ""; else gScript = llList2String(gScriptsShown, (integer)m - 1); llLinksetDataWrite("wolfair_script", gScript); SetupDone(); } } // ─────────────────────────────────────────────────────────────── orders Order(key a, string m) { m = llToLower(llStringTrim(m, STRING_TRIM)); integer mine = (a == gOwner || a == gPilot); if (m == "hud hello 1" || m == "hud hello") { if (!Seated(a)) return; if (llListFindList(gHud, [a]) == -1 && llGetListLength(gHud) < 12) gHud += [a]; Hi(a); Publish(); return; } if (m == "hud bye") { integer i = llListFindList(gHud, [a]); if (i != -1) gHud = llDeleteSubList(gHud, i, i); return; } if (!mine) return; if (m == "dogfight on" || m == "hud dogfight on") Dogfight(a, TRUE, FALSE); else if (m == "dogfight off" || m == "hud dogfight off") Dogfight(a, FALSE, FALSE); else if (m == "hud fire on" && a == gPilot) Trigger(TRUE); else if (m == "hud fire off" && a == gPilot) Trigger(FALSE); else if (m == "fire" && a == gPilot) { gTriggerUntil = llGetTime() + 1.0; Tick(); } else if ((m == "bail" || m == "bail out" || m == "hud bail") && a == gPilot) Bail(); else if (m == "guns" || m == "cannon" || m == "all guns") { gGroup = llList2String(["guns", "cannon", "all"], llListFindList(["guns", "cannon", "all guns"], [m])); llLinksetDataWrite("wolfair_group", gGroup); Tell(a, "Firing: " + gGroup + "."); } else if (m == "home here" && a == gOwner) { SetHome(); Tell(a, "She respawns here now."); } else if (m == "setup" && a == gOwner) SetupStart(); else if (m == "status") Tell(a, Status()); } string Status() { string s = llGetObjectName() + " (Wolf Dogfight " + VER + "): dogfight mode " + gState; if (!Ready()) return s + ". Not set up yet: touch her (owner)."; if (gState != "off") s += ". Damage: " + llDumpList2String(llParseString2List(gHealth, [","], []), " ") + ". Guns " + Field(gGunsSt, 0) + " rounds, " + Field(gGunsSt, 1) + "% hot; cannon " + Field(gCannonSt, 0) + " rounds."; if (gSafe) s += " In safe airspace."; return s; } Listens() { if (gChL) llListenRemove(gChL); if (gHelloL) llListenRemove(gHelloL); if (gZeroL) llListenRemove(gZeroL); gHelloL = llListen(HELLO_CH, "", NULL_KEY, ""); gChL = 0; if (gCh != HELLO_CH) gChL = llListen(gCh, "", NULL_KEY, ""); gZeroL = llListen(0, "", gOwner, ""); } Init() { gOwner = llGetOwner(); gHelm = IsHelmHere(); gMenuCh = -1 - (integer)llFrand(1.0e9); LoadSettings(); Listens(); gOn = FALSE; gState = "off"; FindGunLinks(); Pilot(); if (llLinksetDataRead("wolfair_df") == "on" && Ready()) Dogfight(gOwner, TRUE, TRUE); Tick(); } default { state_entry() { Init(); } on_rez(integer p) { llResetScript(); } changed(integer c) { if (c & CHANGED_OWNER) { llLinksetDataWrite("wolfair_df", "off"); llLinksetDataDelete("wolfair_home"); llResetScript(); } if (c & CHANGED_LINK) Pilot(); if (c & CHANGED_INVENTORY) gHelm = IsHelmHere(); if (c & CHANGED_REGION_START) { // a region restart ends every dogfight (DESIGN s3.10): she rejoins gState = "off"; gSeq = -1; gEvSeq = -1; if (llLinksetDataRead("wolfair_df") == "on" && Ready()) Dogfight(gOwner, TRUE, TRUE); } } touch_start(integer n) { key a = llDetectedKey(0); if (a != gOwner) { Tell(a, Status()); return; } if (!Ready()) SetupStart(); else Menu("main", Status(), ["Dogfight on", "Dogfight off", "Set up again", "Home here", "Close"]); } run_time_permissions(integer p) { if (p & PERMISSION_TAKE_CONTROLS) Grab(); } control(key id, integer level, integer edge) { if (id != gPilot) return; integer c = CONTROL_ML_LBUTTON; if (gFireKey == "shift") c = CONTROL_LEFT | CONTROL_RIGHT; if (gFireKey == "chat") return; Trigger((level & c) != 0 && gState == "on"); } listen(integer ch, string name, key id, string m) { if (ch == gMenuCh) { if (gStep == "main") { gStep = ""; if (m == "Dogfight on") Dogfight(id, TRUE, FALSE); else if (m == "Dogfight off") Dogfight(id, FALSE, FALSE); else if (m == "Set up again") SetupStart(); else if (m == "Home here") { SetHome(); Tell(id, "She respawns here now."); } return; } SetupAnswer(m); return; } if (llGetAgentSize(id) == ZERO_VECTOR) return; // people only Order(id, m); } link_message(integer from, integer num, string s, key k) { if (num != 7300) return; list p = llParseString2List(s, ["|"], []); string w = llList2String(p, 0); if (w == "fire") Trigger((integer)llList2String(p, 1) && gState == "on"); else if (w == "bail") Bail(); else if (w == "crash") { if (gState == "off") return; list r = wolfCall("air.crash", [(float)llList2String(p, 1)]); if (r != []) Absorb(llDumpList2String(["-1"] + llList2List(r, 1, -1), "|")); } } object_rez(key id) { if (llKey2Name(id) == "Wolf Parachute") BailWith((string)id); } dataserver(key q, string data) { if (q == gWatchQ) { gWatchQ = NULL_KEY; if (llGetSubString(data, 0, 5) == "error:") { gWatchRetry = llGetTime() + 5.0; return; } Absorb(data); Watch(); return; } } timer() { float now = llGetTime(); if (gFalling) FallStep(); if (gAxis == "auto" && gClass == "" && gPilot != NULL_KEY) Learn(); if (gState == "on" && (gTrigger || now < gTriggerUntil)) Burst(); else Tracers(FALSE); // the watch: lost, refused or stale (an idle watch is answered within 240 s, AirRules.WATCH_HEARTBEAT_S) if ((gOn || gState != "off") && gWatchQ == NULL_KEY && now >= gWatchRetry) Watch(); if (gWatchQ != NULL_KEY && now - gWatchAt > 300.0) Watch(); Tick(); } }