├── NOTICE ├── Changelog.txt ├── Bot.lua ├── LICENSE └── Bot_Jeffco.lua /NOTICE: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2011 Colin Graf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /Changelog.txt: -------------------------------------------------------------------------------- 1 | 2 | Version 0.4 3 | 4 | Improved fighting skills 5 | Marine: Added automatic power node / player repairing 6 | Marine: Added automatic alert triggering (medpack, ammo, orders) 7 | Skulk: Fixed destroyed power node attacking issue 8 | 9 | Version 0.3 10 | 11 | Added automatic bot count adjusting 12 | Added infantry portal building 13 | 14 | Version 0.2 15 | 16 | Added random walking 17 | Fixed some auto targeting issues 18 | Fixed some aming issues 19 | Improved automatic weapon selection 20 | 21 | Version 0.1 22 | 23 | Added basic alien support 24 | Added auto attacking 25 | Added random environment observing 26 | 27 | Version 0.0 28 | 29 | A simple marine bot that uses the pathfinding system and builds unbuilt structures -------------------------------------------------------------------------------- /Bot.lua: -------------------------------------------------------------------------------- 1 | //============================================================================= 2 | // 3 | // lua\Bot.lua 4 | // 5 | // Implementation of Natural Selection 2 bot commands and event hooks 6 | // 7 | // Copyright 2011 Colin Graf (colin.graf@sovereign-labs.com) 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | //============================================================================= 22 | 23 | class 'Bot' 24 | 25 | function Bot:GetPlayer() 26 | return self.client:GetControllingPlayer() 27 | end 28 | 29 | //============================================================================= 30 | 31 | local botBots = { } 32 | local botMaxCount = 0 33 | 34 | function Bot_OnConsoleSetBots(client, countParam) 35 | 36 | // admin rights? 37 | if client ~= nil and not Shared.GetCheatsEnabled() and not Shared.GetDevMode() then 38 | return 39 | end 40 | 41 | // set max bot count 42 | if countParam then 43 | botMaxCount = math.min(10, math.max(0, tonumber(countParam))) 44 | end 45 | 46 | // compute new bot count 47 | local totalPlayerCount = Shared.GetEntitiesWithClassname("Player"):GetSize() 48 | local normalPlayerCount = totalPlayerCount - table.maxn(botBots) 49 | local botCount = math.min(math.max(botMaxCount + 1 - normalPlayerCount, 0), botMaxCount) 50 | 51 | // add more bots 52 | while table.maxn(botBots) < botCount do 53 | 54 | local bot = BotJeffco() 55 | bot:Initialize() 56 | bot.client = Server.AddVirtualClient() 57 | table.insert(botBots, bot) 58 | 59 | end 60 | 61 | // remove bots 62 | while table.maxn(botBots) > botCount do 63 | 64 | // find larger team 65 | local largerTeam 66 | local rules = GetGamerules() 67 | local playersTeam1 = rules:GetTeam(kTeam1Index):GetNumPlayers() 68 | local playersTeam2 = rules:GetTeam(kTeam2Index):GetNumPlayers() 69 | if playersTeam1 > playersTeam2 then 70 | largerTeam = kTeam1Index 71 | elseif playersTeam2 > playersTeam1 then 72 | largerTeam = kTeam2Index 73 | else 74 | largerTeam = ConditionalValue(math.random() < 0.5, kTeam1Index, kTeam2Index) 75 | end 76 | 77 | // find bot from larger team 78 | local botToRemove = 1 79 | for i, bot in ipairs(botBots) do 80 | local player = bot.client:GetControllingPlayer() 81 | if player:GetTeamNumber() == largerTeam then 82 | botToRemove = i 83 | break 84 | end 85 | end 86 | 87 | // remove bot 88 | local bot = botBots[botToRemove] 89 | Server.DisconnectClient(bot.client) 90 | bot.client = nil 91 | table.remove(botBots, botToRemove) 92 | end 93 | 94 | end 95 | 96 | function Bot_OnConsoleAddBots(client, countParam) 97 | 98 | // admin rights? 99 | if client ~= nil and not Shared.GetCheatsEnabled() and not Shared.GetDevMode() then 100 | return 101 | end 102 | 103 | // update bot count 104 | local count = 1 105 | if countParam then 106 | count = math.max(1, tonumber(countParam)) 107 | end 108 | Bot_OnConsoleSetBots(client, botMaxCount + count) 109 | 110 | end 111 | 112 | function Bot_OnConsoleRemoveBots(client, countParam) 113 | 114 | // admin rights? 115 | if client ~= nil and not Shared.GetCheatsEnabled() and not Shared.GetDevMode() then 116 | return 117 | end 118 | 119 | // update bot count 120 | local count = 1 121 | if countParam then 122 | count = math.max(1, tonumber(countParam)) 123 | end 124 | Bot_OnConsoleSetBots(client, botMaxCount - count) 125 | 126 | end 127 | 128 | function Bot_OnVirtualClientMove(client) 129 | 130 | for _, bot in ipairs(botBots) do 131 | if bot.client == client then 132 | return bot:OnMove() 133 | end 134 | end 135 | 136 | end 137 | 138 | function Bot_OnVirtualClientThink(client, deltaTime) 139 | 140 | for _, bot in ipairs(botBots) do 141 | if bot.client == client then 142 | return bot:OnThink(deltaTime) 143 | end 144 | end 145 | 146 | end 147 | 148 | function Bot_OnUpdateServer() 149 | 150 | if math.random() < .005 then 151 | Bot_OnConsoleSetBots() 152 | end 153 | 154 | end 155 | 156 | Event.Hook("Console_addbot", Bot_OnConsoleAddBots) 157 | Event.Hook("Console_removebot", Bot_OnConsoleRemoveBots) 158 | Event.Hook("Console_addbots", Bot_OnConsoleAddBots) 159 | Event.Hook("Console_removebots", Bot_OnConsoleRemoveBots) 160 | Event.Hook("Console_setbots", Bot_OnConsoleSetBots) 161 | 162 | Event.Hook("VirtualClientThink", Bot_OnVirtualClientThink) 163 | Event.Hook("VirtualClientMove", Bot_OnVirtualClientMove) 164 | 165 | Event.Hook("UpdateServer", Bot_OnUpdateServer) 166 | 167 | Script.Load("lua/Bot_Jeffco.lua") 168 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Bot_Jeffco.lua: -------------------------------------------------------------------------------- 1 | //============================================================================= 2 | // 3 | // lua\Bot_Jeffco.lua 4 | // 5 | // A simple bot implementation for Natural Selection 2 6 | // 7 | // Copyright 2011 Colin Graf (colin.graf@sovereign-labs.com) 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | //============================================================================= 22 | 23 | Script.Load("lua/PathingMixin.lua") 24 | 25 | class 'BotJeffco' (Bot) 26 | 27 | BotJeffco.kBotNames = { 28 | "Whitesides (bot)", "Baptist (bot)", "Fullbright (bot)", "Penhollow (bot)", "Harvill (bot)", "Bossert (bot)", "Claro (bot)", 29 | "Sanders (bot)", "Quiros (bot)", "Wakeland (bot)", "Nims (bot)", "Heroux (bot)", "Palafox (bot)", "Madruga (bot)", "Blane (bot)", 30 | "Welles (bot)", "Vencill (bot)", "Schoenberg (bot)", "Toll (bot)" 31 | } 32 | BotJeffco.kOrder = enum({ "Attack", "Construct", "Move", "Look", "None" }) 33 | BotJeffco.kDebugMode = true 34 | BotJeffco.kRange = 30 35 | BotJeffco.kRepairRange = 10 36 | BotJeffco.kMaxPitch = 89 37 | BotJeffco.kMinPitch = -89 38 | 39 | function BotJeffco:Initialize() 40 | 41 | InitMixin(self, PathingMixin) 42 | 43 | end 44 | 45 | function BotJeffco:GetInfantryPortal() 46 | 47 | local ents = Shared.GetEntitiesWithClassname("InfantryPortal") 48 | if ents:GetSize() > 0 then 49 | return ents:GetEntityAtIndex(0) 50 | end 51 | 52 | end 53 | 54 | function BotJeffco:GetHasCommander() 55 | 56 | local ents = Shared.GetEntitiesWithClassname("MarineCommander") 57 | local count = ents:GetSize() 58 | local player = self:GetPlayer() 59 | local teamNumber = player:GetTeamNumber() 60 | 61 | for i = 0, count - 1 do 62 | local commander = ents:GetEntityAtIndex(i) 63 | if commander:GetTeamNumber() == teamNumber then 64 | return true 65 | end 66 | end 67 | 68 | return false 69 | end 70 | 71 | function BotJeffco:GetCommandStation() 72 | 73 | local ents = Shared.GetEntitiesWithClassname("CommandStation") 74 | local count = ents:GetSize() 75 | local player = self:GetPlayer() 76 | local eyePos = player:GetEyePos() 77 | local closestCommandStation, closestDistance 78 | 79 | for i = 0, count - 1 do 80 | local commandStation = ents:GetEntityAtIndex(i) 81 | local distance = (commandStation:GetOrigin() - eyePos):GetLengthSquared() 82 | if closestCommandStation == nil or distance < closestDistance then 83 | closestCommandStation, closestDistance = commandStation, distance 84 | end 85 | end 86 | 87 | return closestCommandStation 88 | 89 | end 90 | 91 | function BotJeffco:GetMoblieAttackTarget() 92 | 93 | local player = self:GetPlayer() 94 | 95 | if not player.mobileTargetSelector then 96 | if player:isa("Marine") then 97 | player.mobileTargetSelector = TargetSelector():Init( 98 | player, 99 | BotJeffco.kRange, 100 | true, 101 | { kMarineMobileTargets }, 102 | { PitchTargetFilter(player, -BotJeffco.kMaxPitch, BotJeffco.kMaxPitch), CloakTargetFilter(), CamouflageTargetFilter() }) 103 | end 104 | if player:isa("Alien") then 105 | player.mobileTargetSelector = TargetSelector():Init( 106 | player, 107 | BotJeffco.kRange, 108 | true, 109 | { kAlienMobileTargets }, 110 | { PitchTargetFilter(player, -BotJeffco.kMaxPitch, BotJeffco.kMaxPitch) }) 111 | end 112 | end 113 | 114 | if player.mobileTargetSelector then 115 | player.mobileTargetSelector:AttackerMoved() 116 | return player.mobileTargetSelector:AcquireTarget() 117 | end 118 | end 119 | 120 | function BotJeffco:GetStaticAttackTarget() 121 | 122 | local player = self:GetPlayer() 123 | 124 | if not player.staticTargetSelector then 125 | if player:isa("Marine") then 126 | player.staticTargetSelector = TargetSelector():Init( 127 | player, 128 | BotJeffco.kRange, 129 | true, 130 | { kMarineStaticTargets }, 131 | { CloakTargetFilter(), CamouflageTargetFilter() }) 132 | end 133 | if player:isa("Alien") then 134 | player.staticTargetSelector = TargetSelector():Init( 135 | player, 136 | BotJeffco.kRange, 137 | true, 138 | { kAlienStaticTargets }, 139 | { }) 140 | end 141 | end 142 | 143 | if player.staticTargetSelector then 144 | player.staticTargetSelector:AttackerMoved() 145 | return player.staticTargetSelector:AcquireTarget() 146 | end 147 | end 148 | 149 | function BotJeffco:GetRepairTarget() 150 | 151 | local player = self:GetPlayer() 152 | local eyePos = player:GetEyePos() 153 | local repairTarget, closestDistance 154 | local allowedDistance = BotJeffco.kRepairRange * BotJeffco.kRepairRange 155 | 156 | local ents = Shared.GetEntitiesWithClassname("Marine") 157 | local count = ents:GetSize() 158 | for i = 0, count - 1 do 159 | local marine = ents:GetEntityAtIndex(i) 160 | local distance = (marine:GetOrigin() - eyePos):GetLengthSquared() 161 | if distance < allowedDistance and marine:GetIsAlive() and marine:GetArmor() < marine:GetMaxArmor() and (repairTarget == nil or distance < closestDistance) then 162 | repairTarget, closestDistance = marine, distance 163 | end 164 | end 165 | 166 | if repairTarget then 167 | return repairTarget 168 | end 169 | 170 | ents = Shared.GetEntitiesWithClassname("PowerPoint") 171 | count = ents:GetSize() 172 | for i = 0, count - 1 do 173 | local powerPoint = ents:GetEntityAtIndex(i) 174 | local distance = (powerPoint:GetOrigin() - eyePos):GetLengthSquared() 175 | if distance < allowedDistance and powerPoint:GetIsSocketed() and powerPoint:GetHealthScalar() < 1. and (repairTarget == nil or distance < closestDistance) then 176 | repairTarget, closestDistance = powerPoint, distance 177 | end 178 | end 179 | 180 | return repairTarget 181 | end 182 | 183 | function BotJeffco:GetWeapons() 184 | 185 | local player = self:GetPlayer() 186 | local primary, secondary 187 | for _, weapon in ientitychildren(player, "ClipWeapon") do 188 | local slot = weapon:GetHUDSlot() 189 | if slot == kSecondaryWeaponSlot then 190 | secondary = weapon 191 | elseif slot == kPrimaryWeaponSlot then 192 | primary = weapon 193 | end 194 | end 195 | return primary, secondary 196 | end 197 | 198 | function BotJeffco:GetAmmoScalar() 199 | 200 | local player = self:GetPlayer() 201 | local ammo, maxAmmo = 0, 0 202 | for _, weapon in ientitychildren(player, "ClipWeapon") do 203 | ammo = ammo + weapon:GetAmmo() 204 | maxAmmo = maxAmmo + weapon:GetMaxAmmo() 205 | end 206 | if maxAmmo == 0 then // alien 207 | return 1 208 | end 209 | return ammo / maxAmmo 210 | end 211 | 212 | function BotJeffco:LookAtPoint(toPoint, direct) 213 | 214 | local player = self:GetPlayer() 215 | 216 | // compute direction to target 217 | local diff = toPoint - player:GetEyePos() 218 | local direction = GetNormalizedVector(diff) 219 | 220 | // look at target 221 | if direct then 222 | self.move.yaw = GetYawFromVector(direction) - player.baseYaw 223 | else 224 | local turnSpeed = ConditionalValue(player:isa("Alien"), .8, .4) 225 | self.move.yaw = SlerpRadians(self.move.yaw, GetYawFromVector(direction) - player.baseYaw, turnSpeed) 226 | end 227 | self.move.pitch = GetPitchFromVector(direction) - player.basePitch 228 | //self.move.pitch = SlerpRadians(self.move.pitch, GetPitchFromVector(direction) - player.basePitch, .4) 229 | 230 | end 231 | 232 | function BotJeffco:MoveToPoint(toPoint, distablePathing) 233 | 234 | local player = self:GetPlayer() 235 | 236 | // use pathfinder 237 | if distablePathing == nil then 238 | if self:BuildPath(player:GetEyePos(), toPoint) then 239 | local points = self:GetPoints() 240 | if points then 241 | toPoint = points[1] 242 | end 243 | if table.maxn(points) > 1 and math.random() < 0.9 and (player:GetEyePos() - toPoint):GetLengthSquared() < 4 then 244 | toPoint = points[2] 245 | end 246 | end 247 | end 248 | 249 | // look at target 250 | self:LookAtPoint(toPoint) 251 | 252 | // walk forwards 253 | self.move.move.z = 1 254 | end 255 | 256 | function BotJeffco:TriggerAlerts() 257 | 258 | local player = self:GetPlayer() 259 | if not player:isa("Marine") then 260 | return 261 | end 262 | 263 | if self.lastAlertTime and self.currentTime - self.lastAlertTime < 30 then 264 | return 265 | end 266 | 267 | if not self:GetHasCommander() then 268 | return 269 | end 270 | 271 | // ask for for medpack 272 | if player:GetHealthScalar() < .4 then 273 | self.lastAlertTime = self.currentTime 274 | if math.random() < .5 then 275 | // TODO: consider armory distance 276 | player:PlaySound(marineRequestSayingsSounds[2]) 277 | player:GetTeam():TriggerAlert(kTechId.MarineAlertNeedMedpack, player) 278 | end 279 | end 280 | 281 | // ask for ammo pack 282 | if self:GetAmmoScalar() < .6 then 283 | self.lastAlertTime = self.currentTime 284 | if math.random() < .5 then 285 | // TODO: consider armory distance 286 | player:PlaySound(marineRequestSayingsSounds[3]) 287 | player:GetTeam():TriggerAlert(kTechId.MarineAlertNeedAmmo, player) 288 | end 289 | end 290 | 291 | // ask for orders 292 | if not self.lastOrderTime or self.currentTime - self.lastOrderTime > 360 then 293 | self.lastAlertTime = self.currentTime 294 | if math.random() < .5 then 295 | player:PlaySound(marineRequestSayingsSounds[4]) 296 | player:GetTeam():TriggerAlert(kTechId.MarineAlertNeedOrder, player) 297 | end 298 | end 299 | 300 | end 301 | 302 | function BotJeffco:UpdateOrder() 303 | 304 | local player = self:GetPlayer() 305 | self.orderType = BotJeffco.kOrder.None 306 | 307 | // #1 attack opponent players / mobile objects 308 | local target = self:GetMoblieAttackTarget() 309 | if target then 310 | player:GiveOrder(kTechId.Attack, target:GetId(), target:GetEngagementPoint(), nil, true, true) 311 | self.orderType = BotJeffco.kOrder.Attack 312 | self.orderTarget = target 313 | self.lastOrderTime = self.currentTime 314 | return 315 | end 316 | 317 | // #2 follow commander orders 318 | local order = player:GetCurrentOrder() 319 | if order then 320 | local orderType = order:GetType() 321 | local orderTarget = Shared.GetEntity(order:GetParam()) 322 | if orderTarget then 323 | if orderType == kTechId.Attack then 324 | if not orderTarget:isa("PowerPoint") or not orderTarget:GetIsDestroyed() then 325 | self.orderType = BotJeffco.kOrder.Attack 326 | self.orderLocation = orderTarget:GetEngagementPoint() 327 | self.orderTarget = orderTarget 328 | self.lastOrderTime = self.currentTime 329 | return 330 | end 331 | end 332 | if orderType == kTechId.Construct then 333 | self.orderType = BotJeffco.kOrder.Construct 334 | self.orderTarget = orderTarget 335 | self.lastOrderTime = self.currentTime 336 | return 337 | end 338 | end 339 | local orderLocation = order:GetLocation() 340 | if orderLocation then 341 | if orderLocation ~= self.commanderOrderLocation then 342 | self.orderType = BotJeffco.kOrder.Move 343 | self.orderLocation = orderLocation 344 | self.lastOrderTime = self.currentTime 345 | self.commanderOrderLocation = orderLocation 346 | self.commanderOrderLocationReached = false 347 | return 348 | end 349 | end 350 | if self.commanderOrderLocation and not self.commanderOrderLocationReached then 351 | if (player:GetEyePos() - self.commanderOrderLocation):GetLengthSquared() < 5 then 352 | self.commanderOrderLocationReached = true 353 | else 354 | self.orderType = BotJeffco.kOrder.Move 355 | self.orderLocation = self.commanderOrderLocation 356 | self.lastOrderTime = self.currentTime 357 | return 358 | end 359 | end 360 | end 361 | 362 | // #3 repair objects near by? 363 | target = self:GetRepairTarget() 364 | if target then 365 | self.orderType = BotJeffco.kOrder.Construct 366 | self.orderTarget = target 367 | self.lastOrderTime = self.currentTime 368 | return 369 | end 370 | 371 | // #4 attack stationary objects 372 | target = self:GetStaticAttackTarget() 373 | if target then 374 | player:GiveOrder(kTechId.Attack, target:GetId(), target:GetEngagementPoint(), nil, true, true) 375 | self.orderType = BotJeffco.kOrder.Attack 376 | self.orderTarget = target 377 | self.lastOrderTime = self.currentTime 378 | return 379 | end 380 | 381 | end 382 | 383 | function BotJeffco:StateTrace(name) 384 | 385 | if BotJeffco.kDebugMode and self.stateName ~= name then 386 | Print("%s", name) 387 | self.stateName = name 388 | end 389 | 390 | end 391 | 392 | //============================================================================= 393 | 394 | function BotJeffco:OnMove() 395 | return self.move 396 | end 397 | 398 | function BotJeffco:OnThink(deltaTime) 399 | 400 | // 401 | self:UpdateOrder() 402 | self:TriggerAlerts() 403 | 404 | // set default move 405 | local player = self:GetPlayer() 406 | local move = Move() 407 | move.yaw = player:GetAngles().yaw - player.baseYaw // keep the current yaw/pitch 408 | move.pitch = player:GetAngles().pitch - player.basePitch 409 | self.move = move 410 | 411 | // use a state machine to generate a move 412 | local currentTime = Shared.GetTime() 413 | //self.state = nil 414 | if self.state == nil then 415 | self.state = self.InitialState 416 | self.stateEnterTime = currentTime 417 | end 418 | self.stateTime = currentTime - self.stateEnterTime 419 | self.currentTime = currentTime 420 | local newState = self.state(self) 421 | if newState ~= self.state then 422 | self.stateEnterTime = currentTime 423 | self.state = newState 424 | end 425 | 426 | return true 427 | 428 | end 429 | 430 | //============================================================================= 431 | // States 432 | 433 | function BotJeffco:InitialState() 434 | 435 | self:StateTrace("initial") 436 | 437 | // wait a few seconds, set name and start idling 438 | if self.stateTime > 6 then 439 | 440 | local player = self:GetPlayer() 441 | local name = player:GetName() 442 | if name and string.find(string.lower(name), string.lower(kDefaultPlayerName)) then 443 | 444 | name = BotJeffco.kBotNames[math.random(1, table.maxn(BotJeffco.kBotNames))] 445 | OnCommandSetName(self.client, name) 446 | 447 | end 448 | 449 | return self.IdleState 450 | 451 | end 452 | 453 | return self.InitialState 454 | end 455 | 456 | function BotJeffco:JoinTeamState() 457 | 458 | self:StateTrace("join team") 459 | 460 | local player = self:GetPlayer() 461 | if player:GetTeamNumber() ~= 0 then 462 | return self.IdleState 463 | end 464 | 465 | local rules = GetGamerules() 466 | local joinTeam = ConditionalValue(math.random() < .5, 1, 2) 467 | if rules:GetCanJoinTeamNumber(joinTeam) or Shared.GetCheatsEnabled() then 468 | rules:JoinTeam(player, joinTeam) 469 | end 470 | 471 | self.move.move.z = 1 472 | 473 | return self.JoinTeamState 474 | end 475 | 476 | function BotJeffco:IdleState() 477 | 478 | self:StateTrace("idle") 479 | 480 | // in rr? 481 | local player = self:GetPlayer() 482 | if player:GetTeamNumber() == 0 then 483 | return self.JoinTeamState 484 | end 485 | 486 | // respawing? 487 | if player:isa("AlienSpectator") then 488 | return self.HatchState 489 | end 490 | 491 | // commanding? 492 | if player:isa("MarineCommander") then 493 | return self.CommandState 494 | end 495 | 496 | // attack order? 497 | if self.orderType == BotJeffco.kOrder.Attack then 498 | return self.AttackState 499 | end 500 | 501 | // construct order? 502 | if self.orderType == BotJeffco.kOrder.Construct then 503 | return self.ConstructState 504 | end 505 | 506 | // move order? 507 | if self.orderType == BotJeffco.kOrder.Move then 508 | return self.MoveState 509 | end 510 | 511 | // build ip? 512 | if player:isa("Marine") and self:GetInfantryPortal() == nil and not self:GetHasCommander() and self:GetCommandStation() then 513 | return self.EnterCommandStationState 514 | end 515 | 516 | // walk around 517 | if math.random() < .02 then 518 | return self.RandomWalkState 519 | end 520 | 521 | // look around 522 | if math.random() < .1 then 523 | return self.RandomLookState 524 | end 525 | 526 | // stay 527 | return self.IdleState 528 | end 529 | 530 | function BotJeffco:EnterCommandStationState() 531 | 532 | self:StateTrace("enter command station state") 533 | 534 | local player = self:GetPlayer() 535 | if player:isa("MarineCommander") then 536 | return self.CommandState 537 | end 538 | 539 | local commandStation = self:GetCommandStation() 540 | if commandStation == nil then 541 | return self.IdleState 542 | end 543 | 544 | local comLocation = commandStation:GetOrigin() 545 | if (player:GetEyePos() - comLocation):GetLengthSquared() > 18 or self.stateTime > 3 then 546 | if math.random() < .5 then 547 | comLocation.x = comLocation.x + ConditionalValue(math.random() < .5, -3, 3) 548 | else 549 | comLocation.z = comLocation.z + ConditionalValue(math.random() < .5, -3, 3) 550 | end 551 | self.orderLocation = comLocation 552 | self.move.commands = bit.bor(self.move.commands, Move.Jump) 553 | return self.MoveState 554 | end 555 | 556 | self:LookAtPoint(commandStation:GetOrigin(), true) 557 | self.move.commands = bit.bor(self.move.commands, Move.Use) 558 | 559 | comLocation.y = comLocation.y + 1 560 | self:MoveToPoint(comLocation, true) 561 | 562 | if math.random() < .2 and (player:GetEyePos() - comLocation):GetLengthSquared() > 3 then 563 | self.move.commands = bit.bor(self.move.commands, Move.Jump) 564 | end 565 | 566 | return self.EnterCommandStationState 567 | end 568 | 569 | function BotJeffco:CommandState() 570 | 571 | self:StateTrace("command") 572 | 573 | local player = self:GetPlayer() 574 | if not player:isa("MarineCommander") then 575 | return self.IdleState 576 | end 577 | 578 | if self:GetInfantryPortal() then 579 | return self.LeaveCommandStationState 580 | end 581 | 582 | // spawn infantry portal 583 | local commandStation = self:GetCommandStation() 584 | if commandStation == nil then 585 | return self.IdleState 586 | end 587 | local position = commandStation:GetOrigin() 588 | position.x = position.x + ConditionalValue(math.random() < .5, -2.8, 2.8) 589 | position.z = position.z + ConditionalValue(math.random() < .5, -2.8, 2.8) 590 | CreateEntity("infantryportal", position, player:GetTeamNumber()) 591 | 592 | return self.CommandState; 593 | end 594 | 595 | function BotJeffco:LeaveCommandStationState() 596 | 597 | self:StateTrace("leave command station state") 598 | 599 | local player = self:GetPlayer() 600 | if not player:isa("MarineCommander") then 601 | return self.IdleState 602 | end 603 | 604 | // 605 | player:Logout() 606 | 607 | return self.LeaveCommandStationState 608 | end 609 | 610 | function BotJeffco:RandomLookState() 611 | 612 | self:StateTrace("random look") 613 | 614 | // attack? 615 | if self.orderType ~= BotJeffco.kOrder.None then 616 | return self.IdleState 617 | end 618 | 619 | if self.randomLookTarget == nil then 620 | local player = self:GetPlayer() 621 | self.randomLookTarget = player:GetEyePos() 622 | self.randomLookTarget.x = self.randomLookTarget.x + math.random(-50, 50) 623 | self.randomLookTarget.z = self.randomLookTarget.z + math.random(-50, 50) 624 | end 625 | 626 | self:LookAtPoint(self.randomLookTarget) 627 | 628 | if self.lastYaw then 629 | if (math.abs(self.move.yaw - self.lastYaw) < .05 and math.abs(self.move.pitch - self.lastPitch) < .05) or self.stateTime > 10 then 630 | self.randomLookTarget = nil 631 | return self.IdleState 632 | end 633 | end 634 | self.lastYaw = self.move.yaw 635 | self.lastPitch = self.move.pitch 636 | 637 | return self.RandomLookState 638 | end 639 | 640 | function BotJeffco:RandomWalkState() 641 | 642 | self:StateTrace("random walk") 643 | 644 | // attack? 645 | if self.orderType ~= BotJeffco.kOrder.None then 646 | return self.AttackState 647 | end 648 | 649 | local player = self:GetPlayer() 650 | if self.randomWalkTarget == nil then 651 | 652 | self.randomWalkTarget = player:GetEyePos() 653 | self.randomWalkTarget.x = self.randomWalkTarget.x + math.random(-4, 4) 654 | self.randomWalkTarget.z = self.randomWalkTarget.z + math.random(-4, 4) 655 | 656 | if player:isa("Alien") then 657 | local ents = Shared.GetEntitiesWithClassname(ConditionalValue(math.random() < .5, "TechPoint", "ResourcePoint")) 658 | if ents:GetSize() > 0 then 659 | local index = math.floor(math.random() * ents:GetSize()) 660 | local target = ents:GetEntityAtIndex(index) 661 | self.randomWalkTarget = target:GetEngagementPoint() 662 | end 663 | else 664 | if math.random() < .3 and self.commanderOrderLocation then 665 | self.randomWalkTarget = self.commanderOrderLocation 666 | end 667 | end 668 | 669 | end 670 | 671 | self:MoveToPoint(self.randomWalkTarget) 672 | 673 | if (player:GetEyePos() - self.randomWalkTarget):GetLengthSquared() < 4 or self.stateTime > 20 then 674 | self.randomWalkTarget = nil 675 | return self.IdleState 676 | end 677 | 678 | return self.RandomWalkState 679 | end 680 | 681 | function BotJeffco:HatchState() 682 | 683 | self:StateTrace("hatch") 684 | 685 | local player = self:GetPlayer() 686 | if not player:isa("AlienSpectator") then 687 | return self.IdleState 688 | end 689 | 690 | self.move.commands = Move.PrimaryAttack 691 | 692 | return self.HatchState 693 | end 694 | 695 | function BotJeffco:MoveState() 696 | 697 | self:StateTrace("move") 698 | 699 | // move to target 700 | self:MoveToPoint(self.orderLocation) 701 | 702 | // target reached? 703 | local player = self:GetPlayer() 704 | if self.stateTime > 1 and ((player:GetEyePos() - self.orderLocation):GetLengthSquared() < 4.5 or self.stateTime > 5) then 705 | return self.IdleState 706 | end 707 | 708 | // jump? 709 | local player = self:GetPlayer() 710 | if math.random() < ConditionalValue(player:isa("Alien"), .2, .05) then 711 | self.move.commands = bit.bor(self.move.commands, Move.Jump) 712 | end 713 | 714 | return self.MoveState 715 | end 716 | 717 | function BotJeffco:ConstructState() 718 | 719 | self:StateTrace("construct") 720 | 721 | // construct? 722 | if self.orderType ~= BotJeffco.kOrder.Construct then 723 | return self.IdleState 724 | end 725 | 726 | // is target reachable? 727 | local player = self:GetPlayer() 728 | local engagementPoint = self.orderTarget:GetEngagementPoint() 729 | local allowedDistance = 5 730 | if self.orderTarget:isa("RoboticsFactory") then 731 | allowedDistance = 7 732 | end 733 | if (player:GetEyePos() - engagementPoint):GetLengthSquared() > allowedDistance then 734 | self.orderLocation = engagementPoint 735 | return self.MoveState 736 | end 737 | 738 | // timeout? 739 | if self.stateTime > 20 then 740 | self.orderLocation = engagementPoint 741 | return self.MoveState 742 | end 743 | 744 | // look at build object 745 | self:LookAtPoint(engagementPoint, true) 746 | 747 | // construct! 748 | self.move.commands = bit.bor(self.move.commands, Move.Use) 749 | 750 | return self.ConstructState 751 | end 752 | 753 | function BotJeffco:AttackState() 754 | 755 | self:StateTrace("attack") 756 | 757 | // attack? 758 | if self.orderType ~= BotJeffco.kOrder.Attack then 759 | //return self.RecoverState 760 | return self.IdleState 761 | end 762 | local attackTarget = self.orderTarget 763 | 764 | // choose weapon 765 | local player = self:GetPlayer() 766 | local activeWeapon = player:GetActiveWeapon() 767 | local outOfAmmo = activeWeapon == nil or (activeWeapon:isa("ClipWeapon") and activeWeapon:GetAmmo() == 0) 768 | if attackTarget:isa("Structure") and (activeWeapon == nil or not activeWeapon:isa("Axe")) then 769 | self.move.commands = bit.bor(self.move.commands, Move.Weapon3) 770 | elseif attackTarget:isa("Player") then 771 | local primaryWeapon, secondaryWeapon = self:GetWeapons() 772 | if primaryWeapon and (not primaryWeapon:isa("ClipWeapon") or primaryWeapon:GetAmmo() > 0) then 773 | if activeWeapon ~= primaryWeapon then 774 | self.move.commands = bit.bor(self.move.commands, Move.Weapon1) 775 | end 776 | elseif secondaryWeapon and (not secondaryWeapon:isa("ClipWeapon") or secondaryWeapon:GetAmmo() > 0) then 777 | if activeWeapon ~= secondaryWeapon then 778 | self.move.commands = bit.bor(self.move.commands, Move.Weapon2) 779 | end 780 | elseif outOfAmmo then 781 | self.move.commands = bit.bor(self.move.commands, Move.NextWeapon) 782 | end 783 | elseif outOfAmmo then 784 | self.move.commands = bit.bor(self.move.commands, Move.NextWeapon) 785 | end 786 | 787 | // move to axe a target? 788 | local melee = false 789 | if activeWeapon and activeWeapon:isa("Axe") then 790 | melee = true 791 | local engagementPoint = attackTarget:GetEngagementPoint() 792 | local allowedDistance = 5 793 | if attackTarget:isa("Hive") then 794 | allowedDistance = 10 795 | engagementPoint = attackTarget:GetOrigin() 796 | end 797 | if (player:GetEyePos() - engagementPoint):GetLengthSquared() > allowedDistance then 798 | self.orderLocation = engagementPoint 799 | return self.MoveState 800 | elseif not attackTarget:isa("Hive") then 801 | self.move.commands = bit.bor(self.move.commands, Move.Crouch) 802 | end 803 | end 804 | 805 | // as alien move to target 806 | if player:isa("Alien") then 807 | melee = true 808 | local engagementPoint = attackTarget:GetEngagementPoint() 809 | if (player:GetEyePos() - engagementPoint):GetLengthSquared() > 5 then 810 | self.orderLocation = engagementPoint 811 | return self.MoveState 812 | end 813 | end 814 | 815 | // timeout? 816 | if self.stateTime > 20 then 817 | self.orderLocation = attackTarget:GetEngagementPoint() 818 | return self.MoveState 819 | end 820 | 821 | // look at attack target 822 | local targetPosition = attackTarget:GetEngagementPoint() 823 | if activeWeapon and activeWeapon:isa("ClipWeapon") then 824 | targetPosition.x = targetPosition.x + (math.random() - 0.5) * 2 825 | targetPosition.y = targetPosition.y + (math.random() - 0.5) * 2 826 | targetPosition.z = targetPosition.z + (math.random() - 0.5) * 2 827 | end 828 | self:LookAtPoint(targetPosition, melee) 829 | 830 | // attack! 831 | if math.random() < .5 then 832 | self.move.commands = bit.bor(self.move.commands, Move.PrimaryAttack) 833 | end 834 | 835 | return self.AttackState 836 | end 837 | 838 | function BotJeffco:RecoverState() 839 | 840 | self:StateTrace("recover") 841 | 842 | if self.stateTime > 2 or self.orderType == BotJeffco.kOrder.Attack then 843 | return self.IdleState 844 | end 845 | 846 | return self.RecoverState 847 | end 848 | --------------------------------------------------------------------------------