|
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 |
|
|
|
|
|
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, |
|
} |
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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 |
|
|
|
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)) |
|
|
|
|
|
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 |
|
|
|
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] |
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
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 |
|
|
|
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] |
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
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", |
|
} |
|
|
|
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 |
|
|
|
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] |
|
|
|
|
|
if not context.ai_destination then return end |
|
|
|
|
|
local state = action:GetUIState({unit}) |
|
if state ~= "enabled" then return end |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
action_state.stance = "Prone" |
|
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) |
|
|
|
|
|
if not zone then |
|
action_state.action_id = "MGPack" |
|
elseif zone ~= cur_zone then |
|
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 }, |
|
}, |
|
|
|
} |
|
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, |
|
|
|
} |
|
|
|
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 |
|
|