From 95ecd3546d6673dcffd07925f0b518b803870ecf Mon Sep 17 00:00:00 2001 From: ficool2 <34815548+ficool2@users.noreply.github.com> Date: Wed, 21 May 2025 22:49:01 +0100 Subject: [PATCH] Initial framework for bots --- scripts/vscripts/tf2ware_ultimate.nut | 1 + scripts/vscripts/tf2ware_ultimate/bot.nut | 164 ++++++++++++++++++ .../botbehavior/minigames/type_word.nut | 12 ++ scripts/vscripts/tf2ware_ultimate/events.nut | 49 +++--- scripts/vscripts/tf2ware_ultimate/main.nut | 36 +++- 5 files changed, 238 insertions(+), 24 deletions(-) create mode 100644 scripts/vscripts/tf2ware_ultimate/bot.nut create mode 100644 scripts/vscripts/tf2ware_ultimate/botbehavior/minigames/type_word.nut diff --git a/scripts/vscripts/tf2ware_ultimate.nut b/scripts/vscripts/tf2ware_ultimate.nut index e6c0a3420..196175dc0 100644 --- a/scripts/vscripts/tf2ware_ultimate.nut +++ b/scripts/vscripts/tf2ware_ultimate.nut @@ -14,6 +14,7 @@ IncludeScript("tf2ware_ultimate/vcd", ROOT) IncludeScript("tf2ware_ultimate/util", ROOT) IncludeScript("tf2ware_ultimate/config", ROOT) IncludeScript("tf2ware_ultimate/location", ROOT) +IncludeScript("tf2ware_ultimate/bot", ROOT) IncludeScript("tf2ware_ultimate/sdr", ROOT) IncludeScript("tf2ware_ultimate/dev", ROOT) IncludeScript("tf2ware_ultimate/plugin", ROOT) diff --git a/scripts/vscripts/tf2ware_ultimate/bot.nut b/scripts/vscripts/tf2ware_ultimate/bot.nut new file mode 100644 index 000000000..a1351f928 --- /dev/null +++ b/scripts/vscripts/tf2ware_ultimate/bot.nut @@ -0,0 +1,164 @@ +// by ficool2 + +class Ware_BotData +{ + function constructor(entity) + { + me = entity + minigame_timers = [] + } + + me = null + minigame_timers = null +} + +if (!("Ware_Bots" in this)) +{ + Ware_Bots <- [] +} + +SetConvarValue("tf_bot_difficulty", 0) +SetConvarValue("tf_bot_melee_only", 1) +SetConvarValue("tf_bot_sniper_melee_range", -1) +SetConvarValue("tf_bot_reevaluate_class_in_spawnroom", 0) +SetConvarValue("tf_bot_keep_class_after_death", 1) + +Ware_BotMinigameBehaviors <- +{ + type_word = {} +} + +Ware_BotMinigameBehavior <- null + +function Ware_BotLoadBehaviors() +{ + foreach (minigame, scope in Ware_BotMinigameBehaviors) + { + local file_name = "tf2ware_ultimate/botbehavior/minigames/" + minigame + try + { + IncludeScript(file_name, scope) + } + catch (e) + { + Ware_Error("Failed to load '%s.nut'. Missing from disk or syntax error", path) + } + } +} + +function Ware_BotSetup(bot) +{ + // disables visibility of enemies + bot.AddBotAttribute(IGNORE_ENEMIES) + bot.SetMaxVisionRangeOverride(0.01) + // makes spies not attempt to cloak + bot.SetMissionTarget(Ware_IncursionDummy) + // set MISSION_SNIPER which effectively does nothing + bot.SetMission(3, true) + bot.GetScriptScope().bot_data <- Ware_BotData(bot) + + if (Ware_Bots.find(bot) == null) + Ware_Bots.append(bot) +} + +function Ware_BotDestroy(bot) +{ + local bot_data = bot.GetScriptScope().bot_data + foreach (timer in bot_data.minigame_timers) + KillTimer(timer) +} + +function Ware_BotUpdate() +{ + if (Ware_Minigame + && Ware_BotMinigameBehavior + && "OnUpdate" in Ware_BotMinigameBehavior) + { + foreach (bot in Ware_Bots) + Ware_BotMinigameBehavior.OnUpdate(bot) + } + else + { + // TODO roam around + } +} + +function Ware_BotOnMinigameStart() +{ + if (Ware_Minigame.file_name in Ware_BotMinigameBehaviors) + { + Ware_BotMinigameBehavior = Ware_BotMinigameBehaviors[Ware_Minigame.file_name] + + if ("OnStart" in Ware_BotMinigameBehavior) + { + foreach (bot in Ware_Bots) + Ware_BotMinigameBehavior.OnStart(bot) + } + } + else + { + Ware_BotMinigameBehavior = null + } +} + +function Ware_BotOnMinigameEnd() +{ + foreach (bot in Ware_Bots) + { + local bot_data = bot.GetScriptScope().bot_data + foreach (timer in bot_data.minigame_timers) + KillTimer(timer) + bot_data.minigame_timers.clear() + } +} + +function Ware_BotCreateMinigameTimer(bot, callback, delay) +{ + local timer = CreateTimer(callback, delay) + bot.GetScriptScope().bot_data.minigame_timers.append(timer) + return timer +} + +function Ware_BotTryWordTypo(bot, text, chance) +{ + // TODO higher typo chance with longer word + + if (RandomFloat(0.0, 1.0) > chance) + return text + + if (text.len() < 2) + return text + + local i = RandomInt(0, text.len() - 2) + local chars = text.toupper() == text ? text.tolower() : text + local type = RandomInt(0, 3) + + switch (type) + { + case 0: // swap + { + local tmp = chars[i] + chars = chars.slice(0, i) + chars[i + 1].tochar() + tmp.tochar() + chars.slice(i + 2) + break; + } + case 1: // omit + { + chars = chars.slice(0, i) + chars.slice(i + 1) + break + } + case 2: // repeat + { + chars = chars.slice(0, i) + chars[i].tochar() + chars[i].tochar() + chars.slice(i + 1) + break + } + case 3: // wrong + { + local keyboard = "abcdefghijklmnopqrstuvwxyz" + local wrongChar = keyboard[RandomInt(0, keyboard.len() - 1)] + chars = chars.slice(0, i) + wrongChar.tochar() + chars.slice(i + 1) + break + } + } + + return chars +} \ No newline at end of file diff --git a/scripts/vscripts/tf2ware_ultimate/botbehavior/minigames/type_word.nut b/scripts/vscripts/tf2ware_ultimate/botbehavior/minigames/type_word.nut new file mode 100644 index 000000000..565b01dea --- /dev/null +++ b/scripts/vscripts/tf2ware_ultimate/botbehavior/minigames/type_word.nut @@ -0,0 +1,12 @@ +function OnStart(bot) +{ + // TODO scale by difficulty + local chance = 0.3 + local delay = RandomFloat(1.5, 5.0) + + Ware_BotCreateMinigameTimer(bot, function() + { + local word = Ware_BotTryWordTypo(bot, Ware_MinigameScope.word, chance) + Say(bot, word, false) + }, delay) +} \ No newline at end of file diff --git a/scripts/vscripts/tf2ware_ultimate/events.nut b/scripts/vscripts/tf2ware_ultimate/events.nut index 248e7e84f..991718e5f 100644 --- a/scripts/vscripts/tf2ware_ultimate/events.nut +++ b/scripts/vscripts/tf2ware_ultimate/events.nut @@ -329,29 +329,17 @@ function OnGameEvent_recalculate_truce(params) } } -::Ware_PlayerInit <- function() +::Ware_PlayerInitWrapper <- function() { - local player = self - MarkForPurge(player) - - // don't include SourceTV because it's not a real player - if (IsPlayerSourceTV(player)) - return - - player.ValidateScriptScope() - local scope = player.GetScriptScope() - scope.ware_data <- Ware_PlayerData(player) - scope.ware_minidata <- {} - scope.ware_specialdata <- {} - Ware_Players.append(player) - Ware_PlayersData.append(scope.ware_data) - if (Ware_SpecialRound && Ware_SpecialRound.cb_on_player_connect.IsValid()) - Ware_SpecialRound.cb_on_player_connect(player) + Ware_PlayerInit(self) } ::Ware_PlayerPostSpawn <- function() { Ware_UpdatePlayerVoicePitch(self) + + if (self.IsBotOfType(TF_BOT_TYPE)) + Ware_BotSetup(self) local melee = ware_data.special_melee if (melee == null) @@ -382,7 +370,7 @@ function OnGameEvent_recalculate_truce(params) } if (Ware_SpecialRound && Ware_SpecialRound.cb_on_player_postspawn.IsValid()) - Ware_SpecialRound.cb_on_player_postspawn(self) + Ware_SpecialRound.cb_on_player_postspawn(self) } function OnGameEvent_player_spawn(params) @@ -393,8 +381,15 @@ function OnGameEvent_player_spawn(params) if (params.team == TEAM_UNASSIGNED) { - // delay this to end of frame as SourceTV won't be registered here yet - EntityEntFire(player, "CallScriptFunction", "Ware_PlayerInit", 0.0) + if (player.IsBotOfType(TF_BOT_TYPE)) + { + Ware_PlayerInit(player) + } + else + { + // delay this to end of frame as SourceTV won't be registered here yet + EntityEntFire(player, "CallScriptFunction", "Ware_PlayerInitWrapper", 0.0) + } return } @@ -529,11 +524,19 @@ function OnGameEvent_player_disconnect(params) idx = Ware_Players.find(player) if (idx != null) + { Ware_Players.remove(idx) - - idx = Ware_PlayersData.find(data) - if (idx != null) Ware_PlayersData.remove(idx) + } + + if (player.IsBotOfType(TF_BOT_TYPE)) + { + Ware_BotDestroy(player) + + idx = Ware_Bots.find(player) + if (idx != null) + Ware_Bots.remove(idx) + } if (Ware_Minigame == null) return diff --git a/scripts/vscripts/tf2ware_ultimate/main.nut b/scripts/vscripts/tf2ware_ultimate/main.nut index 427229293..04ed630e9 100644 --- a/scripts/vscripts/tf2ware_ultimate/main.nut +++ b/scripts/vscripts/tf2ware_ultimate/main.nut @@ -240,7 +240,11 @@ if (!("Ware_Precached" in this)) } // this shuts up incursion distance warnings from the nav mesh - CreateEntitySafe("base_boss").KeyValueFromString("classname", "point_commentary_viewpoint") + Ware_IncursionDummy <- CreateEntitySafe("base_boss") + Ware_IncursionDummy.KeyValueFromString("classname", "point_commentary_viewpoint") + Ware_IncursionDummy.AddSolidFlags(FSOLID_NOT_SOLID) + // bots now use this as a unreachable dummy target + Ware_IncursionDummy.SetAbsOrigin(Vector(16000, 16000, 16000)) } function Ware_SetupMap() @@ -789,6 +793,26 @@ function Ware_FixupPlayerWeaponSwitch() self.Weapon_Switch(activator) } +function Ware_PlayerInit(player) +{ + MarkForPurge(player) + + // don't include SourceTV because it's not a real player + if (IsPlayerSourceTV(player)) + return + + player.ValidateScriptScope() + local scope = player.GetScriptScope() + scope.ware_data <- Ware_PlayerData(player) + scope.ware_minidata <- {} + scope.ware_specialdata <- {} + Ware_Players.append(player) + Ware_PlayersData.append(scope.ware_data) + if (Ware_SpecialRound && Ware_SpecialRound.cb_on_player_connect.IsValid()) + Ware_SpecialRound.cb_on_player_connect(player) +} + + function Ware_CheckPlayerArray() { // failsafe: if a player entity gets deleted without disconnecting (such as via external plugins) @@ -800,6 +824,9 @@ function Ware_CheckPlayerArray() { Ware_Players.remove(i) Ware_PlayersData.remove(i) + local idx = Ware_Bots.find(player) + if (idx != null) + Ware_Bots.remove(idx) } } for (local i = Ware_MinigamePlayers.len() - 1; i >= 0; i--) @@ -1693,6 +1720,8 @@ function Ware_StartMinigameInternal(is_boss) if (Ware_SpecialRound) Ware_SpecialRound.cb_on_minigame_start() + + Ware_BotOnMinigameStart() Ware_MinigamePreEndTimer = CreateTimer(function() { @@ -2024,6 +2053,8 @@ function Ware_FinishMinigameInternal() if (Ware_SpecialRound) Ware_SpecialRound.cb_on_minigame_end() + + Ware_BotOnMinigameEnd() Ware_Minigame = null Ware_MinigameScope.clear() @@ -2234,6 +2265,8 @@ function Ware_OnUpdate() Ware_UpdateNav() + Ware_BotUpdate() + if (Ware_SpecialRound) Ware_SpecialRound.cb_on_update() @@ -2476,4 +2509,5 @@ IncludeScript("tf2ware_ultimate/api/specialround", ROOT) Ware_SetupMap() Ware_SetupLocations() Ware_PrecacheEverything() +Ware_BotLoadBehaviors() Ware_CheckPlayerArray() \ No newline at end of file