|
MapVar("g_UnitCombatBadgesEnabled", true) |
|
|
|
GroundOrientOffsets = { |
|
Human = { |
|
point(40,-40) * const.SlabSizeX / 100, |
|
point(40, 40) * const.SlabSizeX / 100, |
|
point(-100, 0) * const.SlabSizeX / 100, |
|
}, |
|
Crocodile = { |
|
point(100,-40) * const.SlabSizeX / 100, |
|
point(100, 40) * const.SlabSizeX / 100, |
|
point(-100, 0) * const.SlabSizeX / 100, |
|
}, |
|
OneTile = { |
|
point(40,-40) * const.SlabSizeX / 100, |
|
point(40, 40) * const.SlabSizeX / 100, |
|
point(-40, 0) * const.SlabSizeX / 100, |
|
}, |
|
} |
|
|
|
AppearanceObjectAME.flags.gofUnitLighting = true |
|
|
|
local AnimationStyleUnits = { "Male", "Female", "Crocodile", "Hyena", "Hen", "AmbientLifeMarker" } |
|
|
|
WeaponVisualClasses = { "WeaponVisual", "GrenadeVisual" } |
|
|
|
local GetAnimationStyleUnitEntities = { |
|
Male = "Male", |
|
Female = "Female", |
|
Crocodile = "Animal_Crocodile", |
|
Hyena = "Animal_Hyena", |
|
Hen = "Animal_Hen", |
|
} |
|
|
|
function GetAnimationStyleUnitEntity(set) |
|
return GetAnimationStyleUnitEntities[set] |
|
end |
|
|
|
function GetAnimationStyleUnits() |
|
return AnimationStyleUnits |
|
end |
|
|
|
function Unit:GetAnimationStyleUnit() |
|
return self.species == "Human" and self.gender or self.species |
|
end |
|
|
|
function Unit:ApplyAppearance(appearance, force) |
|
AppearanceObject.ApplyAppearance(self, appearance, force) |
|
self.gender = self:GetGender() |
|
if self.Headshot then |
|
self:SetHeadshot(true) |
|
end |
|
if self.target_dummy then |
|
self.target_dummy:ApplyAppearance(self.Appearance) |
|
end |
|
end |
|
|
|
local maxStainsAtDetailLevel = { |
|
["Very Low"] = 2, |
|
["Low"] = 2, |
|
["Medium"] = 3, |
|
["High"] = 5, |
|
} |
|
|
|
function Unit:UpdateGasMaskVisibility() |
|
if self:GetItemInSlot("Head", "GasMaskBase") then |
|
AppearanceObject.EquipGasMask(self) |
|
else |
|
AppearanceObject.UnequipGasMask(self) |
|
end |
|
end |
|
|
|
function Unit:UpdateOutfit(appearance) |
|
appearance = appearance or self:ChooseAppearance() |
|
local appear_preset = appearance and AppearancePresets[appearance] |
|
if not appear_preset then |
|
appearance = self.spawner and self.spawner.Appearance or nil |
|
end |
|
|
|
self:StopAnimMomentHook() |
|
|
|
if appearance and appearance ~= self.Appearance then |
|
local anim = self:GetStateText() |
|
local phase = self:GetAnimPhase() |
|
self:ApplyAppearance(appearance) |
|
self:SetStateText(anim, const.eKeepComponentTargets) |
|
self:SetAnimPhase(1, phase) |
|
end |
|
|
|
self:FlushCombatCache() |
|
local weapons_set1 |
|
if IsSetpiecePlaying() and IsSetpieceActor(self) then |
|
weapons_set1 = self:GetEquippedWeapons("SetpieceWeapon") |
|
end |
|
if not weapons_set1 or #weapons_set1 == 0 then |
|
weapons_set1 = self:GetEquippedWeapons(self.current_weapon) |
|
end |
|
local weapons_set2 = self:GetEquippedWeapons(self.current_weapon == "Handheld A" and "Handheld B" or "Handheld A") |
|
local equipped_items |
|
if #weapons_set1 > 0 or #weapons_set2 > 0 then |
|
equipped_items = { |
|
weapons_set1[1] or false, |
|
weapons_set1[2] or false, |
|
weapons_set2[1] or false, |
|
weapons_set2[2] or false, |
|
} |
|
end |
|
self:ForEachAttach(WeaponVisualClasses, function(o, equipped_items) |
|
if o.weapon and not table.find(equipped_items, o.weapon) then |
|
DoneObject(o) |
|
end |
|
end, equipped_items) |
|
local item_scale = CheatEnabled("BigGuns") and 250 or 100 |
|
for equip_index, item in ipairs(equipped_items) do |
|
local o = item and IsKindOfClasses(item, "Firearm", "MeleeWeapon", "HeavyWeapon") and item:GetVisualObj() |
|
if o then |
|
o.equip_index = equip_index |
|
o:SetScale(item_scale) |
|
if o ~= self.bombard_weapon then |
|
local parent = o:GetParent() |
|
if parent ~= self then |
|
self:Attach(o) |
|
end |
|
end |
|
end |
|
end |
|
self.anim_moment_fx_target = equipped_items and (equipped_items[1] and equipped_items[1].visual_obj or equipped_items[2] and equipped_items[2].visual_obj) or self:GetAttach("WeaponVisual") |
|
self:UpdateAttachedWeapons() |
|
|
|
self:UpdateGasMaskVisibility() |
|
Msg("OnUpdateItemsVisuals", self) |
|
|
|
self:SetContourOuterOccludeRecursive(true) |
|
self:SetHierarchyGameFlags(const.gofUnitLighting) |
|
self:StartAnimMomentHook() |
|
|
|
DeleteBadgesFromTargetOfPreset("CombatBadge", self) |
|
self.combat_badge = false |
|
self.ui_badge = false |
|
DeleteBadgesFromTargetOfPreset("NpcBadge", self) |
|
|
|
if not self:IsDead() and (GameState.entered_sector or IsCompetitiveGame() or g_TestExploration) and g_UnitCombatBadgesEnabled then |
|
local badge = CreateBadgeFromPreset("CombatBadge", self, self) |
|
self.combat_badge = badge |
|
self.ui_badge = badge.ui |
|
|
|
if self.ImportantNPC then |
|
CreateBadgeFromPreset("NpcBadge", { target = self, spot = self:GetInteractableBadgeSpot() or "Origin" }, self) |
|
end |
|
end |
|
|
|
self:UpdateModifiedAnim() |
|
self:UpdateMoveAnim() |
|
|
|
|
|
for i, stain in ipairs(self.stains) do |
|
|
|
stain:Apply(self) |
|
end |
|
if not IsRealTimeThread() and self:IsIdleCommand() and CurrentThread() ~= self.command_thread then |
|
|
|
|
|
if self.command ~= "Hang" and self.command ~= "Cower" then |
|
self:SetCommand("Idle") |
|
end |
|
end |
|
end |
|
|
|
function Unit:UpdatePreparedAttackAndOutfit() |
|
|
|
self:FlushCombatCache() |
|
if self:HasPreparedAttack() then |
|
local params = g_Combat and self.combat_behavior_params or self.behavior_params or empty_table |
|
local action_id = params[1] |
|
local action = action_id and CombatActions[action_id] |
|
if not action or not action:GetAttackWeapons(self) then |
|
self:InterruptPreparedAttack() |
|
self:RemovePreparedAttackVisuals() |
|
end |
|
end |
|
|
|
self:UpdateOutfit() |
|
end |
|
|
|
function Unit:GetGender() |
|
local appearance = AppearancePresets[self.Appearance] |
|
if appearance and self.species == "Human" then |
|
if appearance.Body then |
|
if IsKindOf(g_Classes[appearance.Body], "CharacterBodyMale") then |
|
return "Male" |
|
end |
|
else |
|
if self:GetEntity() == "Male" then |
|
return "Male" |
|
end |
|
end |
|
return "Female" |
|
end |
|
return "N/A" |
|
end |
|
|
|
function Unit:SetState(anim, flags, crossfade, ...) |
|
AnimChangeHook.SetState(self, anim, flags or 0, not GameTimeAdvanced and 0 or crossfade, ...) |
|
end |
|
|
|
function Unit:SetAnim(channel, anim, flags, crossfade, ...) |
|
AnimChangeHook.SetAnim(self, channel, anim, flags or 0, not GameTimeAdvanced and 0 or crossfade, ...) |
|
end |
|
|
|
function Unit:RotateAnim(angle, anim) |
|
self:SetState(anim, const.eKeepComponentTargets, Presets.ConstDef.Animation.BlendTimeRotateOnSpot.value) |
|
self:SetIK("AimIK", false) |
|
local duration = self:TimeToAnimEnd() |
|
local start_angle = self:GetVisualOrientationAngle() |
|
local delta = AngleDiff(angle, start_angle) |
|
local step_angle = self:GetStepAngle() |
|
if delta * step_angle < 0 then |
|
if delta > 0 then |
|
delta = delta - 360 * 60 |
|
else |
|
delta = delta + 360 * 60 |
|
end |
|
end |
|
local steps = self.ground_orient and 20 or 2 |
|
for i = 1, steps do |
|
local a = start_angle + i * delta / steps |
|
local t = duration * i / steps - duration * (i - 1) / steps |
|
self:SetOrientationAngle(a, t) |
|
Sleep(t) |
|
end |
|
if step_angle == 0 then |
|
local anim = self:ModifyWeaponAnim(self:GetIdleBaseAnim()) |
|
self:SetState(anim, const.eKeepComponentTargets, -1) |
|
self:SetOrientationAngle(angle) |
|
end |
|
end |
|
|
|
function Unit:Rotate180(angle, anim) |
|
anim = self:ModifyWeaponAnim(anim) |
|
self:SetState(anim, const.eKeepComponentTargets, Presets.ConstDef.Animation.BlendTimeRotateOnSpot.value) |
|
self:SetIK("AimIK", false) |
|
local duration = self:TimeToAnimEnd() |
|
local start_angle = self:GetVisualOrientationAngle() |
|
local delta = AngleDiff(angle, start_angle) |
|
local step_angle = self:GetStepAngle() |
|
if delta * step_angle < 0 then |
|
if delta > 0 then |
|
delta = delta - 360 * 60 |
|
else |
|
delta = delta + 360 * 60 |
|
end |
|
end |
|
local steps = self.ground_orient and 20 or 2 |
|
for i = 1, steps do |
|
local a = start_angle + i * delta / steps |
|
local t = duration * i / steps - duration * (i - 1) / steps |
|
self:SetOrientationAngle(a, t) |
|
Sleep(t) |
|
end |
|
end |
|
|
|
function Unit:AnimBlendingRotation(angle) |
|
local start_angle = self:GetVisualOrientationAngle() |
|
local angle_diff = AngleDiff(angle, start_angle) |
|
local abs_angle_diff = abs(angle_diff) |
|
if angle_diff == 0 then |
|
if self:TimeToAngleInterpolationEnd() > 0 then |
|
self:SetOrientationAngle(start_angle) |
|
end |
|
elseif abs_angle_diff < 15*60 then |
|
self:SetOrientationAngle(angle, 300) |
|
Sleep(300) |
|
else |
|
if abs_angle_diff > 150*60 then |
|
local anim = "turn_180" |
|
if IsValidAnim(self, anim) then |
|
self:Rotate180(angle, anim) |
|
return |
|
end |
|
end |
|
self:SetIK("AimIK", false) |
|
local anim1 = angle_diff < 0 and "turn_L_45" or "turn_R_45" |
|
local anim2 = angle_diff < 0 and "turn_L_135" or "turn_R_135" |
|
local destructor |
|
if abs_angle_diff <= 45*60 then |
|
self:SetAnim(1, anim1, const.eKeepComponentTargets, Presets.ConstDef.Animation.BlendTimeRotateOnSpot.value) |
|
elseif abs_angle_diff >= 135*60 then |
|
self:SetAnim(1, anim2, const.eKeepComponentTargets, Presets.ConstDef.Animation.BlendTimeRotateOnSpot.value) |
|
else |
|
destructor = true |
|
self:PushDestructor(function(self) |
|
self:ClearAnim(const.PathTurnAnimChnl) |
|
end) |
|
local weight2 = Clamp(abs_angle_diff - 45*60, 0, 90*60) * 100 / (90*60) |
|
self:SetAnim(1, anim1, const.eKeepComponentTargets, -1, 1000, 100 - weight2) |
|
self:SetAnim(const.PathTurnAnimChnl, anim2, const.eKeepComponentTargets, -1, 1000, weight2) |
|
end |
|
local duration = self:TimeToMoment(1, "end") or self:TimeToAnimEnd() |
|
local steps = self.ground_orient and 20 or 2 |
|
for i = 1, steps do |
|
local a = start_angle + i * angle_diff / steps |
|
local t = duration * i / steps - duration * (i - 1) / steps |
|
self:SetOrientationAngle(a, t) |
|
Sleep(t) |
|
end |
|
if destructor then |
|
self:PopAndCallDestructor() |
|
end |
|
end |
|
end |
|
|
|
function Unit:GetRotateAnim(angle_diff, base_idle) |
|
local prefix |
|
if not base_idle then |
|
base_idle = self:GetIdleBaseAnim(self.stance) |
|
end |
|
if string.ends_with(base_idle, "_Aim") then |
|
prefix = base_idle |
|
else |
|
prefix = string.match(base_idle, "(.*)_%w+$") |
|
end |
|
if not prefix then |
|
return |
|
end |
|
local take_cover_prefix = string.match(prefix, "(.*_)TakeCover") |
|
if take_cover_prefix then |
|
prefix = take_cover_prefix .. "Crouch" |
|
end |
|
local rotate_anim |
|
if abs(angle_diff) >= 150*60 then |
|
local anim = prefix .. "_Turn180" |
|
if IsValidAnim(self, anim) then |
|
rotate_anim = anim |
|
end |
|
end |
|
if not rotate_anim then |
|
local anim = prefix .. (angle_diff < 0 and "_TurnLeft" or "_TurnRight") |
|
if IsValidAnim(self, anim) then |
|
rotate_anim = anim |
|
end |
|
end |
|
if not rotate_anim then |
|
if string.ends_with(prefix, "_Aim") then |
|
local anim = string.sub(prefix, 1, -5) .. (angle_diff < 0 and "_TurnLeft" or "_TurnRight") |
|
if IsValidAnim(self, anim) then |
|
rotate_anim = anim |
|
end |
|
end |
|
end |
|
if rotate_anim then |
|
local anim_rotation_angle = self:GetStepAngle(rotate_anim) |
|
if anim_rotation_angle == 0 then |
|
StoreErrorSource(self, string.format("%s animation %s should have compansated rotation", self:GetEntity(), rotate_anim)) |
|
end |
|
end |
|
return rotate_anim |
|
end |
|
|
|
function Unit:IdleRotation(angle, time) |
|
if not GameTimeAdvanced then |
|
time = 0 |
|
else |
|
time = time or 300 |
|
end |
|
if time > 0 then |
|
local start_angle = self:GetVisualOrientationAngle() |
|
local angle_diff = AngleDiff(angle, start_angle) |
|
local steps = self.ground_orient and 20 or 2 |
|
for i = 1, steps do |
|
local a = start_angle + i * angle_diff / steps |
|
local t = time * i / steps - time * (i - 1) / steps |
|
self:SetOrientationAngle(a, t) |
|
Sleep(t) |
|
end |
|
else |
|
self:SetOrientationAngle(angle) |
|
end |
|
end |
|
|
|
function Unit:AnimatedRotation(angle, base_idle) |
|
if not GameTimeAdvanced then |
|
self:SetOrientationAngle(angle) |
|
return |
|
end |
|
local start_angle = self:GetVisualOrientationAngle() |
|
if angle == start_angle then |
|
return |
|
end |
|
local angle_diff = AngleDiff(angle, start_angle) |
|
if abs(angle_diff) < 45*60 then |
|
self:SetOrientationAngle(angle, 300) |
|
return |
|
end |
|
local move_style = GetAnimationStyle(self, self.cur_move_style) |
|
if move_style then |
|
local rotate_anim |
|
if abs(angle_diff) >= 30*60 then |
|
if angle_diff < 0 then |
|
rotate_anim = move_style.TurnOnSpot_Left |
|
else |
|
rotate_anim = move_style.TurnOnSpot_Right |
|
end |
|
end |
|
if rotate_anim and IsValidAnim(self, rotate_anim) then |
|
self:RotateAnim(angle, rotate_anim) |
|
else |
|
self:SetOrientationAngle(angle, 300) |
|
end |
|
return |
|
end |
|
if self.species ~= "Human" then |
|
self:AnimBlendingRotation(angle) |
|
return |
|
end |
|
if not base_idle then |
|
base_idle = self:GetIdleBaseAnim(self.stance) |
|
end |
|
local rotate_anim = self:GetRotateAnim(angle_diff, base_idle) |
|
if not rotate_anim then |
|
self:SetRandomAnim(base_idle, const.eKeepComponentTargets) |
|
self:IdleRotation(angle) |
|
elseif string.ends_with(rotate_anim, "180") then |
|
self:Rotate180(angle, rotate_anim) |
|
else |
|
self:RotateAnim(angle, rotate_anim) |
|
end |
|
end |
|
|
|
function Unit:PlayTransitionAnims(target_anim, angle) |
|
self:ReturnToCover() |
|
local cur_anim = self:GetStateText() |
|
if IsAnimVariant(cur_anim, target_anim) then |
|
return |
|
end |
|
if self.bombard_weapon then |
|
self:PreparedBombardEnd() |
|
end |
|
local cur_anim_style = GetAnimationStyle(self, self.cur_idle_style) |
|
if cur_anim_style and (cur_anim_style.End or "") ~= "" and not cur_anim_style:HasAnimation(target_anim) and cur_anim_style.Start ~= target_anim then |
|
self:SetState(cur_anim_style.End) |
|
Sleep(self:TimeToAnimEnd()) |
|
end |
|
PlayTransitionAnims(self, target_anim, angle) |
|
end |
|
|
|
local WeaponAttachSpots = { |
|
Hand = { "Weaponr", "Weaponl" }, |
|
Shoulder = { "Weaponrb", "Weaponlb" }, |
|
Leg = { "Weaponrs", "Weaponls" }, |
|
Mortar = { "Mortar", "Mortar" }, |
|
LegKnife = { "Weaponrknife", "Weaponlknife" }, |
|
ShoulderKnife = { "Weaponrbknife", "Weaponlbknife" }, |
|
} |
|
|
|
BlockedSpotsVariants = { |
|
["Weaponrknife"] = "Weaponrs", |
|
["Weaponlknife"] = "Weaponls", |
|
} |
|
|
|
local HolsterAttachSpots = { |
|
Weaponrb = true, |
|
Weaponlb = true, |
|
Weaponrs = true, |
|
Weaponls = true, |
|
Weaponrknife = true, |
|
Weaponlknife = true, |
|
Weaponrbknife = true, |
|
Weaponlbknife = true, |
|
} |
|
|
|
local mkoffset = point(0,0,30*guic) |
|
local WeaponAttachOffset = |
|
{ |
|
Weaponr = { |
|
["mk_Standing_Aim_Forward"] = mkoffset, |
|
["mk_Standing_Aim_Down"] = mkoffset, |
|
["mk_Left_Aim_Start"] = mkoffset, |
|
["mk_Right_Aim_Start"] = mkoffset, |
|
["mk_Standing_Fire"] = mkoffset, |
|
}, |
|
} |
|
|
|
local MortarDrawnAnims = { |
|
nw_Standing_MortarIdle = true, |
|
nw_Standing_MortarEnd = true, |
|
nw_Standing_MortarLoad = true, |
|
nw_Standing_MortarFire = true, |
|
} |
|
|
|
local function GetItemAttachSpot(unit, item, equip_index, holster, avatar) |
|
local slot |
|
if holster == nil then |
|
if equip_index ~= 1 and equip_index ~= 2 then |
|
holster = true |
|
else |
|
local anim = unit:GetStateText() |
|
if item.WeaponType == "Mortar" then |
|
if MortarDrawnAnims[anim] then |
|
return |
|
end |
|
holster = true |
|
elseif (avatar or unit):HasStatusEffect("ManningEmplacement") then |
|
holster = true |
|
else |
|
local starts_with = string.starts_with |
|
if starts_with(anim, "nw_") then |
|
holster = true |
|
elseif starts_with(anim, "gr_") then |
|
holster = true |
|
elseif starts_with(anim, "civ_") then |
|
holster = true |
|
elseif starts_with(anim, "mk_") then |
|
if item.WeaponType ~= "MeleeWeapon" then |
|
holster = true |
|
end |
|
end |
|
end |
|
end |
|
end |
|
if holster then |
|
slot = item.HolsterSlot |
|
if not WeaponAttachSpots[slot] then |
|
slot = item.HandSlot == "OneHanded" and "Leg" or "Shoulder" |
|
end |
|
for i, component in pairs(item.components) do |
|
local visuals = (WeaponComponents[component] or empty_table).Visuals or empty_table |
|
local idx = table.find(visuals, "ApplyTo", item.class) |
|
if idx then |
|
local component_data = visuals[idx] |
|
local override_holster_slot = component_data.OverrideHolsterSlot |
|
if override_holster_slot == "Sholder" then |
|
slot = "Shoulder" |
|
break |
|
elseif override_holster_slot == "Leg" then |
|
slot = "Leg" |
|
end |
|
end |
|
end |
|
else |
|
slot = "Hand" |
|
end |
|
if slot == "Leg" then |
|
if IsKindOf(item, "MeleeWeapon") then |
|
slot = "LegKnife" |
|
end |
|
elseif slot == "Shoulder" then |
|
if IsKindOf(item, "MeleeWeapon") then |
|
slot = "ShoulderKnife" |
|
end |
|
end |
|
local spot = WeaponAttachSpots[slot][(equip_index == 2 or equip_index == 4) and 2 or 1] |
|
return spot |
|
end |
|
|
|
local function GetItemSpotAttachment(unit, spot, attach) |
|
local item = attach.weapon |
|
local attach_axis, attach_angle, attach_offset, attach_state |
|
if HolsterAttachSpots[spot] then |
|
if attach:HasSpot("Holster") then |
|
local offset = GetWeaponRelativeSpotPos(attach, "Holster") |
|
if offset then |
|
attach_offset = -offset |
|
end |
|
if IsKindOf(item, "RPG7") then |
|
attach_axis = axis_z |
|
attach_angle = 180*60 |
|
attach_offset = RotateAxis(attach_offset, attach_axis, attach_angle) |
|
end |
|
end |
|
else |
|
local spot_offset_by_anim = WeaponAttachOffset[spot] |
|
local anim = unit:GetStateText() |
|
if spot_offset_by_anim then |
|
attach_offset = spot_offset_by_anim[anim] |
|
end |
|
if spot == "Weaponr" and IsKindOf(item, "MeleeWeapon") then |
|
if unit.gender == "Female" then |
|
if IsKindOf(item, "MacheteWeapon") then |
|
attach_axis = axis_x |
|
attach_angle = 180*60 |
|
elseif anim == "mk_Standing_Aim_Forward" then |
|
attach_axis = axis_x |
|
attach_angle = 90*60 |
|
attach_offset = point(0*guic,-30*guic,0*guic) |
|
end |
|
elseif IsKindOf(item, "MacheteWeapon") then |
|
attach_offset = false |
|
end |
|
end |
|
end |
|
if attach_offset then |
|
attach_offset = MulDivRound(attach_offset, attach:GetScale(), 100) |
|
end |
|
if item and item.WeaponType == "Mortar" then |
|
attach_state = "packed" |
|
end |
|
return attach_axis or axis_x, attach_angle or 0, attach_offset, attach_state |
|
end |
|
|
|
local function AttachVisualItem(unit, spot, attach) |
|
local attach_axis, attach_angle, attach_offset, attach_state = GetItemSpotAttachment(unit, spot, attach) |
|
unit:Attach(attach, unit:GetSpotBeginIndex(spot)) |
|
attach:SetAttachAxis(attach_axis or axis_x) |
|
attach:SetAttachAngle(attach_angle or 0) |
|
attach:SetAttachOffset(attach_offset or point30) |
|
if attach_state and attach:GetStateText() ~= attach_state then |
|
attach:SetState(attach_state) |
|
end |
|
end |
|
|
|
function GetAttackRelativePos(unit, anim, anim_phase, visual_weapon, weapon_attach_spot, attack_spot) |
|
anim_phase = anim_phase or unit:GetAnimMoment(anim, "hit") or 0 |
|
local offset |
|
if visual_weapon then |
|
if not weapon_attach_spot then |
|
weapon_attach_spot = GetItemAttachSpot(unit, visual_weapon.weapon, visual_weapon.equip_index, false) or "Weaponr" |
|
end |
|
local spot_pos, spot_angle, spot_axis = unit:GetRelativeAttachSpotLoc(anim, anim_phase, unit, unit:GetSpotBeginIndex(weapon_attach_spot)) |
|
local attach_axis, attach_angle, attach_offset = GetItemSpotAttachment(unit, weapon_attach_spot, visual_weapon) |
|
local weapon_axis, weapon_angle = ComposeRotation(attach_axis, attach_angle, spot_axis, spot_angle) |
|
local weapon_spot_offset = GetWeaponRelativeSpotPos(visual_weapon, attack_spot or "Muzzle") |
|
offset = spot_pos + (attach_offset or point30) + (weapon_spot_offset and RotateAxis(weapon_spot_offset, weapon_axis, weapon_angle) or point30) |
|
else |
|
if not attack_spot then |
|
attack_spot = unit.species == "Human" and "Weaponr" or "Head" |
|
end |
|
offset = unit:GetRelativeAttachSpotLoc(anim, anim_phase, unit, unit:GetSpotBeginIndex(attack_spot)) |
|
end |
|
return offset |
|
end |
|
|
|
function GetAttackPos(unit, pos, axis, angle, aim_pos, anim, anim_phase, visual_weapon, weapon_attach_spot, attack_spot) |
|
local offset = GetAttackRelativePos(unit, anim, anim_phase, visual_weapon, weapon_attach_spot, attack_spot) |
|
if not pos:IsValidZ() then |
|
pos = pos:SetTerrainZ() |
|
end |
|
local spot_pos = pos + RotateAxis(offset, axis, angle) |
|
if aim_pos and aim_pos:IsValid() then |
|
local center = pos + RotateAxis(offset:SetX(0), axis, angle) |
|
spot_pos = center + SetLen(aim_pos - center, spot_pos:Dist(center)) |
|
end |
|
return spot_pos |
|
end |
|
|
|
function OnMsg.CombatActionEnd(unit) |
|
unit.action_visual_weapon = false |
|
end |
|
|
|
function Unit:AttachActionWeapon(action) |
|
local visual_weapon |
|
if action and (action.id == "KnifeThrow" or string.starts_with(action.id, "ThrowGrenade")) then |
|
local attack_weapon = action:GetAttackWeapons(self) |
|
if attack_weapon then |
|
if attack_weapon.visual_obj and attack_weapon.visual_obj == self then |
|
visual_weapon = attack_weapon.visual_obj |
|
else |
|
for i, classname in ipairs(WeaponVisualClasses) do |
|
visual_weapon = self:GetAttach(classname, function(o, attack_weapon) |
|
return o.weapon == attack_weapon |
|
end, attack_weapon) |
|
if visual_weapon then |
|
break |
|
end |
|
end |
|
if not visual_weapon then |
|
if IsKindOf(attack_weapon, "Grenade") then |
|
visual_weapon = attack_weapon:GetVisualObj(self) |
|
elseif IsKindOfClasses(attack_weapon, "FirearmBase", "MeleeWeapon") or IsKindOf(attack_weapon, "UnarmedWeapon") then |
|
visual_weapon = attack_weapon:CreateVisualObj(self) |
|
end |
|
end |
|
end |
|
end |
|
end |
|
if visual_weapon then |
|
self.action_visual_weapon = visual_weapon |
|
visual_weapon.custom_equip = true |
|
if visual_weapon:GetParent() ~= self then |
|
visual_weapon:ClearHierarchyEnumFlags(const.efVisible) |
|
self:Attach(visual_weapon) |
|
self:UpdateAttachedWeapons() |
|
end |
|
elseif self.action_visual_weapon then |
|
self.action_visual_weapon = false |
|
self:UpdateAttachedWeapons() |
|
end |
|
end |
|
|
|
function AttachVisualItems(obj, attaches, crossfading, holster, avatar) |
|
if not attaches or #attaches == 0 then |
|
return |
|
end |
|
local hidden |
|
if IsKindOf(obj, "Unit") then |
|
local part_in_combat = g_Combat and obj.team and obj.team.side ~= "neutral" |
|
if not part_in_combat then |
|
if obj:GetCommandParam("weapon_anim_prefix") == "civ_" or obj:GetCommandParam("weapon_anim_prefix", "Idle") == "civ_" then |
|
hidden = true |
|
end |
|
end |
|
if obj.carry_flare then |
|
hidden = not obj.visible |
|
end |
|
|
|
for _, attach in ipairs(attaches) do |
|
if IsKindOfClasses(attach, WeaponVisualClasses) and attach.weapon and obj:GetItemSlot(attach.weapon) == "SetpieceWeapon" then |
|
hidden = false |
|
break |
|
end |
|
end |
|
end |
|
if hidden then |
|
for _, attach in ipairs(attaches) do |
|
attach:ClearHierarchyEnumFlags(const.efVisible) |
|
end |
|
return |
|
end |
|
local custom_equip = obj.action_visual_weapon |
|
if custom_equip or (IsKindOf(obj, "Unit") and obj.carry_flare) then |
|
holster = true |
|
end |
|
for i = #attaches, 1, -1 do |
|
local attach = attaches[i] |
|
if IsKindOfClasses(attach, WeaponVisualClasses) and attach.custom_equip and attach ~= custom_equip and (attach.equip_index or 5) > 4 then |
|
DoneObject(attach) |
|
table.remove(attaches, i) |
|
end |
|
end |
|
local wait_crossfade, grip_modify |
|
local spot_attach = {} |
|
table.sort(attaches, function(o1, o2) return o1.equip_index < o2.equip_index end) |
|
for _, attach in ipairs(attaches) do |
|
local item = attach.weapon |
|
local spot |
|
local cur_spot = attach:GetAttachSpotName() |
|
if attach == custom_equip then |
|
spot = WeaponAttachSpots["Hand"][1] or cur_spot |
|
elseif item then |
|
spot = GetItemAttachSpot(obj, item, attach.equip_index, holster, avatar) or cur_spot |
|
end |
|
if spot then |
|
if spot ~= cur_spot and crossfading and not HolsterAttachSpots[spot] then |
|
wait_crossfade = true |
|
else |
|
AttachVisualItem(obj, spot, attach) |
|
end |
|
spot_attach[spot] = attach |
|
if item and item.class == "Gewehr98" and spot == "Weaponr" then |
|
|
|
end |
|
end |
|
end |
|
local channel = const.AnimChannel_RightHandGrip |
|
if grip_modify then |
|
if GetStateName(obj:GetAnim(channel)) ~= "ar_RHand_AltGrip_Rifles" then |
|
obj:SetAnimMask(channel, "RightHand") |
|
obj:SetAnim(channel, "ar_RHand_AltGrip_Rifles") |
|
obj:SetAnimWeight(channel, 1000) |
|
end |
|
else |
|
obj:ClearAnim(channel) |
|
end |
|
|
|
|
|
local blocked_spots = (avatar or obj).blocked_spots |
|
local flare = IsKindOf(obj, "Unit") and obj.carry_flare and obj.visible |
|
for _, attach in ipairs(attaches) do |
|
local spot = attach:GetAttachSpotName() |
|
local is_blocked = blocked_spots and (blocked_spots[spot] or blocked_spots[BlockedSpotsVariants[spot]]) |
|
if is_blocked or spot_attach[spot] ~= attach then |
|
if flare and IsKindOf(attach, "GrenadeVisual") and attach.fx_actor_class == "FlareStick" then |
|
attach:SetHierarchyEnumFlags(const.efVisible) |
|
else |
|
attach:ClearHierarchyEnumFlags(const.efVisible) |
|
end |
|
else |
|
attach:SetHierarchyEnumFlags(const.efVisible) |
|
attach:SetContourOuterOccludeRecursive(true) |
|
end |
|
local parts = attach.parts |
|
if parts then |
|
local is_holstered = attach.equip_index ~= 1 and attach.equip_index ~= 2 |
|
if parts.Bipod and parts.Bipod:HasState("folded") then |
|
local bipod_state = not is_holstered and IsKindOf(obj, "Unit") and obj.stance == "Prone" and "idle" or "folded" |
|
if parts.Bipod:GetStateText() ~= bipod_state then |
|
parts.Bipod:SetState(bipod_state) |
|
end |
|
end |
|
if parts.Under and parts.Under:HasState("folded") then |
|
local bipod_state = not is_holstered and IsKindOf(obj, "Unit") and obj.stance == "Prone" and "idle" or "folded" |
|
if parts.Under:GetStateText() ~= bipod_state then |
|
parts.Under:SetState(bipod_state) |
|
end |
|
end |
|
if parts.Barrel and parts.Barrel:HasState("folded") then |
|
local bipod_state = not is_holstered and IsKindOf(obj, "Unit") and obj.stance == "Prone" and "idle" or "folded" |
|
if parts.Barrel:GetStateText() ~= bipod_state then |
|
parts.Barrel:SetState(bipod_state) |
|
end |
|
end |
|
end |
|
end |
|
return wait_crossfade |
|
end |
|
|
|
function Unit:UpdateAttachedWeapons(crossfade) |
|
DeleteThread(self.update_attached_weapons_thread) |
|
self.update_attached_weapons_thread = false |
|
local attaches = self:GetAttaches(WeaponVisualClasses) |
|
if not attaches then return end |
|
local wait_crossfade = AttachVisualItems(self, attaches, (crossfade ~= 0) and not IsPaused()) |
|
if wait_crossfade then |
|
self.update_attached_weapons_thread = CreateGameTimeThread(function(self, delay) |
|
Sleep(delay) |
|
self.update_attached_weapons_thread = false |
|
if IsValid(self) then |
|
local attaches = self:GetAttaches(WeaponVisualClasses) |
|
AttachVisualItems(self, attaches) |
|
end |
|
end, self, crossfade and crossfade > 0 and crossfade or hr.ObjAnimDefaultCrossfadeTime) |
|
return |
|
end |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
end |
|
|
|
function Unit:AnimationChanged(channel, old_anim, flags, crossfade) |
|
if channel == 1 then |
|
self:UpdateAttachedWeapons(crossfade) |
|
self:UpdateWeaponGrip() |
|
end |
|
AnimMomentHook.AnimationChanged(self, channel, old_anim, flags, crossfade) |
|
end |
|
|
|
function Unit:GetWeaponAnimPrefix() |
|
if self.species ~= "Human" then |
|
return "" |
|
end |
|
if self.die_anim_prefix then |
|
return self.die_anim_prefix |
|
end |
|
local prefix = self:GetCommandParam("weapon_anim_prefix") or self:GetCommandParam("weapon_anim_prefix", "Idle") |
|
if prefix then |
|
return prefix |
|
end |
|
if self.action_visual_weapon then |
|
prefix = GetWeaponAnimPrefix(self.action_visual_weapon.weapon) |
|
return prefix |
|
end |
|
if self.infected then |
|
return "inf_" |
|
end |
|
local weapon, weapon2 = self:GetActiveWeapons() |
|
if not weapon and (not self.team or self.team.side == "neutral") then |
|
return "civ_" |
|
end |
|
return GetWeaponAnimPrefix(weapon, weapon2) |
|
end |
|
|
|
function Unit:GetWeaponAnimPrefixFallback() |
|
return "" |
|
end |
|
|
|
local human_one_slab_anims = { "DeathOnSpot", "DeathFall", "DeathWindow" } |
|
|
|
function Unit:GetGroundOrientOffsets(anim) |
|
local offsets = GroundOrientOffsets[self.species] |
|
if anim and self.species == "Human" then |
|
for _, pattern in ipairs(human_one_slab_anims) do |
|
if string.match(anim, pattern) then |
|
offsets = GroundOrientOffsets["OneTile"] |
|
end |
|
end |
|
end |
|
return offsets or GroundOrientOffsets["OneTile"] |
|
end |
|
|
|
function Unit:UpdateGroundOrientParams() |
|
local offsets = self:GetGroundOrientOffsets(self:GetStateText()) |
|
pf.SetGroundOrientOffsets(self, table.unpack(offsets)) |
|
end |
|
|
|
|
|
function Unit:GetFootPlantPosProps(stance) |
|
if self.species == "Human" then |
|
if self:HasStatusEffect("ManningEmplacement") then |
|
return false, false |
|
end |
|
if (stance or self.stance) == "Prone" or self:IsDead() then |
|
return false, true |
|
end |
|
return true, false |
|
elseif self.species == "Crocodile" then |
|
return false, true |
|
elseif self.species == "Hyena" then |
|
return true, false |
|
end |
|
return false, false |
|
end |
|
|
|
function Unit:SetFootPlant(set, time, stance) |
|
local footplant, ground_orient |
|
if set and not config.IKDisabled then |
|
footplant, ground_orient = self:GetFootPlantPosProps(stance) |
|
end |
|
local label = "FootPlantIK" |
|
local ikCmp = self:GetAnimComponentIndexFromLabel(1, label) |
|
if ikCmp ~= 0 then |
|
if footplant then |
|
self:SetAnimComponentTarget(1, ikCmp, "IKFootPlant", 10*guic, 10*guic) |
|
else |
|
self:RemoveAnimComponentTarget(1, ikCmp) |
|
end |
|
end |
|
if ground_orient then |
|
if not self.ground_orient then |
|
self.ground_orient = true |
|
self:ChangePathFlags(const.pfmGroundOrient) |
|
self:SetGroundOrientation(self:GetOrientationAngle(), time or 300) |
|
end |
|
else |
|
if self.ground_orient then |
|
self.ground_orient = false |
|
self:ChangePathFlags(0, const.pfmGroundOrient) |
|
self:SetAxisAngle(axis_z, self:GetVisualOrientationAngle(), time or 300) |
|
else |
|
self:ChangePathFlags(0, const.pfmGroundOrient) |
|
self:SetAxis(axis_z) |
|
end |
|
end |
|
end |
|
|
|
MapVar("g_IKDebug", false) |
|
|
|
MapVar("g_IKDebugThread", CreateRealTimeThread(function() |
|
while true do |
|
if g_IKDebug then |
|
DbgClearVectors() |
|
DbgClearTexts() |
|
for unit, target in pairs(g_IKDebug) do |
|
|
|
DbgAddText("Target", target, const.clrWhite) |
|
local weapon = unit:GetActiveWeapons("Firearm") |
|
local spot_obj = weapon and GetWeaponSpotObject(weapon:GetVisualObj(), "Muzzle") |
|
local wpos = spot_obj and spot_obj:GetSpotVisualPos(spot_obj:GetSpotBeginIndex("Muzzle")) |
|
if wpos then |
|
DbgAddVector(wpos, target - wpos, const.clrWhite) |
|
end |
|
|
|
local upos = weapon and GetWeaponSpotPos(weapon:GetVisualObj(), "Muzzle") |
|
if upos then |
|
DbgAddVector(upos, target - upos, const.clrGreen) |
|
end |
|
end |
|
end |
|
Sleep(100) |
|
end |
|
end)) |
|
|
|
function Unit:GetIK(label) |
|
local ikCmp = self:GetAnimComponentIndexFromLabel(1, label) |
|
if ikCmp == 0 then |
|
return |
|
end |
|
local direction = self:GetAnimComponentTargetDirection(1, ikCmp) |
|
return direction |
|
end |
|
|
|
function Unit:UpdateWeaponGrip(anim) |
|
if not IsEditorActive() then |
|
anim = anim or self:GetStateText() |
|
if string.starts_with(anim, "ar_") or string.starts_with(anim, "arg_") then |
|
self:SetWeaponGrip(true) |
|
return |
|
end |
|
end |
|
self:SetWeaponGrip(false) |
|
end |
|
|
|
function Unit:SetWeaponGrip(set) |
|
local ikCmp = self:GetAnimComponentIndexFromLabel(1, "LHandWeaponGrip") |
|
if ikCmp == 0 then |
|
return |
|
end |
|
if set then |
|
if config.Force_Selection_WeaponGripIK and self == SelectedObj then |
|
|
|
elseif config.IKDisabled or config.WeaponGripIKDisabled then |
|
set = false |
|
end |
|
end |
|
if set then |
|
local weapon, weapon2 = self:GetActiveWeapons() |
|
local weapon_obj = not weapon2 and weapon and weapon:GetVisualObj(self) |
|
if weapon_obj and weapon_obj:GetAttachSpotName() == "Weaponr" then |
|
local weapon = weapon_obj.weapon |
|
local spot = weapon and weapon:GetLHandGripSpot() |
|
if spot then |
|
local offset = GetWeaponRelativeSpotPos(weapon_obj, spot) |
|
if offset then |
|
local spot = weapon_obj:GetAttachSpot() |
|
self:SetAnimComponentTarget(1, ikCmp, "IKWeaponGrip", spot, offset) |
|
return |
|
end |
|
end |
|
end |
|
end |
|
self:RemoveAnimComponentTarget(1, ikCmp, true) |
|
end |
|
|
|
function Unit:CalcIKIntermediateTarget(ikCmp, target) |
|
local direction = self:GetAnimComponentTargetDirection(1, ikCmp) |
|
if direction then |
|
local face_angle = self:GetOrientationAngle() |
|
local target_angle = (IsValid(target) and self:AngleToObject(target) or self:AngleToPoint(target)) + self:GetAngle() |
|
local dir_angle = CalcOrientation(direction) |
|
local cur_angle = AngleDiff(dir_angle, face_angle) |
|
local new_angle = AngleDiff(target_angle, face_angle) |
|
if cur_angle * new_angle < 0 and abs(cur_angle - new_angle) > 90*60 then |
|
local pos = self:GetVisualPos() |
|
local target_pos = (IsValid(target) and target:GetVisualPos() or target) |
|
local new_target = pos + Rotate(target_pos - pos, cur_angle + (cur_angle < 0 and 90*60 or -90*60) - new_angle) |
|
return new_target |
|
end |
|
end |
|
end |
|
|
|
function Unit:SetIK(label, target, spot, initial_dir, time, overridePoseTime) |
|
if config.IKDisabled then |
|
target = false |
|
end |
|
if self.setik_thread then |
|
DeleteThread(self.setik_thread) |
|
self.setik_thread = false |
|
end |
|
local ikCmp = self:GetAnimComponentIndexFromLabel(1, label) |
|
if ikCmp == 0 then |
|
if target then |
|
GameTestsErrorf("once", "Missing IK component %s for %s(%s) in state %s", tostring(label), self.unitdatadef_id, self:GetEntity(), self:GetStateText()) |
|
end |
|
else |
|
local intermediate_target |
|
initial_dir = initial_dir or InvalidPos() |
|
overridePoseTime = overridePoseTime or 0 |
|
time = -1000 |
|
if IsPoint(target) then |
|
if not target:IsValidZ() then target = target:SetTerrainZ() end |
|
intermediate_target = time ~= 0 and self:CalcIKIntermediateTarget(ikCmp, target) |
|
if not intermediate_target then |
|
self:SetAnimComponentTarget(1, ikCmp, target, initial_dir, time, overridePoseTime) |
|
end |
|
elseif IsValid(target) then |
|
local spot_idx = target:GetSpotBeginIndex(spot or "Origin") |
|
local bone = target:GetSpotBone(spot_idx) |
|
if bone and bone ~= "" then |
|
intermediate_target = time ~= 0 and self:CalcIKIntermediateTarget(ikCmp, target) |
|
if not intermediate_target then |
|
self:SetAnimComponentTarget(1, ikCmp, target, bone, initial_dir, time, overridePoseTime) |
|
end |
|
else |
|
local pos = target:GetSpotLocPos(spot_idx) |
|
intermediate_target = time ~= 0 and self:CalcIKIntermediateTarget(ikCmp, pos) |
|
if not intermediate_target then |
|
self:SetAnimComponentTarget(1, ikCmp, pos, initial_dir, time, overridePoseTime) |
|
end |
|
end |
|
else |
|
assert(not target) |
|
self:RemoveAnimComponentTarget(1, ikCmp, true) |
|
end |
|
if intermediate_target then |
|
self:SetAnimComponentTarget(1, ikCmp, intermediate_target, initial_dir, time, overridePoseTime) |
|
self.setik_thread = CreateGameTimeThread(function(self, label, target, spot, initial_dir, time, overridePoseTime) |
|
Sleep(25) |
|
self.setik_thread = false |
|
self:SetIK(label, target, spot, initial_dir, time, overridePoseTime) |
|
end, self, label, target, spot, initial_dir, time, overridePoseTime) |
|
end |
|
end |
|
if g_IKDebug then |
|
g_IKDebug[self] = IsPoint(target) and target or IsValid(target) and target:GetSpotLocPos(target:GetSpotBeginIndex(spot or "Origin")) or nil |
|
end |
|
end |
|
|
|
function Unit:AimIdle() |
|
self.aim_rotate_last_angle = false |
|
self.aim_rotate_cooldown_time = false |
|
if not g_Combat then |
|
local x, y, z = GetPassSlabXYZ(self) |
|
if not x or not self:IsEqualPos(x, y, z) or not CanDestlock(self) then |
|
self:GotoSlab() |
|
end |
|
end |
|
while self.aim_action_id do |
|
local time = GameTime() |
|
local attack_args, attack_results = self:GetAimResults() |
|
self:AimTarget(attack_args, attack_results, false) |
|
Msg("AimIdleLoop") |
|
if time == GameTime() then |
|
Sleep(50) |
|
end |
|
end |
|
self:ForEachAttach("GrenadeVisual", DoneObject) |
|
end |
|
|
|
local aim_rotate_cooldown_times = { |
|
Standing = 250, |
|
Crouch = 500, |
|
Prone = 700, |
|
} |
|
|
|
function Unit:AimTarget(attack_args, attack_results, prepare_to_attack) |
|
if self:HasStatusEffect("ManningEmplacement") then |
|
if self:GetStateText() ~= "hmg_Crouch_Idle" then |
|
self:SetState("hmg_Crouch_Idle", const.eKeepComponentTargets, 0) |
|
end |
|
return |
|
end |
|
if not attack_args then |
|
return |
|
end |
|
local action_id = attack_args.action_id |
|
local action = CombatActions[action_id] |
|
local weapon = action and action:GetAttackWeapons(self) |
|
local prepared_attack = attack_args.opportunity_attack_type == "PinDown" or attack_args.opportunity_attack_type == "Overwatch" |
|
local lof_idx = table.find(attack_args.lof, "target_spot_group", attack_args.target_spot_group or "Torso") |
|
local lof_data = attack_args.lof and attack_args.lof[lof_idx or 1] or attack_args |
|
local aim_pos = lof_data.lof_pos2 |
|
local trajectory = attack_results and attack_results.trajectory |
|
if trajectory and #trajectory > 1 then |
|
local p1 = trajectory[1].pos |
|
local p2 = trajectory[2].pos |
|
if p1 ~= p2 then |
|
aim_pos = p1 + SetLen(p2 - p1, 10*guim) |
|
end |
|
end |
|
aim_pos = aim_pos or attack_args.target |
|
if attack_args.OverwatchAction and lof_data.lof_pos1 then |
|
if self.ground_orient then |
|
local axis = self:GetAxis() |
|
local angle = self:GetAngle() |
|
local p1 = RotateAxis(lof_data.lof_pos1, axis, -angle) |
|
local p2 = RotateAxis(aim_pos, axis, -angle) |
|
aim_pos = RotateAxis(p2:SetZ(p1:z()), axis, angle) |
|
else |
|
aim_pos = aim_pos:SetZ(lof_data.lof_pos1:z()) |
|
end |
|
end |
|
|
|
local rotate_to_target = prepare_to_attack or IsValid(attack_args.target) and IsKindOf(attack_args.target, "Unit") |
|
local aimIK = rotate_to_target and self:CanAimIK(weapon) |
|
local stance = rotate_to_target and attack_args.stance or self.stance |
|
local quick_play = not GameTimeAdvanced or self:CanQuickPlayInCombat() |
|
local idle_aiming, rotate_cooldown_disable |
|
|
|
if action_id == "MeleeAttack" then |
|
idle_aiming = true |
|
elseif not rotate_to_target and self.stance == "Prone" and attack_args.stance ~= "Prone" then |
|
idle_aiming = true |
|
elseif not rotate_to_target and aimIK and abs(self:AngleToPoint(aim_pos)) > 50*60 then |
|
if self.last_idle_aiming_time then |
|
if GameTime() - self.last_idle_aiming_time > config.IdleAimingDelay then |
|
idle_aiming = true |
|
end |
|
else |
|
self.last_idle_aiming_time = GameTime() |
|
end |
|
else |
|
self.last_idle_aiming_time = false |
|
end |
|
|
|
local aim_anim |
|
if idle_aiming then |
|
local base_idle = self:GetIdleBaseAnim(stance) |
|
if not IsAnimVariant(self:GetStateText(), base_idle) then |
|
if self.stance == "Prone" then |
|
|
|
local visual_stance = string.match(self:GetStateText(), "^%a+_(%a+)_") |
|
if visual_stance == "Standing" or visual_stance == "Crouch" then |
|
local angle = self:GetPosOrientation() |
|
if quick_play then |
|
self:SetOrientationAngle(angle) |
|
else |
|
self:AnimatedRotation(angle, self:GetIdleBaseAnim(visual_stance)) |
|
end |
|
end |
|
self:SetFootPlant(true) |
|
end |
|
if not quick_play then |
|
PlayTransitionAnims(self, base_idle) |
|
end |
|
self:SetRandomAnim(base_idle) |
|
rotate_cooldown_disable = true |
|
end |
|
self:SetIK("AimIK", false) |
|
aimIK = false |
|
aim_anim = self:GetStateText() |
|
else |
|
if (stance == "Standing" or stance == "Crouch") and self.stance == "Prone" then |
|
local cur_anim = self:GetStateText() |
|
if string.match(cur_anim, "%a+_(%a+).*") == "Prone" then |
|
self:SetFootPlant(true, nil, stance) |
|
if not quick_play then |
|
local base_idle = self:GetIdleBaseAnim(stance) |
|
local angle = lof_data.angle or CalcOrientation(lof_data.step_pos, aim_pos) |
|
PlayTransitionAnims(self, base_idle, angle) |
|
end |
|
end |
|
end |
|
self:AttachActionWeapon(action) |
|
aim_anim = self:GetAimAnim(action_id, stance) |
|
end |
|
|
|
if quick_play then |
|
if not self.return_pos and not IsCloser2D(self, lof_data.step_pos, const.SlabSizeX/2) and not attack_args.circular_overwatch then |
|
self.return_pos = GetPassSlab(self) |
|
end |
|
self:SetPos(lof_data.step_pos) |
|
self:SetOrientationAngle(lof_data.angle or CalcOrientation(lof_data.step_pos, aim_pos)) |
|
if self:GetStateText() ~= aim_anim then |
|
self:SetState(aim_anim, const.eKeepComponentTargets, 0) |
|
end |
|
self:SetFootPlant(true) |
|
if aimIK then |
|
self:SetIK("AimIK", aim_pos, nil, nil, 0) |
|
else |
|
self:SetIK("AimIK", false) |
|
end |
|
return |
|
end |
|
|
|
self:SetIK("LookAtIK", false) |
|
self:SetFootPlant(true, nil, stance) |
|
|
|
if rotate_to_target then |
|
local prefix = string.match(aim_anim, "^(%a+_).*") or self:GetWeaponAnimPrefix() |
|
|
|
while true do |
|
|
|
while not IsCloser2D(self, lof_data.step_pos, const.SlabSizeX/2) do |
|
local dummy_angle |
|
if lof_data.step_pos:Dist2D(self.return_pos or self) == 0 then |
|
dummy_angle = CalcOrientation(self.return_pos, aim_pos) |
|
else |
|
dummy_angle = CalcOrientation(self.return_pos or self, lof_data.step_pos) |
|
end |
|
if self:ReturnToCover(prefix) then |
|
|
|
else |
|
|
|
local angle = CalcOrientation(self, lof_data.step_pos) |
|
local rotate = abs(AngleDiff(angle, self:GetVisualOrientationAngle())) > 90*60 |
|
self:SetIK("AimIK", false) |
|
if rotate then |
|
self:AnimatedRotation(angle, aim_anim) |
|
end |
|
if not rotate or self.command ~= "AimIdle" then |
|
local step_to_target = CalcOrientation(lof_data.step_pos, aim_pos) |
|
local cover_side = AngleDiff(step_to_target, angle) < 0 and "Left" or "Right" |
|
local anim = string.format("%s%s_Aim_Start", prefix, cover_side) |
|
if not self.return_pos and not attack_args.circular_overwatch then |
|
self.return_pos = GetPassSlab(self) |
|
end |
|
if IsValidAnim(self, anim) then |
|
anim = self:ModifyWeaponAnim(anim) |
|
self:SetPos(lof_data.step_pos, self:GetAnimDuration(anim)) |
|
self:RotateAnim(step_to_target, anim) |
|
else |
|
local msg = string.format('Missing animation "%s" for "%s"', anim, self.unitdatadef_id) |
|
StoreErrorSource(self, msg) |
|
self:SetState(aim_anim, const.eKeepComponentTargets) |
|
self:SetAngle(step_to_target, 500) |
|
Sleep(500) |
|
end |
|
end |
|
end |
|
|
|
if self.command ~= "AimIdle" then |
|
if not IsCloser2D(self, lof_data.step_pos, const.SlabSizeX/2) then |
|
return |
|
end |
|
break |
|
end |
|
if not self.aim_action_id then |
|
return |
|
end |
|
attack_args, attack_results = self:GetAimResults() |
|
lof_idx = table.find(attack_args.lof, "target_spot_group", attack_args.target_spot_group or "Torso") |
|
lof_data = attack_args.lof and attack_args.lof[lof_idx or 1] or attack_args |
|
aim_pos = lof_data.lof_pos2 or attack_args.target |
|
if attack_results and attack_results.trajectory then |
|
local p1 = attack_results.trajectory[1].pos |
|
local p2 = attack_results.trajectory[2].pos |
|
aim_pos = p1 + SetLen(p2 - p1, 10*guim) |
|
end |
|
end |
|
|
|
if weapon then |
|
self:SetAimFX(weapon:GetVisualObj(self)) |
|
end |
|
|
|
local angle = CalcOrientation(self, aim_pos) |
|
local start_angle = self:GetVisualOrientationAngle() |
|
local angle_diff = AngleDiff(angle, start_angle) |
|
if stance == "Prone" then |
|
if prepared_attack and not attack_args.circular_overwatch then |
|
angle = start_angle |
|
else |
|
if abs(angle_diff) <= 60*60 then |
|
angle = start_angle |
|
else |
|
angle = FindProneAngle(self, nil, angle, 60*60) |
|
end |
|
end |
|
end |
|
|
|
local played_anims = PlayTransitionAnims(self, aim_anim, angle) |
|
if played_anims and self.command == "AimIdle" then |
|
break |
|
end |
|
if self.command ~= "AimIdle" then |
|
if not attack_args.opportunity_attack or abs(AngleDiff(angle, start_angle)) > 45*60 then |
|
self:AnimatedRotation(angle, aim_anim) |
|
end |
|
break |
|
end |
|
|
|
if abs(AngleDiff(angle, self:GetOrientationAngle())) < 1*60 then |
|
break |
|
end |
|
local max_deviation_angle = 45*60 |
|
if abs(angle_diff) < max_deviation_angle and not (prepare_to_attack and prepared_attack) then |
|
self.aim_rotate_last_angle = false |
|
break |
|
end |
|
if not rotate_cooldown_disable then |
|
if not self.aim_rotate_last_angle or abs(AngleDiff(angle, self.aim_rotate_last_angle)) > max_deviation_angle then |
|
self.aim_rotate_last_angle = angle |
|
self.aim_rotate_cooldown_time = GameTime() + (aim_rotate_cooldown_times[stance] or 1000) |
|
break |
|
end |
|
if GameTime() - self.aim_rotate_cooldown_time < 0 then |
|
break |
|
end |
|
end |
|
self.aim_rotate_last_angle = false |
|
self.aim_rotate_cooldown_time = false |
|
local rotate_anim = self:GetRotateAnim(angle_diff, aim_anim) |
|
if not IsValidAnim(self, rotate_anim) then |
|
self:IdleRotation(angle) |
|
break |
|
end |
|
self:SetIK("AimIK", false) |
|
rotate_anim = self:ModifyWeaponAnim(rotate_anim) |
|
if abs(angle_diff) > 150*60 then |
|
self:Rotate180(angle, rotate_anim) |
|
else |
|
self:SetState(rotate_anim, const.eKeepComponentTargets, Presets.ConstDef.Animation.BlendTimeRotateOnSpot.value) |
|
local anim_rotation_angle = self:GetStepAngle() |
|
local duration = self:TimeToAnimEnd() |
|
local rotation_deviation = 45*60 |
|
local steps = 1 + duration / 20 |
|
for i = 1, steps do |
|
local a = start_angle + i * angle_diff / steps |
|
local t = duration * i / steps - duration * (i - 1) / steps |
|
self:SetOrientationAngle(a, t) |
|
Sleep(t) |
|
end |
|
end |
|
self:SetState(aim_anim, const.eKeepComponentTargets) |
|
if aimIK then |
|
self:SetIK("AimIK", aim_pos) |
|
end |
|
end |
|
else |
|
if self.return_pos then |
|
local prefix = string.match(aim_anim, "^(%a+_).*") or self:GetWeaponAnimPrefix() |
|
self:ReturnToCover(prefix) |
|
end |
|
end |
|
|
|
local cur_anim = self:GetStateText() |
|
if cur_anim ~= aim_anim then |
|
self:SetState(aim_anim, const.eKeepComponentTargets) |
|
end |
|
if aimIK then |
|
if not self.aim_rotate_cooldown_time or GameTime() - self.aim_rotate_cooldown_time >= 0 then |
|
self:SetIK("AimIK", aim_pos) |
|
end |
|
else |
|
self:SetIK("AimIK", false) |
|
end |
|
end |
|
|
|
function Unit:SetAimFX(fx_target, delayed) |
|
if self.aim_fx_thread then |
|
DeleteThread(self.aim_fx_thread) |
|
self.aim_fx_thread = false |
|
end |
|
if self.aim_fx_target == (fx_target or false) then |
|
return |
|
end |
|
if delayed then |
|
self.aim_fx_thread = CreateGameTimeThread(function(self) |
|
Sleep(1) |
|
self.aim_fx_thread = false |
|
self:SetAimFX(fx_target) |
|
end, self, fx_target) |
|
return |
|
end |
|
if self.aim_fx_target then |
|
PlayFX("Aim", "end", self, self.aim_fx_target) |
|
end |
|
if fx_target then |
|
PlayFX("Aim", "start", self, fx_target) |
|
end |
|
self.aim_fx_target = fx_target |
|
end |
|
|
|
function Unit:CanAimIK(weapon) |
|
local weapon_type = weapon and weapon.WeaponType |
|
if not weapon_type then |
|
return false |
|
elseif weapon_type == "Grenade" then |
|
return false |
|
elseif weapon_type == "MeleeWeapon" then |
|
return false |
|
elseif weapon_type == "Mortar" then |
|
return false |
|
elseif weapon_type == "FlareGun" then |
|
return false |
|
elseif self:HasStatusEffect("ManningEmplacement") then |
|
return false |
|
end |
|
return true |
|
end |
|
|
|
function NetSyncEvents.Aim(unit, action_id, target) |
|
if not unit then return end |
|
local action = CombatActions[action_id] |
|
if action and action.DisableAimAnim then return end |
|
|
|
local changed = unit:SetAimTarget(action_id, target) |
|
if changed and unit.team and unit.team.control == "UI" then |
|
local playerId = unit:IsLocalPlayerControlled() and netUniqueId or GetOtherPlayerId() |
|
local targetId = IsKindOf(target, "Unit") and target.session_id |
|
SetCoOpPlayerAimingAtUnit(playerId, targetId) |
|
end |
|
end |
|
|
|
function OnMsg.RunCombatAction(action_id, unit) |
|
if unit and action_id ~= "Aim" and unit.aim_action_id then |
|
unit:SetAimTarget() |
|
end |
|
end |
|
|
|
local function playTurnOnFx(unit, weapon) |
|
local visual = weapon and weapon.visual_obj |
|
if not visual then return end |
|
|
|
for slot, component_id in sorted_pairs(weapon.components) do |
|
local component = WeaponComponents[component_id] |
|
if component and component.EnableAimFX then |
|
local fx_actor |
|
for _, descr in ipairs(component and component.Visuals) do |
|
if descr:Match(weapon.class) then |
|
fx_actor = visual.parts[descr.Slot] |
|
if fx_actor then |
|
break |
|
end |
|
end |
|
end |
|
fx_actor = fx_actor or visual |
|
PlayFX("TurnOn", "start", fx_actor) |
|
unit.weapon_light_fx = unit.weapon_light_fx or {} |
|
unit.weapon_light_fx[#unit.weapon_light_fx + 1] = fx_actor |
|
end |
|
end |
|
end |
|
|
|
function Unit:SetWeaponLightFx(enable) |
|
for _, fx_actor in ipairs(self.weapon_light_fx) do |
|
PlayFX("TurnOn", "end", fx_actor) |
|
end |
|
self.weapon_light_fx = false |
|
|
|
if enable and self.visible and not self:CanQuickPlayInCombat() then |
|
local weapon1, weapon2 = self:GetActiveWeapons() |
|
playTurnOnFx(self, weapon1) |
|
playTurnOnFx(self, weapon2) |
|
end |
|
end |
|
|
|
function Unit:SetAimTarget(action_id, target) |
|
if action_id then |
|
local aim_target = target or false |
|
if IsPoint(aim_target) and not aim_target:IsValidZ() then |
|
aim_target = aim_target:SetTerrainZ(2*const.SlabSizeZ) |
|
end |
|
local aim_action_params = self.aim_action_params |
|
if not aim_action_params then |
|
aim_action_params = {} |
|
self.aim_action_params = aim_action_params |
|
end |
|
if self.aim_action_id == action_id and aim_action_params.target == aim_target then |
|
return false |
|
end |
|
|
|
if self.visible and self.aim_action_id ~= action_id then |
|
self:SetWeaponLightFx(true) |
|
end |
|
self.aim_action_id = action_id |
|
aim_action_params.target = aim_target |
|
if self.command == "Idle" then |
|
self:SetCommand("Idle") |
|
end |
|
elseif self.aim_action_id then |
|
self.aim_action_id = false |
|
self.aim_action_params = false |
|
self.aim_results = false |
|
self.aim_attack_args = false |
|
end |
|
return true |
|
end |
|
|
|
function Unit:GetActionResults(action_id, args) |
|
local action = CombatActions[action_id] |
|
if action then |
|
return action:GetActionResults(self, args) |
|
end |
|
end |
|
|
|
function Unit:GetAimResults() |
|
local action = CombatActions[self.aim_action_id] |
|
if not action then |
|
return |
|
elseif not self.aim_results then |
|
|
|
local target = self.aim_action_params and self.aim_action_params.target |
|
if not IsPoint(target) and not IsValid(target) then return end |
|
self.aim_results, self.aim_attack_args = action:GetActionResults(self, self.aim_action_params) |
|
end |
|
return self.aim_attack_args, self.aim_results |
|
end |
|
|
|
local NonStanceActionAnims = { |
|
["Idle"] = "idle", |
|
["Run"] = "walk", |
|
["Death"] = "death", |
|
} |
|
|
|
function Unit:UpdateModifiedAnim() |
|
local modify_animations_ar = false |
|
local weapon, weapon2 = self:GetActiveWeapons("Firearm") |
|
if weapon and not weapon2 then |
|
if weapon.ModifyRightHandGrip then |
|
modify_animations_ar = true |
|
else |
|
for i, component in pairs(weapon.components) do |
|
local visuals = (WeaponComponents[component] or empty_table).Visuals or empty_table |
|
local idx = table.find(visuals, "ApplyTo", weapon.class) |
|
if idx then |
|
local component_data = visuals[idx] |
|
if component_data.ModifyRightHandGrip then |
|
modify_animations_ar = true |
|
break |
|
end |
|
end |
|
end |
|
end |
|
end |
|
if self.modify_animations_ar ~= modify_animations_ar then |
|
self.modify_animations_ar = modify_animations_ar |
|
local anim = self:GetStateText() |
|
local new_anim = modify_animations_ar and self:ModifyWeaponAnim(anim) or GetUnmodifiedAnim(anim) |
|
if new_anim ~= anim then |
|
self:SetState(new_anim, const.eKeepPhase) |
|
end |
|
end |
|
end |
|
|
|
function Unit:ModifyWeaponAnim(anim) |
|
if self.modify_animations_ar then |
|
if string.starts_with(anim, "ar_") then |
|
local new_anim = "arg_" .. string.sub(anim, 4) |
|
if IsValidAnim(self, new_anim) then |
|
return new_anim |
|
end |
|
end |
|
end |
|
return anim |
|
end |
|
|
|
function GetUnmodifiedAnim(anim) |
|
if string.starts_with(anim, "arg_") then |
|
return "ar_" .. string.sub(anim, 5) |
|
end |
|
return anim |
|
end |
|
|
|
function Unit:GetUnmodifiedAnim() |
|
return GetUnmodifiedAnim(self:GetStateText()) |
|
end |
|
|
|
function Unit:GetValidAnim(prefix, stance, action_full) |
|
local name = stance and stance ~= "" and string.format("%s_%s", stance, action_full) or action_full |
|
local base_anim = name |
|
if stance == "" then |
|
base_anim = NonStanceActionAnims[action_full] or base_anim |
|
end |
|
if prefix and prefix ~= "" then |
|
base_anim = prefix .. base_anim |
|
end |
|
local valid = self:HasState(base_anim) and not IsErrorState(self:GetEntity(), base_anim) |
|
if not valid and action_full == "WalkSlow" then |
|
return self:GetValidAnim(prefix, stance, "Walk") |
|
end |
|
return valid, base_anim, name |
|
end |
|
|
|
function IsAnimVariant(anim, base_anim) |
|
anim = GetUnmodifiedAnim(anim) |
|
return (anim == base_anim or string.starts_with(anim, base_anim) and tonumber(string.sub(anim, #base_anim + 1))) and true or false |
|
end |
|
|
|
function GetAnimVariants(entity, base_anim) |
|
if not HasState(entity, base_anim) or IsErrorState(entity, base_anim) then |
|
return {} |
|
end |
|
|
|
local format = string.match(base_anim, ".*%d$") and "%s_%d" or "%s%d" |
|
local anim_variants = {} |
|
local count = 0 |
|
while true do |
|
count = count + 1 |
|
local anim = (count == 1) and base_anim or string.format(format, base_anim, count) |
|
if not HasState(entity, anim) or IsErrorState(entity, anim) then |
|
break |
|
end |
|
table.insert(anim_variants, anim) |
|
end |
|
|
|
return anim_variants |
|
end |
|
|
|
local anim_variations_weight_cache = {} |
|
local anim_variations_phases_chunk = 1000 |
|
local anim_variations_min_time_offset = 2000 |
|
local nearby_unique_anim_distance = 12*guim |
|
|
|
local function GetRandomAnims(entity, base_anim) |
|
local t = anim_variations_weight_cache[entity] |
|
if not t then |
|
t = {} |
|
anim_variations_weight_cache[entity] = t |
|
end |
|
if not t[base_anim] then |
|
local anims = {} |
|
t[base_anim] = anims |
|
local total_chunks = 0 |
|
local total_weight = 0 |
|
local anim_variants = GetAnimVariants(entity, base_anim) |
|
for idx, anim in ipairs(anim_variants) do |
|
local anim_metadata = (Presets.AnimMetadata[entity] or empty_table)[anim] or empty_table |
|
local anim_weight = anim_metadata.VariationWeight or 100 |
|
local max_random_phase = anim_metadata.RandomizePhase or -1 |
|
if max_random_phase < 0 then |
|
max_random_phase = GetAnimDuration(entity, base_anim) * 70 / 100 |
|
end |
|
local chunks_count = 1 + max_random_phase / anim_variations_phases_chunk |
|
total_weight = total_weight + anim_weight |
|
anims[idx] = { |
|
anim = anim, |
|
anim_weight = anim_weight, |
|
total_weight = total_weight, |
|
max_random_phase = max_random_phase, |
|
chunk_idx = total_chunks, |
|
chunks_count = chunks_count, |
|
} |
|
total_chunks = total_chunks + chunks_count |
|
end |
|
anims.total_weight = total_weight |
|
anims.total_chunks = total_chunks |
|
end |
|
return t[base_anim] |
|
end |
|
|
|
function Unit:GetVariationsCount(base_anim) |
|
if not base_anim then |
|
return |
|
end |
|
local anims = GetRandomAnims(self:GetEntity(), base_anim) |
|
return #anims |
|
end |
|
|
|
function Unit:GetRandomAnim(base_anim) |
|
if not base_anim then |
|
return |
|
end |
|
local anims = GetRandomAnims(self:GetEntity(), base_anim) |
|
if #anims == 0 then |
|
StoreErrorSource(self, string.format("Invalid '%s' variation request", base_anim)) |
|
return base_anim, 1, 1 |
|
end |
|
local roll = self:Random(anims.total_weight) |
|
local idx = GetRandomItemByWeight(anims, roll, "total_weight") |
|
return anims[idx].anim, idx |
|
end |
|
|
|
function Unit:GetNearbyUniqueRandomAnim(base_anim) |
|
if not base_anim then |
|
return |
|
end |
|
local anims = GetRandomAnims(self:GetEntity(), base_anim) |
|
if anims.total_chunks == 1 then |
|
return anims[1].anim, 0, 1 |
|
end |
|
|
|
local anims_locked_chunks = {} |
|
MapForEach(self, nearby_unique_anim_distance, "Unit", function(o, self, anims, anims_locked_chunks) |
|
if o == self then |
|
return |
|
end |
|
if o.gender ~= self.gender then |
|
return |
|
end |
|
local variation_idx = table.find(anims, "anim", o:GetUnmodifiedAnim()) |
|
if not variation_idx then |
|
return |
|
end |
|
local min, max |
|
local entry = anims[variation_idx] |
|
if entry.max_random_phase == 0 then |
|
min, max = 1, 1 |
|
else |
|
local phase = o:GetAnimPhase() |
|
min = Max(0, phase - anim_variations_min_time_offset) / anim_variations_phases_chunk |
|
max = Min(entry.max_random_phase, phase + anim_variations_min_time_offset) / anim_variations_phases_chunk |
|
if min > max then |
|
return |
|
end |
|
end |
|
local chunk_idx = entry.chunk_idx |
|
local locked_count = 0 |
|
for i = min, max do |
|
if not anims_locked_chunks[chunk_idx + i] then |
|
anims_locked_chunks[chunk_idx + i] = true |
|
locked_count = locked_count + 1 |
|
end |
|
end |
|
if locked_count > 0 then |
|
NetUpdateHash("GetNearbyUniqueRandomAnim_locking_anim", o, chunk_idx, variation_idx, locked_count, o:GetUnmodifiedAnim(), o:GetAnimPhase()) |
|
anims_locked_chunks[-variation_idx] = (anims_locked_chunks[-variation_idx] or 0) + locked_count |
|
end |
|
end, self, anims, anims_locked_chunks) |
|
|
|
|
|
local total_free_animations = 0 |
|
for idx, entry in ipairs(anims) do |
|
if entry.chunks_count > 0 and not anims_locked_chunks[entry.chunk_idx] then |
|
total_free_animations = total_free_animations + 1 |
|
end |
|
end |
|
NetUpdateHash("GetNearbyUniqueRandomAnim_total_free_animations", total_free_animations) |
|
if total_free_animations > 0 then |
|
local value = total_free_animations > 1 and self:Random(total_free_animations) or 0 |
|
for idx, entry in ipairs(anims) do |
|
if entry.chunks_count > 0 and not anims_locked_chunks[entry.chunk_idx] then |
|
value = value - 1 |
|
end |
|
if value < 0 then |
|
return entry.anim, 0, idx |
|
end |
|
end |
|
assert(false) |
|
end |
|
|
|
|
|
local total_weight = anims.total_weight |
|
for idx, entry in ipairs(anims) do |
|
local locked_chunks_count = anims_locked_chunks[-idx] |
|
if locked_chunks_count then |
|
local locked_weight = entry.anim_weight * locked_chunks_count / entry.chunks_count |
|
total_weight = total_weight - locked_weight |
|
end |
|
end |
|
if total_weight > 0 then |
|
local value = self:Random(total_weight) |
|
for idx, entry in ipairs(anims) do |
|
local weight = entry.anim_weight |
|
local locked_chunks_count = anims_locked_chunks[-idx] |
|
if locked_chunks_count then |
|
local locked_weight = weight * locked_chunks_count / entry.chunks_count |
|
weight = weight - locked_weight |
|
end |
|
if weight > 0 then |
|
value = value - weight |
|
if value < 0 then |
|
|
|
if not locked_chunks_count then |
|
return entry.anim, 0, idx |
|
end |
|
for i = entry.chunk_idx, entry.chunk_idx + entry.chunks_count - 1 do |
|
if not anims_locked_chunks[i] then |
|
local phase = (i - entry.chunk_idx) * anim_variations_phases_chunk |
|
return entry.anim, phase, idx |
|
end |
|
end |
|
assert(false) |
|
end |
|
end |
|
end |
|
assert(false) |
|
end |
|
|
|
local anim, variation_idx = self:GetRandomAnim(base_anim) |
|
return anim, 0, variation_idx |
|
end |
|
|
|
function Unit:GetNearbyUniqueRandomAnimFromList(list) |
|
local anims = table.icopy(list) |
|
MapForEach(self, nearby_unique_anim_distance, "Unit", function(o, anims) |
|
if o == self then |
|
return |
|
end |
|
local idx = table.find(anims, o:GetUnmodifiedAnim()) |
|
if idx then |
|
table.remove(anims, idx) |
|
end |
|
end, anims) |
|
if #anims > 0 then |
|
return anims[1 + self:Random(#anims)] |
|
end |
|
return list[1 + self:Random(#list)] |
|
end |
|
|
|
local UniversalAnimActions = { |
|
Climb = true, |
|
Drop = true, |
|
JumpOverShort = true, |
|
JumpOverLong = true, |
|
JumpAcross1 = true, |
|
JumpAcross2 = true, |
|
} |
|
|
|
local ActionAnimationPrefixMap = { |
|
["Open_Door"] = { |
|
["inf_"] = "nw_", |
|
}, |
|
["BreakWindow"] = { |
|
["civ_"] = "nw_", |
|
["inf_"] = "nw_", |
|
}, |
|
["Downed"] = { |
|
["civ_"] = "nw_", |
|
["inf_"] = "nw_", |
|
}, |
|
["Death"] = { |
|
["inf_"] = "civ_", |
|
}, |
|
} |
|
|
|
function Unit:TryGetActionAnim(action, stance, action_suffix) |
|
local action_full |
|
if not g_Combat and action == "Idle" or action == "IdlePassive" then |
|
action_full = self:GetCommandParam("idle_action") |
|
stance = self:GetCommandParam("idle_stance") or stance |
|
end |
|
if not action_full then |
|
action_full = action_suffix and action .. action_suffix or action |
|
end |
|
|
|
local prefix |
|
if self.species == "Human" then |
|
if UniversalAnimActions[action] then |
|
prefix = "civ_" |
|
elseif self:HasStatusEffect("ManningEmplacement") and (action == "Idle" or action == "IdlePassive" or action == "Fire") then |
|
prefix = "hmg_" |
|
stance = "Crouch" |
|
elseif action == "Fire" then |
|
local weapon, weapon2 = self:GetActiveWeapons() |
|
prefix = weapon and GetWeaponAnimPrefix(weapon, weapon2) or "nw_" |
|
if prefix == "nw_" then |
|
stance = "Standing" |
|
action_full = "Attack_Down" |
|
end |
|
elseif self.infected then |
|
if action == "Idle" and stance == "Prone" then |
|
prefix = "nw_" |
|
else |
|
prefix = "inf_" |
|
if action ~= "Death" and action ~= "Downed" and (stance == "Prone" or stance == "Crouch") then |
|
stance = "Standing" |
|
end |
|
end |
|
else |
|
if action == "CombatBegin" then |
|
stance = "Standing" |
|
end |
|
prefix = self:GetWeaponAnimPrefix() |
|
end |
|
local action_prefix_map = ActionAnimationPrefixMap[action] |
|
if action_prefix_map and action_prefix_map[prefix] then |
|
prefix = action_prefix_map[prefix] |
|
end |
|
else |
|
if stance == "Downed" then |
|
action = "Downed" |
|
else |
|
stance = "" |
|
end |
|
if action == "Idle" then |
|
if self.species == "Hyena" then |
|
action_full = "idle_Combat" |
|
end |
|
elseif action == "Climb" then |
|
action_full = action_suffix == 1 and "climb_1x" or "climb_2x" |
|
elseif action == "Drop" then |
|
action_full = action_suffix == 1 and "drop_1x" or "drop_2x" |
|
elseif action == "CombatBegin" then |
|
action_full = "combat_Begin" |
|
end |
|
end |
|
|
|
local valid, anim, name = self:GetValidAnim(prefix, stance, action_full) |
|
if valid then |
|
return anim |
|
end |
|
|
|
if action == "Downed" then |
|
if self.species == "Human" then |
|
return "civ_DeathOnSpot_F" |
|
end |
|
return "death" |
|
end |
|
local fallback_prefix = self:GetWeaponAnimPrefixFallback() |
|
if fallback_prefix ~= prefix then |
|
local fallback_anim = string.format("%s%s", fallback_prefix, name) |
|
if self:HasState(fallback_anim) and not IsErrorState(self:GetEntity(), fallback_anim)then |
|
return fallback_anim |
|
end |
|
end |
|
|
|
return false, anim |
|
end |
|
|
|
function Unit:GetActionBaseAnim(action, stance, action_suffix) |
|
local anim, name = self:TryGetActionAnim(action, stance, action_suffix) |
|
if not anim then |
|
local msg = string.format('Missing animation "%s" for "%s"', name, self.unitdatadef_id) |
|
StoreErrorSource(self, msg) |
|
end |
|
return anim |
|
end |
|
|
|
function Unit:GetActionRandomAnim(action, stance, action_suffix) |
|
local base_anim = self:GetActionBaseAnim(action, stance, action_suffix) |
|
local anim = self:GetNearbyUniqueRandomAnim(base_anim) |
|
return anim |
|
end |
|
|
|
function Unit:SetRandomAnim(base_anim, flags, crossfade, force) |
|
if not force and IsAnimVariant(self:GetStateText(), base_anim) then |
|
return |
|
end |
|
local anim, phase = self:GetNearbyUniqueRandomAnim(base_anim) |
|
anim = self:ModifyWeaponAnim(anim) |
|
self:SetState(anim, flags or const.eKeepComponentTargets, crossfade or -1) |
|
if phase > 0 then |
|
self:SetAnimPhase(1, phase) |
|
end |
|
end |
|
|
|
function Unit:RandomizeAnimPhase() |
|
local duration = GetAnimDuration(self:GetEntity(), self:GetState()) |
|
if duration > 1 then |
|
local phase = self:Random(duration - 1) |
|
self:SetAnimPhase(1, phase) |
|
end |
|
end |
|
|
|
function Unit:GetAttackAnim(action_id, stance) |
|
local attack_anim |
|
if self.species == "Human" then |
|
if action_id then |
|
if string.starts_with(action_id, "ThrowGrenade") then |
|
attack_anim = "gr_Standing_Attack" |
|
elseif string.match(action_id, "DoubleToss") then |
|
attack_anim = "gr_Standing_Attack" |
|
elseif action_id == "KnifeThrow" or action_id == "HundredKnives" then |
|
attack_anim = "mk_Standing_Fire" |
|
elseif action_id == "UnarmedAttack" then |
|
attack_anim = "nw_Standing_Attack_Down" |
|
elseif action_id == "Bombard" then |
|
attack_anim = "nw_Standing_MortarFire" |
|
elseif action_id == "FireFlare" then |
|
attack_anim = string.format("hg_%s_Flare_Fire", stance or self.stance) |
|
elseif action_id == "Charge" or action_id == "GloryHog" or action_id == "MeleeAttack" then |
|
attack_anim = IsKindOf(self:GetActiveWeapons(), "MacheteWeapon") and "mk_Standing_Machete_Attack_Forward" |
|
or "mk_Standing_Attack_Forward" |
|
elseif action_id == "Bandage" then |
|
return "nw_Bandaging_Idle" |
|
end |
|
end |
|
if not attack_anim then |
|
attack_anim = self:GetActionBaseAnim("Fire", stance) |
|
end |
|
else |
|
if self:HasState("attack") and not IsErrorState(self:GetEntity(), "attack") then |
|
attack_anim = "attack" |
|
end |
|
end |
|
attack_anim = self:ModifyWeaponAnim(attack_anim) |
|
return attack_anim |
|
end |
|
|
|
function Unit:GetAimAnim(action_id, stance) |
|
local aim_idle |
|
|
|
if self.species == "Human" then |
|
if action_id then |
|
if string.starts_with(action_id, "ThrowGrenade") then |
|
aim_idle = "gr_Standing_Aim" |
|
elseif string.match(action_id, "DoubleToss") then |
|
aim_idle = "gr_Standing_Aim" |
|
elseif action_id == "KnifeThrow" or action_id == "HundredKnives" then |
|
aim_idle = "mk_Standing_Aim_Forward" |
|
|
|
elseif action_id == "UnarmedAttack" then |
|
aim_idle = "nw_Standing_Aim_Forward" |
|
elseif action_id == "Bombard" then |
|
aim_idle = "nw_Standing_Idle" |
|
elseif action_id == "FireFlare" then |
|
aim_idle = string.format("hg_%s_Flare_Aim", stance or self.stance) |
|
end |
|
end |
|
if not aim_idle then |
|
local weapon, weapon2 = self:GetActiveWeapons() |
|
if IsKindOf(weapon, "MeleeWeapon") then |
|
aim_idle = "mk_Standing_Aim_Forward" |
|
elseif weapon then |
|
local attack_anim = self:GetActionBaseAnim("Fire", stance or self.stance) |
|
local prefix, stance = string.match(attack_anim or "", "(%a+)_(%a+).*") |
|
if prefix then |
|
local anim = string.format("%s_%s_Aim", prefix, stance) |
|
if IsValidAnim(self, anim) then |
|
aim_idle = anim |
|
end |
|
end |
|
end |
|
aim_idle = aim_idle or "nw_Standing_Aim_Forward" |
|
end |
|
else |
|
aim_idle = "idle" |
|
end |
|
if not IsValidAnim(self, aim_idle) then |
|
return |
|
end |
|
aim_idle = self:ModifyWeaponAnim(aim_idle) |
|
return aim_idle |
|
end |
|
|
|
function Unit:GetIdleStyle() |
|
local anim_style |
|
if self.species ~= "Human" then |
|
local aware = g_Combat and (self:IsAware() or self:HasStatusEffect("Surprised")) or self:HasStatusEffect("Suspicious") |
|
local cur_style = GetAnimationStyle(self, self.cur_idle_style) |
|
anim_style = |
|
aware and (cur_style and cur_style.VariationGroup == "CombatIdle" and cur_style |
|
or GetRandomAnimationStyle(self, "CombatIdle")) |
|
or cur_style and cur_style.VariationGroup == "Idle" and cur_style |
|
or GetRandomAnimationStyle(self, "Idle") |
|
else |
|
if self.carry_flare then |
|
anim_style = GetRandomAnimationStyle(self, "FlareIdle") |
|
end |
|
end |
|
return anim_style |
|
end |
|
|
|
function Unit:GetIdleBaseAnim(stance) |
|
local cur_style = GetAnimationStyle(self, self.cur_idle_style) |
|
local base_idle = cur_style and cur_style:GetMainAnim() |
|
if base_idle then |
|
if not IsValidAnim(self, base_idle) then |
|
local msg = string.format('GetIdleBaseAnim: Missing animation style "%s - %s" animation "%s". Gender: "%s". Entity: "%s". Appearance: %s', cur_style.group, cur_style.Name, base_idle or "", self.gender, self:GetEntity(), self.Appearance or "false") |
|
StoreErrorSource(self, msg) |
|
end |
|
return base_idle |
|
end |
|
stance = stance or self.stance |
|
local aware = g_Combat and (self:IsAware("pending") or self:HasStatusEffect("Surprised")) or self:HasStatusEffect("Suspicious") |
|
if aware and self.species == "Human" and self.team and self.team.side == "neutral" and not self.conflict_ignore and not self.infected then |
|
base_idle = "civ_Standing_Fear" |
|
end |
|
if not base_idle and not aware and self.species == "Human" then |
|
base_idle = self:TryGetActionAnim("IdlePassive", stance) |
|
end |
|
if not base_idle and self.species == "Human" and (stance == "Standing" or stance == "Crouch") and self:HasStatusEffect("Protected") then |
|
base_idle = self:TryGetActionAnim("TakeCover_Idle", false) |
|
end |
|
if not base_idle then |
|
base_idle = self:TryGetActionAnim("Idle", stance) |
|
end |
|
return base_idle or "idle" |
|
end |
|
|
|
function Unit:ShowActiveMeleeWeapon() |
|
local weapon1 = self:GetActiveWeapons() |
|
local wobj1 = IsKindOf(weapon1, "MeleeWeapon") and weapon1:GetVisualObj() |
|
if not wobj1 then |
|
return false |
|
end |
|
wobj1:SetEnumFlags(const.efVisible) |
|
return true |
|
end |
|
|
|
function Unit:HideActiveMeleeWeapon() |
|
local weapon1 = self:GetActiveWeapons() |
|
local wobj1 = IsKindOf(weapon1, "MeleeWeapon") and weapon1:GetVisualObj() |
|
if not wobj1 then |
|
return false |
|
end |
|
wobj1:ClearEnumFlags(const.efVisible) |
|
return true |
|
end |
|
|
|
local function lGetFallbackUnitAppearance(preset) |
|
if not preset then return "Soldier_Local_01" end |
|
if preset.gender == "Male" then |
|
return "Commando_Foreign_01" |
|
end |
|
return "Soldier_Local_01" |
|
end |
|
|
|
function GetAppearancesListTotalWeight(preset) |
|
local weighted_list = {total_weight = 0} |
|
for _, descr in ipairs(preset.AppearancesList) do |
|
if MatchGameState(descr.GameStates) then |
|
weighted_list.total_weight = weighted_list.total_weight + descr.Weight |
|
table.insert(weighted_list, {weight = weighted_list.total_weight, appearance = descr.Preset}) |
|
end |
|
end |
|
|
|
return weighted_list |
|
end |
|
|
|
function GetWeightedAppearance(weighted_list, slot) |
|
local idx = GetRandomItemByWeight(weighted_list, slot, "weight") |
|
return weighted_list[idx].appearance |
|
end |
|
|
|
function ChooseUnitAppearance(merc_id, handle) |
|
local preset = UnitDataDefs[merc_id] |
|
if not preset or not preset.AppearancesList then |
|
return lGetFallbackUnitAppearance(preset) |
|
end |
|
|
|
local weighted_list = GetAppearancesListTotalWeight(preset) |
|
local slot = handle and (xxhash(handle) % weighted_list.total_weight) or InteractionRand(weighted_list.total_weight, "Appearance") |
|
local appearance = GetWeightedAppearance(weighted_list, slot) |
|
|
|
return appearance or lGetFallbackUnitAppearance(preset) |
|
end |
|
|
|
function Unit:ChooseAppearance() |
|
local forcedAppearance = false |
|
|
|
if self.spawner then |
|
local templates = self.spawner.UnitDataSpawnDefs or empty_table |
|
local data = table.find_value(templates, "UnitDataDefId", self.unitdatadef_id) |
|
forcedAppearance = data and data.ForcedAppearance |
|
end |
|
|
|
local unitData = gv_UnitData[self.session_id] |
|
if not forcedAppearance and unitData and unitData.ForcedAppearance then |
|
forcedAppearance = unitData.ForcedAppearance |
|
end |
|
|
|
if forcedAppearance then |
|
return forcedAppearance |
|
end |
|
|
|
return ChooseUnitAppearance(self.unitdatadef_id, self.handle) |
|
end |
|
|
|
function Unit:ExplosionFly(prev_hit_points) |
|
|
|
self:PushDestructor(function(self) |
|
SetCombatActionState(self, false) |
|
self:InterruptPreparedAttack() |
|
self:RemoveStatusEffect("Protected") |
|
if ShouldDoDestructionPass() then |
|
WaitMsg("DestructionPassDone", 1000) |
|
end |
|
|
|
|
|
if self:IsDead() then |
|
DeleteBadgesFromTargetOfPreset("CombatBadge", self) |
|
DeleteBadgesFromTargetOfPreset("NpcBadge", self) |
|
if self:ShouldGetDowned() and (g_Combat or (not g_Combat and (prev_hit_points > 1))) then |
|
self.HitPoints = 1 |
|
self:SetCommand("GetDowned", false, "skip anim") |
|
elseif self.species == "Human" then |
|
self.on_die_hit_descr = self.on_die_hit_descr or {} |
|
self.on_die_hit_descr.death_explosion = true |
|
self:SetCommand("Die") |
|
else |
|
self:SetCommand("Die") |
|
end |
|
else |
|
self:Pain() |
|
|
|
local pos = GetPassSlab(RotateRadius(guim/2, self:GetOrientationAngle(), self)) or GetPassSlab(self) |
|
if self:GetPos() ~= pos then |
|
self:SetCommand("GotoSlab", pos, nil, nil, nil, nil, nil, "interrupted") |
|
end |
|
end |
|
end) |
|
self:PopAndCallDestructor() |
|
end |
|
|
|
function Unit:AttachGrenade(grenade) |
|
local visual = PlaceObject("GrenadeVisual", {fx_actor_class = grenade.class}) |
|
self:Attach(visual, self:GetSpotBeginIndex("Weaponr")) |
|
grenade:OnPrepareThrow(self, visual) |
|
return visual |
|
end |
|
|
|
function Unit:DetachGrenade(grenade) |
|
self:DestroyAttaches("GrenadeVisual") |
|
grenade:OnFinishThrow(self) |
|
end |
|
|
|
function GravityFall(obj, pos) |
|
obj:SetGravity() |
|
local fall_time = obj:GetGravityFallTime(pos) |
|
obj:SetPos(pos, fall_time) |
|
Sleep(fall_time) |
|
obj:SetGravity(0) |
|
end |
|
|
|
function Unit:FallDown(pos, cower) |
|
pos = ValidateZ(pos) |
|
local myPos = ValidateZ(self:GetPos()) |
|
local height = myPos:z() - pos:z() |
|
if height > 0 then |
|
if self:HasPreparedAttack() then |
|
self:InterruptPreparedAttack() |
|
end |
|
self:LeaveEmplacement(true) |
|
local orientation_angle = self:GetOrientationAngle() |
|
local stance = self:GetValidStance(self.stance, pos) |
|
if self:IsDead() then |
|
local norm = self:GetGroundOrientation(self, pos, orientation_angle) |
|
self:SetOrientation(norm, orientation_angle, 300) |
|
GravityFall(self, pos) |
|
else |
|
if stance == "Prone" then |
|
orientation_angle = FindProneAngle(self, pos, orientation_angle, 60*60) |
|
local norm = self:GetGroundOrientation(self, pos, orientation_angle) |
|
self:SetOrientation(norm, orientation_angle, 300) |
|
else |
|
self:SetOrientation(axis_z, orientation_angle, 300) |
|
end |
|
local anim_style = GetAnimationStyle(self, self.cur_idle_style) |
|
local base_idle = anim_style and anim_style:GetMainAnim() or self:GetIdleBaseAnim(stance) |
|
self:SetRandomAnim(base_idle) |
|
self:SetTargetDummy(pos, orientation_angle, base_idle, 0, stance) |
|
GravityFall(self, pos) |
|
local floors = DivCeil(height, 4 * const.SlabSizeZ) |
|
local damage = 1 + self:Random(floors * 10) |
|
local floating_text = T{443902454775, "<damage> (High Fall)", damage = damage} |
|
self:TakeDirectDamage(damage, floating_text) |
|
if not self:IsDead() then |
|
if stance ~= self.stance then |
|
self:DoChangeStance(stance) |
|
end |
|
self:UninterruptableGoto(self:GetPos()) |
|
end |
|
end |
|
elseif pos ~= myPos then |
|
if self:HasPreparedAttack() then |
|
self:InterruptPreparedAttack() |
|
end |
|
self:LeaveEmplacement() |
|
if not self:IsDead() then |
|
local stance = self:GetValidStance(self.stance, pos) |
|
if stance ~= self.stance then |
|
self:DoChangeStance(stance) |
|
end |
|
self:UninterruptableGoto(pos, true) |
|
end |
|
end |
|
self:SetTargetDummyFromPos() |
|
if cower and not self:IsDead() then |
|
self:SetCommand("Cower", "find cower spot") |
|
self:SetCommandParamValue("Cower", "move_anim", "Run") |
|
self:UpdateMoveAnim() |
|
end |
|
end |
|
|
|
function Unit:PlayAwarenessAnim(followup_cmd) |
|
local setPiece = GetDialog("XSetpieceDlg") |
|
local triggerUnit = setPiece and setPiece.triggerUnits and setPiece.triggerUnits[1] |
|
local isTriggerUnit = triggerUnit and triggerUnit == self |
|
local idleAnim = false |
|
if self.stance == "Prone" then |
|
self:DoChangeStance("Standing") |
|
end |
|
local anims |
|
if self:HasStatusEffect("ManningEmplacement") or self:GetBandageTarget() then |
|
|
|
elseif setPiece and not isTriggerUnit then |
|
anims = { self:TryGetActionAnim("Idle", self.stance) } |
|
idleAnim = true |
|
elseif self.species == "Human" then |
|
local heavyWeaponUsage = IsKindOf(self:GetActiveWeapons(), "HeavyWeapon") |
|
local sniperUsage = IsKindOf(self:GetActiveWeapons(), "SniperRifle") |
|
local base_anim = self:GetActionBaseAnim("CombatBegin", self.stance) |
|
if base_anim then |
|
if self.infected then |
|
|
|
anims = { base_anim } |
|
else |
|
if self.pending_awareness_role == "alerter" then |
|
if heavyWeaponUsage or sniperUsage then |
|
anims = { base_anim } |
|
else |
|
anims = { base_anim .. 3 } |
|
end |
|
elseif self.pending_awareness_role == "alerted" then |
|
anims = { base_anim } |
|
elseif self.pending_awareness_role == "attacked" then |
|
anims = { base_anim .. 4 } |
|
elseif self.pending_awareness_role == "surprised" then |
|
anims = { base_anim .. 2 } |
|
end |
|
end |
|
end |
|
else |
|
if self.pending_awareness_role == "alerter" then |
|
anims = { "combat_Begin" } |
|
else |
|
anims = { "combat_Begin2" } |
|
end |
|
end |
|
if anims then |
|
if self.pending_awareness_role == "alerted" and not IsValid(self.alerted_by_enemy) then |
|
Sleep(self:Random(500)) |
|
end |
|
if IsValid(self.alerted_by_enemy) and not self:HasStatusEffect("ManningEmplacement") and not self:GetBandageTarget() then |
|
local alerted_angle = CalcOrientation(self, self.alerted_by_enemy) |
|
local face_angle = self:GetPosOrientation(nil, alerted_angle, self.stance, false, true) |
|
if face_angle then |
|
self:AnimatedRotation(face_angle, anims[1]) |
|
end |
|
end |
|
local anim = self:GetNearbyUniqueRandomAnimFromList(anims) |
|
anim = self:ModifyWeaponAnim(anim) |
|
self:SetState(anim, const.eKeepComponentTargets) |
|
local weapon = self:GetActiveWeapons() |
|
local fx_target = weapon and weapon:GetVisualObj() or false |
|
if fx_target and not idleAnim then |
|
PlayFX("AwarenessAnim", "start", self, fx_target) |
|
local index = 1 |
|
while true do |
|
local t = self:TimeToMoment(1, "hit", index) |
|
if not t then break end |
|
Sleep(t) |
|
PlayFX("AwarenessAnim", "hit", self, fx_target) |
|
index = index + 1 |
|
end |
|
Sleep(self:TimeToAnimEnd()) |
|
PlayFX("AwarenessAnim", "end", self, fx_target) |
|
elseif not idleAnim then |
|
Sleep(self:TimeToAnimEnd()) |
|
end |
|
end |
|
self.pending_awareness_role = nil |
|
if followup_cmd then |
|
self:SetCommand(followup_cmd) |
|
end |
|
end |
|
|
|
function Unit:BanterIdle(idle_style) |
|
self:PlayIdleStyle(idle_style) |
|
end |
|
|
|
|
|
function Unit:SetpieceIdle(set_idle) |
|
|
|
Msg("OnSetpieceIdleStart", self) |
|
local wasInterruptable = self.interruptable |
|
if wasInterruptable then |
|
self:EndInterruptableMovement() |
|
end |
|
if set_idle then |
|
local base_idle = self:GetIdleBaseAnim() |
|
if not IsAnimVariant(self:GetStateText(), base_idle) then |
|
local anim = self:GetNearbyUniqueRandomAnim(base_idle) |
|
self:SetState(anim) |
|
end |
|
end |
|
|
|
repeat |
|
Sleep(100) |
|
until not IsSetpiecePlaying() |
|
if wasInterruptable then |
|
self:BeginInterruptableMovement() |
|
end |
|
end |
|
|
|
function Unit:SetpieceSetStance(anim_stance) |
|
if Presets.CombatStance.Default[anim_stance] then |
|
self.stance = anim_stance |
|
end |
|
local base_idle = self:GetIdleBaseAnim(anim_stance) |
|
if not IsAnimVariant(self:GetStateText(), base_idle) then |
|
local anim = self:GetNearbyUniqueRandomAnim(base_idle) |
|
self:SetState(anim) |
|
end |
|
self:SetCommand("SetpieceIdle") |
|
end |
|
|
|
function Unit:RestoreAiming(target_pt, lof_params) |
|
local weapon, weapon2 = self:GetActiveWeapons() |
|
if weapon then |
|
local attack_data = self:ResolveAttackParams(nil, target_pt, lof_params) |
|
local aim_idle = self:GetAimAnim(nil, attack_data.stance) |
|
self:SetState(aim_idle, const.eKeepComponentTargets) |
|
self:SetPos(attack_data.step_pos) |
|
else |
|
if self.return_pos then |
|
self:SetPos(self.return_pos) |
|
self.return_pos = false |
|
end |
|
self:SetRandomAnim(self:GetIdleBaseAnim()) |
|
end |
|
self:Face(target_pt) |
|
end |
|
|
|
function Unit:SetpieceAimAt(target_pt) |
|
self:RestoreAiming(target_pt, {can_use_covers = false}) |
|
Msg("SetpieceUnitAimed", self) |
|
self:SetCommand("SetpieceIdle") |
|
end |
|
|
|
function Unit:SetpieceGoto(pos, end_angle, stance, straight_line, animated_rotation, delay) |
|
self.goto_target = false |
|
if delay then |
|
Sleep(delay) |
|
end |
|
if (stance or "") ~= "" and stance ~= self.stance then |
|
self:DoChangeStance(stance) |
|
end |
|
|
|
if animated_rotation then |
|
local face_pos |
|
if straight_line then |
|
face_pos = pos |
|
else |
|
self:FindPath(pos) |
|
local pathlen = pf.GetPathPointCount(self) |
|
for i = pathlen, 1, -1 do |
|
local p = pf.GetPathPoint(self, i) |
|
if p and p:IsValid() and self:GetDist2D(p) > 0 then |
|
face_pos = p |
|
break |
|
end |
|
end |
|
end |
|
if face_pos then |
|
local angle = CalcOrientation(self, face_pos) |
|
if abs(AngleDiff(angle, self:GetOrientationAngle())) > 45*60 then |
|
self:AnimatedRotation(angle) |
|
end |
|
end |
|
end |
|
|
|
self:UninterruptableGoto(pos, straight_line) |
|
|
|
if end_angle then |
|
if animated_rotation and abs(AngleDiff(end_angle, self:GetOrientationAngle())) > 45*60 then |
|
self:AnimatedRotation(end_angle) |
|
else |
|
self:SetOrientationAngle(end_angle, 100) |
|
end |
|
end |
|
self:SetCommand("SetpieceSetStance", self.stance) |
|
end |
|
|
|
function OnMsg.ClassesPreprocess(classdefs) |
|
classdefs.AppearanceObjectPart.flags.gofUnitLighting = true |
|
end |