diff --git a/plugins/l4d2_extraplayeritems.smx b/plugins/l4d2_extraplayeritems.smx index d7b4c94..9a065c6 100644 Binary files a/plugins/l4d2_extraplayeritems.smx and b/plugins/l4d2_extraplayeritems.smx differ diff --git a/scripting/epi/director.sp b/scripting/epi/director.sp new file mode 100644 index 0000000..d8b8117 --- /dev/null +++ b/scripting/epi/director.sp @@ -0,0 +1,240 @@ +// SETTINGS +#define DIRECTOR_WITCH_MIN_TIME 120 // The minimum amount of time to pass since last witch spawn for the next extra witch to spawn +#define DIRECTOR_WITCH_CHECK_TIME 30.0 // How often to check if a witch should be spawned +#define DIRECTOR_WITCH_MAX_WITCHES 6 // The maximum amount of extra witches to spawn +#define DIRECTOR_WITCH_ROLLS 2 // The number of dice rolls, increase if you want to increase freq +#define DIRECTOR_MIN_SPAWN_TIME 20.0 // Possibly randomized, per-special +#define DIRECTOR_SPAWN_CHANCE 30.0 // The raw chance of a spawn +#define DIRECTOR_CHANGE_LIMIT_CHANCE 0.10 // The chance that the maximum amount per-special is changed +#define DIRECTOR_SPECIAL_TANK_CHANCE 0.05 // The chance that specials can spawn when a tank is active +#define DIRECTOR_STRESS_CUTOFF 0.60 // The minimum chance a random cut off stress value is chosen [this, 1.0] + +/// DEFINITIONS +#define NUM_SPECIALS 6 +#define TOTAL_NUM_SPECIALS 8 +char SPECIAL_IDS[TOTAL_NUM_SPECIALS][] = { + "smoker", + "boomer", + "hunter", + "spitter", + "jockey", + "charger", + "witch", + "tank" +}; +enum specialType { + Special_Smoker, + Special_Boomer, + Special_Hunter, + Special_Spitter, + Special_Jockey, + Special_Charger, + Special_Witch, + Special_Tank, +}; + +static float highestFlowAchieved; +static float g_lastSpawnTime[TOTAL_NUM_SPECIALS]; +static int g_spawnLimit[TOTAL_NUM_SPECIALS]; +static int g_spawnCount[TOTAL_NUM_SPECIALS]; +static float g_minFlowSpawn; // The minimum flow for specials to start spawning (waiting for players to leave saferom) +static float g_minStressIntensity; // The minimum stress that specials arent allowed to spawn + +static int extraWitchCount; +static Handle witchSpawnTimer = null; + +float g_extraWitchFlowPositions[DIRECTOR_WITCH_MAX_WITCHES] = {}; + +/// EVENTS + +void Director_OnMapStart() { + if(cvEPISpecialSpawning.BoolValue && abmExtraCount > 4) { + InitExtraWitches(); + } + float time = GetGameTime(); + for(int i = 0; i < TOTAL_NUM_SPECIALS; i++) { + g_lastSpawnTime[i] = time; + g_spawnLimit[i] = 1; + g_spawnCount[i] = 0; + } +} +void Director_OnMapEnd() { + for(int i = 0; i <= DIRECTOR_WITCH_MAX_WITCHES; i++) { + g_extraWitchFlowPositions[i] = 0.0; + } + delete witchSpawnTimer; +} + +void Cvar_SpecialSpawningChange(ConVar convar, const char[] oldValue, const char[] newValue) { + if(convar.IntValue & 2 && abmExtraCount > 4) { + if(witchSpawnTimer == null) + witchSpawnTimer = CreateTimer(DIRECTOR_WITCH_CHECK_TIME, Timer_DirectorWitch, _, TIMER_REPEAT); + } else { + delete witchSpawnTimer; + } +} + +void Event_WitchSpawn(Event event, const char[] name, bool dontBroadcast) { + g_spawnCount[Special_Witch]++; +} +void Director_OnClientPutInServer(int client) { + if(client > 0 && GetClientTeam(client) == 3) { + int class = GetEntProp(client, Prop_Send, "m_zombieClass"); + // Ignore a hacky temp bot spawn + // To bypass director limits many plugins spawn an infected "bot" that immediately gets kicked, which allows a window to spawn a special + static char buf[32]; + GetClientName(special, buf, sizeof(buf)); + if(StrContains(buf, "bot", false) == -1) { + g_spawnCount[class]++; + } + } +} +void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) { + int client = GetClientOfUserId(event.GetInt("userid")); + if(client > 0 && GetClientTeam(client) == 3) { + int class = GetEntProp(client, Prop_Send, "m_zombieClass"); + g_spawnCount[class]--; + } +} + +/// METHODS + +void InitExtraWitches() { + float flowMax = L4D2Direct_GetMapMaxFlowDistance() - FLOW_CUTOFF; + // Just in case we don't have max flow or the map is extremely tiny, don't run: + if(flowMax > 0.0) { + int count = abmExtraCount; + if(count < 4) count = 4; + // Calculate the number of witches we want to spawn. + // We bias the dice roll to the right. We slowly increase min based on player count to shift distribution to the right + int min = RoundToFloor(float(count - 4) / 4.0); + extraWitchCount = DiceRoll(min, DIRECTOR_WITCH_MAX_WITCHES, DIRECTOR_WITCH_ROLLS, BIAS_LEFT); + PrintDebug(DEBUG_SPAWNLOGIC, "InitExtraWitches: %d witches (min=%d, max=%d, rolls=%d) checkInterval=%f", extraWitchCount, min, DIRECTOR_WITCH_MAX_WITCHES, DIRECTOR_WITCH_ROLLS, DIRECTOR_WITCH_CHECK_TIME); + for(int i = 0; i <= extraWitchCount; i++) { + g_extraWitchFlowPositions[i] = GetURandomFloat() * (flowMax-FLOW_CUTOFF) + FLOW_CUTOFF; + PrintDebug(DEBUG_SPAWNLOGIC, "Witch position #%d: flow %.2f (%.0f%%)", i, g_extraWitchFlowPositions[i], g_extraWitchFlowPositions[i] / flowMax); + } + witchSpawnTimer = CreateTimer(DIRECTOR_WITCH_CHECK_TIME, Timer_DirectorWitch, _, TIMER_REPEAT); + } +} + +void Director_PrintDebug(int client) { + PrintToConsole(client, "===Extra Witches==="); + PrintToConsole(client, "Map Bounds: [%f, %f]", FLOW_CUTOFF, L4D2Direct_GetMapMaxFlowDistance() - (FLOW_CUTOFF*2.0)); + PrintToConsole(client, "Total Witches Spawned: %d | Target: %d", g_spawnCount[Special_Witch], extraWitchCount); + for(int i = 0; i < extraWitchCount && i < DIRECTOR_WITCH_MAX_WITCHES; i++) { + PrintToConsole(client, "%d. %f", i, g_extraWitchFlowPositions[i]); + } +} + +void Director_RandomizeLimits() { + // We add +1 to spice it up + int max = RoundToCeil(float(abmExtraCount - 4) / 4) + 1; + for(int i = 0; i < NUM_SPECIALS; i++) { + specialType special = view_as(i); + g_spawnLimit[i] = GetRandomInt(0, max); + } +} +void Director_RandomizeThings() { + g_minStressIntensity = GetRandomFloat(DIRECTOR_STRESS_CUTOFF, 1.0); + g_minFlowSpawn = GetRandomFloat(FLOW_CUTOFF, FLOW_CUTOFF * 2); + +} + +/// TIMERS + +Action Timer_Director(Handle h) { + if(abmExtraCount <= 4) return Plugin_Continue; + float time = GetGameTime(); + + // Calculate the new highest flow + int highestPlayer = L4D_GetHighestFlowSurvivor(); + float flow = L4D2Direct_GetFlowDistance(highestPlayer); + if(flow > highestFlowAchieved) { + highestFlowAchieved = flow; + } + // Only start spawning once they get to g_minFlowSpawn - a little past the start saferoom + if(highestFlowAchieved < g_minFlowSpawn) return Plugin_Continue; + float curAvgStress = L4D_GetAvgSurvivorIntensity(); + // Don't spawn specials when tanks active, but have a small chance (DIRECTOR_SPECIAL_TANK_CHANCE) to bypass + if(L4D2_IsTankInPlay() && GetURandomFloat() > DIRECTOR_SPECIAL_TANK_CHANCE) { + return Plugin_Continue; + } else { + // Stop spawning when players are stressed from a random value chosen by [DIRECTOR_STRESS_CUTOFF, 1.0] + if(curAvgStress >= g_minStressIntensity) return Plugin_Continue; + } + + // TODO: Scale spawning chance based on intensity? 0.0 = more likely, < g_minStressIntensity = less likely + // Scale the chance where stress = 0.0, the chance is 50% more, and stress = 1.0, the chance is 50% less + float spawnChance = DIRECTOR_SPAWN_CHANCE + (0.5 - curAvgStress) / 10 + for(int i = 0; i < NUM_SPECIALS; i++) { + specialType special = view_as(i); + // Skip if we hit our limit, or too soon: + if(g_spawnCount[i] >= g_spawnLimit[i]) continue; + if(time - g_lastSpawnTime[i] < DIRECTOR_MIN_SPAWN_TIME) continue; + + if(GetURandomFloat() < spawnChance) { + DirectorSpawn(special); + } + } + + if(GetURandomFloat() < DIRECTOR_CHANGE_LIMIT_CHANCE) { + Director_RandomizeLimits(); + } + + return Plugin_Continue; +} + + +Action Timer_DirectorWitch(Handle h) { + if(g_spawnCount[Special_Witch] < extraWitchCount) { //&& time - g_lastSpawnTimes.witch > DIRECTOR_WITCH_MIN_TIME + for(int i = 0; i <= extraWitchCount; i++) { + if(g_extraWitchFlowPositions[i] > 0.0 && highestFlowAchieved >= g_extraWitchFlowPositions[i]) { + // Reset the flow so we don't spawn another + g_extraWitchFlowPositions[i] = 0.0; + DirectorSpawn(Special_Witch); + break; + } + } + } + return Plugin_Continue; +} + +// UTIL functions +void DirectorSpawn(specialType special) { + PrintChatToAdmins("EPI: DirectorSpawn(%s) (dont worry about it)", SPECIAL_IDS[view_as(special)]); + int player = GetSuitableVictim(); + PrintDebug(DEBUG_SPAWNLOGIC, "Director: spawning %s from %N (cnt=%d,lim=%d)", SPECIAL_IDS[view_as(special)], player, g_spawnCount[view_as(special)], g_spawnLimit[view_as(special)]); + PrintToServer("[EPI] Spawning %s On %N", SPECIAL_IDS[view_as(special)], player); + if(special != Special_Witch && special != Special_Tank) { + // Bypass director + int bot = CreateFakeClient("EPI_BOT"); + if (bot != 0) { + ChangeClientTeam(bot, 3); + CreateTimer(0.1, Timer_Kick, bot); + } + } + CheatCommand(player, "z_spawn_old", SPECIAL_IDS[view_as(special)], "auto"); + g_lastSpawnTime[view_as(special)] = GetGameTime(); +} + +// TODO: make +void DirectSpawn(specialType special, const float pos[3]) { + +} +// Finds a player that is suitable (lowest intensity) +int GetSuitableVictim() { + int victim = -1; + float lowestIntensity = 0.0; + for(int i = 1; i <= MaxClients; i++) { + if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2 && IsPlayerAlive(i)) { + float intensity = L4D_GetPlayerIntensity(i); + // TODO: possibly add perm health into calculations + if(intensity < lowestIntensity || victim == -1) { + lowestIntensity = intensity; + victim = i; + } + } + } + return victim; +} \ No newline at end of file diff --git a/scripting/l4d2_extraplayeritems.sp b/scripting/l4d2_extraplayeritems.sp index 9d6939a..8bb729a 100644 --- a/scripting/l4d2_extraplayeritems.sp +++ b/scripting/l4d2_extraplayeritems.sp @@ -28,10 +28,7 @@ #define EXTRA_PLAYER_HUD_UPDATE_INTERVAL 0.8 //Sets abmExtraCount to this value if set // #define DEBUG_FORCE_PLAYERS 7 -#define DIRECTOR_WITCH_MIN_TIME 120 // The minimum amount of time to pass since last witch spawn for the next extra witch to spawn -#define DIRECTOR_WITCH_CHECK_TIME 30.0 // How often to check if a witch should be spawned -#define DIRECTOR_WITCH_MAX_WITCHES 6 // The maximum amount of extra witches to spawn -#define DIRECTOR_WITCH_ROLLS 2 // The number of dice rolls, increase if you want to increase freq + #define FLOW_CUTOFF 100.0 // The cutoff of flow, so that witches / tanks don't spawn in saferooms / starting areas, [0 + FLOW_CUTOFF, MapMaxFlow - FLOW_CUTOFF] #define EXTRA_TANK_MIN_SEC 2.0 @@ -82,11 +79,10 @@ public Plugin myinfo = url = "https://github.com/Jackzmc/sourcemod-plugins" }; -static ConVar hExtraItemBasePercentage, hAddExtraKits, hMinPlayers, hUpdateMinPlayers, hMinPlayersSaferoomDoor, hSaferoomDoorWaitSeconds, hSaferoomDoorAutoOpen, hEPIHudState, hExtraFinaleTank, cvDropDisconnectTime, hSplitTankChance, cvFFDecreaseRate, cvZDifficulty, cvEPIHudFlags, cvEPISpecialSpawning; -static int extraKitsAmount, extraKitsStarted, abmExtraCount, firstSaferoomDoorEntity, playersLoadedIn, playerstoWaitFor; +ConVar hExtraItemBasePercentage, hAddExtraKits, hMinPlayers, hUpdateMinPlayers, hMinPlayersSaferoomDoor, hSaferoomDoorWaitSeconds, hSaferoomDoorAutoOpen, hEPIHudState, hExtraFinaleTank, cvDropDisconnectTime, hSplitTankChance, cvFFDecreaseRate, cvZDifficulty, cvEPIHudFlags, cvEPISpecialSpawning; +int extraKitsAmount, extraKitsStarted, abmExtraCount, firstSaferoomDoorEntity, playersLoadedIn, playerstoWaitFor; static int currentChapter; static bool isCheckpointReached, isLateLoaded, firstGiven, isFailureRound, areItemsPopulated; -static float highestFlowAchieved; static ArrayList ammoPacks; static Handle updateHudTimer; static bool showHudPingMode; @@ -94,11 +90,6 @@ static int hudModeTicks; static char gamemode[32]; -static int witchSpawnCount, witchLastSpawnTime, extraWitchCount; -float ExtraWitchFlowPositions[DIRECTOR_WITCH_MAX_WITCHES] = {}; -static Handle witchSpawnTimer = null; - - bool isCoop; enum Difficulty { @@ -236,7 +227,9 @@ enum struct Cabinet { } static Cabinet cabinets[10]; //Store 10 cabinets -//// Definitions complete +//// Definitions completSe + +#include public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { if(late) isLateLoaded = true; @@ -270,6 +263,7 @@ public void OnPluginStart() { //Special Event Tracking HookEvent("player_info", Event_PlayerInfo); HookEvent("player_disconnect", Event_PlayerDisconnect); + HookEvent("player_death", Event_PlayerDeath); HookEvent("charger_carry_start", Event_ChargerCarry); HookEvent("charger_carry_end", Event_ChargerCarry); @@ -369,32 +363,7 @@ public void OnPluginStart() { } -Action Timer_Director(Handle h) { - if(abmExtraCount <= 4) return Plugin_Continue; - // Calculate the new highest flow - int highestPlayer = L4D_GetHighestFlowSurvivor(); - float flow = L4D2Direct_GetFlowDistance(highestPlayer); - if(flow > highestFlowAchieved) { - highestFlowAchieved = flow; - } - return Plugin_Continue; -} -Action Timer_DirectorWitch(Handle h) { - int time = GetTime(); - if(witchSpawnCount < extraWitchCount && time - witchLastSpawnTime > DIRECTOR_WITCH_MIN_TIME) { - for(int i = 0; i <= extraWitchCount; i++) { - if(ExtraWitchFlowPositions[i] > 0.0 && highestFlowAchieved >= ExtraWitchFlowPositions[i]) { - PrintChatToAdmins("EPI: (ignore me) DirectorSpawn(Special_Witch)"); - PrintDebug(DEBUG_SPAWNLOGIC, "DirectorSpawn(Special_Witch)"); - // Reset - ExtraWitchFlowPositions[i] = 0.0; - break; - } - } - } - return Plugin_Continue; -} Action Timer_ForceUpdateInventories(Handle h) { for(int i = 1; i <= MaxClients; i++) { @@ -406,6 +375,7 @@ Action Timer_ForceUpdateInventories(Handle h) { } public void OnClientPutInServer(int client) { + Director_OnClientPutInServer(client); if(!IsFakeClient(client)) { playerData[client].Setup(client); @@ -494,14 +464,6 @@ void Cvar_HudStateChange(ConVar convar, const char[] oldValue, const char[] newV } } } -void Cvar_SpecialSpawningChange(ConVar convar, const char[] oldValue, const char[] newValue) { - if(convar.IntValue & 2 && abmExtraCount > 4) { - if(witchSpawnTimer == null) - witchSpawnTimer = CreateTimer(DIRECTOR_WITCH_CHECK_TIME, Timer_DirectorWitch, _, TIMER_REPEAT); - } else { - delete witchSpawnTimer; - } -} public void Event_GamemodeChange(ConVar cvar, const char[] oldValue, const char[] newValue) { cvar.GetString(gamemode, sizeof(gamemode)); @@ -660,12 +622,7 @@ Action Command_RunExtraItems(int client, int args) { } Action Command_Debug(int client, int args) { PrintToConsole(client, "abmExtraCount = %d", abmExtraCount); - PrintToConsole(client, "===Extra Witches==="); - PrintToConsole(client, "Map Bounds: [%f, %f]", FLOW_CUTOFF, L4D2Direct_GetMapMaxFlowDistance() - (FLOW_CUTOFF*2.0)); - PrintToConsole(client, "Total Witches Spawned: %d | Target: %d", witchSpawnCount, extraWitchCount); - for(int i = 0; i < extraWitchCount && i < DIRECTOR_WITCH_MAX_WITCHES; i++) { - PrintToConsole(client, "%d. %f", i, ExtraWitchFlowPositions[i]); - } + Director_PrintDebug(client); return Plugin_Handled; } Action Command_DebugStats(int client, int args) { @@ -819,10 +776,6 @@ public void OnGetWeaponsInfo(int pThis, const char[] classname) { if(maxClipSize > 0) weaponMaxClipSizes.SetValue(classname, maxClipSize); } -void Event_WitchSpawn(Event event, const char[] name, bool dontBroadcast) { - witchSpawnCount++; - witchLastSpawnTime = GetTime(); -} /////////////////////////////////////////////////////// //// PLAYER STATE MANAGEMENT @@ -1168,8 +1121,6 @@ void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) { public void OnMapStart() { PrintDebug(DEBUG_GENERIC, "OnMapStart"); isCheckpointReached = false; - witchSpawnCount = 0; - witchLastSpawnTime = GetTime(); //If previous round was a failure, restore the amount of kits that were left directly after map transition if(isFailureRound) { extraKitsAmount = extraKitsStarted; @@ -1235,28 +1186,7 @@ public void OnMapStart() { finaleStage = Stage_Inactive; L4D2_RunScript(HUD_SCRIPT_CLEAR); - if(cvEPISpecialSpawning.BoolValue && abmExtraCount > 4) { - InitExtraWitches(); - } -} - -void InitExtraWitches() { - float flowMax = L4D2Direct_GetMapMaxFlowDistance() - (FLOW_CUTOFF*2); // *2 to calculate (max-min), save a minus - // Just in case we don't have max flow or the map is extremely tiny, don't run: - if(flowMax > 0.0) { - int count = abmExtraCount; - if(count < 4) count = 4; - // Calculate the number of witches we want to spawn. - // We bias the dice roll to the right. We slowly increase min based on player count to shift distribution to the right - int min = RoundToFloor(float(count - 4) / 4.0); - extraWitchCount = DiceRoll(min, DIRECTOR_WITCH_MAX_WITCHES, DIRECTOR_WITCH_ROLLS, BIAS_LEFT); - PrintDebug(DEBUG_SPAWNLOGIC, "Extra witch count: %d (%d min)", extraWitchCount, min); - for(int i = 0; i <= extraWitchCount; i++) { - ExtraWitchFlowPositions[i] = GetURandomFloat() * flowMax + FLOW_CUTOFF; - PrintDebug(DEBUG_SPAWNLOGIC, "Spawn location #%d: %f", i, ExtraWitchFlowPositions[i]); - } - witchSpawnTimer = CreateTimer(DIRECTOR_WITCH_CHECK_TIME, Timer_DirectorWitch, _, TIMER_REPEAT); - } + Director_OnMapStart(); } /* @@ -1288,15 +1218,11 @@ public void OnMapEnd() { cabinets[i].items[b] = 0; } } - for(int i = 0; i <= DIRECTOR_WITCH_MAX_WITCHES; i++) { - ExtraWitchFlowPositions[i] = 0.0; - } ammoPacks.Clear(); playersLoadedIn = 0; - highestFlowAchieved = 0.0; // abmExtraCount = 0; delete updateHudTimer; - delete witchSpawnTimer; + Director_OnMapEnd(); } public void Event_RoundFreezeEnd(Event event, const char[] name, bool dontBroadcast) { @@ -2074,40 +2000,7 @@ stock float GetSurvivorFlowDifference() { client = GetLowestFlowSurvivor(); return highestFlow - L4D2Direct_GetFlowDistance(client); } -char SPECIAL_IDS[8][] = { - "smoker", - "boomer", - "hunter", - "spitter", - "jockey", - "charger", - "witch", - "tank" -}; -enum SpecialType { - Special_Smoker, - Special_Boomer, - Special_Hunter, - Special_Spitter, - Special_Jockey, - Special_Charger, - Special_Witch, - Special_Tank, -} -void DirectorSpawn(SpecialType special) { - int player = L4D_GetHighestFlowSurvivor(); - PrintToServer("[EPI] Spawning %s On %N", SPECIAL_IDS[view_as(special)], player); - if(special != Special_Witch && special != Special_Tank) { - // Bypass director - int bot = CreateFakeClient("EPI_BOT"); - if (bot != 0) { - ChangeClientTeam(bot, 3); - CreateTimer(0.1, Timer_Kick, bot); - } - } - CheatCommand(player, "z_spawn_old", SPECIAL_IDS[view_as(special)], "auto"); -} Action Timer_Kick(Handle h, int bot) { KickClient(bot); return Plugin_Handled;