Download Lua/Tactical/UnitAppearance.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 80.2 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/UnitAppearance.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/Lua/Tactical/UnitAppearance.lua
-
curl -L -o UnitAppearance.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/UnitAppearance.lua
80.2 kB
| 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() | |
| --local max_stains = maxStainsAtDetailLevel[EngineOptions.ObjectDetail] or 3 | |
| for i, stain in ipairs(self.stains) do | |
| --if i > max_stains then break end | |
| stain:Apply(self) | |
| end | |
| if not IsRealTimeThread() and self:IsIdleCommand() and CurrentThread() ~= self.command_thread then | |
| --set command is a sync event and shouldn't be done in rtt | |
| --it happens during load session data due to cascade onsetwhatever calls | |
| if self.command ~= "Hang" and self.command ~= "Cower" then | |
| self:SetCommand("Idle") | |
| end | |
| end | |
| end | |
| function Unit:UpdatePreparedAttackAndOutfit() | |
| -- in case of using a prepared attack, check if it can still find its weapon, cancel otherwise | |
| self:FlushCombatCache() -- clear the cache so we don't get a weapon that is no longer equipped | |
| 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) -- 45 degrees -> 0, 135 degrees -> 100 | |
| 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 = | |
| { -- spot - animation - offset | |
| 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 -- the mortar command will handle it | |
| 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 | |
| -- make sure we're not hiding weapons setup by a setpiece | |
| 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 -- prefer displaying the other set weapon attaches | |
| if item and item.class == "Gewehr98" and spot == "Weaponr" then | |
| -- grip_modify = true | |
| 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 | |
| -- update visibility | |
| 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 | |
| --[[ | |
| -- vertical grip support was canceled | |
| local vertical_grip, grip_spot | |
| local weapon1 = attached_weapons and attached_weapons[1] | |
| if not holster_weapon and weapon1 and weapon1.weapon and #attached_weapons == 1 then | |
| grip_spot = weapon1.weapon:GetLHandGripSpot() | |
| end | |
| if grip_spot then | |
| local grip_obj = GetWeaponSpotObject(weapon1, grip_spot) | |
| local grip_spot_idx = grip_obj:GetSpotBeginIndex(grip_spot) | |
| local grip_spot_annotation = grip_obj:GetSpotAnnotation(grip_spot_idx) | |
| if grip_spot_annotation == "Vert" then | |
| vertical_grip = true | |
| end | |
| end | |
| local channel = const.AnimChannel_VerticalGrip | |
| if vertical_grip then | |
| self:SetAnimMask(1, "LeftHand", "inverse") | |
| self:SetAnimMask(channel, "LeftHand") | |
| self:SetAnim(channel, "ar_Standing_Idle_Grip") | |
| --self:SetAnimWeight(channel, 100) | |
| --self:SetAnimBlendComponents(channel, true, true, false) -- channel, translation, orientation, scale | |
| else | |
| self:ClearAnim(channel) | |
| self:SetAnimMask(1, false) | |
| 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 | |
| -- return: footplant, ground_orient | |
| 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 | |
| --DbgAddVector(target, point(0, 0, guim), const.clrWhite) | |
| 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 = unit:GetSpotLoc(unit:GetSpotBeginIndex("Weaponr")) | |
| 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 | |
| -- keep it | |
| 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 | |
| -- first rotate | |
| 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 | |
| -- enter step pos | |
| 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 | |
| -- some time passed, check if the lof_data.step_pos position has been changed | |
| else | |
| -- behind a cover. place the unit to the left or right of the cover. | |
| 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 | |
| -- update aiming position (the cursor position could be changed) | |
| 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 | |
| -- play transition animations to target anim | |
| 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 | |
| -- rotate left or right | |
| 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 | |
| -- check for valid aim target | |
| 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 -- animation, phase, variation index | |
| 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 -- this animation is not supposed to be played more than once | |
| 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) | |
| -- try first not used animation | |
| 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 | |
| -- look for not used animation segment | |
| 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 | |
| -- return the first free chunk | |
| 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 | |
| -- there is no free animations | |
| 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 | |
| -- fallback | |
| 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" | |
| --aim_idle = "mk_Standing_Aim_Down" | |
| 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) | |
| -- not flying anymore(too cartoon) - just play Pain | |
| self:PushDestructor(function(self) | |
| SetCombatActionState(self, false) | |
| self:InterruptPreparedAttack() -- force interrupt when the unit gets thrown off by an explosion | |
| self:RemoveStatusEffect("Protected") -- lose cover | |
| if ShouldDoDestructionPass() then | |
| WaitMsg("DestructionPassDone", 1000) --wait for destro if slabs beneath us get destroyed, so we can get a falldown point | |
| end | |
| --remove badge as the removal of it in unitdiestart is too late in this case | |
| 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 -- make sure the unit is not considered dead and evicted from the UI | |
| 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() | |
| -- the combat use the same GotoSlab (the unit should have already been interrupted) | |
| 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 | |
| -- do nothing | |
| 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 | |
| -- zombies do not have variatins of _CombatBegin | |
| anims = { base_anim } | |
| else | |
| if self.pending_awareness_role == "alerter" then | |
| if heavyWeaponUsage or sniperUsage then | |
| anims = { base_anim } -- variation 3 shoots and with snipers and heavy weapons we do not want that | |
| 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)) -- randomize phase when multiple units playing | |
| 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 | |
| -- setpiece stuff | |
| function Unit:SetpieceIdle(set_idle) | |
| -- default command to put units into while playing setpiece and to store some behavior specific params for the setpiece | |
| 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 | |
| -- initial animated face target | |
| 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 | |
| -- goto | |
| self:UninterruptableGoto(pos, straight_line) | |
| -- finish: face target | |
| 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 |