From c52ce856c3f7493b6036e19227515d5dcd4e2d7a Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Wed, 6 May 2026 19:28:37 +0200 Subject: [PATCH 1/5] clauding --- gunfun/mod/api.gsc | 75 ------------------------------------------ gunfun/mod/main.gsc | 36 +++++++++++++++++--- gunfun/mod/streaks.gsc | 31 ++++++++++------- gunfun/mod/vote.gsc | 7 ++++ 4 files changed, 59 insertions(+), 90 deletions(-) delete mode 100755 gunfun/mod/api.gsc diff --git a/gunfun/mod/api.gsc b/gunfun/mod/api.gsc deleted file mode 100755 index ec17d71..0000000 --- a/gunfun/mod/api.gsc +++ /dev/null @@ -1,75 +0,0 @@ -#include common_scripts\utility; -#include maps\mp\_utility; -#include maps\mp\gametypes\_hud_util; - -// For sending data, you use - as split character and for response I use | as split character -// all-10 => top 10 guids split by | - -Init() -{ - if(true) // unfinished script - return; - //level.api = "http://178.63.44.165:80831/"; - level.api = ""; - level.chatCommands = []; - level.chatCommands[level.chatCommands.size] = "register"; - level.chatCommands[level.chatCommands.size] = "increment"; - level.chatCommands[level.chatCommands.size] = "all"; - level.chatCommands[level.chatCommands.size] = "read"; - thread onSay(); - thread onPlayerConnect(); -} -onPlayerConnect() -{ - while(true) - { - level waittill("connected", player); - player thread tryCallApi("register"); - } -} -onSay() -{ - while(true) - { - level waittill("say", string, player); - iprintlnBold(string + " " + player.name); - player thread checkString(string); - } -} -checkString(string) -{ - if(string[0] != "!" && string[0] != "-") - return; - string = GetSubStr(string,1); //GetSubStr( , , ) - iPrintln(string); - cmd = strTok(string, " "); - foreach(chat in level.chatCommands) - { - if(chat == cmd[0]) - self callApi(chat); - } -} -callApi(string) -{ - list = httpGet(level.api + ""); - list waittill("done", success, data); - if(!success) - return "error"; - iPrintln("^2green"); - return data; -} -tryCallApi(parameter) -{ - data = self callApi("!" + parameter + "-" + self.guid); - if(!isDefined(data)) - self iPrintlnBold(parameter + " was successful"); - else if(data == "error") - self iPrintlnBold("^1error calling the api"); - else - { - wait 2; - ar = strTok(data, "|"); - foreach(a in ar) - self iPrintlnBold(a); - } -} diff --git a/gunfun/mod/main.gsc b/gunfun/mod/main.gsc index 853e585..05f2750 100755 --- a/gunfun/mod/main.gsc +++ b/gunfun/mod/main.gsc @@ -90,7 +90,7 @@ initializeGametype(type) // called in vote.gsc after first map setDvar("speed", 1.5); setDvar("streaks_online", 1); setDvar("jump_height", 0.5); - setDvar("amount_weapons", 0); // if 0 uses whole list of possible guns, if set > 0 uses amount larger than 0 + setDvar("amount_weapons", 10); // if 0 uses whole list of possible guns, if set > 0 uses amount larger than 0 setDvar("gun_kills", 1); break; default: // not required @@ -343,6 +343,16 @@ getEnemyTeam() } updateWeapon() { + // Safety: end this thread if the match is over or the player is gone/dead. + level endon("nuke"); + self endon("disconnect"); + self endon("death"); + // Exclusivity guard: notifying "updateWeapon" kills any previously running + // instance of this function on this player, then we register to die the same + // way when the NEXT call arrives. Only one updateWeapon thread per player. + self notify("updateWeapon"); + self endon("updateWeapon"); + if(self.current > (level.gungameList.size - 1)) { self thread tryNuke(); @@ -382,10 +392,19 @@ updateWeapon() else self setClientDvar("bots_play_knife", 0); } + // NOTE: The old recursive self-call "self updateWeapon()" was removed here. + // It caused unbounded call-stack growth when getCurrentWeapon() returned "none". + // takeInvalidWeapon() polls every frame and will re-issue the thread if needed. - if(self getCurrentWeapon() == "none" && !self isMantling() && !self isOnLadder()) // in rare case weapon does not exist + // Bounded retry: the engine may need a few frames after switchtoweaponimmediate + // to register the active weapon. Loop up to 8 frames re-issuing the switch. + // This is the safe replacement for the old unbounded recursive call. + retries = 0; + while(self getCurrentWeapon() == "none" && !self isMantling() && !self isOnLadder() && retries < 8) { - self updateWeapon(); + self switchtoweaponimmediate(level.gungameList[self.current]); + waitFrame(); + retries++; } } refillOnFire() @@ -624,9 +643,15 @@ lowerMultitext(multiplier) } tryNuke() { - if(isDefined(level.nukeIncoming) || level.state == "aftermatch") + // Use level.nukeTriggered as OUR re-entry guard. + // DO NOT use level.nukeIncoming here — that flag is owned by the engine's + // _nuke.gsc::tryUseNuke(). Setting it before calling tryUseNuke causes the + // engine to see "nuke already on its way" and abort without firing the nuke. + if(isDefined(level.nukeTriggered) || level.state == "aftermatch") return; + // Set our custom flag and state immediately (atomic — no yield before this). + level.nukeTriggered = true; level.state = "aftermatch"; if(getDvarInt("scr_nuke_enabled", 1) == 0) @@ -1150,6 +1175,7 @@ watchDeagleGL() { level endon("nuke"); self endon("disconnect"); + self endon("death"); // prevent thread accumulation across respawns while(true) { self waittill("weapon_fired", weaponName); @@ -1173,6 +1199,7 @@ watchHUD() { level endon("nuke"); self endon("disconnect"); + self endon("death"); // prevent thread accumulation across respawns while(true) { self setClientDvar("ui_drawradar", 1); @@ -1194,6 +1221,7 @@ watchM40A3() { level endon("nuke"); self endon("disconnect"); + self endon("death"); // prevent thread accumulation across respawns while(true) { self waittill("weapon_fired", weaponName); diff --git a/gunfun/mod/streaks.gsc b/gunfun/mod/streaks.gsc index 75d4e94..1ff5d80 100755 --- a/gunfun/mod/streaks.gsc +++ b/gunfun/mod/streaks.gsc @@ -19,6 +19,10 @@ loadStreaks() precacheShader("cardicon_cod4"); precacheShader("cardicon_loadedfinger"); + // Pre-cache Jetpack FX at level init (loadfx must NOT be called at runtime). + level._effect["jetpack_smoke"] = loadfx("smoke/smoke_trail_white_heli"); + level._effect["jetpack_flare"] = loadfx("misc/flares_cobra"); + level.streaks3 = []; level.streaks6 = []; level.streaks9 = []; @@ -260,6 +264,7 @@ giveStreak(streak) break; case "No Reload": self thread NoReload(); + break; // FIX: was missing break, causing fall-through into Radioactive every time case "Radioactive": self thread Radioactive(); break; @@ -337,9 +342,7 @@ Jetpack() self iPrintlnBold("^3Press ^1F ^7to use ^:Jetpack!"); self.jetpack = 80; self maps\mp\perks\_perks::givePerk("specialty_falldamage"); - self.fx = []; - self.fx[0] = loadfx ("smoke/smoke_trail_white_heli"); - self.fx[1] = loadfx( "misc/flares_cobra" ); + // Use pre-cached FX handles from loadStreaks() — loadfx() must not be called at runtime. JETPACKBACK = createPrimaryProgressBar( -275 ); JETPACKBACK.bar.x = 40; JETPACKBACK.x = 100; @@ -358,12 +361,10 @@ Jetpack() { if(self usebuttonpressed() && self.jetpack>0) { - //self playsound("veh_ac130_sonic_boom"); - //self playsound("veh_mig29_sonic_boom"); self playsound("cobra_helicopter_dying_loop"); self setstance("crouch"); - playfx(self.fx[0],self gettagorigin("j_spine4")); - playfx(self.fx[1],self gettagorigin("j_spine4")); + playfx(level._effect["jetpack_smoke"],self gettagorigin("j_spine4")); + playfx(level._effect["jetpack_flare"],self gettagorigin("j_spine4")); earthquake(.15,.2,self gettagorigin("j_spine4"),50); self.jetpack--; if(self getvelocity()[2]<300) @@ -425,8 +426,12 @@ DeleteIMS(ims) DeleteIMS2(ims) { - level waittill("fuckemp"); - ims delete(); + // FIX: The original waited on "fuckemp" which was never notified anywhere — + // this created a permanent zombie thread holding the ims entity reference. + // Now we wait for nuke (end of match) and clean up the entity then. + level waittill("nuke"); + if(isDefined(ims)) + ims delete(); } DeleteIt(block, block2, block3, block4) @@ -647,8 +652,11 @@ Juggernaut() { level endon("nuke"); self.isJugger = true; - self.maxhealth = 400; - self.health = self.maxhealth; + // FIX: Use the custom health system (actual_maxhealth/actual_health) instead of + // the raw engine maxhealth. Setting engine maxhealth directly broke the HUD display + // and regen logic. We double the effective HP pool through the custom system. + self.actual_maxhealth = self.maxhp * 2; + self.actual_health = self.actual_maxhealth; self setMoveSpeedScale(.7); juggIcon = newHudElem(); @@ -673,6 +681,7 @@ destroyJuggOnNuke(juggIcon) } NoReload() { + level endon("nuke"); // FIX: was missing — thread survived past match end self endon("death"); self endon("disconnect"); while(true) diff --git a/gunfun/mod/vote.gsc b/gunfun/mod/vote.gsc index 1568787..f381601 100755 --- a/gunfun/mod/vote.gsc +++ b/gunfun/mod/vote.gsc @@ -428,8 +428,15 @@ startVote() level.elems setPoint("CENTER", "CENTER", 20,-75); level.elems.hideWhenInMenu = true; //level.elems = []; + // FIX: Added maxRetries guard. If the map pool is smaller than maxOptions, the + // i-- retry would loop forever (no wait = instant VM instruction-count crash). + maxRetries = level.maps.size * 3; + retries = 0; for(i = 0;i < level.maxOptions;i++) { + retries++; + if(retries > maxRetries) + break; // pool exhausted: accept fewer options rather than loop forever valid = true; map = level.maps[randomInt(level.maps.size-1)]; gamemode = level.gungamemodes[randomInt(level.gungamemodes.size)]; -- 2.50.1 From f7f30f4e801cb77da8ae36df68cb534af25901f3 Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Wed, 6 May 2026 19:31:45 +0200 Subject: [PATCH 2/5] all fungame weapons! --- gunfun/mod/main.gsc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gunfun/mod/main.gsc b/gunfun/mod/main.gsc index 05f2750..b153eaa 100755 --- a/gunfun/mod/main.gsc +++ b/gunfun/mod/main.gsc @@ -90,7 +90,7 @@ initializeGametype(type) // called in vote.gsc after first map setDvar("speed", 1.5); setDvar("streaks_online", 1); setDvar("jump_height", 0.5); - setDvar("amount_weapons", 10); // if 0 uses whole list of possible guns, if set > 0 uses amount larger than 0 + setDvar("amount_weapons", 0); // if 0 uses whole list of possible guns, if set > 0 uses amount larger than 0 setDvar("gun_kills", 1); break; default: // not required -- 2.50.1 From 32658cb9e97be306d9de6763ae7a8eacb163e2da Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Wed, 6 May 2026 19:35:27 +0200 Subject: [PATCH 3/5] more jumpy heighty --- gunfun/mod/main.gsc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gunfun/mod/main.gsc b/gunfun/mod/main.gsc index b153eaa..7f59d53 100755 --- a/gunfun/mod/main.gsc +++ b/gunfun/mod/main.gsc @@ -89,7 +89,7 @@ initializeGametype(type) // called in vote.gsc after first map setDvar("global_health", 60); setDvar("speed", 1.5); setDvar("streaks_online", 1); - setDvar("jump_height", 0.5); + setDvar("jump_height", 70); setDvar("amount_weapons", 0); // if 0 uses whole list of possible guns, if set > 0 uses amount larger than 0 setDvar("gun_kills", 1); break; -- 2.50.1 From 0be816107c6d816f5c9651dede49e9989e3f444a Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Wed, 6 May 2026 20:48:53 +0200 Subject: [PATCH 4/5] hell yeah --- gunfun/mod/main.gsc | 113 +++++++++++++++++++++++++---------------- gunfun/mod/streaks.gsc | 24 ++++----- 2 files changed, 79 insertions(+), 58 deletions(-) diff --git a/gunfun/mod/main.gsc b/gunfun/mod/main.gsc index 7f59d53..4bf2535 100755 --- a/gunfun/mod/main.gsc +++ b/gunfun/mod/main.gsc @@ -147,12 +147,26 @@ loadSettings() // Bot Management setDvar("bots_main", 1); - setDvar("bots_manage_fill", 12); - setDvar("bots_skill", 5); + setDvar("bots_manage_fill", 10); // total slots: players + bots = 10 + setDvar("bots_manage_fill_mode", 0); // mode 0 = count players AND bots + setDvar("bots_manage_fill_kick", 1); // kick a bot when a human pushes count over 10 + // Skill: 2 hard bots (1 per internal team), rest are brain dead + setDvar("bots_skill", 8); + setDvar("bots_skill_allies_hard", 1); + setDvar("bots_skill_allies_med", 0); + setDvar("bots_skill_axis_hard", 1); + setDvar("bots_skill_axis_med", 0); setDvar("bots_play_knife", 0); setDvar("bots_main_chat", 0); SetDvarIfUninitialized("scr_nuke_enabled", 1); + // Cache mode flags as level vars — avoids repeated getDvar() in hot per-player loops. + level.isTeamGame = (getDvar("g_gametype") == "gungame_team"); + level.isKillConfirmed = (getDvar("gunmode") == "Kill Confirmed"); + // Suppress engine-level developer prints (e.g. "Replacing perk X in slot Y with Z"). + // These come from native C code and cannot be silenced any other way. + // developer 0 is standard for production servers. + setDvar("developer", 0); } deleteSentries() { @@ -308,8 +322,20 @@ loadSetup() self setMoveSpeedScale(getDvarFloat("speed")); self maps\mp\perks\_perks::givePerk("specialty_fastreload"); // due to icys request :) self maps\mp\perks\_perks::givePerk("specialty_falldamage"); // due to icys request :) - self maps\mp\perks\_perks::givePerk("specialty_quickdraw"); - + self maps\mp\perks\_perks::givePerk("specialty_quickdraw"); + // Static HUD dvars set once per spawn — moved from watchHUD's 1-second polling loop. + // These values never change mid-game so there is no need to re-apply them every second. + self setClientDvar("cg_drawRadar", 1); + self setClientDvar("cg_drawStance", 0); + self setClientDvar("cg_drawTeamScores", 0); + self setClientDvar("cg_drawKillfeed", 0); + self setClientDvar("cg_drawBreathHint", 0); + self setClientDvar("cg_drawMantleHint", 0); + self setClientDvar("cg_drawTurretCrosshair", 0); + self setClientDvar("cg_cursorHints", 0); + // Keep g_hardcore=1 for server-side gameplay rules but show the normal client HUD + // so our custom health/weapon overlays render correctly without fighting hardcore suppression. + self setClientDvar("ui_hud_hardcore", 0); self thread takeInvalidWeapon(); if(level.state == "prematch") { @@ -418,28 +444,29 @@ refillOnFire() self giveMaxAmmo(weapon); } } -onKilling() { - self endon("disconnect"); - level endon("nuke"); - self.multiplier = 0; - self.amount = 0; - //kills = self.pers["kills"]; +onKilling() { + self endon("disconnect"); + level endon("nuke"); + self.multiplier = 0; + self.amount = 0; kills = 0; refreshCounter = 0; - self.scoretext = self createText("hudbig", 1, "CENTER", "CENTER", 0, 0, .7,""); - self.scoretext_amount = self createText("hudbig", 1, "CENTER", "CENTER", -10, 20, .7,""); - self.multitext = []; - for(i=2;i<6;i++) - self.multitext[i] = self createText("hudbig", .6, "CENTER", "CENTER", 50, (i * 20), .7,""); + // Cache settings that never change mid-match — eliminates getDvar()/getDvarInt() + // native calls inside this loop which runs every 0.3s per player. + killsPerWeapon = getDvarInt("gun_kills", 1); + self.scoretext = self createText("hudbig", 1, "CENTER", "CENTER", 0, 0, .7,""); + self.scoretext_amount = self createText("hudbig", 1, "CENTER", "CENTER", -10, 20, .7,""); + self.multitext = []; + for(i=2;i<6;i++) + self.multitext[i] = self createText("hudbig", .6, "CENTER", "CENTER", 50, (i * 20), .7,""); while(true) { wait .3; if(kills > self.gungameKills) // not called on team gungame { - self thread scorepopup(-100); + self thread scorepopup(-100); kills--; refreshCounter++; - killsPerWeapon = getDvarInt("gun_kills", 1); if(killsPerWeapon > 1) { killsInGun = (self.gungameKills % killsPerWeapon); @@ -462,10 +489,9 @@ onKilling() { kills++; refreshCounter++; self thread scorepopup(100); - self.streaking++; - if(getDvar("g_gametype") != "gungame_team") + self.streaking++; + if(!level.isTeamGame) // cached in loadSettings — avoids getDvar() per kill { - killsPerWeapon = getDvarInt("gun_kills", 1); if(killsPerWeapon > 1) { if(self.gungameKills % killsPerWeapon == 0) @@ -488,7 +514,7 @@ onKilling() { self thread initCreateMarkerIcon(); self refreshCounter(refreshCounter); self updateRatio(); - } + } } } updateRatio() @@ -586,8 +612,8 @@ scorepopup(amount) self.scoretext.color = color; self.scoretext.glowColor = glowColor; self.scoretext SetPulseFX( 40, 2000, 600 ); - if(getDvar("gunmode") == "Kill Confirmed") - self.scoretext setText("Upgraded!^3"); + if(level.isKillConfirmed) // cached in loadSettings — avoids getDvar() per popup + self.scoretext setText("Upgraded!^3"); else self.scoretext setText("Killed!^3"); self.scoretext_amount.color = color; @@ -679,9 +705,10 @@ tryNuke() } createUI() { - self thread createWeaponHud(); + self thread createWeaponHud(); self thread createKillHud(); - self thread Weaponnumber(); + // Weaponnumber() removed — marked deprecated, waits on "update_weaponNumber" + // which is never notified anywhere. Was a zombie thread per player. self thread createRatioHud(); } createWeaponHud() @@ -795,9 +822,9 @@ takeInvalidWeapon() level endon("nuke"); counter = 0; wait 3; - while(1) + while(1) { - waitFrame(); + wait 0.1; // was waitFrame() (~60/s) — 10/s is ample for a safety-net poller if(!isAlive(self)) continue; if(self isMantling()) @@ -1138,13 +1165,20 @@ watchHealthHUD() self.healthHUD setPoint("BOTTOM RIGHT", "BOTTOM RIGHT", -10, -10); self.healthHUD.label = &"HP: "; + lastHealth = -1; // sentinel: forces first-tick update while(true) { - self.healthHUD setValue(self.actual_health); - if(self.actual_health < (self.actual_maxhealth * 0.3)) - self.healthHUD.color = (1, 0, 0); - else - self.healthHUD.color = (1, 1, 1); + // Only push to the HUD element when the value actually changed. + // Eliminates 10 setValue() native calls/sec when player is at full health. + if(self.actual_health != lastHealth) + { + lastHealth = self.actual_health; + self.healthHUD setValue(self.actual_health); + if(self.actual_health < (self.actual_maxhealth * 0.3)) + self.healthHUD.color = (1, 0, 0); + else + self.healthHUD.color = (1, 1, 1); + } wait 0.1; } } @@ -1202,18 +1236,11 @@ watchHUD() self endon("death"); // prevent thread accumulation across respawns while(true) { + // Only re-apply dvars the engine may reset mid-game (radar via UAV, ammo via class events). + // ui_hud_hardcore is set to 0 once per spawn in loadSetup() and does not need refreshing. self setClientDvar("ui_drawradar", 1); - self setClientDvar("cg_drawRadar", 1); - self setClientDvar("cg_drawAmmo", 1); // Force ammo back on even in hardcore - self setClientDvar("cg_drawStance", 0); - self setClientDvar("cg_drawTeamScores", 0); - self setClientDvar("cg_drawKillfeed", 0); - self setClientDvar("ui_hud_hardcore", 1); - self setClientDvar("cg_drawBreathHint", 0); - self setClientDvar("cg_drawMantleHint", 0); - self setClientDvar("cg_drawTurretCrosshair", 0); - self setClientDvar("cg_cursorHints", 0); - wait 1; + self setClientDvar("cg_drawAmmo", 1); + wait 5; } } diff --git a/gunfun/mod/streaks.gsc b/gunfun/mod/streaks.gsc index 1ff5d80..3cc17b8 100755 --- a/gunfun/mod/streaks.gsc +++ b/gunfun/mod/streaks.gsc @@ -235,11 +235,9 @@ giveStreak(streak) self.speed = true; wait 1; self maps\mp\perks\_perks::givePerk("specialty_lightweight"); - self maps\mp\perks\_perks::givePerk("specialty_lightweight"); self setMoveSpeedScale(1.6); self.setMoveSpeedScale = 1.6; - //self thread spawnFireLoop(); - break; + break; case "Riotshield": self AttachShieldModel( "weapon_riot_shield_mp", "tag_shield_back" ); break; @@ -278,30 +276,26 @@ Radioactive() self endon("disconnect"); self endon("death"); level endon("nuke"); - playFxOnTag( level.spawnGlow["enemy"], self, "pelvis" ); + // Cache once \u2014 getDvar() inside the inner foreach would cost ~120 native calls/sec. + isTeamGame = (getDvar("g_gametype") == "gungame_team"); + playFxOnTag( level.spawnGlow["enemy"], self, "pelvis" ); playFxOnTag( level.spawnGlow["friendly"], self, "j_head" ); - //self SetPlayerIgnoreRadiusDamage( true ); while(1) { wait .1; - //self RadiusDamage(self.origin,120,50,40,self,"MOD_Explosive","nuke_mp"); - //RadiusDamage(self.origin, 50, 30, 20, self ); foreach(player in level.players) { if(player == self) continue; - if(getDvar("g_gametype") == "gungame_team") - { - if(player.team == self.team) - continue; - } + if(isTeamGame && player.team == self.team) + continue; if(Distance(player.origin,self.origin) < 120 && isAlive(player)) { player thread maps\mp\gametypes\_damage::finishPlayerDamageWrapper( self, self, 4, 0, "MOD_EXPLOSIVE", "none", player.origin, player.origin, "none", 0, 0 ); self thread maps\mp\gametypes\_damagefeedback::updateDamageFeedback(""); } - } - } + } + } } Suicidebomber() { @@ -655,7 +649,7 @@ Juggernaut() // FIX: Use the custom health system (actual_maxhealth/actual_health) instead of // the raw engine maxhealth. Setting engine maxhealth directly broke the HUD display // and regen logic. We double the effective HP pool through the custom system. - self.actual_maxhealth = self.maxhp * 2; + self.actual_maxhealth = self.maxhp * 5; // 5× base HP (e.g. 300 in Fungame vs normal 60) self.actual_health = self.actual_maxhealth; self setMoveSpeedScale(.7); -- 2.50.1 From 1611bff8a645c5f5ff70562a99a9747d7cf98efc Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Wed, 6 May 2026 21:06:38 +0200 Subject: [PATCH 5/5] bots can go past the riot shield level and can fulfill win condition --- gunfun/mod/main.gsc | 59 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/gunfun/mod/main.gsc b/gunfun/mod/main.gsc index 4bf2535..6fb7d9c 100755 --- a/gunfun/mod/main.gsc +++ b/gunfun/mod/main.gsc @@ -414,9 +414,16 @@ updateWeapon() if(isDefined(self.pers["isBot"]) && self.pers["isBot"] && getDvar("gunmode") == "Fungame") { if(level.gungameList[self.current] == "riotshield_mp") + { self setClientDvar("bots_play_knife", 1); + // Bots can't trigger the riot shield melee button, so we simulate it. + self thread watchBotRiotShield(); + } else + { self setClientDvar("bots_play_knife", 0); + self notify("botshield"); // shut down any running shield bash thread + } } // NOTE: The old recursive self-call "self updateWeapon()" was removed here. // It caused unbounded call-stack growth when getCurrentWeapon() returned "none". @@ -1271,3 +1278,55 @@ watchM40A3() } } } + +// Scripted riot shield bash for bots. +// Bots cannot press the melee button to bash with the riot shield, so this thread +// checks proximity + forward arc every 0.8s and applies damage directly, +// matching the real shield bash range (~85 units) and cooldown (~0.8s). +watchBotRiotShield() +{ + level endon("nuke"); + self endon("death"); + self endon("disconnect"); + // Exclusivity guard: kill any previous instance when weapon changes re-trigger this. + self notify("botshield"); + self endon("botshield"); + + while(true) + { + wait 0.8; // ~length of real shield bash animation / cooldown + + if(self getCurrentWeapon() != "riotshield_mp") + continue; + + selfPos = self getOrigin(); + forward = anglesToForward(self getPlayerAngles()); + + foreach(player in level.players) + { + if(player == self) continue; + if(!isAlive(player)) continue; + + toPlayer = player getOrigin() - selfPos; + dist = Length(toPlayer); + + if(dist > 85) continue; // riot shield melee range + + // Dot product: check player is within ~75 degree forward arc. + nx = toPlayer[0] / dist; + ny = toPlayer[1] / dist; + nz = toPlayer[2] / dist; + dot = (forward[0] * nx) + (forward[1] * ny) + (forward[2] * nz); + + if(dot > 0.25) // 0.25 ~= 75 degree half-angle + { + // Apply riot shield bash damage (100 = standard MW2 shield bash) + player thread maps\mp\gametypes\_damage::finishPlayerDamageWrapper( + self, self, 100, 0, "MOD_MELEE", "riotshield_mp", + player getOrigin(), player getOrigin(), "none", 0, 0 + ); + break; // one target per swing + } + } + } +} -- 2.50.1