myspace / Lua /Tactical /UnitActions.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
94 kB
--List of grenades that will result in different VR played when thrown
SpecialGrenades = {
[1] = "ConcussiveGrenade",
[2] = "SmokeGrenade",
[3] = "TearGasGrenade",
[4] = "ToxicGasGrenade",
[5] = "Molotov",
[6] = "FlareStick"
}
function Unit:OnAttack(action, target, results, attack_args, holdXpLog)
if type(action) == "string" then
action = CombatActions[action]
end
if IsKindOf(results.weapon, "FirearmBase") then
results.weapon.num_safe_attacks = Max(0, results.weapon.num_safe_attacks - 1)
end
if IsKindOf(results.weapon, "TransmutedItemProperties") and results.weapon.RevertCondition=="attacks" then
results.weapon.RevertConditionCounter = results.weapon.RevertConditionCounter-1
if results.weapon.RevertConditionCounter== 0 then
local slot_name = self:GetItemSlot(results.weapon)
local new, prev = results.weapon:MakeTransmutation("revert")
self:RemoveItem(slot_name, results.weapon)
self:AddItem(slot_name, new)
DoneObject(prev)
self:UpdateOutfit()
end
end
self.last_attack_pos = self:GetPos()
-- Add Exposed on any Melee Attack hit
if action.ActionType == "Melee Attack" and IsKindOf(target, "Unit") and not results.miss then
target:AddStatusEffect("Exposed")
end
if IsValidTarget(target) then
target.attacked_this_turn = target.attacked_this_turn or {}
table.insert(target.attacked_this_turn, self)
end
local kill = #(results.killed_units or empty_table) > 0
if kill then
Msg("OnKill", self, results.killed_units)
self:CallReactions("OnUnitKill", results.killed_units)
end
-- singatures can't recharge themselves
local originAction = CombatActions[attack_args.origin_action_id]
if kill and (action.group ~= "SignatureAbilities" and (not originAction or originAction.group ~= "SignatureAbilities")) then
self:UpdateSignatureRecharges("kill")
end
-- signature actions never recharge themselves, this has to be after UpdateSignatureRecharges
if action.group == "SignatureAbilities" then
local recharge_on_kill = action:ResolveValue("recharge_on_kill") or 0
local rechargeTime = action:ResolveValue("rechargeTime") or const.Combat.SignatureAbilityRechargeTime
if rechargeTime > 0 then
self:AddSignatureRechargeTime(action.id, rechargeTime, recharge_on_kill > 0)
end
end
Msg("OnAttack", self, action, target, results, attack_args)
self:CallReactions("OnUnitAttack", self, action, target, results, attack_args)
if IsKindOf(target, "Unit") then
target:CallReactions("OnUnitAttack", self, action, target, results, attack_args)
end
local hitUnitFromAttack = false
for _, hit in ipairs(results.hit_objs) do
if IsKindOf(hit.obj, "Unit") then
if hit.damage and hit.damage > 0 then
hitUnitFromAttack = true
break
end
end
end
if results.miss then
Msg("AttackMiss", self, target)
if IsKindOf(target, "Unit") and not IsMerc(target) then
target:AddStatusEffect("AITauntCounter")
if not hitUnitFromAttack then
local effect = target:GetStatusEffect("AITauntCounter")
if effect and effect.stacks >= 3 then
PlayVoiceResponse(target, "AITaunt")
end
end
end
end
-- Reward xp to Mercs on kill
if kill and IsMerc(self) then
if not g_AccumulatedTeamXP then g_AccumulatedTeamXP = {} end
for i, unit in ipairs(results.killed_units) do
if self:IsOnEnemySide(unit) then
RewardTeamExperience(unit, GetCampaignPlayerTeam())
end
end
if not holdXpLog then LogAccumulatedTeamXP("debug") end
end
if next(results.killed_units) then
local killCam = not not ActionCameraPlaying
local waitTime = killCam and const.Combat.UnitDeathKillcamWait or const.Combat.UnitDeathWait
Sleep(waitTime)
if killCam then
Msg("ActionCameraWaitSignalEnd")
end
end
end
function Unit:GetLastAttack()
return self.last_attack_session_id and g_Units[self.last_attack_session_id]
end
local function ResetLastAttack(unit)
if unit:IsAmbientUnit() then return end
unit.last_attack_session_id = false
local session_id = unit.session_id
if session_id then
for _, u in ipairs(g_Units) do
if u.last_attack_session_id == session_id then
u.last_attack_session_id = false
end
end
end
end
function IsBasicAttack(action, attack_args)
local basicAttack
if attack_args.origin_action_id then
basicAttack = CombatActions[attack_args.origin_action_id].basicAttack
else
basicAttack = action.basicAttack
end
return basicAttack
end
OnMsg.UnitMovementDone = ResetLastAttack
OnMsg.UnitDied = ResetLastAttack
MapVar("g_CurrentAttackActions", {}) -- stack of all started attack actions
MapVar("g_Interrupt", false)
function Unit:WaitAttack()
self:UninterruptableGoto(self:GetVisualPos())
self.waiting_attack = true
end
function Unit:FirearmAttack(action_id, cost_ap, args, applied_status) -- SingleShot/DualShot
if true then -- net debug code
local effects = {}
for i, effect in ipairs(self.StatusEffects) do
effects[i] = effect.class
end
effects = table.concat(effects, ",")
local target_effects = "-"
if IsKindOf(args.target, "Unit") then
target_effects = {}
for i, effect in ipairs(args.target.StatusEffects) do
target_effects[i] = effect.class
end
target_effects = table.concat(target_effects, ",")
end
NetUpdateHash("Unit:FirearmAttack", action_id, cost_ap, self, effects, args.target, target_effects)
end -- end net debug code
local target = args.target
self:CallReactions("OnFirearmAttackStart", self, target, CombatActions[action_id], args)
if IsKindOf(target, "Unit") then
target:CallReactions("OnFirearmAttackStart", self, target, CombatActions[action_id], args)
end
while not args.opportunity_attack and IsKindOf(target, "Unit") and not target:IsIdleOrRunningBehavior() do
WaitMsg("Idle", 50)
end
if args.replace_action then
action_id = args.replace_action
end
if IsPoint(target) or IsValidTarget(target) then
local action = CombatActions[action_id]
if action.StealthAttack then
args.stealth_kill_roll = 1 + self:Random(100)
end
args.prediction = false
local units_waiting = {}
self:PushDestructor(function()
for _, unit in ipairs(units_waiting) do
unit.waiting_attack = false
end
end)
if not g_Combat and IsKindOf(target, "Unit") then
units_waiting[1] = target
PropagateAwareness(units_waiting)
for _, unit in ipairs(units_waiting) do
if unit:IsInterruptable() then
unit.waiting_attack = true
unit:InterruptCommand("WaitAttack")
end
end
repeat
local waiting = false
for _, unit in ipairs(units_waiting) do
waiting = waiting or (unit.command == "WaitAttack" and not unit.waiting_attack)
end
if waiting then
Sleep(10)
end
until not waiting
end
local results, attack_args = action:GetActionResults(self, args)
self:ExecFirearmAttacks(action, cost_ap, attack_args, results)
self:PopAndCallDestructor()
else
self:GainAP(cost_ap)
CombatActionInterruped(self)
end
end
function Unit:ExecFirearmAttacks(action, cost_ap, attack_args, results)
NetUpdateHash("ExecFirearmAttacks", action, cost_ap, not not g_Combat)
local lof_idx = table.find(attack_args.lof, "target_spot_group", attack_args.target_spot_group or "Torso")
local lof_data = attack_args.lof[lof_idx or 1]
local target = attack_args.target
local target_unit = IsKindOf(target, "Unit") and IsValidTarget(target) and target
local interrupt = attack_args.interrupt
if interrupt then
if ActionCameraPlaying then
RemoveActionCamera(true)
WaitMsg("ActionCameraRemoved", 5000)
end
self:PushDestructor(function()
Msg("InterruptAttackEnd")
end)
Msg("InterruptAttackStart", self, target_unit, action)
end
NetUpdateHash("ExecFirearmAttacks_After_Interrupt_Cam_Wait")
results.attack_from_stealth = not not self:HasStatusEffect("Hidden")
for _, attack in ipairs(results.attacks or {results}) do
if attack.fired then
self:AttackReveal(action, attack_args, results)
break
end
end
local can_provoke_opportunity_attacks = not action or (action.id ~= "CancelShot" and action.id ~= "CancelShotCone")
if can_provoke_opportunity_attacks then
self:ProvokeOpportunityAttacks(action, "attack interrupt")
end
self:PrepareToAttack(attack_args, results)
if can_provoke_opportunity_attacks then
self:ProvokeOpportunityAttacks(action, "attack interrupt")
end
local was_interruptable = self.interruptable
if not was_interruptable then
self:EndInterruptableMovement()
end
NetUpdateHash("ExecFirearmAttacks_Start_Action_Cam")
-- camera effects
if attack_args.opportunity_attack_type ~= "Retaliation" then
local cinematicKill = false
local dontPlayForLocalPlayer = false
if g_Combat and IsEnemyKill(self, results) then
g_Combat:CheckPendingEnd(results.killed_units)
local isKillCinematic
isKillCinematic, dontPlayForLocalPlayer = IsEnemyKillCinematic(self, results, attack_args)
if isKillCinematic then
cameraTac.SetForceMaxZoom(false)
SetAutoRemoveActionCamera(self, results.killed_units[1], nil, nil, nil, nil, nil, dontPlayForLocalPlayer)
cinematicKill = true
end
elseif interrupt then -- the attack is from enemy pindown or overwatch
--[[if self.team.side == "enemy" then
SetAutoRemoveActionCamera(target_unit, self, 1000, true) -- todo: should this use the anim duration?
else
SetAutoRemoveActionCamera(self, target_unit, 1000, true)
end--]]
end
if not cinematicKill and IsKindOf(target, "Unit") then
local cinematicAttack, interpolation = IsCinematicAttack(self, results, attack_args, action)
if cinematicAttack then
local playerUnit = (IsKindOf(target, "Unit") and target:IsLocalPlayerTeam() and target) or (self:IsLocalPlayerTeam() and self)
local enemyUnit = playerUnit and (playerUnit == target and self or target)
if playerUnit and enemyUnit then
SetAutoRemoveActionCamera(playerUnit, enemyUnit, false, false, false, interpolation and default_interpolation_time, nil, dontPlayForLocalPlayer)
end
end
end
end
NetUpdateHash("ExecFirearmAttacks_After_Action_Cam")
-- animspeed modifier & cmd destructor
local asm = self:GetAnimSpeedModifier()
local anim_speed_mod = attack_args.anim_speed_mod or 1000
self:SetAnimSpeedModifier(anim_speed_mod)
self:PushDestructor(function(self)
self:SetAnimSpeedModifier(asm)
if IsValid(target) and target:HasMember("session_id") then
self.last_attack_session_id = target.session_id
else
self.last_attack_session_id = false
end
local cooldown = action:ResolveValue("cooldown")
if cooldown then
self:SetEffectExpirationTurn(action.id, "cooldown", g_Combat.current_turn + cooldown)
end
if IsValid(target) then
ObjModified(target)
end
table.remove(g_CurrentAttackActions) -- pop the pushed attack action
end)
local ap = (cost_ap and cost_ap > 0) and cost_ap or action:GetAPCost(self, attack_args)
table.insert(g_CurrentAttackActions, { action = action, cost_ap = ap, attack_args = attack_args, results = results })
-- start anim, wait hit moment, apply ammo/condition results
local chance_to_hit = results.chance_to_hit
local missed = results.miss
local critical = results.crit
local chance_crit = results.crit_chance
local aim_state = self:GetStateText()
local fired = false
if results.attacks then --- multi-weapon attacks (DualShot)
local shots = results.attacks[1] and results.attacks[1].shots
self:StartFireAnim(shots and shots[1], attack_args)
for _, attack in ipairs(results.attacks) do
attack.weapon:ApplyAmmoUse(self, attack.fired, attack.jammed, attack.condition)
fired = fired or attack.fired
end
else
self:StartFireAnim(results.shots and results.shots[1], attack_args)
results.weapon:ApplyAmmoUse(self, results.fired, results.jammed, results.condition)
fired = results.fired
end
if not fired then
-- none of the weapons fired, abort
if not was_interruptable then
self:BeginInterruptableMovement()
end
Sleep(self:TimeToAnimEnd())
self:PopAndCallDestructor()
if interrupt then
self:PopAndCallDestructor()
end
NetUpdateHash("ExecFirearmAttacks_early_out")
return
end
PushUnitAlert("noise", self, results.weapon.Noise, Presets.NoiseTypes.Default.Gunshot.display_name)
local shot_threads = {}
local attacks = results.attacks or {results}
local attackArgs = results.attacks_args or {attack_args}
if results.shots and #results.shots > 8 and g_Combat and not g_Combat:ShouldEndCombat(results.killed_units) then
if (not results.killed_units or #results.killed_units == 1) then
local vr = IsMerc(self) and "Autofire" or "AIAutofire"
PlayVoiceResponse(self, vr)
end
end
local lowChanceShot
local base_weapon_damage = 0
for attackIdx, attack in ipairs(attacks) do
local attackArg = attackArgs[attackIdx]
local fx_action = attackArg.fx_action
if action.id == "BulletHell" then
BulletHellOverwriteShots(attack)
end
local shots_per_animation = Min(3, #attack.shots)
if action.id == "BurstFire" or action.id == "MGBurstFire" then
shots_per_animation = #attack.shots
end
for i, shot in ipairs(attack.shots) do
-- shot visuals
attack.weapon:FireBullet(self, shot, shot_threads, results, attackArg)
if attackArg.single_fx then
fx_action = ""
end
if i < #attack.shots then -- more shots to fire
if i % shots_per_animation == 0 then
local shotAnimDelay = attackArg.attack_anim_delay or self:TimeToAnimEnd()
self:StartFireAnim(attack.shots[i+1], attackArg, nil, shotAnimDelay) -- fire next shot
else
Sleep(self:GetAnimDuration() / shots_per_animation)
end
elseif attackIdx < #attacks then
Sleep(MulDivRound(self:GetAnimDuration() / shots_per_animation, 30, 100))
end
if IsMerc(self) and attack.target_hit then
if attack.chance_to_hit <= 20 then
lowChanceShot = true
end
end
end
attack.weapon:FireSpread(attack, attackArg) -- deal the area damage, if any
base_weapon_damage = base_weapon_damage + attack.weapon.Damage
end
-- additional damage (e.g. from DualShot perk)
for _, packet in ipairs(results.extra_packets) do
if IsValidTarget(packet.target) then
if packet.damage then
packet.target:TakeDirectDamage(packet.damage, false, "short", packet.message)
end
if packet.effects then
packet.target:ApplyDamageAndEffects(false, false, packet)
end
end
end
-- wait end moment and restore animation
local time_to_fire_end = self:TimeToAnimEnd()
if not attack_args.dont_restore_aim then
if self:CanAimIK(results.weapon) then
local restore_aim_delay = Min(300, time_to_fire_end)
Sleep(restore_aim_delay)
self:SetIK("AimIK", lof_data.lof_pos2, nil, nil, 0)
Sleep(time_to_fire_end - restore_aim_delay)
self:SetState(aim_state, const.eKeepComponentTargets)
else
Sleep(time_to_fire_end)
self:SetState(aim_state, const.eKeepComponentTargets)
end
end
-- special-case: interrupt neutral units with neutral_retaliate flag attacked by player units,
-- so they don't look ridiculous minding their own business for several more seconds until the attack resolves
if self.team.player_team and not g_Combat then
if IsValid(target_unit) and target_unit.team.neutral and target_unit.neutral_retaliate and not target_unit:IsIncapacitated() then
target_unit.neutral_retal_attacked = true
target_unit:SetBehavior()
target_unit:SetCommand("Idle")
end
local hits = #results > 0 and results or results.area_hits
for _, hit in ipairs(hits) do
local unit = IsKindOf(hit.obj, "Unit") and not hit.obj:IsIncapacitated() and hit.obj
if IsValid(unit) and unit.team.neutral and unit.neutral_retaliate then
unit.neutral_retal_attacked = true
unit:SetBehavior()
unit:SetCommand("Idle")
end
end
end
self.interruptable = false
self:PushDestructor(function()
self.interruptable = was_interruptable
end)
if attack_args.external_wait_shots then
table.iappend(attack_args.external_wait_shots, shot_threads)
else
Firearm:WaitFiredShots(shot_threads)
end
-- wait target dodge anim
while target_unit and target_unit.command == "Dodge" do
WaitMsg("Idle")
end
-- play voices
base_weapon_damage = MulDivRound(base_weapon_damage, 120, 100)
if attacks and next(attacks)then
--count shots fired per team for Voice Response
self.team.tactical_situations_vr.shotsFired = self.team.tactical_situations_vr.shotsFired and self.team.tactical_situations_vr.shotsFired + 1 or 1
self.team.tactical_situations_vr.shotsFiredBy = self.team.tactical_situations_vr.shotsFiredBy or {}
self.team.tactical_situations_vr.shotsFiredBy[self.session_id] = true
PlayVoiceResponseTacticalSituation(table.find(g_Teams, self.team), "now")
if missed then
--count missed shots per team for Voice Response
self.team.tactical_situations_vr.missedShots = self.team.tactical_situations_vr.missedShots and self.team.tactical_situations_vr.missedShots + 1 or 1
PlayVoiceResponseTacticalSituation(table.find(g_Teams, self.team), "now")
if chance_to_hit >= 70 then
if not target_unit or not target_unit:IsCivilian() then
PlayVoiceResponseMissHighChance(self)
end
elseif target_unit and chance_to_hit>=50 and base_weapon_damage>=target_unit:GetTotalHitPoints() then
if IsMerc(target_unit) then
target_unit:SetEffectValue("missed_by_kill_shot", true)
end
end
elseif not missed then
if results.stealth_kill and IsMerc(self) and results.killed_units and #results.killed_units > 0 then
elseif lowChanceShot and target_unit and not self:IsOnAllySide(target_unit) and not target_unit:IsCivilian() then
PlayVoiceResponse(self, "LowChanceShot")
end
end
end
for i, attack in ipairs(attacks) do
local holdXpLog = i ~= #attacks
self:OnAttack(action, target_unit, attack, attack_args, holdXpLog)
end
LogAttack(action, attack_args, results)
AttackReaction(action, attack_args, results, "can retaliate")
if not action or (action.id ~= "CancelShot" and action.id ~= "CancelShotCone") then
self:ProvokeOpportunityAttacks(action, "attack reaction")
end
if not was_interruptable then
self:BeginInterruptableMovement()
end
self:PopAndCallDestructor()
self:PopAndCallDestructor()
if interrupt then
self:PopAndCallDestructor()
end
end
function Unit:MGSetup(action_id, cost_ap, args)
self.interruptable = false
if self.stance ~= "Prone" then
self:DoChangeStance("Prone")
end
self:AddStatusEffect("StationedMachineGun")
self:UpdateHidden()
self:FlushCombatCache()
self:RecalcUIActions(true)
ObjModified(self)
return self:MGTarget(action_id, cost_ap, args)
end
function Unit:MGTarget(action_id, cost_ap, args)
args.permanent = true
args.num_attacks = self:GetNumMGInterruptAttacks()
self.interruptable = false
return self:OverwatchAction(action_id, cost_ap, args) -- this would change the command anyway, we can't have code below it
end
function Unit:MGPack()
self:InterruptPreparedAttack()
self:RemoveStatusEffect("StationedMachineGun")
self:UpdateHidden()
self:FlushCombatCache()
self:RecalcUIActions(true)
if HasPerk(self, "KillingWind") then
self:RemoveStatusEffect("FreeMove")
self:AddStatusEffect("FreeMove")
end
ObjModified(self)
end
function Unit:OpportunityAttack(action_id, args, status)--target, target_spot_group, action, status)
-- does basically nothing on its own but changes the name of the current command (relevant for overwatch/to-hit modifiers)
g_Interrupt = true
args.interrupt = true
PlayFX("OpportunityAttack", "start", self)
self:FirearmAttack(action_id, 0, args, status)
end
function Unit:PinDownAttack(target, action_id, target_spot_group, aim, status)
-- does basically nothing on its own but changes the name of the current command (relevant for overwatch/to-hit modifiers)
local args = { target = target, target_spot_group = target_spot_group, aim = aim, interrupt = true, opportunity_attack = true, opportunity_attack_type = "PinDown" }
self:FirearmAttack(action_id, 0, args, status)
end
function Unit:RetaliationAttack(target, target_spot_group, action)
-- does basically nothing on its own but changes the name of the current command
self:CallReactions("OnUnitRetaliation", self, target, action, target_spot_group)
if IsKindOf(target, "Unit") then
target:CallReactions("OnUnitRetaliation", self, target, action, target_spot_group)
end
self:AddStatusEffect("RetaliationCounter")
PlayFX("OpportunityAttack", "start", self)
local args = { target = target, target_spot_group = target_spot_group, interrupt = true, opportunity_attack = true, opportunity_attack_type = "Retaliation" }
if string.match(action.id, "ThrowGrenade") then -- can't Run the action as it will change the command unfortunately
return self:ThrowGrenade(action.id, 0, args)
end
return self:FirearmAttack(action.id, 0, args)
end
function Unit:OpportunityMeleeAttack(target, action)
-- does basically nothing on its own but changes the name of the current command
if self.team and self.team.control == "AI" then
PlayVoiceResponse(self, "AIMeleeOpportunist")
end
PlayFX("OpportunityAttack", "start", self)
self:MeleeAttack(action.id, 0, { target = target, opportunity_attack = true, opportunity_attack_type = "Retaliation"})
end
local tf_smooth_sleep = 100
local tf_smooth_thread = false
function SetTimeFactorSmooth(tf, time)
DeleteThread(tf_smooth_thread)
tf_smooth_thread = CreateRealTimeThread(function()
local curr_tf = GetTimeFactor()
if curr_tf == tf then
return
end
local delta = MulDivRound(tf - curr_tf, tf_smooth_sleep, time)
local cmp = curr_tf < tf
while cmp == (curr_tf + delta < tf) do
curr_tf = curr_tf + delta
SetTimeFactor(curr_tf)
Sleep(tf_smooth_sleep)
end
SetTimeFactor(tf)
end)
end
local smooth_tf_change_duration = 1500
function Unit:RunAndGun(action_id, cost_ap, args)
local action = CombatActions[action_id]
local target = args.goto_pos
local weapon = action:GetAttackWeapons(self)
if not weapon then
self:GainAP(cost_ap)
CombatActionInterruped(self)
return
end
local aim_params = action:GetAimParams(self, weapon)
local num_shots = aim_params.num_shots
if self.stance ~= "Standing" then
self:ChangeStance(action_id, 0, "Standing")
end
-- do the attack/crit rolls
args.attack_rolls = {}
args.crit_rolls = {}
args.stealth_kill_rolls = {}
for i = 1, num_shots do
args.attack_rolls[i] = 1 + self:Random(100)
args.crit_rolls[i] = 1 + self:Random(100)
if action.StealthAttack then
args.stealth_kill_rolls[i] = 1 + self:Random(100)
end
end
args.prediction = false
NetUpdateHash("RunAndGun_0", self, args)
local results = action:GetActionResults(self, args)
local action_camera = false --[[ disable action camera for now ]]
if #(results.attacks or empty_table) == 0 then
self:GainAP(cost_ap)
CombatActionInterruped(self)
return
end
local pathObj, path
self:PushDestructor(function(self)
if pathObj then
DoneObject(pathObj)
end
end)
pathObj = CombatPath:new()
if action_camera then
local tf = GetTimeFactor()
self:PushDestructor(function()
SetTimeFactorSmooth(tf, smooth_tf_change_duration)
end)
end
local base_idle = self:GetIdleBaseAnim()
local shot_threads
for i, attack in ipairs(results.attacks) do
if not self:CanUseWeapon(weapon) then -- might jam, run out of ammo, etc
goto continue
end
NetUpdateHash("RunAndGun_1", self, attack.mobile_attack_pos, attack.mobile_attack_target)
if attack.mobile_attack_pos and (not IsValidTarget(attack.mobile_attack_target) or attack.mobile_attack_target:IsIncapacitated()) then
local enemies = table.ifilter(action:GetTargets({self}), function(idx, u) return IsValidTarget(u) and not u:IsIncapacitated() end)
NetUpdateHash("RunAndGun_Branch_1", self, attack.mobile_attack_pos, #enemies)
attack.mobile_attack_target = FindTargetFromPos(action_id, self, action, enemies, point(point_unpack(attack.mobile_attack_pos)), weapon)
end
if attack.mobile_attack_pos and IsValidTarget(attack.mobile_attack_target) then
if action_camera and i == 1 then
SetTimeFactorSmooth(tf/2, smooth_tf_change_duration)
end
-- We need to build the path outside of the function so that it
-- doesn't refund us the ap cost difference.
local targetPos = point(point_unpack(attack.mobile_attack_pos))
local occupiedPos = self:GetOccupiedPos()
if self:GetDist(occupiedPos) > const.SlabSizeX / 2 and self:GetDist(targetPos) < const.SlabSizeX / 2 then
-- already at target position because of expose/aim
self:SetTargetDummy(nil, nil, base_idle, 0)
else
pathObj:RebuildPaths(self, aim_params.move_ap)
path = pathObj:GetCombatPathFromPos(targetPos)
self:CombatGoto(action_id, 0, nil, path, true, i == #results.attacks and args.toDoStance)
end
-- recheck target, as they might have died while we were moving
if not IsValidTarget(attack.mobile_attack_target) or attack.mobile_attack_target:IsIncapacitated() then
local enemies = table.ifilter(action:GetTargets({self}), function(idx, u) return IsValidTarget(u) and not u:IsIncapacitated() end)
NetUpdateHash("RunAndGun_Branch_1_1", self, attack.mobile_attack_pos, #enemies)
attack.mobile_attack_target = FindTargetFromPos(action_id, self, action, enemies, point(point_unpack(attack.mobile_attack_pos)), weapon)
if not IsValidTarget(attack.mobile_attack_target) then
goto continue
end
end
if action_camera then
if i == #results.attacks then
SetTimeFactorSmooth(tf, smooth_tf_change_duration)
end
SetActionCamera(self, attack.mobile_attack_target)
end
self:SetRandomAnim(base_idle)
local atk_action = CombatActions[attack.mobile_attack_id] or action
-- rerun simulation to account for changes happened in the meantime (broken covers, etc)
local atk_args = {
prediction = false,
target = attack.mobile_attack_target,
stance = "Standing",
can_use_covers = i == #results.attacks,
used_action_id = action_id, -- so that cth is calculated for the master/parent action instead of the actual attack action
}
NetUpdateHash("RunAndGun_2", self, atk_args.target, args.goto_pos)
local atk_results, attack_args = atk_action:GetActionResults(self, atk_args)
attack_args.origin_action_id = action_id
attack_args.keep_ui_mode = true
attack_args.unit_moved = true
attack_args.dont_restore_aim = true
if atk_action.id == "KnifeThrow" then
self:ExecKnifeThrow(atk_action, cost_ap, attack_args, atk_results)
else
shot_threads = shot_threads or {}
attack_args.external_wait_shots = shot_threads
self:ExecFirearmAttacks(atk_action, cost_ap, attack_args, atk_results)
end
end
::continue::
end
local cooldown = action:ResolveValue("cooldown")
if cooldown then
self:SetEffectExpirationTurn(action.id, "cooldown", g_Combat.current_turn + cooldown)
end
if action_camera then
RemoveActionCamera()
self:PopAndCallDestructor() -- camera
end
-- if not at target loc, goto there (there mustn't be a target when that happens)
local occupiedPos = self:GetOccupiedPos()
if self.return_pos and self.return_pos:Dist(target) < const.SlabSizeX / 2 then
self:ReturnToCover()
elseif self:GetDist(occupiedPos) > const.SlabSizeX / 2 and self:GetDist(target) < const.SlabSizeX / 2 then
self:SetTargetDummyFromPos()
else
pathObj:RebuildPaths(self, aim_params.move_ap)
path = pathObj:GetCombatPathFromPos(target)
self:CombatGoto(action_id, 0, nil, path, true)
end
if shot_threads then
Firearm:WaitFiredShots(shot_threads)
end
self:PopAndCallDestructor() -- pathObj
end
function Unit:HundredKnives(action_id, cost_ap, args)
local action = CombatActions[action_id]
local recharge_on_kill = action:ResolveValue("recharge_on_kill") or 0
self:AddSignatureRechargeTime(action_id, const.Combat.SignatureAbilityRechargeTime, recharge_on_kill > 0)
self:RunAndGun(action_id, cost_ap, args)
end
function Unit:RecklessAssault(action_id, cost_ap, args)
self:RunAndGun(action_id, cost_ap, args)
self:SetTired(self.Tiredness + 1)
end
function Unit:HeavyWeaponAttack(action_id, cost_ap, args)
local target = args.target
if not IsPoint(target) and not IsValidTarget(target) then
self:GainAP(cost_ap)
CombatActionInterruped(self)
return
end
local action = CombatActions[action_id]
self:ProvokeOpportunityAttacks(action, "attack interrupt")
local weapon = action:GetAttackWeapons(self)
args.prediction = false
local results, attack_args = action:GetActionResults(self, args)
results.attack_from_stealth = not not self:HasStatusEffect("Hidden")
if results.fired then
self:AttackReveal(action, attack_args, results)
end
self:PrepareToAttack(attack_args, results)
self:ProvokeOpportunityAttacks(action, "attack interrupt")
self:PushDestructor(function()
local ap = (cost_ap and cost_ap > 0) and cost_ap or action:GetAPCost(self, attack_args)
table.insert(g_CurrentAttackActions, { action = action, cost_ap = ap, attack_args = attack_args, results = results })
local aim_pos = results and results.trajectory and results.trajectory[2] and results.trajectory[2].pos
self:StartFireAnim(nil, attack_args, aim_pos)
local anim_end_time = GameTime() + self:TimeToAnimEnd()
local prev = weapon.Condition
weapon.Condition = results.condition
if prev ~= results.condition then
Msg("ItemChangeCondition", weapon, prev, results.condition, self)
end
weapon:ApplyAmmoUse(self, results.fired, results.jammed, results.condition)
if not results.jammed and results.trajectory then
local ordnance = results.ordnance
local trajectory = results.trajectory
local action_dir = SetLen(trajectory[2].pos - trajectory[1].pos, 4096)
local visual_obj = weapon:GetVisualObj(self)
-- temporarily change the fx actor class of the visual obj if it doesn't match (subweapons)
local actor_class = visual_obj.fx_actor_class
visual_obj.fx_actor_class = weapon:GetFxClass()
PlayFX("WeaponFire", "start", visual_obj, nil, trajectory[1].pos, action_dir)
visual_obj.fx_actor_class = actor_class
-- animate trajectory
local attaches = visual_obj:GetAttaches("OrdnanceVisual")
local projectile
if attaches then
projectile = attaches[1]
projectile:Detach()
else
projectile = PlaceObject("OrdnanceVisual", {fx_actor_class = ordnance.class})
end
if IsKindOf(weapon, "RocketLauncher") then
weapon:UpdateRocket()
PlayFX("RocketFire", "start", projectile)
end
local backfire_results = table.copy(results)
for i = #backfire_results, 1, -1 do
if not backfire_results[i].backfire then
table.remove(backfire_results, i)
end
end
for i = #results, 1, -1 do
if results[i].backfire then
table.remove(results, i)
end
end
ApplyExplosionDamage(self, nil, backfire_results, nil, "disable burn FXes")
local rpm_range = const.Combat.GrenadeMaxRPM - const.Combat.GrenadeMinRPM
local rpm = const.Combat.GrenadeMinRPM + self:Random(rpm_range)
local rotation_axis = RotateAxis(axis_x, axis_z, CalcOrientation(trajectory[2].pos, trajectory[1].pos))
if weapon.trajectory_type == "line" then
-- disable rotation and make sure the rocket is oriented towards the target point
rpm = 0
projectile:SetAxis(axis_z)
projectile:SetAngle(CalcOrientation(trajectory[1].pos, trajectory[2].pos))
end
local throw_thread = CreateGameTimeThread(AnimateThrowTrajectory, projectile, trajectory, rotation_axis, -rpm, "GrenadeDrop")
Sleep(self:TimeToAnimEnd())
if IsValidThread(throw_thread) then
local anim = self:GetAimAnim()
self:SetState(anim, const.eKeepComponentTargets)
while IsValidThread(throw_thread) do
WaitMsg("GrenadeDoneThrow", 20)
end
end
-- recheck results to handle possible changes in unit positions (exploration mode)
args.explosion_pos = results.explosion_pos or projectile:GetPos()
results, attack_args = action:GetActionResults(self, args)
-- do explosion/apply results
ApplyExplosionDamage(self, projectile, results)
LogAttack(action, attack_args, results)
PushUnitAlert("noise", projectile, ordnance.Noise, Presets.NoiseTypes.Default.Explosion.display_name)
if IsValid(projectile) then
DoneObject(projectile)
end
AttackReaction(action, attack_args, results)
self:OnAttack(action_id, nil, results, attack_args)
end
if anim_end_time - GameTime() > 0 then
Sleep(anim_end_time - GameTime())
end
local dlg = GetInGameInterfaceModeDlg()
if dlg and dlg:HasMember("dont_return_camera_on_close") then
dlg.dont_return_camera_on_close = true
end
local cooldown = action:ResolveValue("cooldown")
if cooldown then
self:SetEffectExpirationTurn(action.id, "cooldown", g_Combat.current_turn + cooldown)
end
self.last_attack_session_id = false
self:ProvokeOpportunityAttacks(action, "attack reaction")
table.remove(g_CurrentAttackActions)
end)
self:PopAndCallDestructor()
end
function Unit:FireFlare(action_id, cost_ap, args)
local target = args.target
if not IsPoint(target) and not IsValidTarget(target) then
self:GainAP(cost_ap)
CombatActionInterruped(self)
return
end
local action = CombatActions[action_id]
self:ProvokeOpportunityAttacks(action, "attack interrupt")
local weapon = action:GetAttackWeapons(self)
args.prediction = false
local results, attack_args = action:GetActionResults(self, args)
results.attack_from_stealth = not not self:HasStatusEffect("Hidden")
if results.fired then
self:AttackReveal(action, attack_args, results)
end
self:PrepareToAttack(attack_args, results)
self:ProvokeOpportunityAttacks(action, "attack interrupt")
local fire_anim = self:GetAttackAnim(action_id)
local aim_anim = self:GetAimAnim(action_id)
self:SetState(fire_anim)
local duration = self:TimeToAnimEnd()
local hit_moment = self:TimeToMoment(1, "hit") or duration/2
Sleep(hit_moment)
local weapon_visual = weapon:GetVisualObj(self)
PlayFX("FlareHandgun_Fire", "start", weapon_visual)
local thread = not results.jammed and CreateGameTimeThread(function()
local visual = PlaceObject("GrenadeVisual", {fx_actor_class = "FlareBullet"})
local offset = point(0, 0, 200*guic)
offset = offset + Rotate(point(30*guic, 0, 0), self:GetAngle() + 90*60)+ Rotate(point(20*guic, 0, 0), self:GetAngle())
local pos = self:GetPos()
if not pos:IsValidZ() then
pos = pos:SetTerrainZ()
end
pos = pos + offset
visual:SetPos(pos)
Sleep(100)
visual:SetPos(pos + point(0, 0, 20*guim), 1500)
Sleep(2000)
local explosion_pos = results.explosion_pos +point(0, 0, 10*guic)
local sky_pos = explosion_pos + point(0, 0, 20*guim)
local col, pts = CollideSegmentsNearest(sky_pos, explosion_pos)
if col then
explosion_pos = pts[1]
end
visual:SetPos(sky_pos)
local fall_time = MulDivRound(sky_pos:Dist(explosion_pos), 1000, const.Combat.MortarFallVelocity/5)
visual:SetPos(explosion_pos, fall_time)
Sleep(fall_time)
local flare = PlaceObject("FlareOnGround", {
visual_obj = visual,
remaining_time = 4*5000,
Despawn = true,
campaign_time = Game.CampaignTime,
})
flare:SetPos(explosion_pos)
flare:UpdateVisualObj()
PushUnitAlert("thrown", flare, self)
Wakeup(self.command_thread)
end)
Sleep(duration - hit_moment)
self:SetRandomAnim(self:GetIdleBaseAnim())
results.weapon:ApplyAmmoUse(self, results.fired, results.jammed, results.condition)
while IsValidThread(thread) do
WaitWakeup(50)
end
self.last_attack_session_id = false
self:ProvokeOpportunityAttacks(action, "attack reaction")
end
function Unit:ThrowGrenade(action_id, cost_ap, args)
local stealth_attack = not not self:HasStatusEffect("Hidden")
local target_pos = args.target
if self.stance ~= "Standing" then
self:ChangeStance(nil, nil, "Standing")
end
local action = CombatActions[action_id]
self:ProvokeOpportunityAttacks(action, "attack interrupt")
local grenade = action:GetAttackWeapons(self)
args.prediction = false -- mishap needs to happen now
local results, attack_args = action:GetActionResults(self, args) -- early check for PrepareToAttack
self:PrepareToAttack(attack_args, results)
self:UpdateAttachedWeapons()
self:ProvokeOpportunityAttacks(action, "attack interrupt")
self:EndInterruptableMovement()
-- camera effects
if not attack_args.opportunity_attack_type == "Retaliation" then
if g_Combat and IsEnemyKill(self, results) then
g_Combat:CheckPendingEnd(results.killed_units)
local isKillCinematic, dontPlayForLocalPlayer = IsEnemyKillCinematic(self, results, attack_args)
if isKillCinematic then
cameraTac.SetForceMaxZoom(false)
SetAutoRemoveActionCamera(self, results.killed_units[1], nil, nil, nil, nil, nil, dontPlayForLocalPlayer)
end
end
end
self:RemoveStatusEffect("FirstThrow")
-- multi-throw support
local attacks = results.attacks or {results}
local ap = (cost_ap and cost_ap > 0) and cost_ap or action:GetAPCost(self, attack_args)
table.insert(g_CurrentAttackActions, { action = action, cost_ap = ap, attack_args = attack_args, results = results })
self:PushDestructor(function(self)
self:ForEachAttach("GrenadeVisual", DoneObject)
table.remove(g_CurrentAttackActions)
self.last_attack_session_id = false
local dlg = GetInGameInterfaceModeDlg()
if dlg and dlg:HasMember("dont_return_camera_on_close") then
dlg.dont_return_camera_on_close = true
end
end)
Msg("ThrowGrenade", self, grenade, #attacks)
-- throw anim
self:SetState("gr_Standing_Attack", const.eKeepComponentTargets)
-- pre-create visual objs and play activate fx
local visual_objs = {}
for i = 1, #attacks do
local visual_obj = grenade:GetVisualObj(self, i > 1)
visual_objs[i] = visual_obj
PlayFX("GrenadeActivate", "start", visual_obj)
end
local time_to_hit = self:TimeToMoment(1, "hit") or 20
self:Face(target_pos, time_to_hit/2)
Sleep(time_to_hit)
if results.miss or not results.killed_units or not (#results.killed_units > 1) then
local specialNadeVr = table.find(SpecialGrenades, grenade.class) and (IsMerc(self) and "SpecialThrowGrenade" or "AIThrowGrenadeSpecial")
local standardNadeVr = IsMerc(self) and "ThrowGrenade" or "AIThrowGrenade"
PlayVoiceResponse(self, specialNadeVr or standardNadeVr)
end
local thread = CreateGameTimeThread(function()
-- create visuals and start anim thread for each throw
local threads = {}
for i, attack in ipairs(attacks) do
visual_objs[i]:Detach()
visual_objs[i]:SetHierarchyEnumFlags(const.efVisible)
local trajectory = attack.trajectory
if #trajectory > 0 then
local rpm_range = const.Combat.GrenadeMaxRPM - const.Combat.GrenadeMinRPM
local rpm = const.Combat.GrenadeMinRPM + self:Random(rpm_range)
local rotation_axis = RotateAxis(axis_x, axis_z, CalcOrientation(trajectory[2].pos, trajectory[1].pos))
threads[i] = CreateGameTimeThread(AnimateThrowTrajectory, visual_objs[i], trajectory, rotation_axis, rpm, "GrenadeDrop")
else
-- try to find a fall down pos
threads[i] = CreateGameTimeThread(ItemFallDown, visual_objs[i])
end
end
grenade:OnThrow(self, visual_objs)
-- wait until all threads are done
while #threads > 0 do
Sleep(25)
for i = #threads, 1, -1 do
if not IsValidThread(threads[i]) then
table.remove(threads, i)
end
end
end
-- real check when the grenade(s) landed must use the current position(s)
if #attacks > 1 then
args.explosion_pos = {}
for i, res in ipairs(attacks) do
args.explosion_pos[i] = res.explosion_pos or visual_objs[i]:GetVisualPos()
end
else
args.explosion_pos = results.explosion_pos or visual_objs[1]:GetVisualPos()
end
results, attack_args = action:GetActionResults(self, args)
local attacks = results.attacks or {results}
results.attack_from_stealth = stealth_attack
-- needs to be after GetActionResults
local destroy_grenade
if not self.infinite_ammo then
grenade.Amount = grenade.Amount - #attacks
destroy_grenade = grenade.Amount <= 0
if destroy_grenade then
local slot = self:GetItemSlot(grenade)
self:RemoveItem(slot, grenade)
end
ObjModified(self)
end
self:AttackReveal(action, attack_args, results)
self:OnAttack(action_id, nil, results, attack_args)
LogAttack(action, attack_args, results)
for i, attack in ipairs(attacks) do
grenade:OnLand(self, attack, visual_objs[i])
end
if destroy_grenade then
DoneObject(grenade)
end
AttackReaction(action, attack_args, results)
Msg(CurrentThread())
end)
Sleep(self:TimeToAnimEnd())
self:SetRandomAnim(self:GetIdleBaseAnim())
if IsValidThread(thread) then
WaitMsg(thread)
end
self:ProvokeOpportunityAttacks(action, "attack reaction")
self:PopAndCallDestructor()
end
function Unit:RemoteDetonate(action_id, cost_ap, args)
local target_pos = args.target
local action = CombatActions[action_id]
local detonator = action:GetAttackWeapons(self)
local traps = detonator:GetAttackResults(false, { target_pos = target_pos })
self.performed_action_this_turn = true -- the action doesn't consume ap, so set it manually
for i, t in ipairs(traps) do
t.obj:TriggerTrap(nil, self)
end
end
function Unit:FaceAttackerCommand(attacker, angle)
angle = angle or CalcOrientation(self, attacker)
self:AnimatedRotation(angle)
self:SetRandomAnim(self:GetIdleBaseAnim(), const.eKeepComponentTargets)
self:SetCommand("WaitAttacker")
end
function Unit:WaitAttacker(timeout)
Sleep(timeout or 2000)
end
function Unit:MeleeAttack(action_id, cost_ap, args)
local new_stance = self.stance ~= "Standing" and self.species == "Human" and "Standing"
local stealth_attack = not not self:GetStatusEffect("Hidden")
if new_stance then
local stance = self.stance
self:PushDestructor(function(self)
self:GainAP(cost_ap)
self:ChangeStance(nil, nil, stance)
end)
if new_stance then
self:ChangeStance(nil, nil, "Standing")
end
self:PopDestructor()
end
local action = CombatActions[action_id]
local weapon = action:GetAttackWeapons(self)
local target = args.target
if IsKindOf(target, "Unit") and not IsMeleeRangeTarget(self, nil, self.stance, target, nil, target.stance) then
self:GainAP(cost_ap)
ShowBadgeOfAttacker(self, false)
return
end
-- do the attack/crit rolls
args.attack_roll = 1 + self:Random(100)
args.crit_roll = 1 + self:Random(100)
if action.StealthAttack then
args.stealth_attack = stealth_attack
args.stealth_kill_roll = 1 + self:Random(100)
end
args.prediction = false
local face_thread
self:PushDestructor(function(self)
table.remove(g_CurrentAttackActions)
if IsValid(target) and (target.command == "WaitAttacker" or target.command == "FaceAttackerCommand") then
target:SetCommand("Idle")
end
DeleteThread(face_thread)
end)
local results, attack_args = action:GetActionResults(self, args)
results.attack_from_stealth = stealth_attack
local ap = (cost_ap and cost_ap > 0) and cost_ap or action:GetAPCost(self, attack_args)
table.insert(g_CurrentAttackActions, { action = action, cost_ap = ap, attack_args = attack_args, results = results })
self:AttackReveal(action, attack_args, results)
self.marked_target_attack_args = nil
self:ProvokeOpportunityAttacks(action, "attack interrupt", nil, "melee")
-- camera effects
if not attack_args.opportunity_attack_type == "Retaliation" then
if g_Combat and IsEnemyKill(self, results) then
g_Combat:CheckPendingEnd(results.killed_units)
--cameraTac.SetForceMaxZoom(false)
--SetAutoRemoveActionCamera(self, results.killed_units[1], 1000)
end
if IsKindOf(target, "Unit") and IsCinematicAttack(self, results, attack_args, action) then
SetAutoRemoveActionCamera(self, target, false, false, false, default_interpolation_time)
end
end
self:EndInterruptableMovement()
local anim, face_angle, fx_actor
if self.species == "Human" then
local base_anim
if self.infected then
fx_actor = "fist"
base_anim = "inf_Standing_Attack"
else
local spot_relative_z
if IsKindOf(target, "Unit") then
local BodyParts = UnitColliders[target.species].BodyParts
local idx = table.find(BodyParts, "id", attack_args.target_spot_group)
if not idx then
if attack_args.target_spot_group == "Neck" then
idx = table.find(BodyParts, "id", "Head")
end
end
local target_spot = idx and BodyParts[idx].TargetSpots[1]
if target_spot and target:HasSpot(target_spot) then
local spot_pos = target:GetSpotLocPos(target:GetSpotBeginIndex(target_spot))
local aposx, aposy, aposz = self:GetPosXYZ()
spot_relative_z = spot_pos:z() - (aposz or terrain.GetHeight(aposx, aposy))
end
end
local attach_forward = not spot_relative_z or spot_relative_z >= 700
if weapon.IsUnarmed then
fx_actor = "fist"
base_anim = attach_forward and "nw_Standing_Attack_Forward" or "nw_Standing_Attack_Down"
else
fx_actor = "knife"
if IsKindOf(weapon, "MacheteWeapon") then
fx_actor = "machete"
base_anim = attach_forward and "mk_Standing_Machete_Attack_Forward" or "mk_Standing_Machete_Attack_Down"
else
base_anim = attach_forward and "mk_Standing_Attack_Forward" or "mk_Standing_Attack_Down"
end
end
end
anim = self:GetRandomAnim(base_anim)
face_angle = CalcOrientation(self, target)
else
anim = "attack"
fx_actor = "jaws"
local can_attack
can_attack, face_angle = IsMeleeRangeTarget(self, nil, self.stance, target, nil, target.stance)
if self.species == "Crocodile" then
local head_pos = SnapToVoxel(RotateRadius(const.SlabSizeX, face_angle, self))
local adiff = AngleDiff(CalcOrientation(head_pos, target), face_angle)
local variant = Clamp(4 + adiff/(45*60), 1, 7)
if variant > 1 then
anim = anim .. variant
end
end
end
fx_actor = self:CallReactions_Modify("OnUnitChooseMeleeAttackFxActor", fx_actor, action, weapon, target)
face_thread = CreateGameTimeThread(function(self, anim, face_angle)
self:PlayTransitionAnims(anim)
if face_angle then
self:AnimatedRotation(face_angle)
end
self:SetRandomAnim(self:GetIdleBaseAnim("anim"))
Msg(self)
end, self, anim, face_angle)
-- target face attacker
if g_Combat
and action_id ~= "Charge"
and not args.opportunity_attack
and IsKindOf(target, "Unit")
and not target:IsDead()
and target.stance ~= "Prone"
and not target:IsDowned()
and not target:HasStatusEffect("ManningEmplacement")
then
local target_face_angle
if target.body_type == "Large animal" then
if abs(target:AngleToObject(self)) > 90*60 then
target_face_angle = target:GetAngle() + 180*60
end
else
local target_angle = CalcOrientation(target, self)
if abs(AngleDiff(target_angle, target:GetOrientationAngle())) > 45*60 then
target_face_angle = target_angle
end
end
if target_face_angle then
if target:IsCommandThread() then
local speed_mod = target:GetAnimSpeedModifier()
target:SetAnimSpeedModifier(1000) -- restore the move speed modified in Unit:InterruptBegin()
target:AnimatedRotation(target_face_angle)
target:SetAnimSpeedModifier(speed_mod)
else
if target:IsInterruptable() then
target:SetCommand("FaceAttackerCommand", self, target_face_angle)
for i = 1, 200 do
if target.command ~= "FaceAttackerCommand" then
break
end
Sleep(50)
end
end
end
end
end
if IsValidThread(face_thread) then
WaitMsg(self, 2000)
end
DeleteThread(face_thread)
face_thread = nil
if g_AIExecutionController and not ActionCameraPlaying then
local targetPos = target:GetVisualPos()
local cameraIsNear = DoPointsFitScreen({targetPos}, nil, const.Camera.BufferSizeNoCameraMov)
if not cameraIsNear then
AdjustCombatCamera("set", nil, targetPos, GetStepFloor(targetPos), nil, "NoFitCheck")
end
end
--handle badges as melee doesn't use prepare to attack logic
if not g_AITurnContours[self.handle] and g_Combat and g_AIExecutionController then
local enemy = self.team.side == "enemy1" or self.team.side == "enemy2" or self.team.side == "neutralEnemy"
g_AITurnContours[self.handle] = SpawnUnitContour(self, enemy and "CombatEnemy" or "CombatAlly")
ShowBadgeOfAttacker(self, true)
end
ShowBadgesOfTargets({target}, "show")
self:SetAnim(1, anim)
local fx_target
if IsKindOf(target, "Unit") then
fx_target = target
elseif IsValid(target) then
fx_target = GetObjMaterial(target:GetPos(), target) or target
elseif IsPoint(target) then
fx_target = GetObjMaterial(target) or "air"
end
PlayFX("MeleeAttack", "start", fx_actor, fx_target, self:GetVisualPos())
local tth = self:TimeToMoment(1, "hit") or (self:TimeToAnimEnd() / 2)
repeat
Sleep(tth)
tth = self:TimeToMoment(1, "hit", 2)
if tth and not results.miss and IsKindOf(target, "Unit") then
target:Pain()
end
until not tth
local attack_roll= results.attack_roll
local roll = type(attack_roll) == "number" and attack_roll or type(attack_roll) == "table" and Untranslated(table.concat(attack_roll, ", ")) or nil
if results.miss then
CreateFloatingText(target, T(699485992722, "Miss"), "FloatingTextMiss")
PlayFX("MeleeAttack", "miss", fx_actor, false, self:GetVisualPos())
else
local resolve_steroid_punch = action_id == "SteroidPunch" and IsValidTarget(target) and IsKindOf(target, "Unit")
for _, hit in ipairs(results) do
local obj = hit.obj
if IsValid(obj) and not obj:IsDead() and hit.damage > 0 then
if IsKindOf(obj, "Unit") then
obj:ApplyDamageAndEffects(self, hit.damage, hit, hit.armor_decay)
else
obj:TakeDamage(hit.damage, self, hit)
end
end
end
PlayFX("MeleeAttack", "hit", fx_actor, fx_target, IsValid(target) and target:GetVisualPos() or self:GetVisualPos())
if resolve_steroid_punch then
self:ResolveSteroidPunch(args, results)
end
end
self:OnAttack(action_id, target, results, attack_args)
Sleep(self:TimeToAnimEnd())
LogAttack(action, attack_args, results)
self:ProvokeOpportunityAttacks(action, "attack reaction", nil, "melee")
AttackReaction(action, attack_args, results, "can retaliate")
ShowBadgesOfTargets({target}, "hide")
if IsValid(target) then
ObjModified(target)
end
self.last_attack_session_id = false
self:PopAndCallDestructor()
end
function Unit:ExplodingPalm(action_id, cost_ap, args)
return self:MeleeAttack(action_id, cost_ap, args)
end
function Unit:GetNumBrutalizeAttacks(goto_pos)
local ap = self:GetUIActionPoints()
if goto_pos then
local cp = GetCombatPath(self)
local cost = cp and cp:GetAP(goto_pos) or 0
ap = Min(self:GetUIActionPoints(), self.ActionPoints - cost)
end
local action = self:GetDefaultAttackAction()
local base_cost = action:GetAPCost(self)
local num = 3
if base_cost then
num = ap / MulDivRound(base_cost, 66, 100)
end
return Max(3, num)
end
function Unit:Brutalize(action_id, cost_ap, args)
local target = args.target
if not IsKindOf(target, "Unit") then return end
local action = CombatActions[action_id]
local weapon = action:GetAttackWeapons(self)
if not IsKindOf(weapon, "MeleeWeapon") then return end
local bodyParts = target:GetBodyParts(weapon)
local num_attacks = args.num_attacks or 3
for i = 1, num_attacks do
local bodyPart = table.interaction_rand(bodyParts, "Combat")
args.target_spot_group = bodyPart.id
args.target_spot_group = bodyPart.id
self:MeleeAttack(action_id, 0, args)
if not IsValid(self) or self:IsIncapacitated() or not IsValidTarget(target) then
break
end
end
local target = args.target
if IsKindOf(target, "Unit") and IsValidTarget(target) then
target:AddStatusEffect("Exposed")
end
self.ActionPoints = 0
if self:IsLocalPlayerControlled() then
GetInGameInterfaceModeDlg():NextUnit(self.team, "force")
end
end
function Unit:MarkTarget(action_id, cost_ap, attack_args)
--for _, unit in ipairs(g_Units) do
--unit.marked_target_attack_args = nil
--end
if not g_Combat then
self.marked_target_attack_args = attack_args
ShowSneakModeTutorialPopup(self)
ShowSneakApproachTutorialPopup(self)
local target = attack_args.target
CreateBadgeFromPreset("MarkedBadge", target, target)
end
end
function Unit:CancelMark()
self.marked_target_attack_args = nil
end
function Unit:IsMarkedForStealthAttack(attacker)
for _, unit in ipairs(g_Units) do
if unit ~= self and unit.marked_target_attack_args and unit.marked_target_attack_args.target == self then
if (not attacker) or (attacker == unit) then
return true
end
end
end
end
function Unit:ThrowKnife(action_id, cost_ap, args)
local target = args.target
if not IsPoint(target) and not IsValidTarget(target) then
self:GainAP(cost_ap)
CombatActionInterruped(self)
return
end
local action = CombatActions[action_id]
if action.StealthAttack then
args.stealth_kill_roll = 1 + self:Random(100)
end
args.prediction = false
local results, attack_args = action:GetActionResults(self, args)
self:ExecKnifeThrow(action, cost_ap, attack_args, results)
end
function Unit:ExecKnifeThrow(action, cost_ap, attack_args, results)
local target = attack_args.target
local target_unit = IsKindOf(target, "Unit") and IsValidTarget(target) and target
local target_pos = IsPoint(target) and target or target:GetPos()
results.attack_from_stealth = not not self:HasStatusEffect("Hidden")
self:AttackReveal(action, attack_args, results)
self:ProvokeOpportunityAttacks(action, "attack interrupt")
if self.stance == "Prone" then
self:DoChangeStance("Standing")
end
self:PrepareToAttack(attack_args, results)
self:ProvokeOpportunityAttacks(action, "attack interrupt")
self:EndInterruptableMovement()
-- animation
local weapon = action:GetAttackWeapons(self)
self:PushDestructor(function(self)
self:AttachActionWeapon(action)
local visual_obj = self.custom_weapon_attach or weapon:GetVisualObj(self)
-- camera effects
if g_Combat and IsEnemyKill(self, results) then
g_Combat:CheckPendingEnd(results.killed_units)
local target
for _, unit in ipairs(results.killed_units) do
if unit ~= self then
target = unit
break
end
end
local isKillCinematic, dontPlayForLocalPlayer = IsEnemyKillCinematic(self, results, attack_args)
if target and isKillCinematic then
cameraTac.SetForceMaxZoom(false)
SetAutoRemoveActionCamera(self, target, nil, nil, nil, nil, nil, dontPlayForLocalPlayer)
end
end
-- throw anim
self:SetState("mk_Standing_Fire", const.eKeepComponentTargets)
local time_to_hit = self:TimeToMoment(1, "hit") or 20
self:Face(target_pos, time_to_hit/2)
Sleep(time_to_hit)
-- grenade trajectory
--local weapon_pos = visual_obj:GetSpotLocPos()
local visual_attach_spot = visual_obj:GetAttachSpot()
local visual_attach_parent = visual_obj:GetParent()
local start_pos, start_angle, start_axis = visual_obj:GetSpotLoc(-1)
visual_obj:Detach()
visual_obj:SetPos(start_pos)
visual_obj:SetAxisAngle(start_axis, start_angle, 0)
--visual_obj:SetPos(weapon_pos)
PlayFX("ThrowKnife", "start", visual_obj)
local trajectory = results.trajectory
local throw_thread
if #trajectory > 0 then
local rotation_axis = RotateAxis(axis_y, axis_z, CalcOrientation(trajectory[2].pos, trajectory[1].pos))
local rpm_range = const.Combat.KnifeMaxRPM - const.Combat.KnifeMinRPM
local rpm = const.Combat.KnifeMinRPM + self:Random(rpm_range)
throw_thread = CreateGameTimeThread(AnimateThrowTrajectory, visual_obj, trajectory, rotation_axis, -rpm)
end
while IsValidThread(throw_thread) do
Sleep(20)
end
if self:IsMerc() and not self:HasStatusEffect("HundredKnives") then
-- move the item to the proper container (unit or otherwise)
local container = target
if results.miss or not IsKindOf(target, "Unit") or not target:CanAddItem("Inventory", weapon) then
local drop_pos = terrain.FindPassable(visual_obj, 0, -1, -1, const.pfmVoxelAligned)
container = GetDropContainer(self, drop_pos)
end
local slot = self:GetItemSlot(weapon)
local thrownKnife = weapon:SplitStack(1, "splitIfEqual")
assert(thrownKnife)
if container then
thrownKnife.drop_chance = 100 -- make sure the weapon will be properly dropped as loot once the enemy dies
AddItemsToInventory(container, {thrownKnife})
end
local item_class = weapon.class
local spare
self:ForEachItemInSlot("Inventory", item_class, function(item)
if item.class == item_class then
spare = item
return "break"
end
end)
if slot and spare then
if weapon:MergeStack(spare) then
self:RemoveItem("Inventory", spare)
DoneObject(spare)
end
end
if slot and weapon.Amount <= 0 then
self:RemoveItem(slot, weapon)
DoneObject(weapon)
end
self:FlushCombatCache()
self:RecalcUIActions()
ObjModified(self)
ObjModified(container)
if IsValid(visual_obj) then
DoneObject(visual_obj)
end
self:UpdateOutfit()
else
if IsValid(visual_obj) and IsValid(visual_attach_parent) then
visual_attach_parent:Attach(visual_obj, visual_attach_spot)
else
DoneObject(visual_obj)
weapon:GetVisualObj(self) -- recreate
end
end
if results.miss then
CreateFloatingText(target, T(699485992722, "Miss"), "FloatingTextMiss")
else
for _, hit in ipairs(results) do
if IsValid(hit.obj) and not hit.obj:IsDead() and hit.damage > 0 then
if IsKindOf(hit.obj, "Unit") then
hit.obj:ApplyDamageAndEffects(self, hit.damage, hit, hit.armor_decay)
else
hit.obj:TakeDamage(hit.damage, self, hit)
end
end
end
local fx_actor = "knife"
PlayFX("MeleeAttack", "hit", fx_actor, self:GetVisualPos())
end
self:OnAttack(action.id, target, results, attack_args)
LogAttack(action, attack_args, results)
AttackReaction(action, attack_args, results, "can retaliate")
if not attack_args.keep_ui_mode then
SetInGameInterfaceMode("IModeCombatMovement") -- revert to movement mode first to avoid having attack modes try to do stuff with a missing weapon
end
self:ProvokeOpportunityAttacks(action, "attack reaction")
self.last_attack_session_id = false
table.remove(g_CurrentAttackActions)
end)
local ap = (cost_ap and cost_ap > 0) and cost_ap or action:GetAPCost(self, attack_args)
table.insert(g_CurrentAttackActions, { action = action, cost_ap = ap, attack_args = attack_args, results = results })
self:RemoveStatusEffect("FirstThrow")
self:PopAndCallDestructor()
end
function Unit:UpdateBandageConsistency()
if self:HasStatusEffect("BeingBandaged") then
local medic
for _, unit in ipairs(g_Units) do
if unit:GetBandageTarget() == self then
return
end
end
self:RemoveStatusEffect("BeingBandaged")
end
if self:HasStatusEffect("BandageInCombat") then
local patient = self:GetBandageTarget()
if not patient or not patient:HasStatusEffect("BeingBandaged") then
self:RemoveStatusEffect("BandageInCombat")
end
end
end
function Unit:Bandage(action_id, cost_ap, args)
local goto_ap = args.goto_ap or 0
local action_cost = cost_ap - goto_ap
local pos = args.goto_pos
local target = args.target
local sat_view = args.sat_view or false -- in sat_view form inventory, skip all sleeps and anims
local target_self = target == self
if g_Combat then
if goto_ap > 0 then
self:PushDestructor(function(self)
self:GainAP(action_cost)
end)
local result = self:CombatGoto(action_id, goto_ap, args.goto_pos)
self:PopDestructor()
if not result then
self:GainAP(action_cost)
return
end
end
elseif not target_self then
self:GotoSlab(pos)
end
local myVoxel = SnapToPassSlab(self:GetPos())
if pos and myVoxel:Dist(pos) ~= 0 then
if self.behavior == "Bandage" then
self:SetBehavior()
end
if self.combat_behavior == "Bandage" then
self:SetCombatBehavior()
end
self:GainAP(action_cost)
return
end
local action = CombatActions[action_id]
local medicine = GetUnitEquippedMedicine(self)
if not medicine then
if self.behavior == "Bandage" then
self:SetBehavior()
end
if self.combat_behavior == "Bandage" then
self:SetCombatBehavior()
end
self:GainAP(action_cost)
return
end
self:SetBehavior("Bandage", {action_id, cost_ap, args})
self:SetCombatBehavior("Bandage", {action_id, cost_ap, args})
if not target_self then
self:Face(target, 200)
Sleep(200)
end
if not sat_view then
if self.stance ~= "Crouch" then
self:ChangeStance(false, 0, "Crouch")
end
self:SetState(target_self and "nw_Bandaging_Self_Start" or "nw_Bandaging_Start")
Sleep(self:TimeToAnimEnd() or 100)
if not args.provoked then
self:ProvokeOpportunityAttacks(action, "attack interrupt")
args.provoked = true
self:SetBehavior("Bandage", {action_id, cost_ap, args})
self:SetCombatBehavior("Bandage", {action_id, cost_ap, args})
end
self:SetState(target_self and "nw_Bandaging_Self_Idle" or "nw_Bandaging_Idle")
if not g_Combat and not GetMercInventoryDlg() then
SetInGameInterfaceMode("IModeExploration")
end
elseif not g_Combat then
-- insta-heal in sat view
while IsValid(target) and not target:IsDead() and target.HitPoints < target.MaxHitPoints and medicine.Condition > 0 do
target:GetBandaged(medicine, self)
end
end
self:SetCommand("CombatBandage", target, medicine)
end
function Unit:IsBeingBandaged()
for _, unit in ipairs(g_Units) do
if unit:GetBandageTarget() == self then
return true
end
end
end
function Unit:GetBandageTarget()
if self.combat_behavior == "Bandage" and not self:IsDead() then
local args = self.combat_behavior_params[3]
return args.target
end
end
function Unit:GetBandageMedicine()
if self.combat_behavior == "Bandage" and not self:IsDead() then
return GetUnitEquippedMedicine(self)
end
end
function Unit:CombatBandage(target, medicine)
target:AddStatusEffect("BeingBandaged")
ObjModified(target)
if IsValid(target) then
self:Face(target, 0)
end
if g_Combat then
-- play anim, etc
local heal_anim
if self == target then
heal_anim = "nw_Bandaging_Self_Idle"
else
heal_anim = "nw_Bandaging_Idle"
PlayVoiceResponse(self, "BandageDownedUnit")
end
self:SetState(heal_anim, const.eKeepComponentTargets)
self:AddStatusEffect("BandageInCombat")
if not GetMercInventoryDlg() then
SetInGameInterfaceMode("IModeCombatMovement")
end
Halt()
else
self:PushDestructor(function()
self:SetCombatBehavior()
self:SetBehavior()
self:RemoveStatusEffect("BandageInCombat")
target:RemoveStatusEffect("BeingBandaged")
ObjModified(target)
ObjModified(self)
end)
self:AddStatusEffect("BandageInCombat")
while IsValid(target) and not target:IsDead() and (target.HitPoints < target.MaxHitPoints or target:HasStatusEffect("Bleeding")) and medicine.Condition > 0 do
Sleep(5000)
target:GetBandaged(medicine, self)
end
self:SetState(self == target and "nw_Bandaging_Self_End" or "nw_Bandaging_End")
Sleep(self:TimeToAnimEnd() or 100)
self:PopAndCallDestructor()
end
end
function Unit:EndCombatBandage(no_ui_update, instant)
local target = self:GetBandageTarget()
self:RemoveStatusEffect("BandageInCombat")
ObjModified(self)
if IsValid(target) then
target:RemoveStatusEffect("BeingBandaged")
ObjModified(target)
end
local normal_anim = self:TryGetActionAnim("Idle", self.stance)
if not instant then
self:PlayTransitionAnims(normal_anim)
end
self:SetCombatBehavior()
self:SetBehavior()
if not no_ui_update and ((self == SelectedObj or target == SelectedObj)) and g_Combat then
SetInGameInterfaceMode("IModeCombatMovement") -- force update to redraw the combat path areas now that movement is allowed
end
if self.command == "EndCombatBandage" then
self:SetCommand("Idle")
end
end
function OnMsg.UnitMovementStart(unit)
for _, u in ipairs(g_Units) do
if u:GetBandageTarget() == unit then
u:SetCommand("EndCombatBandage", "no update")
end
end
end
function Unit:DownedRally(medic, medicine)
self:SetCombatBehavior()
self:RemoveStatusEffect("Stabilized")
self:RemoveStatusEffect("BleedingOut")
self:RemoveStatusEffect("Unconscious")
self:RemoveStatusEffect("Downed")
self:SetTired(Min(self.Tiredness, 2))
self.downed_check_penalty = 0
if medic then
if medicine then
medicine.Condition = medicine.Condition - CombatActions.Bandage:ResolveValue("ReviveConditionLoss")
end
self:GetBandaged(medicine, medic)
local slot = medic:GetItemSlot(medicine)
if slot and medicine.Condition <= 0 then
CombatLog("short", T{831717454393, "<merc>'s <item> has been depleted", merc = medic.Nick, item = medicine.DisplayName})
medic:RemoveItem(slot, medicine)
DoneObject(medicine)
end
medic:SetCommand("EndCombatBandage")
else
-- still check if another unit is tending to us
for _, unit in ipairs(self.team.units) do
if unit:GetBandageTarget() == self then
unit:SetCommand("EndCombatBandage")
end
end
end
local stance = self.immortal and "Standing" or self.stance
self.stance = stance
local normal_anim = self:TryGetActionAnim("Idle", self.stance)
self:PlayTransitionAnims(normal_anim)
if g_Combat then
self:GainAP(self:GetMaxActionPoints() - self.ActionPoints) -- rally happens at the start of turn, restore to full ap
end
self.TempHitPoints = 0
ObjModified(self)
ObjModified(self.team)
ForceUpdateCommonUnitControlUI("recreate")
CreateFloatingText(self, T(979333850225, "Recovered"))
PlayFX("UnitDownedRally", "start", self)
Msg("OnDownedRally", medic, self)
self:CallReactions("OnUnitRallied", medic, self)
if medic ~= self and IsKindOf(medic, "Unit") then
medic:CallReactions("OnUnitRallied", medic, self)
end
self:SetCommand("Idle")
end
function Unit:Retaliate(attacker, attack_reason, fnGetAttackAndWeapon)
if not IsKindOf(attacker, "Unit") or attacker.team ~= g_Teams[g_CurrentTeam] or attacker == self then
return false
end
if self:IsDead() or self:IsDowned() or not self:IsAware() or self:HasPreparedAttack() then
return false
end
local retaliated = false
local num_attacks = HasPerk(self, "Killzone") and 2 or 1
for i = 1, num_attacks do
local action, weapon
if fnGetAttackAndWeapon then
action, weapon = fnGetAttackAndWeapon(self)
else
weapon = self:GetActiveWeapons("Firearm")
if IsKindOf(weapon, "HeavyWeapon") then
weapon = nil
else
action = self:GetDefaultAttackAction()
end
end
if not weapon or not action or not self:CanAttack(attacker, weapon, action, 0, nil, "skip_ap_check") then
break
end
local lof_data = GetLoFData(self, { attacker }, { action_id = action.id })
if lof_data[1].los == 0 then
break
end
if i == 1 then
self:SetAttackReason(attack_reason, true)
attacker:InterruptBegin()
end
if IsValidTarget(attacker) then
retaliated = true
self:QueueCommand("RetaliationAttack", attacker, false, action)
while not self:IsIdleCommand() do
WaitMsg("Idle")
end
end
end
ClearAITurnContours()
g_Interrupt = true
self:SetAttackReason()
return retaliated
end
function NetSyncEvents.InvetoryAction_RealoadWeapon(session_id, ap, weapon_args, src_ammo_type)
local combat_mode = g_Units[session_id] and InventoryIsCombatMode(g_Units[session_id] )
local unit = (not gv_SatelliteView or combat_mode) and g_Units[session_id] or gv_UnitData[session_id]
if combat_mode and gv_SatelliteView then
unit:SyncWithSession("session")
end
if combat_mode and ap>0 and unit:UIHasAP(ap) then
assert(IsKindOf(unit,"Unit"), "Consume AP called for UnitData")
unit:ConsumeAP(ap, "Reload")
end
local weapon = g_ItemIdToItem[weapon_args.item_id]
assert(weapon)
unit:ReloadWeapon(weapon, src_ammo_type)
if combat_mode and gv_SatelliteView then
unit:SyncWithSession("map")
end
if unit:CanBeControlled() then InventoryUpdate(unit) end
end
function NetSyncEvents.InvetoryAction_UnjamWeapon(session_id, ap, weapon_args)
local combat_mode = g_Units[session_id] and InventoryIsCombatMode(g_Units[session_id] )
local unit = (not gv_SatelliteView or combat_mode) and g_Units[session_id] or gv_UnitData[session_id]
if combat_mode and gv_SatelliteView then
unit:SyncWithSession("session")
end
if combat_mode and ap>0 and unit:UIHasAP(ap) then
assert(IsKindOf(unit,"Unit"), "Consume AP called for UnitData")
unit:ConsumeAP(ap, "Unjam")
end
local weapon = g_ItemIdToItem[weapon_args.item_id]
assert(weapon)
weapon:Unjam(unit)
if combat_mode and gv_SatelliteView then
unit:SyncWithSession("map")
end
if unit:CanBeControlled() then InventoryUpdate(unit) end
end
function NetSyncEvents.InvetoryAction_SwapWeapon(session_id, ap)
local combat_mode = g_Units[session_id] and InventoryIsCombatMode(g_Units[session_id] )
local unit = (not gv_SatelliteView or combat_mode) and g_Units[session_id] or gv_UnitData[session_id]
if combat_mode and gv_SatelliteView then
unit:SyncWithSession("session")
end
if combat_mode and ap>0 and unit:UIHasAP(ap) then
assert(IsKindOf(unit,"Unit"), "Consume AP called for UnitData")
unit:ConsumeAP(ap, "ChangeWeapon")
end
unit:SwapActiveWeapon()
if combat_mode and gv_SatelliteView then
unit:SyncWithSession("map")
end
if unit:CanBeControlled() then InventoryUpdate(unit) end
end
function NetSyncEvents.InvetoryAction_UseItem(session_id, item_id)
local combat_mode = g_Units[session_id] and InventoryIsCombatMode(g_Units[session_id] )
local unit = (not gv_SatelliteView or combat_mode) and g_Units[session_id] or gv_UnitData[session_id]
if combat_mode and gv_SatelliteView then
unit:SyncWithSession("session")
end
local item = g_ItemIdToItem[item_id]
if not item then return end
if combat_mode then
unit:ConsumeAP(item.APCost * const.Scale.AP)
end
ExecuteEffectList(item.Effects, unit)
unit:CallReactions("OnItemUsed", item)
if combat_mode and gv_SatelliteView then
unit:SyncWithSession("map")
end
if unit:CanBeControlled() then InventoryUpdate(unit) end
end
function Unit:ReloadAction(action_id, cost_ap, args)
if args.reload_all then
local _, _, weapons = self:GetActiveWeapons()
for _, weapon in ipairs(weapons) do
local ammo = weapon.ammo and weapon.ammo.class
self:ReloadWeapon(weapon, ammo, args.reload_all)
end
else
local ammo
if args and args.target then
ammo = self:GetItem(args.target)
end
if not ammo then
local bag = self.Squad and GetSquadBagInventory(self.Squad)
if bag then
ammo = bag:GetItem(args.target)
end
end
local weapon = args and args.weapon
-- Index
if type(weapon) == "number" then
local w1, w2, wl = self:GetActiveWeapons()
weapon = wl[weapon]
-- Template name or nothing
else
weapon = self:GetWeaponByDefIdOrDefault("Firearm", weapon, args and args.pos, args and args.item_id)
end
self:ReloadWeapon(weapon, ammo, args and args.delayed_fx)
end
end
function Unit:UnjamWeapon(action_id, cost_ap, args)
self:ProvokeOpportunityAttacks(action_id and CombatActions[action_id], "attack interrupt")
local weapon = false
if args and args.pos then
weapon = self:GetItemAtPackedPos(args.pos)
elseif args and args.weapon then
weapon = self:GetWeaponByDefIdOrDefault("Firearm", args and args.weapon, args and args.pos, args and args.item_id)
end
if weapon then --Inventory Weapon
weapon:Unjam(self)
else
local weapon1, weapon2 = self:GetActiveWeapons()
if weapon1.jammed and not weapon1:IsCondition("Broken") then
weapon1:Unjam(self)
elseif weapon2.jammed and not weapon2:IsCondition("Broken") then
weapon2:Unjam(self)
end
end
end
function Unit:EnterEmplacement(obj, instant)
local fire_pos = obj:GetOperatePos()
if not instant then
if self.stance == "Prone" then
self:DoChangeStance("Standing")
end
if not IsCloser(self, fire_pos, const.SlabSizeX/2) then
self:Goto(fire_pos, "sl")
end
end
self:SetAxis(axis_z)
self:SetAngle(obj:GetAngle(), instant and 0 or 200)
self:SetTargetDummy(nil, nil, "hmg_Crouch_Idle", 0, "Crouch")
self:AddStatusEffect("ManningEmplacement") -- affect weapon holster too
if instant then
self:SetPos(fire_pos)
self:SetState("hmg_Crouch_Idle", 0, 0)
else
self:SetState("hmg_Standing_to_Crouch")
self:SetPos(fire_pos, 500)
Sleep(self:TimeToAnimEnd())
self:SetState("hmg_Crouch_Idle")
end
if self.stance ~= "Crouch" then
self.stance = "Crouch"
Msg("UnitStanceChanged", self)
end
self:SetEffectValue("hmg_emplacement", obj.handle)
self:SetEffectValue("hmg_sector", gv_CurrentSectorId)
obj.manned_by = self
obj.weapon.owner = self.session_id
end
function Unit:LeaveEmplacement(instant, exit_combat)
if not self:HasStatusEffect("ManningEmplacement") then return end
local handle = self:GetEffectValue("hmg_emplacement")
local obj = HandleToObject[handle]
if obj then
if exit_combat and obj.exploration_manned and self.team.player_enemy then
-- enemy units manning important emplacements should stay in them
return
end
obj.manned_by = nil
end
local exit_pos = not IsPassSlab(self) and SnapToPassSlab(self)
if instant then
if exit_pos then
self:SetPos(exit_pos)
end
else
self:SetAnim(1, "hmg_Crouch_to_Standing")
if exit_pos then
Sleep(Max(0, self:TimeToAnimEnd() - 500))
self:SetPos(exit_pos, 200)
end
Sleep(self:TimeToAnimEnd())
end
if self.stance ~= "Standing" then
self.stance = "Standing"
Msg("UnitStanceChanged", self)
end
self:RemoveStatusEffect("ManningEmplacement")
self:SetEffectValue("hmg_emplacement")
if obj and obj.weapon then obj.weapon.owner = nil end
self:InterruptPreparedAttack()
self:FlushCombatCache()
self:RecalcUIActions(true)
self:UpdateOutfit()
ObjModified(self)
end
function Unit:StartBombard()
local weapon = self:GetActiveWeapons("Mortar")
if weapon and self.prepared_bombard_zone then
weapon:ApplyAmmoUse(self, self.prepared_bombard_zone.num_shots, false, self.prepared_bombard_zone.weapon_condition)
end
local fired = self.prepared_bombard_zone.num_shots and self.prepared_bombard_zone.num_shots > 0
if not fired and self.prepared_bombard_zone then
DoneObject(self.prepared_bombard_zone)
end
self.prepared_bombard_zone = nil
self:SetCombatBehavior()
end
function Unit:ExplorationStartCombatAction(action_id, ap, args)
local action = CombatActions[action_id]
if g_Combat or not action then return end
self.ActionPoints = self:GetMaxActionPoints() -- always start combat actions out of combat at max ap
ap = action:GetAPCost(self, args)
self:AddStatusEffect("SpentAP", ap)
end
function Unit:LightningReactionCheck(effect)
if g_Combat and g_Teams[g_Combat.team_playing] == self.team then return end -- Don't proc in your turn
if self.stance == "Prone" or self:HasStatusEffect("ManningEmplacement") then return end
local chance = effect:ResolveValue("chance")
if not chance or (self:Random(100) < chance) then
self:SetActionCommand("ChangeStance", nil, nil, "Prone")
CreateFloatingText(self, T(726050447294, "Lightning Reaction"), nil, nil, true)
return true
end
end
-- signature abilities
function Unit:AddSignatureRechargeTime(id, duration, recharge_on_kill)
if CheatEnabled("SignatureNoCD") then return end
local recharges = self.signature_recharge or {}
self.signature_recharge = recharges
if string.match(id, "DoubleToss") then
id = "DoubleToss"
end
local idx = recharges[id]
if not idx then
idx = #recharges + 1
recharges[id] = idx
end
recharges[idx] = { id = id, expire_campaign_time = Game.CampaignTime + duration, on_kill = recharge_on_kill }
self:RecalcUIActions()
ObjModified(self)
end
function Unit:GetSignatureRecharge(id)
if string.match(id, "DoubleToss") then
id = "DoubleToss"
end
local idx = self.signature_recharge and self.signature_recharge[id]
return idx and self.signature_recharge[idx] or false
end
function Unit:UpdateSignatureRecharges(trigger)
local recharges = self.signature_recharge or empty_table
for i = #recharges, 1, -1 do
local recharge = recharges[i]
if (trigger == "kill" and recharge.on_kill) or (Game.CampaignTime > recharge.expire_campaign_time) then
local id = recharge.id
self:RechargeSignature(id)
end
end
end
function Unit:RechargeSignature(id)
local i = self.signature_recharge[id]
table.remove(self.signature_recharge, i)
self.signature_recharge[id] = nil
end
function Unit:HasSignatures()
local perks = self:GetPerks()
for _, perk in ipairs(perks) do
if perk.Tier == "Personal" and
self.ui_actions[perk.class] and self.ui_actions[perk.class] ~= "hidden" then
return true
end
end
return false
end
local function UpdateAllRecharges()
for _, unit in ipairs(g_Units) do
unit:UpdateSignatureRecharges()
end
end
OnMsg.SatelliteTick = UpdateAllRecharges
function Unit:Nazdarovya(action_id, cost_ap, args)
-- restore hp & gain status
local action = CombatActions[action_id]
self:ApplyTempHitPoints(action:ResolveValue("tempHp"))
self:AddStatusEffect("Drunk")
local recharge_on_kill = action:ResolveValue("recharge_on_kill") or 0
self:AddSignatureRechargeTime(action_id, const.Combat.SignatureAbilityRechargeTime, recharge_on_kill > 0)
end
function Unit:DoubleToss(action_id, cost_ap, args)
self:ThrowGrenade(action_id, cost_ap, args)
local action = CombatActions[action_id]
local recharge_on_kill = action:ResolveValue("recharge_on_kill") or 0
self:AddSignatureRechargeTime("DoubleToss", const.Combat.SignatureAbilityRechargeTime, recharge_on_kill > 0)
end
function Unit:OnMyTarget(action_id, cost_ap, args)
local action = CombatActions[action_id]
-- find allies who can make an attack, have them make a basic attack at the target (so long as target isn't dead)
local fired = {}
for _, ally in ipairs(self.team.units) do
if ally ~= self then
local attack = ally:OnMyTargetGetAllyAttack(args.target)
if attack then
local ap = ally.ActionPoints -- give temporary ap to avoid safeguards that would stop us from firing
ally.ActionPoints = ally:GetMaxActionPoints()
ally:SetCommand("FirearmAttack", attack.id, 0, args)
fired[#fired + 1] = ally
ally.ActionPoints = ap
end
end
end
while #fired > 0 do
Sleep(100)
for i = #fired, 1, -1 do
if fired[i]:IsIdleCommand() then
table.remove(fired, i)
end
end
end
local recharge_on_kill = action:ResolveValue("recharge_on_kill") or 0
self:AddSignatureRechargeTime(action_id, const.Combat.SignatureAbilityRechargeTime, recharge_on_kill > 0)
SetInGameInterfaceMode("IModeCombatMovement")
end
function Unit:OnMyTargetGetAllyAttack(target)
local attack = self.command ~= "Downed" and self:GetDefaultAttackAction("ranged")
local weapon = attack:GetAttackWeapons(self)
if attack and attack.id ~= "UnarmedAttack" and HasVisibilityTo(self, target) and IsKindOf(weapon, "Firearm") and
not IsKindOf(weapon, "HeavyWeapon") and self:CanAttack(target, weapon, attack, nil, nil, "skip_ap_check") then
return attack
end
end
function Unit:SteroidPunch(action_id, cost_ap, args)
self:MeleeAttack(action_id, cost_ap, args)
end
function Unit:ResolveSteroidPunch(args, results)
local target = args.target
if target:IsDead() then
if target.on_die_attacker == self then
target.on_die_hit_descr = target.on_die_hit_descr or {}
target.on_die_hit_descr.death_blow = true
target.on_die_hit_descr.falldown_callback = "SteroidPunchExplosion"
end
return
end
if target.stance == "Prone" or target:HasStatusEffect("Unconscious") then
return
end
local angle = CalcOrientation(self, target)
local pushSlabs = CombatActions.SteroidPunch:ResolveValue("pushSlabs")
local fromPos = GetPassSlab(target) or target:GetPos()
local curPos = fromPos
local toPos = fromPos
local free_slabs = 0
while free_slabs < pushSlabs + 1 do
local nextPos = GetPassSlab(RotateRadius((free_slabs + 1) * const.SlabSizeX, angle, fromPos))
if not nextPos then
break
elseif not IsPassSlabStep(curPos, nextPos, const.TunnelTypeWalk) then
break
elseif IsOccupiedExploration(nil, nextPos:xyz()) then
break
end
toPos = curPos
curPos = nextPos
free_slabs = free_slabs + 1
end
local anim
local toMove = Max(0, free_slabs - 1)
angle = angle + 180*60
if free_slabs > 0 then
anim = self:GetRandomAnim("civ_KnockDown_B")
else
anim = self:GetRandomAnim("civ_KnockDown_OnSpot_B")
if self.species == "Human" then
angle = FindProneAngle(self, toPos, angle)
end
end
target:SetCommand("Punched", self, toPos, angle, anim)
end
function SteroidPunchExplosion(attacker, target, pos)
local mockGrenade = PlaceInventoryItem("SteroidPunchGrenade")
local ignore_targets = { [attacker] = true, [target] = true }
ExplosionDamage(attacker, mockGrenade, pos, nil, nil, "disableBurnFx", ignore_targets)
end
function Unit:Punched(attacker, pos, angle, anim)
anim = anim or "civ_KnockDown_OnSpot_B"
local hit_moment = self:GetAnimMoment(anim, "hit") or self:GetAnimMoment(anim, "end") or self:GetAnimDuration(anim) - 1
CreateGameTimeThread(function(delay, target, pos, attacker)
Sleep(delay)
SteroidPunchExplosion(attacker, target, pos)
end, hit_moment, self, pos, attacker)
if self.species == "Human" then
self.stance = "Prone"
end
self:MovePlayAnim(anim, self:GetPos(), pos, 0, nil, true, angle, nil, nil, nil, true)
end
function Unit:TakeSuppressionFire()
self:Pain()
if self.stance ~= "Prone" then
self:SetActionCommand("ChangeStance", nil, nil, "Prone")
end
end
function Unit:AlwaysReadyFindCover(enemy)
-- calc CombatPath with a set amount of AP (70% of max)
local ap = MulDivRound(self:GetMaxActionPoints(), const.Combat.RepositionAPPercent, 100)
local path = CombatPath:new()
local cost_extra = GetStanceToStanceAP("Standing", "Crouch")
path:RebuildPaths(self, ap)
local best_ppos, best_score, best_ap, stance, score
-- find nearest reachable position that has cover from the attacker
DbgClearVectors()
for ppos, ap in pairs(path.paths_ap) do
local x, y, z = point_unpack(ppos)
local pos = point(x, y, z)
local cover, any, coverage = self:GetCoverPercentage(enemy:GetPos(), pos, "Crouch")
DbgAddVector(point(x, y, z), guim, const.clrGray)
if cover and cover == const.CoverLow and self.stance == "Standing" and ap < cost_extra then
DbgAddVector(point(x, y, z), 2*guim, const.clrRed)
cover = false
end
if cover then
DbgAddVector(point(x, y, z), 2*guim, const.clrYellow)
score = cover * coverage
if not best_ppos then
best_ppos, best_score, best_ap, stance = ppos, score, ap
if cover == const.CoverLow and self.stance == "Standing" then
stance = "Crouch"
else
stance = nil
end
elseif score > best_score or (score > MulDivRound(best_score, 90, 100) and ap > best_ap) then
best_ppos, best_score, best_ap, stance = ppos, score, ap
if cover == const.CoverLow and self.stance == "Standing" then
stance = "Crouch"
else
stance = nil
end
end
end
end
end
MapVar("g_AlwaysReadyThread", false)
function Unit:TryActivateAlwaysReady(enemy)
if IsValidThread(g_AlwaysReadyThread) then
return
end
local cover, any, coverage = self:GetCoverPercentage(enemy:GetPos())
if cover then
if cover == const.CoverLow and self.stance == "Standing" then
-- change stance first to get in the cover
CancelWaitingActions(-1)
NetStartCombatAction("StanceCrouch", self, 0)
end
return
end
-- override standard reposition dest picking logic
-- calc CombatPath with a set amount of AP (70% of max)
local ap = MulDivRound(self:GetMaxActionPoints(), const.Combat.RepositionAPPercent, 100)
local cost_extra = GetStanceToStanceAP("Standing", "Crouch")
local path = CombatPath:new()
path:RebuildPaths(self, ap)
local best_ppos, best_score, best_ap, stance
-- find nearest reachable position that has cover from the attacker
for ppos, ap in pairs(path.paths_ap) do
local pos = point(point_unpack(ppos))
local cover, any, coverage = self:GetCoverPercentage(enemy:GetPos(), pos, "Crouch")
if cover and cover == const.CoverLow and self.stance == "Standing" and ap < cost_extra then
cover = false
end
if cover then
local score = cover * coverage
if not best_ppos then
best_ppos, best_score, best_ap, stance = ppos, score, ap
if cover == const.CoverLow and self.stance == "Standing" then
stance = "Crouch"
else
stance = nil
end
elseif score > best_score or (score > MulDivRound(best_score, 90, 100) and ap > best_ap) then
best_ppos, best_score, best_ap, stance = ppos, score, ap
if cover == const.CoverLow and self.stance == "Standing" then
stance = "Crouch"
else
stance = nil
end
end
end
end
if not best_ppos then
-- no cover available, abort
CreateFloatingText(self, T(103063369185, "Always Ready: No covers nearby"), "FloatingTextMiss")
return
end
local path_to_dest = path:GetCombatPathFromPos(best_ppos)
DoneObject(path)
g_AlwaysReadyThread = CreateGameTimeThread(Unit.ActivateAlwaysReady, self, best_ppos, path_to_dest, stance)
end
function Unit:ActivateAlwaysReady(reposition_dest, reposition_path, stance)
local controller = CreateAIExecutionController{ -- todo: custom notifications maybe
label = "AlwaysReady",
reposition = true,
activator = self,
}
-- hide AP changes & store current AP to restore them at the end
-- note: this isn't a command (the execution would override it) so make sure nothing can break in the code below
-- as we can't rely on command destructors to restore the state
local start_ap = self.ActionPoints
self.ui_override_ap = self:GetUIActionPoints()
-- setup the reposition dest - the controller will not use the default logic with "AlwaysReady" label
local x, y, z = point_unpack(reposition_dest)
self.reposition_dest = stance_pos_pack(x, y, z, StancesList[stance or self.stance])
self.reposition_path = reposition_path
-- exec the reposition
CancelWaitingActions(-1)
controller.restore_camera_obj = SelectedObj
g_Combat:SetRepositioned(self, false) -- we can do this more than once
controller:Execute({self}) -- internally it is sprocall so the code below will execute even if something breaks
DoneObject(controller)
-- restore ap to their previous value
self.ActionPoints = start_ap
self.ui_override_ap = false
g_AlwaysReadyThread = false
end
function Unit:ChargeAttack(action_id, cost_ap, args)
self:PushDestructor(function()
g_TrackingChargeAttacker = false
self.move_attack_action_id = nil
end)
--handle badges as melee doesn't use prepare to attack logic
if not g_AITurnContours[self.handle] and g_Combat and g_AIExecutionController then
local enemy = self.team.side == "enemy1" or self.team.side == "enemy2" or self.team.side == "neutralEnemy"
g_AITurnContours[self.handle] = SpawnUnitContour(self, enemy and "CombatEnemy" or "CombatAlly")
ShowBadgeOfAttacker(self, true)
end
--handle camera for charge attack
ShouldTrackMeleeCharge(self, args.target)
self.move_attack_action_id = action_id
self:SetCommandParamValue(self.command, "move_anim", "Run")
if args.goto_pos then
args.unit_moved = true
self:CombatGoto(action_id, args.goto_ap or 0, args.goto_pos)
end
self:MeleeAttack(action_id, cost_ap, args)
self:PopAndCallDestructor()
end
function Unit:GloryHogCharge(action_id, cost_ap, args)
local action = CombatActions[action_id]
self:ApplyTempHitPoints(action:ResolveValue("tempHp"))
self:ChargeAttack(action_id, cost_ap, args)
local recharge_on_kill = action:ResolveValue("recharge_on_kill") or 0
self:AddSignatureRechargeTime(action_id, const.Combat.SignatureAbilityRechargeTime, recharge_on_kill > 0)
end
function Unit:HyenaCharge(action_id, cost_ap, args)
if self.species ~= "Hyena" then
self:GainAP(cost_ap)
CombatActionInterruped(self)
return
end
local target = args.target
local action = CombatActions[action_id]
args.prediction = false
args.unit_moved = true
local results, attack_args = action:GetActionResults(self, args)
local atk_pos, atk_jmp_pos = GetHyenaChargeAttackPosition(self, target, attack_args.move_ap, attack_args.jump_dist, action_id)
if not atk_pos then
self:GainAP(cost_ap)
CombatActionInterruped(self)
return
end
self:PushDestructor(function()
g_TrackingChargeAttacker = false
table.remove(g_CurrentAttackActions)
self.move_attack_action_id = nil
end)
--handle badges as melee doesn't use prepare to attack logic
if not g_AITurnContours[self.handle] and g_Combat and g_AIExecutionController then
local enemy = self.team.side == "enemy1" or self.team.side == "enemy2" or self.team.side == "neutralEnemy"
g_AITurnContours[self.handle] = SpawnUnitContour(self, enemy and "CombatEnemy" or "CombatAlly")
ShowBadgeOfAttacker(self, true)
end
--handle camera for charge attack
ShouldTrackMeleeCharge(self, target)
self.move_attack_action_id = action_id
table.insert(g_CurrentAttackActions, { action = action, cost_ap = cost_ap, attack_args = attack_args, results = results })
self:AttackReveal(action, attack_args, results)
self:SetCommandParamValue(self.command, "move_anim", "Run")
self:CombatGoto(action_id, attack_args.move_ap, atk_jmp_pos)
self:Face(atk_pos)
if self:CallReactions_And("OnCheckInterruptAttackAvailable", target, action) then
self:ProvokeOpportunityAttacks(action, "attack interrupt", nil, "melee")
end
--handle badges as melee doesn't use prepare to attack logic
ShowBadgesOfTargets({target}, "show")
self:SetState("attack_Charge")
local fx_actor = "jaws"
PlayFX("MeleeAttack", "start", fx_actor, self:GetVisualPos())
local tth = self:TimeToMoment(1, "hit") or (self:TimeToAnimEnd() / 2)
self:SetPos(atk_pos, self:TimeToAnimEnd())
Sleep(tth)
if results.miss then
CreateFloatingText(target, T(699485992722, "Miss"), "FloatingTextMiss")
else
for _, hit in ipairs(results) do
if IsValid(hit.obj) and not hit.obj:IsDead() and hit.damage > 0 then
if IsKindOf(hit.obj, "Unit") then
hit.obj:ApplyDamageAndEffects(self, hit.damage, hit, hit.armor_decay)
else
hit.obj:TakeDamage(hit.damage, self, hit)
end
end
end
PlayFX("MeleeAttack", "hit", fx_actor, self:GetVisualPos())
end
self:OnAttack(action_id, target, results, attack_args)
Sleep(self:TimeToAnimEnd())
LogAttack(action, attack_args, results)
AttackReaction(action, attack_args, results, "can retaliate")
if IsValid(target) then
ObjModified(target)
end
self.last_attack_session_id = false
ShowBadgesOfTargets({target}, "hide")
self:PopAndCallDestructor()
end
function Unit:DanceForMe(action_id, cost_ap, args)
local action = CombatActions[action_id]
local weapon = self:GetActiveWeapons()
local aoeParams = weapon:GetAreaAttackParams(action_id, self)
local attackData = self:ResolveAttackParams(action_id, args.target, {})
local attackerPos = attackData.step_pos
local attackerPos3D = attackerPos
if not attackerPos3D:IsValidZ() then
attackerPos3D = attackerPos3D:SetTerrainZ()
end
local targetPos = args.target
local targetAngle = CalcOrientation(attackerPos, targetPos)
local distance = Clamp(attackerPos3D:Dist(targetPos), aoeParams.min_range * const.SlabSizeX, aoeParams.max_range * const.SlabSizeX)
local enemies = GetEnemies(self)
local maxValue, losValues = CheckLOS(enemies, attackerPos, distance, attackData.stance, aoeParams.cone_angle, targetAngle, false)
if maxValue then
for i, los in ipairs(losValues) do
if los then
local defaultAttack = self:GetDefaultAttackAction("ranged")
local tempArgs = table.copy(args)
tempArgs.target = enemies[i]
tempArgs.target_spot_group = "Legs"
if defaultAttack and self:CanAttack(tempArgs.target, weapon, defaultAttack, nil, nil, "skip_ap_check") then
self:FirearmAttack(defaultAttack.id, 0, tempArgs)
end
end
end
end
local recharge_on_kill = action:ResolveValue("recharge_on_kill") or 0
self:AddSignatureRechargeTime(action_id, const.Combat.SignatureAbilityRechargeTime, recharge_on_kill > 0)
self:SetActionCommand("OverwatchAction", action_id, cost_ap, args)
end
-- attack each body part
function Unit:IceAttack(action_id, cost_ap, args)
local target = args.target
if not IsKindOf(target, "Unit") then return end
local action = CombatActions[action_id]
local weapon = self:GetActiveWeapons()
local bodyParts = target:GetBodyParts(weapon)
for i=#bodyParts, 1, -1 do
if not self:CanUseWeapon(weapon, 1) then break end
local bodyPart = bodyParts[i]
args.target_spot_group = bodyPart.id
args.ice_attack_num = i
self:FirearmAttack(action_id, 0, args)
end
local recharge_on_kill = action:ResolveValue("recharge_on_kill") or 0
self:AddSignatureRechargeTime(action_id, const.Combat.SignatureAbilityRechargeTime, recharge_on_kill > 0)
end
-- bypass armor
function Unit:KalynaShot(action_id, cost_ap, args)
self:FirearmAttack(action_id, 0, args)
local action = CombatActions[action_id]
local recharge_on_kill = action:ResolveValue("recharge_on_kill") or 0
self:AddSignatureRechargeTime(action_id, const.Combat.SignatureAbilityRechargeTime, recharge_on_kill > 0)
end
function Unit:EyesOnTheBack(action_id, cost_ap, args)
local action = CombatActions[action_id]
local recharge_on_kill = action:ResolveValue("recharge_on_kill") or 0
self:AddSignatureRechargeTime(action_id, const.Combat.SignatureAbilityRechargeTime, recharge_on_kill > 0)
self:SetActionCommand("OverwatchAction", action_id, cost_ap, args)
end
function Unit:BulletHell(action_id, cost_ap, args)
args.attack_anim_delay = 50
self:SetActionCommand("FirearmAttack", action_id, cost_ap, args)
end
-- makes the actual bullets do no damage and spreads them in a repeating pattern
function BulletHellOverwriteShots(attack)
local weapon = attack.weapon
local halfAngle = DivRound(weapon.OverwatchAngle, 2)
local newAngle = halfAngle
local angleStep = MulDivRound(weapon.OverwatchAngle, 2, #attack.shots)
for i, shot in ipairs(attack.shots) do
shot.target_pos = RotateAxis(shot.target_pos, point(0, 0, 4069) , newAngle, shot.attack_pos)
shot.stuck_pos = RotateAxis(shot.stuck_pos, point(0, 0, 4069) , newAngle, shot.attack_pos)
if abs(newAngle) >= halfAngle then
angleStep = -angleStep
end
newAngle = newAngle + angleStep
end
end
function Unit:GrizzlyPerk(action_id, cost_ap, args)
self:FirearmAttack(action_id, 0, args)
end
function OnMsg.CombatEnd()
for _, unit in ipairs(g_Units) do
unit.last_attack_pos = false
end
end