|
DefineClass.MeleeWeapon = { |
|
__parents = { "InventoryItem", "MeleeWeaponProperties", "BaseWeapon", "BobbyRayShopMeleeWeaponProperties" }, |
|
WeaponType = "MeleeWeapon", |
|
ImpactForce = 2, |
|
base_skill = "Dexterity", |
|
base_action = "MeleeAttack", |
|
|
|
ComponentSlots = {}, |
|
Color = "Default", |
|
components = {}, |
|
|
|
neck_attack_descriptions = { |
|
["choke"] = T(545528819211, "<newline><newline>Unarmed: Inflicts <em>Choking</em> on hit."), |
|
["bleed"] = T(251225177855, "<newline><newline>Knife: Inflicts <em>Bleeding</em> on hit."), |
|
["lethal"] = T(775626326541, "<newline><newline>Machete: Chance for a lethal attack based on your Strength."), |
|
}, |
|
} |
|
|
|
function MeleeWeapon:GetRolloverType() |
|
return self.ItemType or "MeleeWeapon" |
|
end |
|
|
|
function MeleeWeapon:GetAccuracy(dist, unit, action, ranged) |
|
if not ranged then |
|
return self.BaseChanceToHit |
|
end |
|
return GetRangeAccuracy(self, dist, unit, action) |
|
end |
|
|
|
function MeleeWeapon:GetBaseAttack(unit, force) |
|
return self.base_action |
|
end |
|
|
|
function MeleeWeapon:GetCustomNeckAttackDescription() |
|
return self.neck_attack_descriptions[self.NeckAttackType] |
|
end |
|
|
|
function MeleeWeapon:PrecalcDamageAndStatusEffects(attacker, target, attack_pos, damage, hit, effect, attack_args, record_breakdown, action, prediction) |
|
local effects = EffectsTable(effect) |
|
local strMod = MulDivRound(attacker.Strength, self.DamageMultiplier, 100) |
|
if record_breakdown then record_breakdown[#record_breakdown + 1] = { name = T(162618960967, "Strength"), value = strMod } end |
|
local mod = 100 + strMod |
|
mod = mod + (hit.damage_bonus or 0) |
|
|
|
local actionType = hit.actionType |
|
if actionType == "Melee Attack" then |
|
if IsKindOf(target, "Unit") then |
|
if target.species == "Human" and target.stance == "Prone" then |
|
local value = const.Combat.MeleeAttackProneMod |
|
mod = mod + value |
|
if record_breakdown then record_breakdown[#record_breakdown + 1] = { name = T(848625832174, "Prone Target"), value = value } end |
|
end |
|
end |
|
end |
|
damage = MulDivRound(damage, mod, 100) |
|
BaseWeapon.PrecalcDamageAndStatusEffects(self, attacker, target, attack_pos, damage, hit, effects, attack_args, record_breakdown, action, prediction) |
|
end |
|
|
|
function MeleeWeapon:GetAttackResults(action, attack_args) |
|
|
|
local attacker = attack_args.obj |
|
local attack_pos = attack_args.step_pos |
|
local target = attack_args.target or attack_args.target_pos |
|
|
|
local prediction = attack_args.prediction |
|
local stealth_kill_chance = attack_args.stealth_kill_chance or 0 |
|
local stealth_crit_chance = attack_args.stealth_bonus_crit_chance or 0 |
|
|
|
|
|
local attack_results = {} |
|
attack_results.crit_chance = attacker:CalcCritChance(self, target, attack_args, attack_pos) |
|
|
|
if action.AlwaysHits then |
|
attack_results.chance_to_hit = 100 |
|
elseif attack_args.cth_breakdown then |
|
local cth, baseCth, modifiers = attacker:CalcChanceToHit(target, action, attack_args) |
|
attack_results.chance_to_hit = cth |
|
attack_results.chance_to_hit_modifiers = modifiers |
|
else |
|
attack_results.chance_to_hit = attacker:CalcChanceToHit(target, action, attack_args, "chance_only") |
|
end |
|
if IsKindOf(target, "Unit") and action.id == "UnarmedAttack" then |
|
attack_results.knockdown_chance = Max(0, 20 + attacker.Strength - target.Agility) |
|
else |
|
attack_results.knockdown_chance = 0 |
|
end |
|
if attack_args.chance_only and not attack_args.damage_breakdown then return attack_results end |
|
|
|
if prediction then |
|
attack_results.attack_roll = -1 |
|
attack_results.knockdown_roll = 101 |
|
attack_results.crit_roll = 101 |
|
if stealth_kill_chance > 0 then |
|
attack_args.stealth_kill_roll = 101 |
|
end |
|
else |
|
attack_results.attack_roll = attack_args.attack_roll or attacker:Random(100) |
|
attack_results.crit_roll = attack_args.crit_roll or attacker:Random(100) |
|
if stealth_kill_chance > 0 then |
|
attack_args.stealth_kill_roll = attack_args.stealth_kill_roll or attacker:Random(100) |
|
end |
|
if attack_results.knockdown_chance > 0 then |
|
attack_results.knockdown_roll = attacker:Random(100) |
|
else |
|
attack_results.knockdown_roll = 100 |
|
end |
|
end |
|
|
|
local miss = attack_results.attack_roll >= attack_results.chance_to_hit |
|
local crit = attack_results.crit_roll < attack_results.crit_chance |
|
local knockdown = attack_results.knockdown_roll < attack_results.knockdown_chance |
|
local kill |
|
if not miss and stealth_kill_chance > 0 then |
|
kill = attack_args.stealth_kill_roll < stealth_kill_chance |
|
end |
|
|
|
attack_results.weapon = self |
|
attack_results.crit = crit |
|
attack_results.stealth_attack = attack_args.stealth_attack |
|
attack_results.stealth_kill_chance = stealth_kill_chance |
|
attack_results.stealth_kill = kill |
|
attack_results.num_hits = miss and 0 or 1 |
|
attack_results.friendly_fire_dmg = 0 |
|
attack_results.killed_units = false |
|
attack_results.attack_pos = attack_pos |
|
attack_results.hit_objs = {} |
|
attack_results.aim = attack_args.aim |
|
attack_results.dmg_breakdown = attack_args.damage_breakdown and {} or false |
|
attack_results.lof = attack_args.lof |
|
|
|
local target_grazing_hit, stuck |
|
if action.ActionType == "Ranged Attack" then |
|
|
|
local lof_params = { |
|
obj = attacker, |
|
output_collisions = true, |
|
range = range, |
|
max_pierced_objects = 0, |
|
target_spot_group = "Torso", |
|
action_id = action.id, |
|
seed = prediction and 0 or attacker:Random(), |
|
step_pos = attack_args.step_pos or nil, |
|
} |
|
local attack_data = GetLoFData(attacker, target, lof_params) |
|
assert(attack_data) |
|
local lof_idx = table.find(attack_data.lof, "target_spot_group", attack_data.target_spot_group) |
|
local lof_data = attack_data.lof[lof_idx or 1] |
|
|
|
if not lof_data or lof_data.stuck then |
|
attack_results.chance_to_hit = 0 |
|
stuck = true |
|
local mods = attack_results.chance_to_hit_modifiers or {} |
|
mods[#mods + 1] = { |
|
{ |
|
id = "NoLineOfFire", |
|
name = T(604792341662, "No Line of Fire"), |
|
value = 0 |
|
} |
|
} |
|
end |
|
|
|
local attack_pos = lof_data.attack_pos |
|
local hit_pos = lof_data.target_pos |
|
target_grazing_hit = not lof_data.stuck and lof_data.target_grazing_hit |
|
|
|
if miss and not prediction then |
|
local dispersion = Firearm:GetMaxDispersion(attacker:GetDist(target)) |
|
local misses = Firearm:CalcMissVectors(attacker, action.id, target, attack_pos, hit_pos, dispersion, 10*guic) |
|
|
|
local main, backup = misses.clear, misses.obstructed |
|
local tbl = #main > 0 and main or backup |
|
assert(#tbl > 0) |
|
|
|
|
|
hit_pos = table.interaction_rand(tbl, "Combat") |
|
end |
|
|
|
|
|
local throw_velocity = const.Combat.KnifeThrowVelocity |
|
local dist = attack_pos:Dist(hit_pos) |
|
local tth = MulDivRound(dist, 1000, throw_velocity) |
|
attack_results.trajectory = { |
|
{ pos = attack_pos, t = 0 }, |
|
{ pos = hit_pos, t = tth }, |
|
} |
|
|
|
|
|
if miss and hit_pos:IsValidZ() and hit_pos:z() > terrain.GetHeight(hit_pos) then |
|
local throw_vector = hit_pos - attack_pos |
|
if throw_vector:Len() == 0 then |
|
throw_vector = Rotate(point(guim, 0, 0), attacker:GetAngle()) |
|
end |
|
local bounce_diminish = const.Combat.KnifeBounceVelocityLoss |
|
local trajectory = CalcBounceParabolaTrajectory(hit_pos, SetLen(throw_vector, throw_velocity), const.Combat.Gravity, 10000, 20, 0, bounce_diminish) |
|
for _, step in ipairs(trajectory) do |
|
if step.t > 0 then |
|
step.t = step.t + tth |
|
attack_results.trajectory[#attack_results.trajectory + 1] = step |
|
end |
|
end |
|
end |
|
miss = miss or not lof_data or lof_data.stuck |
|
else |
|
attack_results.melee_attack = true |
|
end |
|
|
|
local total_damage = 0 |
|
if not miss then |
|
local hit = { |
|
obj = target, |
|
stealth_kill = kill, |
|
stealth_crit = crit and (stealth_crit_chance > 0), |
|
weapon = self, |
|
critical = crit, |
|
spot_group = attack_args.target_spot_group, |
|
actionType = action.ActionType, |
|
damage_bonus = attack_args.damage_bonus, |
|
impact_force = self:GetImpactForce(), |
|
melee_attack = attack_results.melee_attack, |
|
grazing = target_grazing_hit, |
|
} |
|
|
|
local record_breakdown = attack_results.dmg_breakdown |
|
if record_breakdown and attack_args.damage_bonus then |
|
record_breakdown[#record_breakdown + 1] = { name = action and action.DisplayName or T(328963668848, "Base"), value = attack_args.damage_bonus } |
|
end |
|
|
|
local damage = attacker:GetBaseDamage(self, nil, attack_results.dmg_breakdown) |
|
if not prediction then |
|
damage = RandomizeWeaponDamage(damage) |
|
end |
|
local effects = attack_args.applied_status |
|
if knockdown then |
|
effects = EffectsTable(effects) |
|
EffectTableAdd(effects, "KnockDown") |
|
end |
|
if attack_args.target_spot_group == "Neck" then |
|
if self.NeckAttackType == "choke" then |
|
effects = EffectsTable(effects) |
|
EffectTableAdd(effects, "Choking") |
|
elseif self.NeckAttackType == "bleed" then |
|
effects = EffectsTable(effects) |
|
EffectTableAdd(effects, "Bleeding") |
|
elseif self.NeckAttackType == "lethal" and kill then |
|
attack_results.decapitate = true |
|
end |
|
end |
|
self:PrecalcDamageAndStatusEffects(attacker, target, attack_pos, damage, hit, effects, attack_args, record_breakdown, action, prediction) |
|
total_damage = total_damage + hit.damage |
|
if kill then |
|
hit.damage = MulDivRound(target:GetTotalHitPoints(), 125, 100) |
|
end |
|
attack_results.hits = { hit } |
|
attack_results[1] = hit |
|
attack_results.hit_objs[#attack_results.hit_objs + 1] = target |
|
attack_results.hit_objs[target] = true |
|
attack_results.unit_damage = { [target] = hit.damage } |
|
if IsKindOf(target, "Unit") and not target:IsDead() and hit.damage >= target:GetTotalHitPoints() then |
|
attack_results.killed_units = {target} |
|
end |
|
elseif stuck then |
|
local hit = { |
|
obj = target, |
|
weapon = self, |
|
damage = 0, |
|
spot_group = attack_args.target_spot_group, |
|
actionType = action.ActionType, |
|
damage_bonus = attack_args.damage_bonus, |
|
impact_force = self:GetImpactForce(), |
|
melee_attack = attack_results.melee_attack, |
|
stuck = true, |
|
effects = {}, |
|
} |
|
attack_results.hits = { hit } |
|
attack_results[1] = hit |
|
end |
|
|
|
attack_results.total_damage = total_damage |
|
attack_results.miss = miss |
|
attack_results.target_hit = not miss |
|
return attack_results |
|
end |
|
|
|
function MeleeWeapon:CreateVisualObj(owner) |
|
return self:CreateVisualObjEntity(owner, IsValidEntity(self.Entity) and self.Entity or "Weapon_FC_AMZ_Knife_01") |
|
end |
|
|
|
DefineClass.StackableMeleeWeapon = { |
|
__parents = { "MeleeWeapon", "InventoryStack" }, |
|
properties = { |
|
|
|
{ id = "Condition" }, |
|
{ id = "RepairCost" }, |
|
{ id = "Repairable" }, |
|
{ category = "Scrap", id = "ScrapParts", name = "Scrap Parts", help = "The number for Parts that are given to the player when its scraped", |
|
editor = "number", default = 0, template = true, min = 0, max = 1000, }, |
|
} |
|
} |
|
|
|
DefineClass.UnarmedWeapon = { |
|
__parents = { "MeleeWeapon" }, |
|
base_action = "UnarmedAttack", |
|
} |
|
|
|
DefineClass.CrocodileWeapon = { |
|
__parents = { "MeleeWeapon" }, |
|
base_action = "CrocodileBite", |
|
} |
|
|
|
DefineClass.HyenaWeapon = { |
|
__parents = { "MeleeWeapon" }, |
|
base_action = "HyenaBite", |
|
} |
|
|