DefineClass.HeavyWeapon = { __parents = { "HeavyWeaponProperties", "BaseWeapon", "Firearm" }, trajectory_type = false, base_skill = "Explosives", -- they don't have chance to hit, but mishap chance is based on Explosives so it makes sense for the AI trajectory_attack_action = { ["line"] = "RocketLauncherFire", ["parabola"] = "GrenadeLauncherFire", ["bombard"] = "Bombard" } } function HeavyWeapon:GetBaseAttack() return self.trajectory_attack_action[self.trajectory_type] end function HeavyWeapon:GetBaseDamage() if self.ammo then return self.ammo.BaseDamage end return self.BaseDamage end function HeavyWeapon:GetMaxRange() return self.WeaponRange * const.SlabSizeX end function HeavyWeapon:ValidatePos(explosion_pos) return explosion_pos end function HeavyWeapon:GetJamChance() return 0 end function HeavyWeapon:GetAttackResults(action, attack_args) local attacker = attack_args.obj local prediction = attack_args.prediction local trajectory, stealth_kill local lof_idx = table.find(attack_args.lof, "target_spot_group", attack_args.target_spot_group or "Torso") local lof_data = (attack_args.lof or empty_table)[lof_idx or 1] local target_pos = attack_args.target_pos or lof_data and lof_data.target_pos or (IsValid(attack_args.target) and attack_args.target:GetPos()) local ordnance = self.ammo if not target_pos:IsValidZ() then target_pos = target_pos:SetTerrainZ() end -- mishap & stealth kill checks local mishap if not prediction and not attack_args.explosion_pos and IsKindOf(self, "MishapProperties") then local chance = self:GetMishapChance(attacker, target_pos) if CheatEnabled("AlwaysMiss") or attacker:Random(100) < chance then local dv = self:GetMishapDeviationVector(attacker, target_pos) mishap = true target_pos = target_pos + dv attacker:ShowMishapNotification(action) end end if self.trajectory_type == "line" then attack_args.max_pierced_objects = 0 attack_args.can_use_covers = false if not prediction then attack_args.prediction = false attack_args.can_use_covers = false attack_args.seed = attacker:Random() local attack_data = GetLoFData(attacker, target_pos, attack_args) attack_args.lof = attack_data.lof lof_idx = table.find(attack_args.lof, "target_spot_group", attack_args.target_spot_group or "Torso") lof_data = attack_args.lof[lof_idx or 1] end -- trajectory from lof (shot origin -> first hit/target_pos) if lof_data then local hits = lof_data.hits or empty_table local hit_pos if #hits > 0 then hit_pos = hits[1].pos else hit_pos = target_pos end hit_pos = attack_args.explosion_pos or hit_pos local dist = lof_data.attack_pos:Dist(hit_pos) local time = MulDivRound(dist, 1000, const.Combat.RocketVelocity) trajectory = { { pos = lof_data.attack_pos, t = 0 }, { pos = hit_pos, t = time }, } end elseif self.trajectory_type == "parabola" then attack_args.can_bounce = ordnance and ordnance.CanBounce trajectory = Grenade:GetTrajectory(attack_args, nil, target_pos, mishap) elseif self.trajectory_type == "bombard" then -- no parabola for bombard else assert(false, string.format("unknown trajectory type '%s' used in heavy weapon '%s' of class %s", tostring(self.trajectory_type), self.class, self.class)) end if not attack_args.explosion_pos and ((not trajectory or #trajectory == 0) and self.trajectory_type ~= "bombard" or not self.ammo or self.ammo.Amount <= 0) then return {} end local jammed, condition = false, false if prediction then attack_args.jam_roll = 0 attack_args.condition_roll = 0 else attack_args.jam_roll = attack_args.jam_roll or (1 + attacker:Random(100)) attack_args.condition_roll = attack_args.condition_roll or (1 + attacker:Random(100)) jammed, condition = self:ReliabilityCheck(attacker, 1, attack_args.jam_roll, attack_args.condition_roll) end if jammed then return {jammed = true, condition = condition} end local impact_pos = attack_args.explosion_pos or (trajectory and #trajectory > 0 and trajectory[#trajectory].pos) or target_pos local aoe_params = self:GetAreaAttackParams(action.id, attacker, impact_pos) aoe_params.stealth_kill = stealth_kill if attack_args.stealth_attack then aoe_params.stealth_attack_roll = not prediction and attacker:Random(100) or 100 end aoe_params.prediction = prediction local results = GetAreaAttackResults(aoe_params, nil, not prediction and ordnance.AppliedEffects) results.trajectory = trajectory results.ordnance = ordnance results.weapon = ordnance results.jammed = jammed results.condition = condition results.fired = not jammed and 1 results.mishap = mishap results.burn_ground = ordnance.BurnGround if self.trajectory_type == "bombard" then results.explosion_pos = target_pos if not jammed then results.fired = Min(attack_args.bombard_shots, ordnance.Amount) end elseif self.trajectory_type == "line" then -- add cone aoe behind attacker assert(#trajectory > 1) local range = self.BackfireRange local step_pos = attack_args.step_pos local target_pos = step_pos + SetLen(trajectory[1].pos - trajectory[2].pos, range * const.SlabSizeX) local cone_params = { can_be_damaged_by_attack = false, cone_angle = self.BackfireConeAngle, max_range = range, target_pos = target_pos, attacker = attacker, step_pos = step_pos, explosion = true, weapon = aoe_params.weapon, damage_override = self.BackfireDamage, damage_mod = 100, attribute_bonus = 0, prediction = prediction, } local cone_results = GetAreaAttackResults(cone_params) -- merge results from the cone attack into 'results' for _, hit in ipairs(cone_results) do hit.backfire = true results[#results + 1] = hit end for _, obj in ipairs(cone_results.hit_objs) do results.hit_objs = results.hit_objs or {} table.insert_unique(results.hit_objs, obj) end results.total_damage = (results.total_damage or 0) + (cone_results.total_damage or 0) results.friendly_fire_dmg = (results.friendly_fire_dmg or 0) + (cone_results.friendly_fire_dmg or 0) end CompileKilledUnits(results) return results end function HeavyWeapon:GetAreaAttackParams(action_id, attacker, target_pos, step_pos) target_pos = target_pos or attacker:GetVisualPos() local ordnance = self.ammo local params = { attacker = false, weapon = ordnance or nil, target_pos = target_pos, step_pos = step_pos or target_pos, stance = "Prone", min_range = ordnance and ordnance.AreaOfEffect or 0, max_range = ordnance and ordnance.AreaOfEffect or 0, center_range = ordnance and ordnance.CenterAreaOfEffect or 0, aoe_type = ordnance and ordnance.aoeType or nil, damage_mod = 100, attribute_bonus = 0, can_be_damaged_by_attack = true, explosion = true, -- damage dealt depends on target stance explosion_fly = ordnance and ordnance.DeathType == "BlowUp" or false, used_ammo = 1, } if IsKindOf(attacker, "Unit") then params.attacker = attacker params.attribute_bonus = GetGrenadeDamageBonus(attacker) end if self.trajectory_type == "bombard" then -- increase range with the size of the bombard (weapon property) params.max_range = params.max_range + self.BombardRadius end return params end function MishapChanceByDist(item, attacker, target, async) local chance = MishapProperties.GetMishapChance(item, attacker, target, async) local range = item.WeaponRange * const.SlabSizeX local dist = attacker:GetDist(target) if dist > range / 2 then chance = Min(100, chance + MulDivRound(dist - range/2, 100 - chance, range/2)) end return chance end function MishapDeviationVectorByDist(item, attacker, target) local dv = MishapProperties.GetMishapDeviationVector(item, attacker, target) local range = item.WeaponRange * const.SlabSizeX local dist = attacker:GetDist(target) if dist > range / 2 then local mod = Min(100, MulDivRound(dist - range/2, 100, range/2)) dv = MulDivRound(dv, 100 + mod, 100) end return dv end DefineClass.RocketLauncher = { __parents = {"HeavyWeapon"}, properties = { { category = "Combat", id = "BackfireRange", editor = "number", min = 0, default = 3, template = true, }, { category = "Combat", id = "BackfireConeAngle", editor = "number", min = 0, scale = "deg", default = 30*60, template = true, }, { category = "Combat", id = "BackfireDamage", editor = "number", min = 0, default = 10, template = true, }, }, trajectory_type = "line", WeaponType = "MissileLauncher", RolloverClassTemplate = "HeavyWeapon", } DefineClass.GrenadeLauncher = { __parents = {"HeavyWeapon"}, trajectory_type = "parabola", WeaponType = "GrenadeLauncher", RolloverClassTemplate = "HeavyWeapon"} DefineClass.Mortar = { __parents = {"HeavyWeapon"}, trajectory_type = "bombard", WeaponType = "Mortar", RolloverClassTemplate = "HeavyWeapon"} GrenadeLauncher.GetMishapChance = MishapChanceByDist GrenadeLauncher.GetMishapDeviationVector = MishapDeviationVectorByDist RocketLauncher.GetMishapChance = MishapChanceByDist RocketLauncher.GetMishapDeviationVector = MishapDeviationVectorByDist function GrenadeLauncher:GetBaseDegradePerShot() return const.Weapons.DegradePerShot_GrenadeLauncher end function RocketLauncher:GetBaseDegradePerShot() return const.Weapons.DegradePerShot_RocketLauncher end function Mortar:GetBaseDegradePerShot() return const.Weapons.DegradePerShot_Mortar end function RocketLauncher:UpdateRocket() local visual_obj = self.visual_obj if not IsValid(visual_obj) then return end visual_obj:DestroyAttaches("OrdnanceVisual") if self.ammo and self.ammo.Amount > 0 then local rocket = PlaceObject("OrdnanceVisual", {fx_actor_class = self.ammo.class}) visual_obj:Attach(rocket, visual_obj:GetSpotBeginIndex("Muzzle")) end end function RocketLauncher:OnUnloadWeapon() self:UpdateRocket() end function RocketLauncher:Reload(...) Firearm.Reload(self, ...) self:UpdateRocket() end function RocketLauncher:UpdateVisualObj(...) Firearm.UpdateVisualObj(self, ...) self:UpdateRocket() end