Download Lua/Tactical/AIActions.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 32 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/AIActions.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/Lua/Tactical/AIActions.lua
-
curl -L -o AIActions.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/AIActions.lua
32 kB
| function AIKeywordsCombo() | |
| return { | |
| "Control", | |
| "Explosives", | |
| "Sniper", | |
| "Soldier", | |
| "Ordnance", | |
| "Smoke", | |
| "Flank", | |
| "MobileShot", | |
| "RunAndGun", | |
| "Stim", | |
| "Nova", | |
| "Heal", | |
| } | |
| end | |
| function AIEnvStateCombo() | |
| local items = {} | |
| ForEachPresetInGroup("GameStateDef", "weather", function(item) table.insert(items, item.id) end) | |
| ForEachPresetInGroup("GameStateDef", "time of day", function(item) table.insert(items, item.id) end) | |
| return items | |
| end | |
| -- Base class | |
| DefineClass.AISignatureAction = { | |
| __parents = { "AIBiasObj", }, | |
| properties = { | |
| { id = "NotificationText", name = "Notification Text", editor = "text", translate = true, default = "" }, | |
| { id = "RequiredKeywords", editor = "string_list", default = {}, item_default = "", items = AIKeywordsCombo, arbitrary_value = true, }, | |
| { id = "AvailableInState", name = "Available In", editor = "set", default = set(), items = AIEnvStateCombo }, | |
| { id = "ForbiddenInState", name = "Forbidden In", editor = "set", default = set(), items = AIEnvStateCombo }, | |
| }, | |
| hidden = false, | |
| movement = false, | |
| voice_response = false, -- if a non-empty string, play that responce; if empty string play nothing, if false play default response | |
| } | |
| function AISignatureAction:GetEditorView() | |
| return self.class | |
| end | |
| function AISignatureAction:MatchUnit(unit) | |
| for state, _ in pairs(self.AvailableInState) do | |
| if not GameStates[state] then | |
| return | |
| end | |
| end | |
| for state, _ in pairs(self.ForbiddenInState) do | |
| if GameStates[state] then | |
| return | |
| end | |
| end | |
| for _, keyword in ipairs(self.RequiredKeywords) do | |
| if not table.find(unit.AIKeywords or empty_table, keyword) then | |
| return | |
| end | |
| end | |
| return true | |
| end | |
| function AISignatureAction:PrecalcAction(context, action_state) | |
| end | |
| function AISignatureAction:IsAvailable(context, action_state) | |
| return false | |
| end | |
| function AISignatureAction:Execute(context, action_state) | |
| end | |
| function AISignatureAction:GetVoiceResponse() | |
| return self.voice_response | |
| end | |
| function AISignatureAction:OnActivate(unit) | |
| if (self.NotificationText or "") ~= "" then | |
| ShowTacticalNotification("enemyAttack", false, self.NotificationText) | |
| end | |
| return AIBiasObj.OnActivate(self, unit) | |
| end | |
| --------------------------------------- | |
| DefineClass.AIActionBasicAttack = { | |
| __parents = { "AISignatureAction", }, | |
| } | |
| function AIActionBasicAttack:PrecalcAction(context, action_state) | |
| local unit = context.unit | |
| local dest = context.ai_destination or GetPackedPosAndStance(unit) | |
| local target = (context.dest_target or empty_table)[dest] | |
| if not IsValidTarget(target) then | |
| return | |
| end | |
| local cost = context.default_attack_cost | |
| if cost >= 0 and unit:HasAP(cost) then | |
| action_state.args = {target = target} | |
| action_state.has_ap = true | |
| end | |
| end | |
| function AIActionBasicAttack:IsAvailable(context, action_state) | |
| return action_state.has_ap | |
| end | |
| function AIActionBasicAttack:Execute(context, action_state) | |
| assert(action_state.has_ap) | |
| AIPlayCombatAction(context.default_attack.id, context.unit, nil, action_state.args) | |
| end | |
| -- area attack base classes | |
| DefineClass.AIActionBaseZoneAttack = { | |
| __parents = { "AISignatureAction", }, | |
| properties = { | |
| { id = "enemy_score", name = "Enemy Hit Score", editor = "number", default = 100, }, | |
| { id = "team_score", name = "Teammate Hit Score", editor = "number", default = -1000, }, | |
| { id = "self_score_mod", name = "Self Score Modifier", editor = "number", scale = "percent", default = -100, help = "Score will be modified with this value if the targeted zone includes the unit performing the attack" }, | |
| { id = "min_score", name = "Score Threshold", editor = "number", default = 200, help = "Action will not be taken if best score is lower than this", }, | |
| }, | |
| action_id = false, | |
| hidden = true, | |
| } | |
| function AIEvalZones(context, zones, min_score, enemy_score, team_score, self_score_mod) | |
| local best_target, best_score = nil, (min_score or 0) - 1 | |
| for _, zone in ipairs(zones) do | |
| local score | |
| local selfmod = 0 | |
| for _, unit in ipairs(zone.units) do | |
| local uscore = 0 | |
| if not unit:IsDead() and not unit:IsDowned() then | |
| if unit:IsOnEnemySide(context.unit) then | |
| uscore = enemy_score or 0 | |
| elseif unit.team == context.unit.team then | |
| uscore = team_score or 0 | |
| if unit == context.unit then | |
| selfmod = self_score_mod or 0 | |
| end | |
| end | |
| end | |
| score = (score or 0) + uscore | |
| end | |
| score = score and MulDivRound(score, zone.score_mod or 100, 100) | |
| score = score and MulDivRound(score, 100 + selfmod, 100) | |
| if score and score > best_score then | |
| best_target, best_score = zone, score | |
| end | |
| zone.score = score | |
| end | |
| return best_target, best_score | |
| end | |
| function AIActionBaseZoneAttack:EvalZones(context, zones) | |
| return AIEvalZones(context, zones, self.min_score, self.enemy_score, self.team_score, self.self_score_mod) | |
| end | |
| DefineClass.AIActionBaseConeAttack = { | |
| __parents = { "AIActionBaseZoneAttack", }, | |
| properties = { | |
| { id = "self_score_mod", editor = "number", default = 0, no_edit = true }, | |
| }, | |
| } | |
| MapVar("g_LastSelectedZone", false) | |
| function DbgShowLastSelectedZone() | |
| if not g_LastSelectedZone then return end | |
| DbgClearVectors() | |
| local start = g_LastSelectedZone.poly[#g_LastSelectedZone.poly] | |
| for _, pt in ipairs(g_LastSelectedZone.poly) do | |
| DbgAddVector(start:SetTerrainZ(guim), (pt - start):SetZ(0), const.clrWhite) | |
| start = pt | |
| end | |
| end | |
| function AIActionBaseConeAttack:PrecalcAction(context, action_state) | |
| if not IsKindOf(context.weapon, "Firearm") then | |
| return | |
| end | |
| local caction = CombatActions[self.action_id] | |
| if not caction or caction:GetUIState({context.unit}) ~= "enabled" then return end | |
| local args, has_ap = AIGetAttackArgs(context, caction, nil, "None") | |
| action_state.has_ap = has_ap | |
| if not has_ap then return end | |
| local zones = AIPrecalcConeTargetZones(context, self.action_id, nil, action_state.stance) | |
| local zone, best_score = self:EvalZones(context, zones) | |
| action_state.score = best_score | |
| args.target_pos = zone and zone.target_pos | |
| args.target = zone and zone.target_pos | |
| action_state.args = args | |
| g_LastSelectedZone = zone | |
| end | |
| function AIActionBaseConeAttack:IsAvailable(context, action_state) | |
| return action_state.has_ap and action_state.args.target_pos | |
| end | |
| function AIActionBaseConeAttack:Execute(context, action_state) | |
| assert(action_state.has_ap) | |
| AIPlayCombatAction(self.action_id, context.unit, nil, action_state.args) | |
| end | |
| -- actions | |
| --------------------------------------- | |
| DefineClass.AIActionThrowGrenade = { | |
| __parents = { "AIActionBaseZoneAttack", }, | |
| properties = { | |
| { id = "MinDist", editor = "number", scale = "m", default = 2*guim, min = 0 }, | |
| { id = "MaxDist", editor = "number", scale = "m", default = 100*guim, min = 0 }, | |
| { id = "AllowedAoeTypes", editor = "set", items = {"none", "fire", "smoke", "teargas", "toxicgas"}, default = set("none") }, | |
| { id = "TargetLastAttackPos", editor = "bool", default = false }, | |
| }, | |
| hidden = false, | |
| voice_response = "AIThrowGrenade", | |
| } | |
| function AIActionThrowGrenade:PrecalcAction(context, action_state) | |
| local action_id, grenade | |
| local actions = { "ThrowGrenadeA", "ThrowGrenadeB", "ThrowGrenadeC", "ThrowGrenadeD" } | |
| for _, id in ipairs(actions) do | |
| local caction = CombatActions[id] | |
| local cost = caction and caction:GetAPCost(context.unit) or -1 | |
| if cost > 0 and context.unit:HasAP(cost) then | |
| action_id = id | |
| local weapon = caction:GetAttackWeapons(context.unit) | |
| local aoetype = weapon.aoeType or "none" | |
| if IsKindOf(weapon, "Grenade") and self.AllowedAoeTypes[aoetype] then | |
| grenade = weapon | |
| break | |
| end | |
| end | |
| end | |
| if not action_id or not grenade then | |
| return | |
| end | |
| local max_range = Min(self.MaxDist, grenade:GetMaxAimRange(context.unit) * const.SlabSizeX) | |
| local blast_radius = grenade.AreaOfEffect * const.SlabSizeX | |
| local target_pts | |
| if self.TargetLastAttackPos then | |
| -- collect enemy last attack positions and pass them as target_pos array to AIPrecalcGrenadeZones | |
| for _, enemy in ipairs(context.enemies) do | |
| if enemy.last_attack_pos then | |
| target_pts = target_pts or {} | |
| target_pts[#target_pts + 1] = enemy.last_attack_pos | |
| end | |
| end | |
| end | |
| local zones = AIPrecalcGrenadeZones(context, action_id, self.MinDist, max_range, blast_radius, grenade.aoeType, target_pts) | |
| local zone, score = self:EvalZones(context, zones) | |
| if zone then | |
| action_state.action_id = action_id | |
| action_state.target_pos = zone.target_pos | |
| action_state.score = score | |
| end | |
| end | |
| function AIActionThrowGrenade:IsAvailable(context, action_state) | |
| return not not action_state.action_id | |
| end | |
| function AIActionThrowGrenade:Execute(context, action_state) | |
| assert(action_state.action_id and action_state.target_pos) | |
| AIPlayCombatAction(action_state.action_id, context.unit, nil, {target = action_state.target_pos}) | |
| end | |
| --------------------------------------- | |
| DefineClass.AIConeAttack = { | |
| __parents = { "AIActionBaseConeAttack", }, | |
| properties = { | |
| { id = "action_id", editor = "dropdownlist", items = {"Buckshot", "DoubleBarrel", "Overwatch"}, default = "Buckshot" }, | |
| }, | |
| hidden = false, | |
| } | |
| function AIConeAttack:GetEditorView() | |
| return string.format("Cone Attack (%s)", self.action_id) | |
| end | |
| function AIConeAttack:Execute(context, action_state) | |
| AIActionBaseConeAttack.Execute(self, context, action_state) | |
| if self.action_id == "Overwatch" then | |
| return "done" | |
| end | |
| end | |
| function AIConeAttack:GetVoiceResponse() | |
| if self.action_id == "Overwatch" then | |
| return "AIOverwatch" | |
| end | |
| return self.voice_response | |
| end | |
| --------------------------------------- | |
| DefineClass.AIActionBandage = { | |
| __parents = { "AISignatureAction", "AIBaseHealPolicy", }, | |
| voice_response = "", | |
| } | |
| function AIActionBandage:IsAvailable(context, action_state) | |
| return action_state.has_ap | |
| end | |
| function AIActionBandage:Execute(context, action_state) | |
| assert(action_state.has_ap) | |
| if action_state.args.target then | |
| if not IsMeleeRangeTarget(context.unit, nil, nil, action_state.args.target) then | |
| return | |
| end | |
| context.unit:Face(action_state.args.target) | |
| end | |
| AIPlayCombatAction("Bandage", context.unit, nil, action_state.args) | |
| return "stop" | |
| end | |
| function AIActionBandage:PrecalcAction(context, action_state) | |
| local unit = context.unit | |
| local x, y, z = unit:GetGridCoords() | |
| local grid_voxel = point_pack(x, y, z) | |
| local dest = GetPackedPosAndStance(unit) | |
| local target = AISelectHealTarget(context, dest, grid_voxel, self) | |
| if target then | |
| action_state.args = { | |
| target = target, | |
| goto_pos = SnapToVoxel(unit:GetPos()), | |
| } | |
| local cost = CombatActions.Bandage:GetAPCost(unit, action_state.args) | |
| action_state.has_ap = (cost >= 0) and unit:HasAP(cost) | |
| end | |
| end | |
| --------------------------------------- | |
| DefineClass.AIStimRule = { | |
| __parents = { "PropertyObject" }, | |
| properties = { | |
| { id = "Keyword", editor = "dropdownlist", default = "", items = AIKeywordsCombo }, | |
| { id = "Weight", editor = "number", default = 0 }, | |
| }, | |
| } | |
| DefineClass.AIActionStim = { | |
| __parents = { "AISignatureAction", }, | |
| properties = { | |
| { id = "TargetRules", editor = "nested_list", default = false, base_class = "AIStimRule", inclusive = true }, | |
| { id = "CanTargetSelf", editor = "bool", default = false }, | |
| }, | |
| voice_response = "", | |
| } | |
| function AIActionStim:IsAvailable(context, action_state) | |
| return action_state.has_ap and IsValid(action_state.target) | |
| end | |
| function AIActionStim:Execute(context, action_state) | |
| assert(action_state.has_ap and IsValid(action_state.target)) | |
| -- just fake it | |
| context.unit:ConsumeAP(CombatStim.APCost * const.Scale.AP) | |
| for _, effect in ipairs(CombatStim.Effects) do | |
| effect:__exec(action_state.target) | |
| end | |
| end | |
| function AIActionStim:PrecalcAction(context, action_state) | |
| local cost = CombatStim.APCost * const.Scale.AP | |
| local unit = context.unit | |
| action_state.has_ap = unit:HasAP(cost) | |
| if not action_state.has_ap then return end | |
| local best_score, best_target = 0, false | |
| if self.CanTargetSelf then | |
| best_score = AIEvalStimTarget(unit, unit, self.TargetRules) | |
| best_target = (best_score > 0) and unit | |
| end | |
| for _, ally in ipairs(context.allies) do | |
| if IsMeleeRangeTarget(unit, nil, nil, ally) then | |
| local score = AIEvalStimTarget(unit, ally, self.TargetRules) | |
| if score > best_score then | |
| best_score, best_target = score, ally | |
| elseif score == best_score and IsValid(best_target) then | |
| if unit:GetDist(ally) < unit:GetDist(best_target) then | |
| best_target = ally | |
| end | |
| end | |
| end | |
| end | |
| action_state.target = best_target | |
| end | |
| --------------------------------------- | |
| DefineClass.AIActionCharge = { | |
| __parents = { "AISignatureAction", }, | |
| properties = { | |
| { id = "DestPreference", editor = "dropdownlist", items = {"score", "nearest"}, default = "score", help = "Specifies the way a charge destination and target are selected when the destination picked by the general AI logic isn't a valid Charge destination.\n'score' picks the destination with highest evaluation, while 'nearest' opts for the destination nearest to the general destination already picked" }, | |
| }, | |
| movement = true, | |
| action_id = "Charge", | |
| } | |
| function AIActionCharge:IsAvailable(context, action_state) | |
| return not not action_state.args | |
| end | |
| function AIActionCharge:GetActionId(unit) | |
| return HasPerk(unit, "GloryHog") and "GloryHog" or "Charge" | |
| end | |
| function AIActionCharge:Execute(context, action_state) | |
| assert(action_state.has_ap) | |
| if #CombatActions_Waiting > 0 or (next(CombatActions_RunningState) ~= nil) then | |
| -- do not queue together with other move actions to avoid shooting actions going back and forth between units | |
| return "restart" | |
| end | |
| local action_id = self:GetActionId(context.unit) | |
| AIPlayCombatAction(action_id, context.unit, nil, action_state.args) | |
| end | |
| function AIActionCharge:PrecalcAction(context, action_state) | |
| local unit = context.unit | |
| local action_id = self:GetActionId(unit) | |
| local action = CombatActions[action_id] | |
| -- check action state | |
| local units = {unit} | |
| local state = action:GetUIState(units) | |
| local cost = action:GetAPCost(unit) | |
| if state ~= "enabled" or (cost > 0 and not unit:HasAP(cost)) then return end | |
| -- check if we have valid targets and the resulting positions | |
| local targets = action:GetTargets(units) | |
| local move_ap = action:ResolveValue("move_ap") * const.Scale.AP | |
| local args, score, dist | |
| local pref = context.ai_destination and self.DestPreference or "score" | |
| for _, target in ipairs(targets) do | |
| local atk_pos = GetChargeAttackPosition(unit, target, move_ap, action_id) | |
| local atk_dest = stance_pos_pack(atk_pos, StancesList.Standing) | |
| local atk_dist = context.ai_destination and stance_pos_dist(context.ai_destination, atk_dest) | |
| -- prefer selected dest if possible, select the dest with highest overall score otherwise? | |
| if atk_dist and atk_dist == 0 then | |
| args = { target = target, goto_pos = atk_pos } | |
| break | |
| end | |
| if pref == "score" then | |
| local dest_score = context.dest_scores[atk_dest] or 0 | |
| if not args or (dest_score > score) then | |
| args = {target = target, goto_pos = atk_pos} | |
| score = dest_score | |
| end | |
| elseif pref == "nearest" then | |
| if not args or atk_dist < dist then | |
| args = {target = target, goto_pos = atk_pos} | |
| dist = atk_dist | |
| end | |
| else | |
| assert(false, string.format("unknown dest preference for AI Charge (%s), aborting", tostring(pref))) | |
| break | |
| end | |
| end | |
| if not args then return end | |
| args.goto_ap = CombatActions.Move:GetAPCost(unit, {goto_pos = args.goto_pos}) | |
| action_state.args = args | |
| end | |
| --------------------------------------- | |
| DefineClass.AIActionHyenaCharge = { | |
| __parents = { "AISignatureAction", }, | |
| properties = { | |
| { id = "DestPreference", editor = "dropdownlist", items = {"score", "nearest"}, default = "score", help = "Specifies the way a charge destination and target are selected when the destination picked by the general AI logic isn't a valid Charge destination.\n'score' picks the destination with highest evaluation, while 'nearest' opts for the destination nearest to the general destination already picked" } | |
| }, | |
| movement = true, | |
| action_id = "HyenaCharge", | |
| } | |
| function AIActionHyenaCharge:IsAvailable(context, action_state) | |
| return not not action_state.args | |
| end | |
| function AIActionHyenaCharge:Execute(context, action_state) | |
| assert(action_state.args) | |
| if #CombatActions_Waiting > 0 or (next(CombatActions_RunningState) ~= nil) then | |
| -- do not queue together with other move actions to avoid shooting actions going back and forth between units | |
| return "restart" | |
| end | |
| AIPlayCombatAction(self.action_id, context.unit, nil, action_state.args) | |
| end | |
| function AIActionHyenaCharge:PrecalcAction(context, action_state) | |
| local unit = context.unit | |
| local action = CombatActions[self.action_id] | |
| -- check action state | |
| local units = {unit} | |
| local state = action:GetUIState(units) | |
| local cost = action:GetAPCost(unit) | |
| if state ~= "enabled" or (cost > 0 and not unit:HasAP(cost)) then return end | |
| -- check if we have valid targets and the resulting positions | |
| local targets = action:GetTargets(units) | |
| local move_ap = action:ResolveValue("move_ap") * const.Scale.AP | |
| local args, score, dist | |
| local pref = context.ai_destination and self.DestPreference or "score" | |
| for _, target in ipairs(targets) do | |
| local atk_pos = GetHyenaChargeAttackPosition(unit, target, move_ap, false, self.action_id) | |
| local atk_dest = stance_pos_pack(atk_pos, 0) | |
| local atk_dist = context.ai_destination and stance_pos_dist(context.ai_destination, atk_dest) | |
| -- prefer selected dest if possible, select the dest with highest overall score otherwise? | |
| if atk_dist and atk_dist == 0 then | |
| args = { target = target } | |
| break | |
| end | |
| if pref == "score" then | |
| local dest_score = context.dest_scores[atk_dest] or 0 | |
| if not args or (dest_score > score) then | |
| args = {target = target } | |
| score = dest_score | |
| end | |
| elseif pref == "nearest" then | |
| if not args or atk_dist < dist then | |
| args = {target = target } | |
| dist = atk_dist | |
| end | |
| else | |
| assert(false, string.format("unknown dest preference for AI Charge (%s), aborting", tostring(pref))) | |
| break | |
| end | |
| end | |
| action_state.args = args | |
| end | |
| --------------------------------------- | |
| DefineClass.AIActionMobileShot = { | |
| __parents = { "AISignatureAction", }, | |
| properties = { | |
| { id = "action_id", name = "Action", editor = "dropdownlist", items = {"MobileShot", "RunAndGun"}, default = "MobileShot" }, | |
| }, | |
| movement = true, | |
| default_notification_texts = { | |
| MobileShot = T(222119395990, "Mobile Shot"), | |
| RunAndGun = T(439839298337, "Run and Gun"), | |
| }, | |
| voice_response = "AIMobile", -- both attacks use the same VR | |
| } | |
| function AIActionMobileShot:GetDefaultPropertyValue(prop, prop_meta) | |
| if prop == "NotificationText" then | |
| return self.default_notification_texts[self.action_id] or prop_meta.default | |
| end | |
| return AISignatureAction.GetDefaultPropertyValue(self, prop, prop_meta) | |
| end | |
| function AIActionMobileShot:SetProperty(property, value) | |
| if property == "action_id" then | |
| local meta = self:GetPropertyMetadata("NotificationText") | |
| local cur_default_text = self.default_notification_texts[self.action_id] or meta.default | |
| local new_default_text = self.default_notification_texts[value] or meta.default | |
| if self.NotificationText == cur_default_text then | |
| self:SetProperty("NotificationText", new_default_text) | |
| end | |
| end | |
| return AISignatureAction.SetProperty(self, property, value) | |
| end | |
| function AIActionMobileShot:GetEditorView() | |
| return string.format("Mobile Attack (%s)", self.action_id) | |
| end | |
| function AIActionMobileShot:IsAvailable(context, action_state) | |
| return action_state.has_ap | |
| end | |
| function AIActionMobileShot:Execute(context, action_state) | |
| assert(action_state.has_ap) | |
| if #CombatActions_Waiting > 0 or (next(CombatActions_RunningState) ~= nil) then | |
| -- do not queue together with other move actions to avoid shooting actions going back and forth between units | |
| return "restart" | |
| end | |
| AIPlayCombatAction(self.action_id, context.unit, nil, action_state.args) | |
| end | |
| function AIActionMobileShot:PrecalcAction(context, action_state) | |
| local unit = context.unit | |
| local action = CombatActions[self.action_id] | |
| -- only available to reach the already chosen dest | |
| if not context.ai_destination then return end | |
| -- check action state | |
| local state = action:GetUIState({unit}) | |
| if state ~= "enabled" then return end | |
| -- check if the action would do something | |
| local x, y, z = stance_pos_unpack(context.ai_destination) | |
| local target_pos = point(x, y, z) | |
| local shot_voxels, shot_targets, shot_ch, canceling_reason = CalcMobileShotAttacks(unit, action, target_pos) | |
| shot_voxels = shot_voxels or empty_table | |
| shot_targets = shot_targets or empty_table | |
| if shot_voxels[1] and not canceling_reason[1] and IsValidTarget(shot_targets[1]) then | |
| action_state.args = { | |
| goto_pos = target_pos, | |
| } | |
| local cost = action:GetAPCost(unit, action_state.args) | |
| action_state.has_ap = (cost >= 0) and unit:HasAP(cost) | |
| end | |
| end | |
| --------------------------------------- | |
| DefineClass.AIActionPinDown = { | |
| __parents = { "AISignatureAction", }, | |
| voice_response = "AIPinDown", | |
| } | |
| function AIActionPinDown:PrecalcAction(context, action_state) | |
| if IsKindOf(context.weapon, "Firearm") then | |
| local args, has_ap = AIGetAttackArgs(context, CombatActions.PinDown, nil, "None") | |
| action_state.args = args | |
| action_state.has_ap = has_ap | |
| end | |
| end | |
| function AIActionPinDown:IsAvailable(context, action_state) | |
| if not action_state.has_ap then | |
| return false | |
| end | |
| local target = action_state.args.target | |
| -- filter targets that are already pinned down | |
| for attacker, descr in pairs(g_Pindown) do | |
| if descr.target == target then | |
| return false | |
| end | |
| end | |
| return IsValidTarget(target) and context.unit:HasPindownLine(target, action_state.args.target_spot_group or "Torso") | |
| end | |
| function AIActionPinDown:Execute(context, action_state) | |
| assert(action_state.has_ap) | |
| local target = action_state.args.target | |
| AIPlayCombatAction("PinDown", context.unit, nil, action_state.args) | |
| return "done" | |
| end | |
| --------------------------------------- | |
| DefineClass.AIActionShootLandmine = { | |
| __parents = { "AIActionBaseZoneAttack", }, | |
| hidden = false, | |
| } | |
| function AIActionShootLandmine:PrecalcAction(context, action_state) | |
| local zones = AIPrecalcLandmineZones(context) | |
| local zone, score = self:EvalZones(context, zones) | |
| if zone then | |
| local args, has_ap = AIGetAttackArgs(context, context.default_attack, nil, "None", zone.target) | |
| if has_ap then | |
| action_state.score = score | |
| action_state.args = args | |
| action_state.has_ap = has_ap | |
| end | |
| end | |
| end | |
| function AIActionShootLandmine:IsAvailable(context, action_state) | |
| return action_state.has_ap | |
| end | |
| function AIActionShootLandmine:Execute(context, action_state) | |
| assert(action_state.has_ap) | |
| AIPlayCombatAction(context.default_attack.id, context.unit, nil, action_state.args) | |
| end | |
| --------------------------------------- | |
| DefineClass.AIActionSingleTargetShot = { | |
| __parents = { "AISignatureAction", }, | |
| properties = { | |
| { id = "action_id", editor = "dropdownlist", items = {"SingleShot", "BurstFire", "AutoFire", "Buckshot", "DoubleBarrel", "KnifeThrow"}, default = "SingleShot" }, | |
| { id = "Aiming", | |
| editor = "choice", default = "None", items = function (self) return { "None", "Remaining AP", "Maximum"} end, }, | |
| { id = "AttackTargeting", help = "if any parts are set the unit will pick one of them randomly for each of its basic attacks; otherwise it will always use the default (torso) attacks", | |
| editor = "set", default = false, items = function (self) return table.keys2(Presets.TargetBodyPart.Default) end, }, | |
| }, | |
| default_notification_texts = { | |
| AutoFire = T(730263043731, "Full Auto"), | |
| DoubleBarrel = T(937676786920, "Double Barrel Shot"), | |
| }, | |
| } | |
| function AIActionSingleTargetShot:GetDefaultPropertyValue(prop, prop_meta) | |
| if prop == "NotificationText" then | |
| return self.default_notification_texts[self.action_id] or prop_meta.default | |
| end | |
| return AISignatureAction.GetDefaultPropertyValue(self, prop, prop_meta) | |
| end | |
| function AIActionSingleTargetShot:SetProperty(property, value) | |
| if property == "action_id" then | |
| local meta = self:GetPropertyMetadata("NotificationText") | |
| local cur_default_text = self.default_notification_texts[self.action_id] or meta.default | |
| local new_default_text = self.default_notification_texts[value] or meta.default | |
| if self.NotificationText == cur_default_text then | |
| self:SetProperty("NotificationText", new_default_text) | |
| end | |
| end | |
| return AISignatureAction.SetProperty(self, property, value) | |
| end | |
| function AIActionSingleTargetShot:GetEditorView() | |
| return string.format("Single Target Attack (%s)", self.action_id) | |
| end | |
| function AIActionSingleTargetShot:PrecalcAction(context, action_state) | |
| if IsKindOf(context.weapon, "Firearm") and not IsKindOf(context.weapon, "HeavyWeapon") then | |
| local action = CombatActions[self.action_id] | |
| local unit = context.unit | |
| local upos = GetPackedPosAndStance(unit) | |
| local target = context.dest_target[upos] | |
| local body_parts = AIGetAttackTargetingOptions(unit, context, target, action, self.AttackTargeting) | |
| local targeting | |
| if body_parts and #body_parts > 0 then | |
| local pick = table.weighted_rand(body_parts, "chance", InteractionRand(1000000, "Combat")) | |
| targeting = pick and pick.id or nil | |
| end | |
| assert(action) | |
| local args, has_ap = AIGetAttackArgs(context, action, targeting or "Torso", self.Aiming) | |
| action_state.args = args | |
| action_state.has_ap = has_ap | |
| if has_ap and IsValidTarget(args.target) then | |
| local results = action:GetActionResults(context.unit, args) | |
| action_state.has_ammo = not not results.fired | |
| action_state.can_hit = results.chance_to_hit > 0 | |
| end | |
| end | |
| end | |
| function AIActionSingleTargetShot:IsAvailable(context, action_state) | |
| if not action_state.has_ap or not action_state.has_ammo or not action_state.can_hit then | |
| return false | |
| end | |
| return IsValidTarget(action_state.args.target) | |
| end | |
| function AIActionSingleTargetShot:Execute(context, action_state) | |
| assert(action_state.has_ap) | |
| AIPlayCombatAction(self.action_id, context.unit, nil, action_state.args) | |
| end | |
| function AIActionSingleTargetShot:GetVoiceResponse() | |
| local action_id = self.action_id | |
| if action_id and (action_id == "DoubleBarrel" or action_id == "Buckshot" or action_id == "BuckshotBurst") then | |
| return "AIDoubleBarrel" | |
| end | |
| return self.voice_response | |
| end | |
| --------------------------------------- | |
| DefineClass.AIAttackSingleTarget = { | |
| __parents = { "AIActionSingleTargetShot", }, | |
| } | |
| --------------------------------------- | |
| DefineClass.AIActionCancelShot = { | |
| __parents = { "AIActionSingleTargetShot", }, | |
| properties = { | |
| { id = "action_id", editor = "dropdownlist", items = {"CancelShot"}, default = "CancelShot", no_edit = true }, | |
| }, | |
| } | |
| function AIActionCancelShot:IsAvailable(context, action_state) | |
| if not action_state.has_ap then | |
| return false | |
| end | |
| local target = action_state.args.target | |
| return IsValidTarget(target) and (target:HasPreparedAttack() or target:CanActivatePerk("MeleeTraining")) | |
| end | |
| --------------------------------------- | |
| DefineClass.AIActionMGSetup = { | |
| __parents = { "AIActionBaseConeAttack", }, | |
| properties = { | |
| { id = "cur_zone_mod", name = "Current Zone Modifier", editor = "number", scale = "%", default = 100, help = "Modifier applied when scoring the already set zone" }, | |
| }, | |
| action_id = "MGSetup", | |
| hidden = false, | |
| } | |
| function AIActionMGSetup:PrecalcAction(context, action_state) | |
| if not context.unit:HasStatusEffect("StationedMachineGun") then | |
| -- setup | |
| action_state.stance = "Prone" -- MGSetup will change the stance so we need to check LOS in that stance | |
| AIActionBaseConeAttack.PrecalcAction(self, context, action_state) | |
| else | |
| local curr_target_pt = g_Overwatch[context.unit] and g_Overwatch[context.unit].target_pos | |
| local zones = AIPrecalcConeTargetZones(context, self.action_id, curr_target_pt) | |
| local cur_zone = zones[#zones] | |
| if not cur_zone then | |
| return | |
| end | |
| cur_zone.score_mod = self.cur_zone_mod | |
| local zone, best_score = self:EvalZones(context, zones) | |
| -- check best zone: | |
| if not zone then -- no suitable zone, pack up | |
| action_state.action_id = "MGPack" | |
| elseif zone ~= cur_zone then -- another best zone, rotate | |
| action_state.action_id = "MGRotate" | |
| action_state.target_pos = zone.target_pos | |
| end | |
| if action_state.action_id then | |
| action_state.score = best_score | |
| action_state.target_pos = zone and zone.target_pos | |
| local caction = CombatActions[action_state.action_id] | |
| if not caction then return end | |
| local args, has_ap = AIGetAttackArgs(context, caction, nil, "None") | |
| action_state.has_ap = has_ap | |
| if has_ap then | |
| g_LastSelectedZone = zone | |
| end | |
| end | |
| end | |
| end | |
| function AIActionMGSetup:IsAvailable(context, action_state) | |
| return action_state.has_ap and (action_state.args and action_state.args.target_pos or action_state.action_id == "MGPack") | |
| end | |
| function AIActionMGSetup:Execute(context, action_state) | |
| assert(action_state.has_ap) | |
| local args = {} | |
| if action_state.action_id ~= "MGPack" then | |
| assert(action_state.args) | |
| args.target = action_state.args.target_pos | |
| end | |
| AIPlayCombatAction(action_state.action_id or self.action_id, context.unit, nil, args) | |
| if action_state.action_id == "MGPack" then | |
| return "restart" | |
| end | |
| end | |
| --------------------------------------- | |
| DefineClass.AIActionMGBurstFire = { | |
| __parents = { "AIActionSingleTargetShot", }, | |
| properties = { | |
| { id = "action_id", editor = "dropdownlist", items = { "MGBurstFire" }, default = "MGBurstFire", no_edit = true }, | |
| }, | |
| --action_id = "MGBurstFire", | |
| } | |
| function AIActionMGBurstFire:PrecalcAction(context, action_state) | |
| if context.unit:HasStatusEffect("StationedMachineGun") then | |
| return AIActionSingleTargetShot.PrecalcAction(self, context, action_state) | |
| end | |
| end | |
| --------------------------------------- | |
| DefineClass.AIActionHeavyWeaponAttack = { | |
| __parents = { "AIActionBaseZoneAttack", }, | |
| properties = { | |
| { id = "MinDist", editor = "number", scale = "m", default = 2*guim, min = 0 }, | |
| { id = "MaxDist", editor = "number", scale = "m", default = 100*guim, min = 0 }, | |
| { id = "SmokeGrenade", editor = "bool", default = false, }, | |
| { id = "action_id", editor = "dropdownlist", items = { "GrenadeLauncherFire", "RocketLauncherFire", "Bombard" }, default = "GrenadeLauncherFire" }, | |
| { id = "LimitRange", editor = "bool", default = false }, | |
| { id = "MaxTargetRange", editor = "number", min = 1, max = 100, default = 20, slider = true, no_edit = function(self) return not self.LimitRange end, }, | |
| }, | |
| hidden = false, | |
| --voice_response = "AIThrowGrenade", | |
| } | |
| function AIActionHeavyWeaponAttack:GetEditorView() | |
| return string.format("Heavy Attack (%s)", self.action_id) | |
| end | |
| function AIActionHeavyWeaponAttack:PrecalcAction(context, action_state) | |
| local caction = CombatActions[self.action_id] | |
| local cost = caction and caction:GetAPCost(context.unit) or -1 | |
| local weapon = caction and caction:GetAttackWeapons(context.unit) | |
| if not weapon or cost < 0 or not context.unit:HasAP(cost) or not weapon.ammo or weapon.ammo.Amount < 1 then | |
| return | |
| end | |
| if self.SmokeGrenade ~= (weapon.ammo.aoeType == "smoke") then | |
| return | |
| end | |
| if self.action_id == "Bombard" and context.unit.indoors then | |
| return | |
| end | |
| local max_range = Min(self.MaxDist, caction:GetMaxAimRange(context.unit, weapon) * const.SlabSizeX) | |
| local blast_radius = weapon.ammo.AreaOfEffect * const.SlabSizeX | |
| local zones = AIPrecalcGrenadeZones(context, self.action_id, self.MinDist, max_range, blast_radius, weapon.ammo.aoeType) | |
| if self.LimitRange then | |
| local attacker = context.unit | |
| local range = self.MaxTargetRange * const.SlabSizeX | |
| zones = table.ifilter(zones, function(idx, zone) return attacker:GetDist(zone.target_pos) <= range end) | |
| end | |
| local zone, score = self:EvalZones(context, zones) | |
| if zone then | |
| action_state.action_id = self.action_id | |
| action_state.target_pos = zone.target_pos | |
| action_state.score = score | |
| end | |
| end | |
| function AIActionHeavyWeaponAttack:IsAvailable(context, action_state) | |
| return not not action_state.action_id | |
| end | |
| function AIActionHeavyWeaponAttack:Execute(context, action_state) | |
| assert(action_state.action_id and action_state.target_pos) | |
| AIPlayCombatAction(action_state.action_id, context.unit, nil, {target = action_state.target_pos}) | |
| end | |
| --------------------------------------- |