#pragma semicolon 1 #pragma newdecls required #define DEBUG #define DEBUG_SHOW_POINTS #define DEBUG_BOT_MOVE #define DEBUG_BLOCKERS // #define DEBUG_MOVE_ATTEMPTS // #define DEBUG_SEEKER_PATH_CREATION 1 #define PLUGIN_VERSION "1.0" #define BOT_MOVE_RANDOM_MIN_TIME 2.0 // The minimum random time for Timer_BotMove to activate (set per bot, per round) #define BOT_MOVE_RANDOM_MAX_TIME 3.0 // The maximum random time for Timer_BotMove to activate (set per bot, per round) #define BOT_MOVE_CHANCE 0.96 // The chance the bot will move each Timer_BotMove #define BOT_MOVE_AVOID_FLOW_DIST 12.0 // The flow range of flow distance that triggers avoid #define BOT_MOVE_AVOID_SEEKER_CHANCE 0.50 // The chance that if the bot gets too close to the seeker, it runs away #define BOT_MOVE_AVOID_MIN_DISTANCE 200.0 // The minimum distance for a far away point. (NOT flow) #define BOT_MOVE_USE_CHANCE 0.001 // Chance the bots will use +USE (used for opening doors, buttons, etc) #define BOT_MOVE_JUMP_CHANCE 0.001 #define BOT_MOVE_SHOVE_CHANCE 0.0015 #define BOT_MOVE_RUN_CHANCE 0.15 #define BOT_MOVE_NOT_REACHED_DISTANCE 60.0 // The distance that determines if a bot reached a point #define BOT_MOVE_NOT_REACHED_ATTEMPT_RUNJUMP 6 // The minimum amount of attempts where bot will run or jump to dest #define BOT_MOVE_NOT_REACHED_ATTEMPT_RETRY 9 // The minimum amount of attempts where bot gives up and picks new #define DOOR_TOGGLE_INTERVAL 5.0 // Interval that loops throuh all doors to randomly toggle #define DOOR_TOGGLE_CHANCE 0.01 // Chance that every Timer_DoorToggles triggers a door to toggle state #define HIDER_SWAP_COOLDOWN 30.0 // Amount of seconds until they can swap #define HIDER_SWAP_LIMIT 3 // Amount of times a hider can swap per round #define FLOW_BOUND_BUFFER 200.0 // Amount to add to calculated bounds (Make it very generous) #define HIDER_MIN_AVG_DISTANCE_AUTO_VOCALIZE 300.0 // The average minimum distance a hider is from the player that triggers auto vocalizating #define HIDER_AUTO_VOCALIZE_GRACE_TIME 20.0 // Number of seconds between auto vocalizations #define DEFAULT_MAP_TIME 480 #if defined DEBUG #define SEED_TIME 1.0 #else #define SEED_TIME 30.0 // Time the seeker is blind, used to gather locations for bots #endif #define SMOKE_PARTICLE_PATH "particles/smoker_fx.pcf" // #define SMOKE_PARTICLE_PATH "materials/particle/splashsprites/largewatersplash.vmt" // #define SMOKE_PARTICLE_PATH "materials/particle/fire_explosion_1/fire_explosion_1.vmt" #define SOUND_MODEL_SWAP "ui/pickup_secret01.wav" #define MAX_VALID_LOCATIONS 2000 // The maximum amount of locations to hold, once this limit is reached only MAX_VALID_LOCATIONS_KEEP_PERCENT entries will be kept at random #define MAX_VALID_LOCATIONS_KEEP_PERCENT 0.30 // The % of locations to be kept when dumping validLocations float DEBUG_POINT_VIEW_MIN[3] = { -5.0, -5.0, 0.0 }; float DEBUG_POINT_VIEW_MAX[3] = { 5.0, 5.0, 2.0 }; int SEEKER_GLOW_COLOR[3] = { 128, 0, 0 }; int PLAYER_GLOW_COLOR[3] = { 0, 255, 0 }; #include #include #include #include #include #include #include char SURVIVOR_MODELS[8][] = { "models/survivors/survivor_namvet.mdl", "models/survivors/survivor_teenangst.mdl", "models/survivors/survivor_biker.mdl", "models/survivors/survivor_manager.mdl", "models/survivors/survivor_gambler.mdl", "models/survivors/survivor_producer.mdl", "models/survivors/survivor_coach.mdl", "models/survivors/survivor_mechanic.mdl" }; enum struct LocationMeta { float pos[3]; float ang[3]; bool runto; bool jump; int attempts; // # of attempts player has moved until they will try to manage } // Game settings ArrayList validLocations; //ArrayList LocationMeta activeBotLocations[MAXPLAYERS]; enum GameState { State_Unknown = 0, State_Starting, State_Active, State_HidersWin, State_SeekerWon, } // Game state specific int currentSeeker; bool hasBeenSeeker[MAXPLAYERS+1]; bool ignoreSeekerBalance; int hiderSwapTime[MAXPLAYERS+1]; int hiderSwapCount[MAXPLAYERS+1]; bool isStarting; // Temp Ent Materials & Timers Handle spawningTimer; Handle hiderCheckTimer; Handle recordTimer; Handle timesUpTimer; Handle acquireLocationsTimer; Handle moveTimers[MAXPLAYERS+1]; UserMsg g_FadeUserMsgId; int g_iSmokeParticle; int g_iTeamNum = -1; // Cvars StringMap previousCvarValues; ConVar cvar_survivorLimit; ConVar cvar_seekerFailDamageAmount; // Bot Movement specifics float flowMin, flowMax; float seekerPos[3]; float seekerFlow = 0.0; float vecLastLocation[MAXPLAYERS+1][3]; #include public Plugin myinfo = { name = "L4D2 Guess Who", author = "jackzmc", description = "", version = PLUGIN_VERSION, url = "" }; public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { lateLoaded = late; return APLRes_Success; } public void OnPluginStart() { EngineVersion g_Game = GetEngineVersion(); if(g_Game != Engine_Left4Dead2) { SetFailState("This plugin is for L4D2 only."); } g_iTeamNum = FindSendPropInfo("CTerrorPlayerResource", "m_iTeam"); if (g_iTeamNum == -1) SetFailState("CTerrorPlayerResource \"m_iTeam\" offset is invalid"); validMaps = new ArrayList(ByteCountToCells(64)); validSets = new ArrayList(ByteCountToCells(16)); mapConfigs = new StringMap(); g_FadeUserMsgId = GetUserMessageId("Fade"); validLocations = new ArrayList(sizeof(LocationMeta)); previousCvarValues = new StringMap(); ConVar hGamemode = FindConVar("mp_gamemode"); hGamemode.AddChangeHook(Event_GamemodeChange); Event_GamemodeChange(hGamemode, gamemode, gamemode); cvar_seekerFailDamageAmount = CreateConVar("guesswho_seeker_damage", "20.0", "The amount of damage the seeker takes when they attack a bot.", FCVAR_NONE, true, 1.0); RegAdminCmd("sm_guesswho", Command_GuessWho, ADMFLAG_KICK); RegAdminCmd("sm_gw", Command_GuessWho, ADMFLAG_KICK); RegConsoleCmd("sm_joingame", Command_Join); } public void OnPluginEnd() { Cleanup(); } public void Event_GamemodeChange(ConVar cvar, const char[] oldValue, const char[] newValue) { cvar.GetString(gamemode, sizeof(gamemode)); bool shouldEnable = StrEqual(gamemode, "guesswho", false); if(isEnabled == shouldEnable) return; if(spawningTimer != null) delete spawningTimer; if(shouldEnable) { SetCvars(true); PrintToChatAll("[GuessWho] Gamemode is starting"); HookEvent("round_start", Event_RoundStart); HookEvent("player_death", Event_PlayerDeath); HookEvent("player_bot_replace", Event_PlayerToBot); HookEvent("player_ledge_grab", Event_LedgeGrab); AddCommandListener(OnGoAwayFromKeyboard, "go_away_from_keyboard"); } else if(!lateLoaded) { UnsetCvars(); UnhookEvent("round_start", Event_RoundStart); UnhookEvent("player_death", Event_PlayerDeath); UnhookEvent("player_bot_replace", Event_PlayerToBot); UnhookEvent("player_ledge_grab", Event_LedgeGrab); Cleanup(); PrintToChatAll("[GuessWho] Gamemode unloaded but cvars have not been reset."); RemoveCommandListener(OnGoAwayFromKeyboard, "go_away_from_keyboard"); } isEnabled = shouldEnable; } public Action OnGoAwayFromKeyboard(int client, const char[] command, int argc) { return Plugin_Handled; } void Event_LedgeGrab(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if(client > 0) { L4D_ReviveSurvivor(client); } } void Event_PlayerToBot(Event event, const char[] name, bool dontBroadcast) { int player = GetClientOfUserId(event.GetInt("player")); int bot = GetClientOfUserId(event.GetInt("bot")); // Do not kick bots being spawned in if(spawningTimer == null) { PrintToServer("[GuessWho/debug] possible idle bot: %d (player: %d)", bot, player); // ChangeClientTeam(player, 0); // L4D_SetHumanSpec(bot, player); L4D_TakeOverBot(player); // KickClient(bot); } } void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); int attacker = GetClientOfUserId(event.GetInt("attacker")); if(client > 0 && GetState() == State_Active) { if(client == currentSeeker) { PrintToChatAll("The seeker, %N, has died. Hiders win!", currentSeeker); SetState(State_HidersWin); EndGame(State_HidersWin); } else if(!IsFakeClient(client)) { if(attacker == currentSeeker) { PrintToChatAll("%N was killed", client); } else { PrintToChatAll("%N died", client); } } else { KickClient(client); PrintToServer("[GuessWho] Bot(%d) was killed", client); } } if(GetPlayersLeftAlive() == 0) { if(GetState() == State_Active) { PrintToChatAll("Everyone has died. %N wins!", currentSeeker); EndGame(State_SeekerWon); } } } void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) { CreateTimer(5.0, Timer_WaitForPlayers, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); } public void OnMapStart() { isStarting = false; if(!isEnabled) return; SDKHook(FindEntityByClassname(0, "terror_player_manager"), SDKHook_ThinkPost, ThinkPost); char map[128]; GetCurrentMap(map, sizeof(map)); if(!StrEqual(currentMap, map)) { strcopy(currentSet, sizeof(currentSet), "default"); if(!StrEqual(currentMap, "")) { if(!SaveMapData(currentMap, currentSet)) { LogError("Could not save map data to disk"); } } ReloadMapDB(); strcopy(currentMap, sizeof(currentMap), map); LoadMapData(map, currentSet); } g_iLaserIndex = PrecacheModel("materials/sprites/laserbeam.vmt"); g_iSmokeParticle = GetParticleIndex(SMOKE_PARTICLE_PATH); if(g_iSmokeParticle == INVALID_STRING_INDEX) { LogError("g_iSmokeParticle (%s) is invalid", SMOKE_PARTICLE_PATH); } // g_iSmokeParticle = PrecacheModel(SMOKE_PARTICLE_PATH); PrecacheSound(SOUND_MODEL_SWAP); SetCvars(false); if(lateLoaded) { int seeker = GetSeeker(); if(seeker > -1) { currentSeeker = seeker; PrintToServer("[GuessWho] Late load, found seeker %N", currentSeeker); } for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && IsClientInGame(i)) { ClearInventory(i); if(i == currentSeeker) { CheatCommand(i, "give", "fireaxe"); } else { CheatCommand(i, "give", "gnome"); } SDKHook(i, SDKHook_WeaponDrop, OnWeaponDrop); SDKHook(i, SDKHook_OnTakeDamageAlive, OnTakeDamageAlive); } } InitGamemode(); } SetState(State_Unknown); } public void ThinkPost(int entity) { static int iTeamNum[MAXPLAYERS+1]; GetEntDataArray(entity, g_iTeamNum, iTeamNum, sizeof(iTeamNum)); for(int i = 1 ; i<= MaxClients; i++) { if(IsClientConnected(i) && IsClientInGame(i) && IsFakeClient(i)) { iTeamNum[i] = 1; } } SetEntDataArray(entity, g_iTeamNum, iTeamNum, sizeof(iTeamNum)); } public void OnMapEnd() { for(int i = 1; i <= MaxClients; i++) { if(moveTimers[i] != null) { delete moveTimers[i]; } } } public void OnClientPutInServer(int client) { if(isEnabled && !IsFakeClient(client)) { ChangeClientTeam(client, 1); isPendingPlay[client] = true; PrintToChatAll("%N will play next round", client); TeleportToSpawn(client); } } public void OnClientDisconnect(int client) { if(!isEnabled) return; if(client == currentSeeker) { PrintToChatAll("The seeker has disconnected"); EndGame(State_HidersWin); } else if(!IsFakeClient(client) && GetState() == State_Active) { PrintToChatAll("A hider has left (%N)", client); if(GetPlayersLeftAlive() == 0 && GetState() == State_Active) { PrintToChatAll("Game Over. %N wins!", currentSeeker); EndGame(State_SeekerWon); } } } public bool SetCvarString(ConVar cvar, const char[] value, bool record) { if(cvar == null) return false; char prevValue[32]; if(record) { char name[32]; cvar.GetName(name, sizeof(name)); cvar.GetString(prevValue, sizeof(prevValue)); previousCvarValues.SetString(name, prevValue); } cvar.SetString(value); return true; } public bool SetCvarValue(ConVar cvar, any value, bool record) { if(cvar == null) return false; if(record) { char name[32]; cvar.GetName(name, sizeof(name)); previousCvarValues.SetValue(name, float(value)); } cvar.FloatValue = value; return true; } void SetCvars(bool record = false) { PrintToServer("[GuessWho] Setting convars"); cvar_survivorLimit = FindConVar("survivor_limit"); ConVar cvar_separationMinRange = FindConVar("sb_separation_danger_min_range"); ConVar cvar_separationMaxRange = FindConVar("sb_separation_danger_max_range"); ConVar cvar_abmAutoHard = FindConVar("abm_autohard"); ConVar cvar_sbFixEnabled = FindConVar("sb_fix_enabled"); ConVar cvar_sbPushScale = FindConVar("sb_pushscale"); if(cvar_survivorLimit != null) { cvar_survivorLimit.SetBounds(ConVarBound_Upper, true, 64.0); SetCvarValue(cvar_survivorLimit, MaxClients, record); cvar_survivorLimit.IntValue = MaxClients; } SetCvarValue(cvar_separationMinRange, 1000, record); SetCvarValue(cvar_separationMaxRange, 1200, record); SetCvarValue(cvar_abmAutoHard, 0, record); SetCvarValue(cvar_sbFixEnabled, 0, record); SetCvarValue(cvar_sbPushScale, 0, record); SetCvarValue(FindConVar("sb_battlestation_give_up_range_from_human"), 5000.0, record); SetCvarValue(FindConVar("sb_max_battlestation_range_from_human"), 5000.0, record); SetCvarValue(FindConVar("sb_enforce_proximity_range"), 10000, record); } void UnsetCvars() { StringMapSnapshot snapshot = previousCvarValues.Snapshot(); char key[32], valueStr[32]; PrintToServer("[GuessWho] Restoring %d convars", snapshot.Length); for(int i = 0; i < snapshot.Length; i++) { snapshot.GetKey(i, key, sizeof(key)); ConVar convar = FindConVar(key); if(convar != null) { float value; if(previousCvarValues.GetValue(key, value)) { convar.FloatValue = value; } else if(previousCvarValues.GetString(key, valueStr, sizeof(valueStr))) { convar.SetString(valueStr); } else { LogError("[GuessWho] Invalid cvar (\"%s\")", key); } } else { PrintToServer("[GuessWho] Cannot restore cvar (\"%s\") that does not exist", key); } } previousCvarValues.Clear(); } void InitGamemode() { if(isStarting && GetState() != State_Unknown) { PrintToServer("[GuessWho] Warn: InitGamemode() called in an incorrect state (%d)", GetState()); return; } SetupEntities(); PrintToChatAll("InitGamemode(): activating"); ArrayList validPlayerIds = new ArrayList(); for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2) { ChangeClientTeam(i, 2); activeBotLocations[i].attempts = 0; if(IsFakeClient(i)) { ClearInventory(i); KickClient(i); } else { if(!IsPlayerAlive(i)) { L4D_RespawnPlayer(i); } hiderSwapCount[i] = 0; distQueue[i].Clear(); ChangeClientTeam(i, 2); if(!hasBeenSeeker[i] || ignoreSeekerBalance) validPlayerIds.Push(GetClientUserId(i)); } } } if(validPlayerIds.Length == 0) { PrintToServer("[GuessWho] Warn: Ignoring InitGamemode() with no valid survivors"); return; } ignoreSeekerBalance = false; int newSeeker = GetClientOfUserId(validPlayerIds.Get(GetURandomInt() % validPlayerIds.Length)); delete validPlayerIds; if(newSeeker > 0) { hasBeenSeeker[newSeeker] = true; PrintToChatAll("%N is the seeker", newSeeker); SetSeeker(newSeeker); SetPlayerBlind(newSeeker, 255); SetEntPropFloat(newSeeker, Prop_Send, "m_flLaggedMovementValue", 0.0); // L4D2_SetPlayerSurvivorGlowState(newSeeker, true); L4D2_SetEntityGlow(newSeeker, L4D2Glow_Constant, 0, 10, SEEKER_GLOW_COLOR, false); } TeleportAllToStart(); spawningTimer = CreateTimer(0.2, Timer_SpawnBots, 16, TIMER_REPEAT); } Action Timer_SpawnBots(Handle h, int max) { static int count; if(count < max) { if(AddSurvivor()) { count++; return Plugin_Continue; } else { PrintToChatAll("GUESS WHO: FATAL ERROR: AddSurvivor() failed"); LogError("Guess Who: Fatal Error: AddSurvivor() failed"); count = 0; return Plugin_Stop; } } count = 0; CreateTimer(1.0, Timer_SpawnPost); return Plugin_Stop; } Action Timer_SpawnPost(Handle h) { spawningTimer = null; PrintToChatAll("Timer_SpawnPost(): activating"); bool isL4D1 = L4D2_GetSurvivorSetMap() == 1; int remainingSeekers; int survivorMaxIndex = isL4D1 ? 3 : 7; int survivorIndexBot; for(int i = 1; i <= MaxClients; i++) { if(i != currentSeeker && IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2) { int survivor; if(IsFakeClient(i)) { // Set bot models uniformly survivor = survivorIndexBot; if(++survivorIndexBot > survivorMaxIndex) { survivorIndexBot = 0; } } else { // Set hiders models randomly survivor = GetURandomInt() % survivorMaxIndex; if(!hasBeenSeeker[i]) { remainingSeekers++; } PrintToChat(i, "You can change your model %d times by looking at a player and pressing RELOAD", HIDER_SWAP_LIMIT); } SDKHook(i, SDKHook_OnTakeDamageAlive, OnTakeDamageAlive); SDKHook(i, SDKHook_WeaponDrop, OnWeaponDrop); ClearInventory(i); int item = GivePlayerItem(i, "weapon_gnome"); EquipPlayerWeapon(i, item); SetEntityModel(i, SURVIVOR_MODELS[survivor]); SetEntProp(i, Prop_Send, "m_survivorCharacter", survivor); } } if(remainingSeekers == 0) { PrintToChatAll("All players have been seekers once"); for(int i = 0; i <= MaxClients; i++) { hasBeenSeeker[i] = false; } } PrintToChatAll("[debug] waiting for safe area leave"); CreateTimer(1.0, Timer_WaitForStart, _, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); return Plugin_Handled; } Action Timer_WaitForStart(Handle h) { if(mapConfig.hasSpawnpoint || L4D_HasAnySurvivorLeftSafeArea()) { int targetPlayer = L4D_GetHighestFlowSurvivor(); if(targetPlayer > 0) { GetClientAbsOrigin(targetPlayer, seekerPos); } seekerFlow = L4D2Direct_GetFlowDistance(currentSeeker); acquireLocationsTimer = CreateTimer(0.5, Timer_AcquireLocations, _, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); hiderCheckTimer = CreateTimer(5.0, Timer_CheckHiders, _, TIMER_REPEAT); CreateTimer(DOOR_TOGGLE_INTERVAL, Timer_DoorToggles, _, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); for(int i = 1; i <= MaxClients; i++) { if(i != currentSeeker && IsClientConnected(i) && IsClientInGame(i)) { TeleportEntity(i, seekerPos, NULL_VECTOR, NULL_VECTOR); if(IsFakeClient(i)) { moveTimers[i] = CreateTimer(GetRandomFloat(BOT_MOVE_RANDOM_MIN_TIME, BOT_MOVE_RANDOM_MAX_TIME), Timer_BotMove, GetClientUserId(i), TIMER_REPEAT); validLocations.GetArray(GetURandomInt() % validLocations.Length, activeBotLocations[i]); TeleportEntity(i, activeBotLocations[i].pos, activeBotLocations[i].ang, NULL_VECTOR); } } } PrintToChatAll("[GuessWho] Seeker will start in %.0f seconds", SEED_TIME); SetState(State_Starting); SetTick(0); SetMapTime(RoundFloat(SEED_TIME)); CreateTimer(SEED_TIME, Timer_StartSeeker); return Plugin_Stop; } return Plugin_Continue; } Action Timer_StartSeeker(Handle h) { CPrintToChatAll("{blue}%N{default} : Here I come", currentSeeker); TeleportToSpawn(currentSeeker); SetPlayerBlind(currentSeeker, 0); SetState(State_Active); SetTick(0); SetEntPropFloat(currentSeeker, Prop_Send, "m_flLaggedMovementValue", 1.0); if(mapConfig.mapTime == 0) { mapConfig.mapTime = DEFAULT_MAP_TIME; } SetMapTime(mapConfig.mapTime); timesUpTimer = CreateTimer(float(mapConfig.mapTime), Timer_TimesUp, _, TIMER_FLAG_NO_MAPCHANGE); return Plugin_Continue; } Action Timer_TimesUp(Handle h) { PrintToChatAll("The seeker ran out of time. Hiders win!"); EndGame(State_HidersWin); return Plugin_Handled; } Action OnWeaponDrop(int client, int weapon) { return Plugin_Handled; } Action OnTakeDamageAlive(int victim, int& attacker, int& inflictor, float& damage, int& damagetype) { if(attacker == currentSeeker) { damage = 100.0; ClearInventory(victim); if(IsFakeClient(victim)) { PrintToChat(attacker, "That was a bot! -%.0f health", cvar_seekerFailDamageAmount.FloatValue); SDKHooks_TakeDamage(attacker, 0, 0, cvar_seekerFailDamageAmount.FloatValue, DMG_DIRECT); } return Plugin_Changed; } else if(attacker > 0 && attacker <= MaxClients) { damage = 0.0; return Plugin_Changed; } else { return Plugin_Continue; } } Action Timer_DoorToggles(Handle h) { int entity = INVALID_ENT_REFERENCE; while ((entity = FindEntityByClassname(entity, "prop_door_rotating")) != INVALID_ENT_REFERENCE) { if(GetURandomFloat() < DOOR_TOGGLE_CHANCE) AcceptEntityInput(entity, "Toggle"); } return Plugin_Handled; } Action Timer_AcquireLocations(Handle h) { bool ignoreSeeker = true; #if defined DEBUG_SEEKER_PATH_CREATION ignoreSeeker = false; #endif seekerFlow = L4D2Direct_GetFlowDistance(currentSeeker); GetClientAbsOrigin(currentSeeker, seekerPos); for(int i = 1; i <= MaxClients; i++) { if((!ignoreSeeker || i != currentSeeker) && IsClientConnected(i) && IsClientInGame(i) && !IsFakeClient(i) && IsPlayerAlive(i) && GetClientTeam(i) == 2 && GetEntityFlags(i) & FL_ONGROUND ) { LocationMeta meta; GetClientAbsOrigin(i, meta.pos); GetClientEyeAngles(i, meta.ang); if(meta.pos[0] != vecLastLocation[i][0] || meta.pos[1] != vecLastLocation[i][1] || meta.pos[2] != vecLastLocation[i][2]) { validLocations.PushArray(meta); if(validLocations.Length > MAX_VALID_LOCATIONS) { PrintToServer("[GuessWho] Hit MAX_VALID_LOCATIONS (%d), clearing some locations", MAX_VALID_LOCATIONS); validLocations.Sort(Sort_Random, Sort_Float); validLocations.Erase(RoundFloat(MAX_VALID_LOCATIONS * MAX_VALID_LOCATIONS_KEEP_PERCENT)); } #if defined DEBUG_SHOW_POINTS Effect_DrawBeamBoxRotatableToClient(i, meta.pos, DEBUG_POINT_VIEW_MIN, DEBUG_POINT_VIEW_MAX, NULL_VECTOR, g_iLaserIndex, 0, 0, 0, 150.0, 0.1, 0.1, 0, 0.0, {0, 0, 255, 64}, 0); #endif vecLastLocation[i] = meta.pos; } } } return Plugin_Continue; } void GetMovePoint(int i) { activeBotLocations[i].runto = GetURandomFloat() < BOT_MOVE_RUN_CHANCE; activeBotLocations[i].attempts = 0; validLocations.GetArray(GetURandomInt() % validLocations.Length, activeBotLocations[i]); #if defined DEBUG_SHOW_POINTS Effect_DrawBeamBoxRotatableToAll(activeBotLocations[i].pos, DEBUG_POINT_VIEW_MIN, DEBUG_POINT_VIEW_MAX, NULL_VECTOR, g_iLaserIndex, 0, 0, 0, 150.0, 0.1, 0.1, 0, 0.0, {255, 0, 255, 120}, 0); #endif } Action Timer_BotMove(Handle h, int userid) { int i = GetClientOfUserId(userid); if(i == 0) return Plugin_Stop; if(GetURandomFloat() > BOT_MOVE_CHANCE) { L4D2_RunScript("CommandABot({cmd=1,bot=GetPlayerFromUserID(%i),pos=Vector(%f,%f,%f)})", GetClientUserId(i), activeBotLocations[i].pos[0], activeBotLocations[i].pos[1], activeBotLocations[i].pos[2] ); #if defined DEBUG_SHOW_POINTS Effect_DrawBeamBoxRotatableToAll(activeBotLocations[i].pos, DEBUG_POINT_VIEW_MIN, DEBUG_POINT_VIEW_MAX, NULL_VECTOR, g_iLaserIndex, 0, 0, 0, 150.0, 0.1, 0.1, 0, 0.0, {157, 0, 255, 255}, 0); #endif return Plugin_Continue; } float botFlow = L4D2Direct_GetFlowDistance(i); static float pos[3]; if(botFlow < flowMin || botFlow > flowMax) { activeBotLocations[i].runto = GetURandomFloat() > 0.90; TE_SetupBeamLaser(i, currentSeeker, g_iLaserIndex, 0, 0, 0, 8.0, 0.5, 0.1, 0, 1.0, {255, 255, 0, 125}, 1); TE_SendToAll(); L4D2_RunScript("CommandABot({cmd=1,bot=GetPlayerFromUserID(%i),pos=Vector(%f,%f,%f)})", GetClientUserId(i), seekerPos[0], seekerPos[1], seekerPos[2]); #if defined DEBUG_BOT_MOVE PrintToConsoleAll("[gw/debug] BOT %N TOO FAR (%f) BOUNDS (%f, %f)-> Moving to seeker (%f %f %f)", i, botFlow, flowMin, flowMax, seekerPos[0], seekerPos[1], seekerPos[2]); #endif activeBotLocations[i].attempts = 0; } else if(validLocations.Length > 0) { GetAbsOrigin(i, pos); float distanceToPoint = GetVectorDistance(pos, activeBotLocations[i].pos); if(distanceToPoint < BOT_MOVE_NOT_REACHED_DISTANCE || GetURandomFloat() < 0.20) { activeBotLocations[i].attempts = 0; #if defined DEBUG_BOT_MOVE L4D2_SetPlayerSurvivorGlowState(i, false); L4D2_RemoveEntityGlow(i); #endif // Has reached destination if(mapConfig.hasSpawnpoint && FloatAbs(botFlow - seekerFlow) < BOT_MOVE_AVOID_FLOW_DIST && GetURandomFloat() < BOT_MOVE_AVOID_SEEKER_CHANCE) { if(!FindPointAway(seekerPos, activeBotLocations[i].pos, BOT_MOVE_AVOID_MIN_DISTANCE)) { #if defined DEBUG_BOT_MOVE PrintToConsoleAll("[gw/debug] BOT %N TOO CLOSE -> Failed to find far point, falling back to spawn", i); #endif activeBotLocations[i].pos = mapConfig.spawnpoint; } else { #if defined DEBUG_BOT_MOVE PrintToConsoleAll("[gw/debug] BOT %N TOO CLOSE -> Moving to far point (%f %f %f) (%f units away)", i, activeBotLocations[i].pos[0], activeBotLocations[i].pos[1], activeBotLocations[i].pos[2], GetVectorDistance(seekerPos, activeBotLocations[i].pos)); #endif } activeBotLocations[i].runto = GetURandomFloat() < 0.75; #if defined DEBUG_SHOW_POINTS Effect_DrawBeamBoxRotatableToAll(activeBotLocations[i].pos, DEBUG_POINT_VIEW_MIN, DEBUG_POINT_VIEW_MAX, NULL_VECTOR, g_iLaserIndex, 0, 0, 0, 150.0, 0.2, 0.1, 0, 0.0, {255, 255, 255, 255}, 0); #endif } else { GetMovePoint(i); } if(!L4D2_IsReachable(i, activeBotLocations[i].pos)) { #if defined DEBUG_BOT_MOVE PrintToChatAll("[gw/debug] POINT UNREACHABLE (Bot:%d) (%f %f %f)", i, activeBotLocations[i].pos[0], activeBotLocations[i].pos[1], activeBotLocations[i].pos[2]); PrintToServer("[gw/debug] POINT UNREACHABLE (Bot:%d) (%f %f %f)", i, activeBotLocations[i].pos[0], activeBotLocations[i].pos[1], activeBotLocations[i].pos[2]); Effect_DrawBeamBoxRotatableToAll(activeBotLocations[i].pos, DEBUG_POINT_VIEW_MIN, view_as({ 10.0, 10.0, 100.0 }), NULL_VECTOR, g_iLaserIndex, 0, 0, 0, 400.0, 2.0, 3.0, 0, 0.0, {255, 0, 0, 255}, 0); #endif GetMovePoint(i); } } else { // Has not reached dest activeBotLocations[i].attempts++; #if defined DEBUG_MOVE_ATTEMPTS PrintToConsoleAll("[gw/debug] Bot %d - move attempt %d - dist: %f", i, activeBotLocations[i].attempts, distanceToPoint); #endif if(activeBotLocations[i].attempts == BOT_MOVE_NOT_REACHED_ATTEMPT_RUNJUMP) { if(distanceToPoint <= (BOT_MOVE_NOT_REACHED_DISTANCE * 2)) { #if defined DEBUG_BOT_MOVE PrintToConsoleAll("[gw/debug] Bot %d still has not reached point (%f %f %f), jumping", i, activeBotLocations[i].pos[0], activeBotLocations[i].pos[1], activeBotLocations[i].pos[2]); L4D2_SetPlayerSurvivorGlowState(i, true); L4D2_SetEntityGlow(i, L4D2Glow_Constant, 0, 10, PLAYER_GLOW_COLOR, true); #endif activeBotLocations[i].jump = true; } else { activeBotLocations[i].runto = true; #if defined DEBUG_BOT_MOVE PrintToConsoleAll("[gw/debug] Bot %d not reached point (%f %f %f), running", i, activeBotLocations[i].pos[0], activeBotLocations[i].pos[1], activeBotLocations[i].pos[2]); L4D2_SetEntityGlow(i, L4D2Glow_Constant, 0, 10, PLAYER_GLOW_COLOR, true); L4D2_SetPlayerSurvivorGlowState(i, true); #endif } } else if(activeBotLocations[i].attempts > BOT_MOVE_NOT_REACHED_ATTEMPT_RETRY) { #if defined DEBUG_BOT_MOVE PrintToConsoleAll("[gw/debug] Bot %d giving up at reaching point (%f %f %f)", i, activeBotLocations[i].pos[0], activeBotLocations[i].pos[1], activeBotLocations[i].pos[2]); L4D2_SetEntityGlow(i, L4D2Glow_Constant, 0, 10, SEEKER_GLOW_COLOR, true); L4D2_SetPlayerSurvivorGlowState(i, true); #endif GetMovePoint(i); } #if defined DEBUG_SHOW_POINTS int color[4]; color[0] = 255; color[2] = 255; color[3] = 120 + activeBotLocations[i].attempts * 45; Effect_DrawBeamBoxRotatableToAll(activeBotLocations[i].pos, DEBUG_POINT_VIEW_MIN, DEBUG_POINT_VIEW_MAX, NULL_VECTOR, g_iLaserIndex, 0, 0, 0, 150.0, 0.1, 0.1, 0, 0.0, color, 0); #endif } LookAtPoint(i, activeBotLocations[i].pos); L4D2_RunScript("CommandABot({cmd=1,bot=GetPlayerFromUserID(%i),pos=Vector(%f,%f,%f)})", GetClientUserId(i), activeBotLocations[i].pos[0], activeBotLocations[i].pos[1], activeBotLocations[i].pos[2] ); } return Plugin_Continue; } public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3], float angles[3], int& weapon, int& subtype, int& cmdnum, int& tickcount, int& seed, int mouse[2]) { if(!isEnabled) return Plugin_Continue; if(IsFakeClient(client)) { if(activeBotLocations[client].jump) { activeBotLocations[client].jump = false; buttons |= (IN_WALK | IN_JUMP | IN_FORWARD); return Plugin_Changed; } buttons |= (activeBotLocations[client].runto ? IN_WALK : IN_SPEED); if(GetURandomFloat() < BOT_MOVE_USE_CHANCE) { buttons |= IN_USE; } float random = GetURandomFloat(); if(random < BOT_MOVE_JUMP_CHANCE) { buttons |= IN_JUMP; } else if(random < BOT_MOVE_SHOVE_CHANCE) { buttons |= IN_ATTACK2; } return Plugin_Changed; } else if(client != currentSeeker && buttons & IN_RELOAD) { if(hiderSwapCount[client] >= HIDER_SWAP_LIMIT) { PrintHintText(client, "Swap limit reached"); } else { int target = GetClientAimTarget(client, true); if(target > 0) { int time = GetTime(); float diff = float(time - hiderSwapTime[client]); if(diff > HIDER_SWAP_COOLDOWN) { hiderSwapTime[client] = GetTime(); hiderSwapCount[client]++; /*float pos[3], pos2[3]; GetClientAbsOrigin(client, pos); GetClientEyePosition(client, pos2); TE_SetupParticle(g_iSmokeParticle, pos, pos2, .iEntity = client); TE_SendToAllInRange(pos, RangeType_Audibility, 0.0);*/ char modelName[64]; GetClientModel(target, modelName, sizeof(modelName)); int type = GetEntProp(target, Prop_Send, "m_survivorCharacter"); SetEntityModel(client, modelName); SetEntProp(client, Prop_Send, "m_survivorCharacter", type); EmitSoundToAll("ui/pickup_secret01.wav", client, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); PrintHintText(client, "You have %d swaps remaining", HIDER_SWAP_LIMIT - hiderSwapCount[client]); } else { PrintHintText(client, "You can swap in %.0f seconds", HIDER_SWAP_COOLDOWN - diff); } } } } return Plugin_Continue; } void ClearInventory(int client) { for(int i = 0; i <= 5; i++) { int item = GetPlayerWeaponSlot(client, i); if(item > 0) { RemovePlayerItem(client, item); RemoveEdict(item); // AcceptEntityInput(item, "Kill"); } } } bool AddSurvivor() { if (GetClientCount(false) >= MaxClients - 1) { return false; } int i = CreateFakeClient("GuessWhoBot"); bool result; if (i > 0) { if (DispatchKeyValue(i, "classname", "SurvivorBot")) { ChangeClientTeam(i, 2); if (DispatchSpawn(i)) { result = true; } } CreateTimer(0.2, Timer_Kick, GetClientUserId(i)); } return result; } Action Timer_Kick(Handle h, int u) { int i = GetClientOfUserId(u); if(i > 0) KickClient(i); return Plugin_Handled; } stock void L4D2_RunScript(const char[] sCode, any ...) { static int iScriptLogic = INVALID_ENT_REFERENCE; if(iScriptLogic == INVALID_ENT_REFERENCE || !IsValidEntity(iScriptLogic)) { iScriptLogic = EntIndexToEntRef(CreateEntityByName("logic_script")); if(iScriptLogic == INVALID_ENT_REFERENCE|| !IsValidEntity(iScriptLogic)) SetFailState("Could not create 'logic_script'"); DispatchSpawn(iScriptLogic); } static char sBuffer[512]; VFormat(sBuffer, sizeof(sBuffer), sCode, 2); SetVariantString(sBuffer); AcceptEntityInput(iScriptLogic, "RunScriptCode"); } public void OnSceneStageChanged(int scene, SceneStages stage) { if(isEnabled && stage == SceneStage_Started) { int activator = GetSceneInitiator(scene); if(activator == 0) { CancelScene(scene); } } } bool SaveMapData(const char[] map, const char[] set = "default") { char buffer[256]; // guesswho folder should be created by ReloadMapDB BuildPath(Path_SM, buffer, sizeof(buffer), "data/guesswho/%s", map); CreateDirectory(buffer, FOLDER_PERMS); BuildPath(Path_SM, buffer, sizeof(buffer), "data/guesswho/%s/%s.txt", map, set); File file = OpenFile(buffer, "w+"); if(file != null) { file.WriteLine("px\tpy\tpz\tax\tay\taz"); LocationMeta meta; for(int i = 0; i < validLocations.Length; i++) { validLocations.GetArray(i, meta); file.WriteLine("%.1f %.1f %.1f %.1f %.1f %.1f", meta.pos[0], meta.pos[1], meta.pos[2], meta.ang[0], meta.ang[1], meta.ang[2]); } PrintToServer("[GuessWho] Saved %d locations to %s/%s.txt", validLocations.Length, map, set); file.Flush(); delete file; return true; } PrintToServer("[GuessWho] OpenFile(w+) returned null for %s", buffer); return false; } bool LoadMapData(const char[] map, const char[] set = "default") { validLocations.Clear(); char buffer[256]; BuildPath(Path_SM, buffer, sizeof(buffer), "data/guesswho/%s/%s.txt", map, set); LoadConfigForMap(map); File file = OpenFile(buffer, "r+"); if(file != null) { char line[64]; char pieces[16][6]; file.ReadLine(line, sizeof(line)); // Skip header flowMin = L4D2Direct_GetMapMaxFlowDistance(); flowMax = 0.0; while(file.ReadLine(line, sizeof(line))) { ExplodeString(line, " ", pieces, 6, 16, false); LocationMeta meta; meta.pos[0] = StringToFloat(pieces[0]); meta.pos[1] = StringToFloat(pieces[1]); meta.pos[2] = StringToFloat(pieces[2]); meta.ang[0] = StringToFloat(pieces[3]); meta.ang[1] = StringToFloat(pieces[4]); meta.ang[2] = StringToFloat(pieces[5]); // Calculate the flow bounds Address nav = L4D2Direct_GetTerrorNavArea(meta.pos); if(nav == Address_Null) { nav = L4D_GetNearestNavArea(meta.pos); if(nav == Address_Null) { PrintToServer("[GuessWho] WARN: POINT AT (%f,%f,%f) IS INVALID (NO NAV AREA); skipping", meta.pos[0], meta.pos[1], meta.pos[2]); continue; } } float flow = L4D2Direct_GetTerrorNavAreaFlow(nav); if(flow < flowMin) flowMin = flow; else if(flow > flowMax) flowMax = flow; validLocations.PushArray(meta); } // Give some buffer space, to not trigger TOO FAR flowMin -= FLOW_BOUND_BUFFER; flowMax += FLOW_BOUND_BUFFER; PrintToServer("[GuessWho] Loaded %d locations (bounds (%.0f, %.0f)) for %s/%s", validLocations.Length, flowMin, flowMax, map, set); delete file; return true; } PrintToServer("[GuessWho] OpenFile(r+) returned null for %s", buffer); return false; }