Files
gunfun/mod/main.gsc

1446 lines
48 KiB
Plaintext
Executable File

#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
/////////////////////
//
// Gamemodes:
//
// - basic
// - team gungame
// - no streaks
// - snipers only
// - kill confirmed
// - pistol & shotgun only (disabled)
Init()
{
level._effect["claymore_explode"] = loadfx("explosions/tanker_explosion");
loadSettings();
thread mod\weapons::loadWeapons();
thread mod\streaks::loadStreaks();
thread mod\vote::loadVote(); // does not work
thread onPlayerConnect();
thread deleteSentries();
thread launchGame();
}
setGamemodes()
{
level.gungamemodes = [];
level.gungamemodes[level.gungamemodes.size] = "Fungame"; // God of Hellfire addition
level.gungamemodes[level.gungamemodes.size] = "Classic";
// level.gungamemodes[level.gungamemodes.size] = "Classic"; // raising the chance
// level.gungamemodes[level.gungamemodes.size] = "Classic";
// level.gungamemodes[level.gungamemodes.size] = "No Streaks";
// level.gungamemodes[level.gungamemodes.size] = "Snipers only";
//level.gungamemodes[level.gungamemodes.size] = "Pistol & Shotguns only"; // works but is it fun?
// level.gungamemodes[level.gungamemodes.size] = "Team Gungame";
// level.gungamemodes[level.gungamemodes.size] = "Kill Confirmed";
}
initializeGametype(type) // called in vote.gsc after first map
{
setDvar("gunmode", type);
setDvar("gun_kills", 1); // Default to 1 kill per weapon
if(type == "Team Gungame")
setDvar("g_gametype", "gungame_team");
else
setDvar("g_gametype", "gungame");
switch(type)
{
case "Classic":
setDvar("global_health", 60);
setDvar("speed", 1.2);
setDvar("streaks_online", 1);
setDvar("jump_height", 60);
setDvar("amount_weapons", 100);
setDvar("shuffle_weapons", 1);
break;
case "No Streaks":
setDvar("global_health", 70);
setDvar("speed", 1.2);
setDvar("streaks_online", 0);
setDvar("jump_height", 70);
break;
case "Snipers only":
setDvar("global_health", 70);
setDvar("speed", 1.2);
setDvar("streaks_online", 1);
setDvar("jump_height", 70);
break;
case "Pistol & Shotguns only":
setDvar("global_health", 70);
setDvar("speed", 1.2);
setDvar("streaks_online", 1);
setDvar("jump_height", 70);
break;
case "Team Gungame":
setDvar("global_health", 70);
setDvar("speed", 1.2);
setDvar("streaks_online", 1);
setDvar("jump_height", 70);
break;
case "Kill Confirmed":
setDvar("global_health", 100);
setDvar("speed", 1);
setDvar("streaks_online", 1);
setDvar("jump_height", 70);
break;
case "Fungame":
setDvar("global_health", 60);
setDvar("speed", 1.5);
setDvar("streaks_online", 1);
setDvar("jump_height", 70);
setDvar("amount_weapons", 0); // 0 = full 162-weapon progression
setDvar("shuffle_weapons", 0); // weapons play in fixed order
setDvar("gun_kills", 1);
break;
default: // not required
setDvar("global_health", 70);
setDvar("speed", 1.2);
setDvar("streaks_online", 1);
setDvar("jump_height", 70);
setDvar("amount_weapons", 100);
break;
}
}
loadSettings()
{
/////////////////////// CUSTOM SETTINGS /////////////////////////////
//SetDvarIfUninitialized("gunmode", "Classic");
SetDvarIfUninitialized("gunmode", "Fungame");
initializeGametype(getDvar("gunmode"));
if(getDvar("g_gametype") == "gungame_team")
setDvar("amount_weapons", int(getDvarInt("amount_weapons")/4));
setDvar("shuffle_weapons", 0);
setDvar("intermission", 15);
SetDvarIfUninitialized("show_damage_ui", 1);
setDvar("gunversion", "1.5 Remaster by ^1Santahunter - Modified by God of Hellfire");
/////////////////////////////////////////////////////////////////////
setDvar("scr_" + (getDvar("g_gametype")) + "_timelimit", 0);
setDvar("scr_" + (getDvar("g_gametype")) + "_scorelimit", 0);
setDvar("ui_allow_teamchange", 0);
setDvar("scr_game_allowkillcam", 0);
setDvar("testClients_watchKillcam", 0);
setDvar("scr_game_hardpoints", 0);
setDvar("scr_game_graceperiod", 0);
setDvar("scr_game_matchstarttime", 0);
setDvar("scr_game_playerwaittime", 0);
setDvar("g_hardcore", 1);
setDvar("bg_viewKickScale", 0);
setDvar("cg_viewkickscale", 0);
setDvar("cg_drawDamageFlash", 0);
setDvar("bg_shock_lookControl_mousesensitivityscale", 1);
setDvar("bg_shock_movement", 0);
setDvar("bg_shock_movement_duration", 0);
setDvar("bg_shock_movement_magnitude", 0);
setDvar("bg_shock_lookControl", 0);
setDvar("bg_shock_lookControl_duration", 0);
setDvar("bg_shock_screenBlurBlendTime", 0);
setDvar("bg_shock_screenBlurBlendFadeTime", 0);
setDvar("player_sprintUnlimited", 1); // Marathon: unlimited sprint
level.state = "prematch";
level.markerIcon = "ui_host";
level.teamKills = [];
level.teamKills["allies"] = 0;
level.teamKills["axis"] = 0;
level.teamKills["allies_weapon"] = 1;
level.teamKills["axis_weapon"] = 1;
precacheShader(level.markerIcon);
setGamemodes();
// Bot Management
setDvar("bots_main", 1);
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);
// Map voting: set to 1 to show custom vote screen at end of game,
// 0 to use the normal leaderboard + server map rotation instead.
SetDvarIfUninitialized("vote_enabled", 0);
// 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()
{
turret = GetEntArray("misc_turret","classname");
for(i=0;i<turret.size;i++)
turret[i] delete();
}
launchGame()
{
// Wait for gametype to initialize _hud_util globals (level.fontHeight, level.uiParent).
// launchGame runs from Init() which fires before the gametype callback.
while(!isDefined(level.fontHeight) || !isDefined(level.uiParent))
waitFrame();
hud = createServerFontString("hudbig", 1);
hud setPoint("CENTER", "CENTER", 0,-80);
hud.color = (1,1,0);
hud.glowAlpha = 1;
hud.glowColor = (.7,0,0);
hud setText("Game starting in");
value = createServerFontString("hudbig", 1.75);
value setPoint("CENTER", "CENTER", 0,0);
value.glowAlpha = 1;
value.color = (1,1,0);
value.glowAlpha = 1;
value.glowColor = (.7,0,0);
for(i = getDvarInt("intermission");i > 0;i--)
{
value changeFontScaleOverTime(1);
value.fontScale = 0.5;
value setValue(i);
wait 1;
value.fontScale = 1.75;
foreach(player in level.players)
{
player playLocalSound("ui_mp_suitcasebomb_timer");
}
}
level.state = "ingame";
value setValue(0);
foreach(player in level.players)
{
player enableWeapons();
player visionSetNakedForPlayer(getDvar("mapname"), 2);
// Use the mode-configured speed dvar, not a hardcoded value.
// Fungame = 1.5, Classic = 1.2, etc. — 1.25 was always wrong for Fungame.
player setMoveSpeedScale(getDvarFloat("speed"));
}
setDvar("jump_height", 60);
hud destroy();
value destroy();
//player freezeControls(false);
}
onPlayerConnect()
{
level endon("nuke");
while(true)
{
level waittill("connected", player);
player thread onPlayerSpawned();
}
}
onPlayerSpawned()
{
level endon("nuke");
self endon("disconnect");
self thread firstSpawn();
while(true)
{
self waittill("spawned_player");
self thread loadSetup();
// self thread test();
}
}
firstSpawn()
{
self endon("disconnect");
team = getCorrectTeam();
self thread setStartWeapon(team);
self.firstSpawn = true;
self.knifeKills = 0;
self.gungameKills = 0;
self.isJugger = false;
self.streaks = [];
self setClientDvar("cg_drawSplatter", 0);
self setClientDvar("cg_drawDamageFlash", 0);
self setClientDvar("cg_viewkickscale", 0.1);
self setClientDvar("bg_viewKickScale", 0.1);
self setClientDvar("bg_shock_lookControl_mousesensitivityscale", 1);
self setClientDvar("bg_shock_movement", 0);
self setClientDvar("bg_shock_lookControl", 0);
self setClientDvar("scr_game_allowkillcam", 0);
if(isDefined(self.hud_damagefeedback))
self.hud_damagefeedback.color = (1,0,0);
self.line = self createRectangle("CENTER", "LEFT", 0,-90,300,5,(1,1,0),"line_horizontal",1);
// Subtle crosshair dot — light blue, low alpha
self.crosshair = self createRectangle("CENTER", "CENTER", 0, 0, 3, 3, (0.5, 0.85, 1.0), "white", 1);
self.crosshair.alpha = 0.4;
self.crosshair.hideWhenInMenu = true;
self thread onKilling();
self thread createUI();
self thread refillOnFire();
self thread watchVersion();
self thread FPSBoost();
if(getDvar("g_gametype") == "gungame_team")
self thread upgradeOnTeamKills();
for(i=1;i<3;i++)
self setClientDvar("lowAmmoWarningNoAmmoColor" + i, 0, 0, 0, 0);
wait .2;
self notify("menuresponse", game["menu_team"], team);
wait .1;
self notify("menuresponse", "changeclass", "class1");
wait .1;
// Re-apply perks here — the engine's class-load triggered by changeclass above
// can wipe perk state before loadSetup() gets a chance to set them on first spawn.
self maps\mp\perks\_perks::givePerk("specialty_fastreload"); // Sleight of Hand
self maps\mp\perks\_perks::givePerk("specialty_falldamage");
self maps\mp\perks\_perks::givePerk("specialty_quickdraw");
self maps\mp\perks\_perks::givePerk("specialty_lightweight");
self maps\mp\perks\_perks::givePerk("specialty_marathon");
self maps\mp\perks\_perks::givePerk("specialty_fastmantle"); // Marathon Pro: faster mantle
self setPlayerData("challengeState", "ch_marathon_pro", 2); // unlock Marathon Pro
self.firstSpawn = false;
self thread tryCreateMarkerIcons();
}
getCorrectTeam()
{
if(getDvar("g_gametype") != "gungame_team")
return "allies";
allies = 0;
axis = 0;
foreach(player in level.players)
{
if(player.team == "allies")
allies++;
else if(player.team == "axis")
axis++;
}
if(allies >= axis)
return "allies";
return "axis";
}
setStartWeapon(team)
{
if(getDvar("g_gametype") != "gungame_team")
self.current = 1;
else
self.current = level.teamKills[team + "_weapon"];
}
loadSetup()
{
self hide();
self thread mod\streaks::setStreaks();
self takeAllWeapons();
self _clearPerks();
self thread updateWeapon();
self.maxhp = getDvarInt("global_health");
self.actual_maxhealth = self.maxhp;
self.actual_health = self.actual_maxhealth;
self.maxhealth = 1000;
self.health = 1000;
// watchHealthHUD removed — HP display not wanted
self thread watchRegen();
self thread watchDeagleGL();
self thread watchM40A3();
self thread watchHUD();
self.streaking = 0;
self.speed = false;
self.isJugger = false;
self.moveSpeedScaler = getDvarFloat("speed");
self setMoveSpeedScale(getDvarFloat("speed"));
self thread enforceSpeed(); // override any engine speed penalty every frame
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_lightweight");
self maps\mp\perks\_perks::givePerk("specialty_marathon");
self maps\mp\perks\_perks::givePerk("specialty_fastmantle"); // Marathon Pro: faster mantle
self setPlayerData("challengeState", "ch_marathon_pro", 2); // unlock Marathon Pro
// Static HUD dvars set once per spawn.
// g_hardcore=1 handles minimap/radar/teamscore suppression at engine level.
// We only need to override what we want TO show (ammo, custom health HUD).
self setClientDvar("cg_drawStance", 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);
self setClientDvar("ui_hud_hardcore", 0); // show custom HUD elements despite g_hardcore
// Re-apply shock suppression every spawn — engine can reset client dvars on respawn
self setClientDvar("bg_shock_movement", 0);
self setClientDvar("bg_shock_movement_duration", 0);
self setClientDvar("bg_shock_movement_magnitude", 0);
self setClientDvar("bg_shock_lookControl", 0);
self thread takeInvalidWeapon();
if(level.state == "prematch")
{
//self freezeControls(true);
self setMoveSpeedScale(0);
self disableWeapons();
self visionSetNakedForPlayer("blacktest", 0);
waitFrame();
if(level.state == "prematch")
self setMoveSpeedScale(0);
}
}
upgradeOnTeamKills()
{
self endon("disconnect");
while(true)
{
level waittill("upgrade_" + self.team);
wait .3;
enemyTeam = self getEnemyTeam();
self.current = level.teamKills[self.team + "_weapon"];
self thread updateWeapon();
// setText() is safe here: bounded to ~N unique strings where N = weapon count.
// Configstrings are deduplicated, so all players sharing the same weapon# reuse one slot.
self.weaponhud setText("Gun: " + self.current + " / " + (level.gungameList.size) + " / ^3" + level.teamKills[enemyTeam + "_weapon"]);
}
}
getEnemyTeam()
{
if(self.team == "allies")
return "axis";
return "allies";
}
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");
// Don't proceed until the weapon list is fully built
if(!isDefined(level.weaponsLoaded) || !level.weaponsLoaded)
return;
if(self.current > (level.gungameList.size - 1))
{
self thread tryNuke();
return;
}
//self iPrintlnBold(level.gungameList[self.current]);
weaponName = level.gungameList[self.current];
// Guard: skip if the weapon entry is missing, empty, or "none".
if(!isDefined(weaponName) || weaponName == "none" || weaponName == "")
return;
variant = randomInt(9);
if(getDvar("gunmode") == "Fungame")
variant = 0;
if (isSubstr(weaponName, "_akimbo"))
self giveWeapon(weaponName, variant, true);
else
self giveWeapon(weaponName, variant, false);
// Verify the weapon was actually loaded — giveWeapon silently fails if
// the weapon asset doesn't exist in the engine.
if(!self hasWeapon(weaponName))
{
self.current++;
return;
}
self giveWeapon("onemanarmy_mp");
self takeWeapon("onemanarmy_mp");
// Skip ammo operations for weapons with no ammo pool (riotshield, defaultweapon, etc.)
// giveMaxAmmo on these causes "Weapon name none" engine errors.
if(weaponName != "riotshield_mp" && weaponName != "defaultweapon_mp")
{
self giveMaxAmmo(weaponName);
self setWeaponAmmoClip(weaponName, 9999);
}
waitFrame();
self switchtoweaponimmediate(weaponName);
waitFrame();
if(weaponName != "riotshield_mp" && weaponName != "defaultweapon_mp")
self setWeaponAmmoClip(weaponName, 9999);
if(level.state == "prematch" || level.state == "ingame")
self show();
// Always restore the correct speed, clamped to the mode's configured base floor.
// getBaseSpeed() ensures no path can silently drop below the dvar-configured minimum.
if(level.state != "prematch")
{
if(self.speed)
self setMoveSpeedScale(1.6); // Speed streak: above base, always fine
else
self setMoveSpeedScale(getBaseSpeed()); // Juggernaut and normal: always >= base speed
}
if(level.state == "prematch")
{
self setMoveSpeedScale(0);
if(level.state == "prematch")
self setMoveSpeedScale(0);
}
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".
// takeInvalidWeapon() polls every frame and will re-issue the thread if needed.
// 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 switchtoweaponimmediate(level.gungameList[self.current]);
waitFrame();
retries++;
}
}
refillOnFire()
{
level endon("nuke");
self endon("disconnect");
while(true)
{
self waittill("weapon_fired");
weapon = self getCurrentWeapon();
if(weapon != "none" && weapon != "")
self giveMaxAmmo(weapon);
}
}
onKilling() {
self endon("disconnect");
level endon("nuke");
self.multiplier = 0;
self.amount = 0;
kills = 0;
refreshCounter = 0;
// 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);
// Guard: only create HUDs if they don't already exist (prevents leak on reconnect)
if(!isDefined(self.scoretext))
self.scoretext = self createText("hudbig", 1, "CENTER", "CENTER", 0, 0, .7,"");
if(!isDefined(self.scoretext_amount))
self.scoretext_amount = self createText("hudbig", 1, "CENTER", "CENTER", -10, 20, .7,"");
if(!isDefined(self.multitext) || !isDefined(self.multitext[2]))
{
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);
kills--;
refreshCounter++;
if(killsPerWeapon > 1)
{
killsInGun = (self.gungameKills % killsPerWeapon);
self.weaponhud setText("Gun: " + self.current + " / " + (level.gungameList.size - 1));
self.killhud setValue(killsInGun);
self.killhud.alpha = 1;
self.killtotalhud.alpha = 1;
}
else
{
self.weaponhud setText("Gun: " + self.current + " / " + (level.gungameList.size - 1));
self.killhud.alpha = 0;
self.killtotalhud.alpha = 0;
}
self refreshCounter(refreshCounter);
self updateRatio();
}
else if(kills < self.gungameKills)
{
kills++;
refreshCounter++;
self thread scorepopup(100);
self.streaking++;
if(!level.isTeamGame) // cached in loadSettings — avoids getDvar() per kill
{
if(killsPerWeapon > 1)
{
if(self.gungameKills % killsPerWeapon == 0)
{
self.current++;
self thread updateWeapon();
}
killsInGun = (self.gungameKills % killsPerWeapon);
self.weaponhud setText("Gun: " + self.current + " / " + (level.gungameList.size - 1));
self.killhud setValue(killsInGun);
}
else
{
self.current++;
self thread updateWeapon();
self.weaponhud setText("Gun: " + self.current + " / " + (level.gungameList.size - 1));
}
}
if(self.current >= (level.gungameList.size-5) && !isDefined(self.markerIcon))
self thread initCreateMarkerIcon();
self refreshCounter(refreshCounter);
self updateRatio();
}
}
}
updateRatio()
{
if(self.deaths != 0)
ratio = (self.kills/self.deaths);
else
ratio = self.kills;
self.ratiohud setValue(floatToString(ratio));
}
refreshCounter(refreshCounter)
{
if(refreshCounter == 10)
{
refreshCounter = 0;
self.weaponhud destroy();
if(isDefined(self.weapontotalhud))
self.weapontotalhud destroy();
self createWeaponHud();
}
}
warning(friendly)
{
self notify("warning");
if(friendly == 0)
warning = self createText("hudbig", .8, "CENTER", "CENTER", 0, 130, 1,"Enemy's powershield absorbed damage!");
else if(friendly == 1)
warning = self createText("hudbig", .8, "CENTER", "CENTER", 0, 130, 1,"Powershield absorbed damage!");
else if(friendly == 2)
warning = self createText("hudbig", .8, "CENTER", "CENTER", 0, 130, 1,"Enemy demoted you!");
else
warning = self createText("hudbig", .8, "CENTER", "CENTER", 0, 130, 1,"You demoted yourself!");
self thread clearWarning(warning);
warning.glowColor = (1,0,0);
warning fadeovertime(5);
warning.alpha = 0;
wait 5;
if(isDefined(warning))
warning destroy();
self notify("warning_end");
}
clearWarning(hud)
{
self endon("warning_end");
self waittill("warning");
if(isDefined(hud))
hud destroy();
}
createText(font,fontScale,align,relative,x,y,alpha,input,color,gcolor,galpha)
{
text = self createFontString(font,fontScale);
text setPoint(align,relative,x,y);
text.sort = 999;
text.alpha = alpha;
if(isDefined(color))
text.color = color;
if(isDefined(gcolor))
text.glowcolor = gcolor;
if(isDefined(galpha))
text.glowalpha = galpha;
else
text.glowalpha = 1;
text setText(input);
text.HideWhenInMenu = true;
return text;
}
scorepopup(amount)
{
self endon("disconnect");
self notify("scoring");
self endon("scoring");
self.multiplier++;
self.amount = self.amount + amount;
color = (0,0,0);
glowColor = (0,0,0);
switch(self.multiplier)
{
case 1:
color = (1,1,1);
glowColor = (0,1,0);
break;
case 2:
color = (1,1,0);
glowColor = (1,0,0);
break;
case 3:
color = (0,0,0);
glowColor = (1,0,0);
break;
default:
color = (0,0,0);
glowColor = (0,0,1);
break;
}
self.scoretext.alpha = 1;
self.scoretext_amount.alpha = 1;
self.scoretext.color = color;
self.scoretext.glowColor = glowColor;
self.scoretext SetPulseFX( 40, 2000, 600 );
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;
self.scoretext_amount.glowColor = glowColor;
self.scoretext_amount SetPulseFX( 40, 2000, 600 );
// FIX: Use .label + setValue() instead of setText("+" + amount).
// Each unique amount string burned a permanent configstring slot.
self.scoretext_amount.label = &"+";
self.scoretext_amount setValue(self.amount);
// Clamp multitext index to the valid range [2..5] to prevent undefined access
mtIdx = self.multiplier;
if(mtIdx > 5) mtIdx = 5;
if(self.multiplier > 1 && isDefined(self.multitext[mtIdx]))
{
self.multitext[mtIdx].alpha = 1;
self.multitext[mtIdx] fadeOverTime(0.1);
self.multitext[mtIdx].color = color;
self.multitext[mtIdx].glowColor = glowColor;
}
if(self.multiplier == 2)
{
self thread lowerMultitext(self.multiplier);
self.multitext[mtIdx] setText("Double Kill!");
}
else if(self.multiplier == 3)
self.multitext[mtIdx] setText("Triple Kill!");
else if(self.multiplier == 4)
self.multitext[mtIdx] setText("Multi Kill!");
else if(self.multiplier > 4)
{
difference = self.multiplier - 4;
// FIX: Use .label + setValue() to avoid configstring leak per spree level.
self.multitext[5].label = &"^1K^2i^3l^4l^5i^6n^7g ^1S^2p^3r^4e^5e ^3x";
self.multitext[5] setValue(difference);
}
self thread lowerMultiplier();
}
lowerMultiplier()
{
self endon("disconnect");
self notify("lowering");
self endon("lowering");
wait 5;
self notify("start_lowering");
self.multiplier = 0;
self.amount = 0;
}
lowerMultitext(multiplier)
{
self waittill_any("start_lowering", "disconnect");
for(i=2;i<6;i++)
{
if(isDefined(self.multitext[i]))
{
self.multitext[i] fadeOverTime(3);
self.multitext[i].alpha = 0;
}
wait .75;
}
wait 4;
}
tryNuke()
{
// 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)
{
level notify("nuke");
foreach(player in level.players)
player notify("nuke");
thread maps\mp\gametypes\_gamelogic::endGame( self, game["strings"]["score_limit_reached"] );
return;
}
iPrintLnBold("^1NUKE INCOMING!!");
self thread maps\mp\killstreaks\_nuke::tryUseNuke(undefined, false);
level notify("nuke");
foreach(player in level.players)
{
player hide();
}
if(getDvar("g_gametype") != "gungame_team")
self.weaponhud setText("Gun: " + (level.gungameList.size - 1));
}
createUI()
{
self thread createWeaponHud();
self thread createKillHud();
// Weaponnumber() removed — marked deprecated, waits on "update_weaponNumber"
// which is never notified anywhere. Was a zombie thread per player.
self thread createRatioHud();
}
createWeaponHud()
{
self.weaponhud = self createFontString("hudsmall", 1.2);
self.weaponhud setPoint("LEFT", "LEFT", 10, -105);
self.weaponhud.glowalpha = .5;
self.weaponhud.color = (1,1,0);
self.weaponhud.hideWhenInMenu = true;
self.weaponhud.glowcolor = (1,0,0);
// setText() is safe here: max ~162 unique strings, configstrings are deduplicated.
self.weaponhud setText("Gun: " + self.current + " / " + (level.gungameList.size - 1));
// weapontotalhud kept defined but hidden — total is inline in weaponhud text
self.weapontotalhud = self createFontString("hudsmall", 1.2);
self.weapontotalhud.alpha = 0;
}
createKillHud()
{
self.killhud = self createFontString("hudsmall", 1.2);
self.killhud setPoint("RIGHT", "CENTER", -350, -85);
self.killhud.glowalpha = .5;
self.killhud.color = (1,1,0);
self.killhud.hideWhenInMenu = true;
self.killhud.glowcolor = (1,0,0);
self.killtotalhud = self createFontString("hudsmall", 1.2);
self.killtotalhud setPoint("LEFT", "CENTER", -350, -85);
self.killtotalhud.glowalpha = .5;
self.killtotalhud.color = (1,1,0);
self.killtotalhud.hideWhenInMenu = true;
self.killtotalhud.glowcolor = (1,0,0);
killsPerWeapon = getDvarInt("gun_kills", 1);
if(killsPerWeapon > 1)
{
self.killhud.label = &"Kills: ";
self.killhud setValue(0);
self.killtotalhud.label = &" / ";
self.killtotalhud setValue(killsPerWeapon);
self.killhud.alpha = 1;
self.killtotalhud.alpha = 1;
}
else
{
self.killhud.alpha = 0;
self.killtotalhud.alpha = 0;
}
}
createRatioHud()
{
self.ratiohud = self createFontString("hudsmall", 1);
self.ratiohud setPoint("LEFT", "LEFT", 10, -125);
self.ratiohud.color = (1,1,0);
self.ratiohud.glowalpha = .3;
self.ratiohud.glowcolor = (1,0,0);
self.ratiohud.label = &"Ratio: ";
self.ratiohud.alpha = 0;
self waittill_any("death", "killed_lethal");
self.ratiohud fadeOverTime(2);
self.ratiohud.alpha = 1;
}
Weaponnumber() // deprecated
{
self notify("weaponnumber");
self endon("weaponnumber");
self endon("disconnect");
kills = 0;
oldkills = 0;
wait 1;
while(1)
{
kills++;
self.weaponhud setValue(self.current);
//self waittill("killed_enemy");
self waittill("update_weaponNumber");
//progress updateBar(self.current/level.weapon.size);
if(kills >= 10)
{
self.weaponhud destroy();
self createWeaponHud();
self thread Weaponnumber();
}
if(oldkills != self.kills)
{
oldkills = self.kills;
if(self.deaths != 0)
ratio = (self.kills/self.deaths);
else
ratio = self.kills;
self.ratiohud setValue(floatToString(ratio));
}
}
}
floatToString(float)
{
string = "" + float;
if(string.size > 5)
string = getSubStr(string, 0, 5);
return StringToFloat(string);
}
// Returns the mode-configured base speed, clamped so nothing can go below it.
// All setMoveSpeedScale calls for non-prematch state should go through this
// (or compare against it) to honour the 1.5 Fungame / 1.2 Classic floor contract.
getBaseSpeed()
{
base = getDvarFloat("speed");
return base;
}
enforceSpeed()
{
// Calls setMoveSpeedScale every server frame — brute-forces over any engine
// speed penalty (bullet pain, bg_shock, mantle exit, etc.) with no gaps.
level endon("nuke");
self endon("disconnect");
self endon("death");
while(true)
{
waitFrame();
if(level.state != "ingame")
continue;
if(self.speed)
self setMoveSpeedScale(1.6);
else
self setMoveSpeedScale(getBaseSpeed());
}
}
takeInvalidWeapon()
{
self endon("disconnect");
self endon("death");
level endon("nuke");
// Skip the first 5 frames so the initial updateWeapon() from loadSetup() has
// time to complete its switchtoweaponimmediate before we start polling.
// This prevents a false-positive correction that was causing the 2.5s delay.
for(i = 0; i < 5; i++)
waitFrame();
while(1)
{
wait 0.1; // 10/s is ample for a safety-net poller
if(!isAlive(self))
continue;
if(self isMantling())
continue;
// Don't poll until weapons are fully loaded
if(!isDefined(level.weaponsLoaded) || !level.weaponsLoaded)
continue;
weapon = self getCurrentWeapon();
// Skip if player is in a weapon transition ("none" is normal during switches)
if(weapon == "none" || weapon == "")
continue;
if(weapon != level.gungameList[self.current])
{
self takeAllWeapons();
self thread updateWeapon();
// Short cooldown — just enough for switchtoweaponimmediate to settle.
wait 0.5;
}
}
}
takeInvalidWeapon2()
{
self endon("disconnect");
level endon("nuke");
counter = 0;
wait 3;
while(1)
{
wait .25;
if(!isAlive(self))
continue;
weapon = self getCurrentWeapon();
if((weapon != level.gungameList[self.current] && weapon != "killstreak_uav_mp") || weapon == "none")
{
weaponArray = strTok(weapon, "_");
//if(weapon == ("gl_" + weaponArray[1] + "_mp"))
// continue;
//if(weapon == "none" && self isMantling())
// continue;
//self iPrintln("false");
//iPrintlnBold(weapon);
self takeAllWeapons();
//if(!self hasWeapon(level.gungameList[self.current]) || weapon == "none")
//{
counter++;
if(counter == 40) // debug
{
self.gungameKills++;
counter = 0;
self.current++;
self thread updateWeapon();
wait 1;
}
else
self updateWeapon();
wait .2;
//}
//if(level.gungameList[self.current] == "throwingknife_mp")
// self thread ThrowingKnife();
if(!self.speed)
{
speed = level.speed;
self setMoveSpeedScale(speed);
if(self.hardmode == 1)
{
speed = level.speed * 0.8;
self setMoveSpeedScale(speed);
}
self.moveSpeedScaler = speed;
}
}
else if(weapon == "usp_tactical_mp")
{
speed = level.speed * 1.2;
self setMoveSpeedScale(speed);
self setWeaponAmmoClip("usp_tactical_mp", 0);
self setWeaponAmmoStock("usp_tactical_mp", 0);
}
}
}
ThrowingKnife()
{
level endon("nuke");
self endon("disconnect");
self notify("tk");
self endon("tk");
for(;;)
{
self waittill( "grenade_fire", lightstick, weapName );
if(weapName == "throwingknife_mp")
{
self giveWeapon("throwingknife_mp");
self giveMaxAmmo("throwingknife_mp");
}
}
}
giveGun(weapon, variant)
{
if (!isDefined(variant))
variant = randomInt(9);
if (isSubstr(weapon, "_akimbo"))
self giveWeapon(weapon, variant, true);
else
self giveWeapon(weapon, variant, false);
}
initCreateMarkerIcon()
{
rank = 1;
foreach(player in level.players)
{
if(isDefined(player.markerIcon))
rank++;
}
self.markerIcon = rank;
self.markerIconsEnemies = [];
self thread createWarnHUD();
if(self.isJugger)
self notify("isMarked");
foreach(player in level.players)
{
if(player != self)
{
self thread createMarkerIcon(player);
}
}
self waittill_any("disconnect", "nuke");
foreach(icon in self.markerIconsEnemies)
{
if(isDefined(icon))
icon destroy();
}
}
createMarkerIcon(player)
{
self.markerIconsEnemies[self.markerIconsEnemies.size] = NewClientHudElem(player);
self.markerIconsEnemies[self.markerIconsEnemies.size-1] SetShader(level.markerIcon, 10, 10 );
self.markerIconsEnemies[self.markerIconsEnemies.size-1].width = 10;
self.markerIconsEnemies[self.markerIconsEnemies.size-1].height = 10;
self.markerIconsEnemies[self.markerIconsEnemies.size-1].color = getColor(self.markerIcon);
self.markerIconsEnemies[self.markerIconsEnemies.size-1].x = self.origin[ 0 ];
self.markerIconsEnemies[self.markerIconsEnemies.size-1].y = self.origin[ 1 ];
self.markerIconsEnemies[self.markerIconsEnemies.size-1].z = self.origin[ 2 ] + 55;
self.markerIconsEnemies[self.markerIconsEnemies.size-1] SetWayPoint( true, true );
self.markerIconsEnemies[self.markerIconsEnemies.size-1] SetTargetEnt(self);
}
getColor(rank)
{
if(rank == 1)
return (1, 215/255, 0); // gold
if(rank == 2)
return (155/255,0,0); // darkred
if(rank == 3)
return (0,0,155/255); // darkblue
return (191/255, 137/255, 112/255); // bronze
}
createWarnHUD()
{
/*
warnHUD = self createFontString("hudsmall", 1.2);
warnHUD setPoint("CENTER", "CENTER", 0, -10);
warnHUD.glowalpha = .5;
warnHUD.color = (155/255,0,0);
warnHUD.hideWhenInMenu = true;
warnHUD.glowcolor = (0,0,0);
warnHUD setText("Final Game: You are marked for all enemies now!");
wait 3;
warnHUD destroy();
*/
self thread maps\mp\gametypes\_hud_message::hintMessage("Final Game: You are marked for all enemies now!");
}
tryCreateMarkerIcons()
{
foreach(player in level.players)
{
if(isDefined(player.markerIcon))
{
player thread createMarkerIcon(self);
}
}
}
createRectangle(align, relative, x, y, width, height, color, shader, sort)
{
hud = newClientHudElem(self);
hud.elemType = "icon";
hud.width = width;
hud.height = height;
hud.xOffset = 0;
hud.yOffset = 0;
hud.children = [];
hud.sort = sort;
hud.color = color;
hud setParent(level.uiParent);
hud setShader(shader,width,height);
hud.hidden = false;
hud.HideWhenInMenu = true;
hud setPoint(align,relative,x,y);
return hud;
}
upgradeTeamUI(team)
{
fontElem = newTeamHudElem( team );
fontElem.elemType = "font";
fontElem.font = "default";
fontElem.fontscale = 1.6;
fontElem.baseFontScale = 1.6;
fontElem.glowAlpha = 1;
fontElem.color = (1,1,0);
fontElem.glowColor = (1,0,0);
fontElem.x = 0;
fontElem.y = 0;
fontElem.width = 0;
fontElem.height = int(level.fontHeight * 1.6);
fontElem.xOffset = 0;
fontElem.yOffset = 0;
fontElem.children = [];
fontElem setParent( level.uiParent );
fontElem.hidden = false;
fontElem setText("Weaponupgrade for your team!");
fontElem setPoint("CENTER", "TOP", 0,200);
wait 3;
fontElem fadeOverTime(2);
fontElem.alpha = 0;
wait 2;
fontElem destroy();
}
upgradeEnemyWeaponUI(team)
{
foreach(player in level.players)
{
if(player.team == team)
{
player.weaponhud setText("Weapon: " + player.current + "/" + (level.gungameList.size) + "/^3" + level.teamKills[player getEnemyTeam() + "_weapon"]);
}
}
}
spawnDogTag(victim, attacker)
{
picked = false;
dogtag = spawn("script_model", victim.origin+(0,0,30));
//dogtag setModel("test_sphere_silver");
//playFxOnTag( level.spawnGlow["enemy"], self, "pelvis" );
//playFxOnTag( level.spawnGlow["friendly"], self, "j_head" );
fx = spawnFx(level.dogtag, dogtag.origin);
fx2 = spawnFx(level.dogtag2, dogtag.origin);
triggerFx(fx);
triggerFx(fx2);
while(!isDefined(level.startNuke) && !picked)
{
foreach(player in level.players)
{
if(Distance(player.origin, dogtag.origin) < 75 && isAlive(player))
{
if(player != victim)
player.gungameKills++;
dogtag delete();
fx delete();
fx2 delete();
picked = true;
}
}
wait .1;
}
if(isDefined(fx))
{
fx delete();
fx2 delete();
}
if(!picked)
dogtag delete();
}
FPSBoost()
{
level endon("nuke");
self endon("disconnect");
self notifyonplayercommand("FPS", "+actionslot 2");
for(;;)
{
self waittill("FPS");
self setClientDvar("r_fullbright", 1);
self waittill("FPS");
self setClientDvar("r_fullbright", 0);
}
}
test()
{
self notifyOnPlayerCommand("F", "+activate");
self thread test2();
while(true)
{
self waittill("F");
//self thread tryNuke();
// setDvar("g_gametype", "gungame_team");
// map("mp_rust");
self suicide();
}
}
test2()
{
self notifyOnPlayerCommand("G", "+frag");
self thread test2();
while(true)
{
self waittill("G");
setDvar("g_gametype", "gungame");
setDvar("gunmode", "Kill Confirmed");
map("mp_rust");
}
}
watchVersion()
{
self endon("disconnect");
self notifyOnPlayerCommand("open", "+scores");
self notifyOnPlayerCommand("close", "-scores");
while(true)
{
self waittill("open");
self.versionText = self createFontString("hudbig", .60);
self.versionText setPoint("CENTER", "BOTTOM", 0,-40);
self.versionText setText(getDvar("gunversion"));
self waittill("close");
self.versionText destroy();
}
}
watchHealthHUD()
{
level endon("nuke");
self endon("disconnect");
self endon("death");
if(isDefined(self.healthHUD))
self.healthHUD destroy();
self.healthHUD = self createFontString("hudsmall", 1.2);
self.healthHUD setPoint("BOTTOM RIGHT", "BOTTOM RIGHT", -10, -10);
self.healthHUD.label = &"HP: ";
lastHealth = -1; // sentinel: forces first-tick update
while(true)
{
// 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;
}
}
watchRegen()
{
level endon("nuke");
self endon("disconnect");
self endon("death");
while(true)
{
self waittill("takeDamage");
wait 5; // wait 5 seconds after damage
while(self.actual_health < self.actual_maxhealth)
{
self.actual_health += 5;
if(self.actual_health > self.actual_maxhealth)
self.actual_health = self.actual_maxhealth;
wait 0.1;
}
}
}
watchDeagleGL()
{
level endon("nuke");
self endon("disconnect");
self endon("death"); // prevent thread accumulation across respawns
while(true)
{
self waittill("weapon_fired", weaponName);
if(weaponName == "deserteagle_akimbo_mp")
{
angles = self getPlayerAngles();
forward = anglesToForward(angles);
start = self getEye();
end = start + (forward * 3000);
MagicBullet("gl_mp", start, end, self);
if(isDefined(self.pers["isBot"]) && self.pers["isBot"])
wait .6;
else
wait .2;
}
}
}
watchHUD()
{
level endon("nuke");
self endon("disconnect");
self endon("death"); // prevent thread accumulation across respawns
while(true)
{
// Re-apply ammo visibility (engine may reset it on class/weapon events).
self setClientDvar("cg_drawAmmo", 1);
wait 5;
}
}
watchM40A3()
{
level endon("nuke");
self endon("disconnect");
self endon("death"); // prevent thread accumulation across respawns
while(true)
{
self waittill("weapon_fired", weaponName);
if(weaponName == "m40a3_mp")
{
angles = self getPlayerAngles();
forward = anglesToForward(angles);
start = self getEye();
end = start + (forward * 10000);
trace = BulletTrace(start, end, true, self);
if(isDefined(trace["position"]))
{
pos = trace["position"] + (trace["normal"] * 10);
RadiusDamage(pos, 150, 200, 50, self);
PlayFX(level._effect["claymore_explode"], pos);
self PlaySound("claymore_activated");
wait .1;
}
}
}
}
// 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
{
// The mod pins engine health at 1000 and tracks real HP in actual_health.
// finishPlayerDamageWrapper only hits engine health (which never changes),
// so we apply damage directly to the scripted HP instead.
bashDamage = 100;
player.actual_health -= bashDamage;
player notify("takeDamage"); // reset regen timer
if(player.actual_health <= 0)
{
player.actual_health = 0;
// Drive a lethal hit through the engine so the kill is attributed correctly.
player thread maps\mp\gametypes\_damage::finishPlayerDamageWrapper(
self, self, 1000, 0, "MOD_MELEE", "riotshield_mp",
player getOrigin(), player getOrigin(), "none", 0, 0
);
}
break; // one target per swing
}
}
}
}