|
DefineClass.ZuluFloatingText = { |
|
__parents = { "XFloatingText" }, |
|
|
|
expire_time = 3000, |
|
fade_start = 0, |
|
MaxWidth = 450, |
|
HAlign = "left", |
|
TextHAlign = "center", |
|
|
|
TextStyle = "FloatingTextDefault", |
|
interpolate_opacity = true, |
|
} |
|
|
|
DefineClass.DamageFloatingText = { |
|
__parents = { "ZuluFloatingText" }, |
|
TextStyle = "FloatingTextDamage", |
|
always_show_on_distance = true, |
|
WordWrap = false |
|
} |
|
|
|
function CreateDamageFloatingText(target, text, style) |
|
if not config.FloatingTextEnabled or CheatEnabled("CombatUIHidden") then return end |
|
local valid_target |
|
if IsPoint(target) then |
|
valid_target = target:IsValid() |
|
elseif IsValid(target) then |
|
if IsKindOf(target, "Unit") and not target.visible then |
|
return |
|
end |
|
valid_target = target:IsValidPos() |
|
end |
|
|
|
assert(valid_target) |
|
if not valid_target then |
|
return |
|
end |
|
local ftext = XTemplateSpawn("DamageFloatingText", EnsureDialog("FloatingTextDialog"), false) |
|
return CreateCustomFloatingText(ftext, target, text, style, nil, "stagger_spawn") |
|
end |
|
|
|
DefineClass.DepositionCombatObject = { |
|
__parents = { "Deposition", "CombatObject" }, |
|
flags = { efSelectable = true }, |
|
} |
|
|
|
DefineClass.CombatObject = { |
|
__parents = { "GameDynamicDataObject", "CommandObject" }, |
|
flags = { efSelectable = true }, |
|
MaxHitPoints = 0, |
|
HitPoints = -1, |
|
TempHitPoints = 0, |
|
armor_class = 1, |
|
invulnerable = false, |
|
impenetrable = false, |
|
lastFloatingDamageText = false, |
|
} |
|
|
|
function CombatObject:IsInvulnerable() |
|
if IsObjVulnerableDueToLDMark(self) then |
|
return false |
|
end |
|
|
|
return self.invulnerable or IsObjInvulnerableDueToLDMark(self) or TemporarilyInvulnerableObjs[self] |
|
end |
|
|
|
function CombatObject:GetDynamicData(data) |
|
if self.HitPoints ~= self.MaxHitPoints then |
|
data.HitPoints = self.HitPoints |
|
end |
|
data.TempHitPoints = (self.TempHitPoints ~= 0) and self.TempHitPoints or nil |
|
end |
|
|
|
function CombatObject:SetDynamicData(data) |
|
self.HitPoints = data.HitPoints or self.MaxHitPoints |
|
self.TempHitPoints = data.TempHitPoints or 0 |
|
end |
|
|
|
function CombatObject:GameInit() |
|
self:InitFromMaterial() |
|
end |
|
|
|
function CombatObject:InitFromMaterial() |
|
local material_type = self:GetMaterialType() |
|
if material_type then |
|
local preset = Presets.ObjMaterial.Default[material_type] |
|
if preset then |
|
self:InitFromMaterialPreset(preset) |
|
else |
|
StoreErrorSource(self, string.format("[WARNING] Object of class %s set to invalid combat material type '%s'", self.class, material_type)) |
|
end |
|
end |
|
end |
|
|
|
function CombatObject:GetCombatMaterial() |
|
|
|
|
|
local material_type = self:GetMaterialType() |
|
if material_type then |
|
return Presets.ObjMaterial.Default[material_type] |
|
end |
|
end |
|
|
|
function CombatObject:SetMaterialType(id) |
|
local material_type = self:GetMaterialType() |
|
if id == material_type then |
|
return |
|
end |
|
local preset = Presets.ObjMaterial.Default[id] |
|
if not preset then |
|
print("once", string.format("[WARNING] Object of class %s set to invalid combat material type '%s'", self.class, id)) |
|
return |
|
end |
|
self.material_type = id |
|
self:InitFromMaterialPreset(preset) |
|
end |
|
|
|
function OnMsg.NewMapLoaded() |
|
MapForEach("map", "CObject", nil, nil, nil, nil, const.cofComponentCollider, function(obj, materials) |
|
if obj:IsKindOf("CombatObject") then |
|
return |
|
end |
|
local preset = materials[obj.material_type] |
|
if not preset or preset.impenetrable then |
|
return |
|
end |
|
collision.SetPenetratingDefense(obj, 1) |
|
end, Presets.ObjMaterial.Default) |
|
end |
|
|
|
function CombatObject:InitFromMaterialPreset(preset) |
|
self.MaxHitPoints = preset.max_hp |
|
self.HitPoints = self.MaxHitPoints |
|
self.armor_class = preset.armor_class |
|
local forced_invulnerable = self:HasMember("forceInvulnerableBecauseOfGameRules") and self.forceInvulnerableBecauseOfGameRules |
|
self.invulnerable = self.invulnerable or forced_invulnerable or preset.invulnerable |
|
|
|
local defense = not self.impenetrable and preset and not preset.impenetrable and self.armor_class or -1 |
|
collision.SetPenetratingDefense(self, defense) |
|
end |
|
|
|
function CombatObject:IsDead() |
|
return self.HitPoints <= 0 |
|
end |
|
|
|
function CombatObject:IsPlayerAlly() |
|
return false |
|
end |
|
|
|
function Slab:OnDie() |
|
assert(self.isVisible) |
|
CombatObject.OnDie(self) |
|
end |
|
|
|
function CombatObject:OnDie() |
|
self:SetCommand("Die") |
|
end |
|
|
|
|
|
function OnMsg.CombatEnd() |
|
for i, unit in ipairs(g_Units) do |
|
unit.TempHitPoints = 0 |
|
ObjModified(unit) |
|
end |
|
end |
|
|
|
function CombatObject:ApplyTempHitPoints(value) |
|
self.TempHitPoints = Clamp(self.TempHitPoints + value, 0, const.Combat.MaxGrit) |
|
ObjModified(self) |
|
end |
|
|
|
function CombatObject:GetTotalHitPoints() |
|
if self.TempHitPoints and self.TempHitPoints > 0 then |
|
return self.HitPoints + self.TempHitPoints |
|
else |
|
return self.HitPoints |
|
end |
|
end |
|
|
|
function CombatObject:PrecalcDamageTaken(dmg, hp, temp_hp) |
|
hp = hp or self.HitPoints |
|
temp_hp = temp_hp or self.TempHitPoints |
|
local damage_dealt = 0 |
|
|
|
if not self:IsInvulnerable() then |
|
if CheatEnabled("WeakDamage") then |
|
dmg = dmg / 100 |
|
elseif CheatEnabled("StrongDamage") then |
|
dmg = dmg * 100 |
|
end |
|
|
|
damage_dealt = Max(0, dmg - self.TempHitPoints) |
|
temp_hp = Max(0, temp_hp - dmg) |
|
hp = Max(0, hp - damage_dealt) |
|
end |
|
return hp, temp_hp, damage_dealt |
|
end |
|
|
|
function CombatObject:TakeDirectDamage(dmg, floating, log_type, log_msg, attacker, hit_descr) |
|
if self:IsInvulnerable() then |
|
return |
|
end |
|
if CheatEnabled("WeakDamage") then |
|
dmg = dmg / 100 |
|
elseif CheatEnabled("StrongDamage") then |
|
dmg = dmg * 100 |
|
end |
|
|
|
hit_descr = hit_descr or {} |
|
hit_descr.prev_hit_points = self.HitPoints |
|
hit_descr.raw_damage = dmg |
|
|
|
|
|
local hp, thp, damage_taken = self:PrecalcDamageTaken(dmg) |
|
self.TempHitPoints = thp |
|
self.HitPoints = hp |
|
self:OnHPLoss(dmg, attacker) |
|
self:NetUpdateHash("TakeDirectDamage", dmg, hit_descr.prev_hit_points, self.HitPoints, self.TempHitPoints, damage_taken) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.HitPoints == 0 then |
|
self:OnDie(attacker, hit_descr) |
|
end |
|
|
|
if log_type and log_msg then |
|
CombatLog(log_type, log_msg) |
|
end |
|
if floating and not hit_descr.setpiece then |
|
CreateDamageFloatingText(self, floating) |
|
end |
|
end |
|
|
|
function CombatObject:TakeDamage(dmg, attacker, hit_descr) |
|
if not IsValid(self) or self:IsDead() or self:IsInvulnerable() then |
|
return |
|
end |
|
|
|
hit_descr = hit_descr or {} |
|
|
|
self:LogDamage(dmg, attacker, hit_descr) |
|
|
|
self:TakeDirectDamage(dmg, nil, nil, nil, attacker, hit_descr) |
|
Msg("DamageDone", attacker, self, dmg, hit_descr) |
|
if IsKindOf(attacker, "Unit") then |
|
attacker:CallReactions("OnDamageDone", self, dmg, hit_descr) |
|
end |
|
Msg("DamageTaken", attacker, self, dmg, hit_descr) |
|
if IsKindOf(self, "Unit") then |
|
self:CallReactions("OnDamageTaken", attacker, dmg, hit_descr) |
|
end |
|
end |
|
|
|
function CombatObject:OnHPLoss(dmg, attacker) |
|
end |
|
|
|
function CombatObject:DisplayFloatingTextDamage(damage, hit, accumulate) |
|
if accumulate and not hit.grazing then |
|
local lastText = self.lastFloatingDamageText |
|
local marginOfTime = 700 |
|
local oldTextNotFaded = lastText and (GetPreciseTicks() - lastText.timeNow) < marginOfTime |
|
if lastText and lastText.window_state == "open" and oldTextNotFaded then |
|
local accumulatedDamage = damage + lastText.Text.num |
|
lastText.Text.num = accumulatedDamage |
|
lastText:SetText(lastText.Text) |
|
lastText:UpdateDrawCache(lastText.draw_cache_text_width, lastText.draw_cache_text_height, true) |
|
return |
|
end |
|
end |
|
|
|
local txt |
|
if not hit.setpiece then |
|
if hit.grazing then |
|
if hit.grazing_reason == "fog" then |
|
txt = CreateDamageFloatingText(self, T{554948654101, "<num> Grazed (Fog)", num = damage}, "FloatingTextMiss") |
|
elseif hit.grazing_reason == "duststorm" then |
|
txt = CreateDamageFloatingText(self, T{395798135760, "<num> Grazed (Dust Storm)", num = damage}, "FloatingTextMiss") |
|
else |
|
txt = CreateDamageFloatingText(self, T{970945572773, "<num> Grazed", num = damage}, "FloatingTextMiss") |
|
end |
|
elseif hit.critical then |
|
txt = CreateDamageFloatingText(self, T{307116587677, "<num> CRIT!", num = damage}, "FloatingTextCrit") |
|
else |
|
txt = CreateDamageFloatingText(self, T{867764319678, "<num>", num = damage}, nil) |
|
end |
|
end |
|
if not hit.grazing then |
|
self.lastFloatingDamageText = txt |
|
end |
|
end |
|
|
|
|
|
function CombatObject:LogDamage(dmg, attacker, hit, reductionInfo) |
|
local logName = self:GetLogName() |
|
|
|
if IsKindOf(self, "Unit") then |
|
if hit.spot_group and not hit.explosion and not hit.aoe then |
|
local part = Presets.TargetBodyPart.Default[hit.spot_group].display_name |
|
if (hit.armor_prevented or 0) > 0 then |
|
if hit.grazing then |
|
CombatLog("debug", T{Untranslated(" Grazing hit. <em><target></em> was hit in the <bodypart> for <em><num> damage</em>, <num2> absorbed"), target = logName, num = dmg, num2 = hit.armor_prevented, bodypart = part}) |
|
elseif hit.critical then |
|
if hit.stealth_crit then |
|
CombatLog("debug", T{Untranslated(" Stealth Critical hit! <em><target></em> was hit in the <bodypart> for <em><num> damage</em>, <num2> absorbed"), target = logName, num = dmg, num2 = hit.armor_prevented, bodypart = part}) |
|
else |
|
CombatLog("debug", T{Untranslated(" Critical hit! <em><target></em> was hit in the <bodypart> for <em><num> damage</em>, <num2> absorbed"), target = logName, num = dmg, num2 = hit.armor_prevented, bodypart = part}) |
|
end |
|
elseif hit.stray then |
|
CombatLog("debug", T{Untranslated(" Stray shot. <em><target></em> was hit in the <bodypart> for <em><num> damage</em>, <num2> absorbed"), target = logName, num = dmg, num2 = hit.armor_prevented, bodypart = part}) |
|
else |
|
CombatLog("debug", T{Untranslated(" <em><target></em> was hit in the <bodypart> for <em><num> damage</em>, <num2> absorbed"), target = logName, num = dmg, num2 = hit.armor_prevented, bodypart = part}) |
|
end |
|
else |
|
if hit.grazing then |
|
CombatLog("debug", T{Untranslated(" Grazing hit. <em><target></em> was hit in the <bodypart> for <em><num> damage</em>"), target = logName, bodypart = part, num = dmg}) |
|
elseif hit.critical then |
|
if hit.stealth_crit then |
|
CombatLog("debug", T{Untranslated(" Stealth Critical hit! <em><target></em> was hit in the <bodypart> for <em><num> damage</em>"), target = logName, num = dmg, bodypart = part}) |
|
else |
|
CombatLog("debug", T{Untranslated(" Critical hit! <em><target></em> was hit in the <bodypart> for <em><num> damage</em>"), target = logName, num = dmg, bodypart = part}) |
|
end |
|
elseif hit.stray then |
|
CombatLog("debug", T{Untranslated(" Stray shot. <em><target></em> was hit in the <bodypart> for <em><num> damage</em>"), target = logName, bodypart = part, num = dmg}) |
|
else |
|
CombatLog("debug", T{Untranslated(" <em><target></em> was hit in the <bodypart> for <em><num> damage</em>"), target = logName, bodypart = part, num = dmg}) |
|
end |
|
end |
|
else |
|
CombatLog("debug", T{Untranslated(" <em><target></em> was hit for <em><num> damage</em>"), target = logName, num = dmg}) |
|
end |
|
|
|
self:DisplayFloatingTextDamage(dmg, hit, true) |
|
|
|
if reductionInfo then |
|
for _, s in ipairs(reductionInfo) do |
|
CombatLog("debug", T{Untranslated(" <amount> damage was reduced by <statusEffect>"), amount = s.Value, statusEffect = s.Effect.DisplayName}) |
|
end |
|
end |
|
else |
|
CombatLog("debug", T{Untranslated(" <em><target></em> was hit for <em><num> damage</em>"), target = logName, num = dmg}) |
|
end |
|
if hit.stuck then |
|
CombatLog("debug", T(Untranslated(" Bullet got stuck"))) |
|
end |
|
end |
|
|
|
function CombatObject:Die() |
|
CombatLog("debug", T{Untranslated(" <name> was destroyed"), name = self:GetLogName()}) |
|
Msg("CombatObjectDied", self, self:GetObjectBBox()) |
|
DoneCombatObject(self) |
|
end |
|
|
|
function CombatObject:GetLogName() |
|
if IsKindOf("PropertyObj") and self:HasMember("DisplayName") then |
|
return self.DisplayName |
|
end |
|
if Platform.developer then |
|
return Untranslated(self.class) |
|
end |
|
return "" |
|
end |
|
|
|
function CombatObject:GetHealthPercentage() |
|
return MulDivRound(100, self.HitPoints, self.MaxHitPoints) |
|
end |
|
|
|
CombatObject.SpreadDebris = DestroyableSlab.SpreadDebris |
|
CombatObject.GetDebrisInfo = DestroyableSlab.GetDebrisInfo |
|
|
|
AppendClass.Slab = { |
|
__parents = { "CombatObject" }, |
|
|
|
material_type = false, |
|
SpreadDebris = DestroyableSlab.SpreadDebris, |
|
GetDebrisInfo = DestroyableSlab.GetDebrisInfo, |
|
} |
|
|
|
function OnMsg.ClassesGenerate(classdefs) |
|
local prop_meta = table.find_value(classdefs.AppearanceObject.properties, "id", "Appearance") |
|
prop_meta.default = "Ivan" |
|
end |
|
|
|
local function LogAreaDamageHits(hits, attacker, indent, no_units_text, results) |
|
local units_hit = 0 |
|
for _, hit in ipairs(hits) do |
|
local target = hit.obj |
|
if IsValid(target) and hit.damage > 0 and IsKindOf(target, "CombatObject") then |
|
local is_unit = IsKindOf(target, "Unit") |
|
local lt = is_unit and "helper" or "debug" |
|
local prefix = T(951707939968, "(<em>Hit</em>) ") |
|
units_hit = units_hit + (is_unit and 1 or 0) |
|
if table.find(results.killed_units or empty_table, target) then |
|
prefix = T(545544029910, "(<em>Kill</em>) ") |
|
end |
|
|
|
if attacker:IsOnAllySide(target) then |
|
prefix = T(322086931590, "(<em>Friendly fire</em>) ") |
|
end |
|
|
|
local log_name = target:GetLogName() |
|
if log_name ~= "" and IsT(log_name) and (type(log_name) == "table" and not log_name.untranslated) then |
|
CombatLog(lt, T{800299292975, "<prefix><target> takes <em><num> damage</em> by area attack", prefix = prefix, indent = indent or "", target = log_name, num = hit.damage}) |
|
end |
|
end |
|
end |
|
if no_units_text and units_hit == 0 then |
|
CombatLog("helper", T{646611561441, "No targets hit", indent = indent or ""}) |
|
end |
|
end |
|
|
|
local function LogDirectDamage(results, attacker, target, context, indent) |
|
local damage, hits, crits = 0, 0, 0 |
|
local processed = {} |
|
local stray, grazing |
|
local cth = results.chance_to_hit or 100 |
|
local shot_index = 1 |
|
local absorbed_total = 0 |
|
local inaccurate_grazed = 0 |
|
for i,shot in ipairs(results.shots) do |
|
local cth = 0 |
|
local damage = 0 |
|
local absorbed = 0 |
|
local grazed_miss |
|
if not results.obstructed then |
|
cth = shot.cth or 0 |
|
for _, hit in ipairs(shot.hits) do |
|
if hit.obj == target then |
|
damage = damage + (hit.damage or 0) |
|
absorbed = absorbed + hit.armor_prevented |
|
grazed_miss = grazed_miss or hit.grazed_miss |
|
end |
|
end |
|
end |
|
absorbed_total = absorbed_total + absorbed |
|
local absorbed_text = (absorbed > 0) and T{101651236091, "(<absorbed> absorbed)",absorbed = absorbed} or "" |
|
local outcome |
|
if grazed_miss then |
|
outcome = Untranslated("Grazed (inaccurate)") |
|
elseif shot.miss then |
|
outcome = Untranslated("Miss") |
|
else |
|
outcome = Untranslated("Hit") |
|
end |
|
CombatLog("debug", T{Untranslated("Shot <id> at <target> CtH: <percent(cth)>, roll: <num>/100 <hit_miss> <damage> damage <absorbed_text> "), |
|
id = i, target = target:GetLogName(), cth = cth, num = shot.roll or 100, |
|
hit_miss = outcome, |
|
damage = damage, absorbed_text = absorbed_text |
|
}) |
|
end |
|
for _, hit in ipairs(results) do |
|
if hit.obj == target then |
|
damage = damage + hit.damage |
|
hits = hits + 1 |
|
crits = crits + (hit.critical and 1 or 0) |
|
stray = stray or hit.stray |
|
grazing = grazing or hit.grazing |
|
if hit.grazed_miss then |
|
inaccurate_grazed = inaccurate_grazed + 1 |
|
end |
|
end |
|
end |
|
|
|
if results.miss and (inaccurate_grazed == 0) and not stray or results.obstructed then |
|
CombatLog("helper",T{556012296568, "<em>Missed</em> <target>",indent=indent, target = target:GetLogName()}) |
|
return |
|
end |
|
|
|
if not IsT(target:GetLogName()) then return end |
|
|
|
local prefix, suffix = "", "" |
|
|
|
if results.stealth_attack then |
|
if results.stealth_kill then |
|
CombatLog("debug",T{Untranslated("<em>Stealth Kill</em> successful (<percent(stealth_chance)> chance)"),indent = indent,stealth_chance = context.stealth_kill_chance}) |
|
else |
|
CombatLog("debug",T{Untranslated("<em>Stealth Kill</em> failed (<percent(stealth_chance)> chance)"),indent = indent,stealth_chance = context.stealth_kill_chance}) |
|
end |
|
end |
|
|
|
if crits > 1 then |
|
suffix = T{820883776569, " (<num> crits)", num = crits} |
|
elseif crits == 1 then |
|
suffix = T(886703526051, " (crit)") |
|
end |
|
|
|
if results.stealth_kill then |
|
prefix = T(159664158022, "(<em>Stealth Kill</em>) ") |
|
elseif table.find(results.killed_units or empty_table, target) then |
|
prefix = T(545544029910, "(<em>Kill</em>) ") |
|
elseif hits > 1 then |
|
prefix = T{284567652570, "(<em><accurate> Hits</em>) ",accurate = hits} |
|
elseif grazing then |
|
prefix = T(226851065912, "(<em>Grazing hit</em>) ") |
|
else |
|
prefix = T(951707939968, "(<em>Hit</em>) ") |
|
end |
|
|
|
if attacker:IsOnAllySide(target) then |
|
if stray then |
|
prefix = T(806182260858, "(<em>Stray friendly fire</em>) ") |
|
else |
|
prefix = T(322086931590, "(<em>Friendly fire</em>) ") |
|
end |
|
elseif stray then |
|
prefix = T(623586221175, "(<em>Stray shot</em>) ") |
|
end |
|
|
|
if inaccurate_grazed > 0 then |
|
CombatLog("helper", T{901890498660, "<number> inaccurate shot(s) grazed the target", number = inaccurate_grazed}) |
|
end |
|
|
|
local absorbed_text = absorbed_total > 0 and T{101651236091, "(<absorbed> absorbed)", absorbed = absorbed_total} or "" |
|
CombatLog("helper", T{575621720323, "<prefix><target> takes <em><num> damage</em> <absorbed_text><suffix>", |
|
target = target:GetLogName(), |
|
prefix = prefix, |
|
suffix = suffix, |
|
num = damage, |
|
indent = indent or "", |
|
absorbed_text = absorbed_text}, indent) |
|
end |
|
|
|
function LogAttack(action, attack_args, results) |
|
local attacker = attack_args.obj |
|
local target = attack_args.target |
|
local weapon = results.weapon |
|
|
|
if attack_args.used_action_id then |
|
action = CombatActions[attack_args.used_action_id] or action |
|
end |
|
local spot = attack_args.target_spot_group |
|
local spotname = spot and Presets.TargetBodyPart.Default[spot] and Presets.TargetBodyPart.Default[spot].display_name |
|
|
|
local context = { |
|
attacker = attacker:GetLogName(), |
|
target = IsKindOf(target, "Unit") and target:GetLogName() or "", |
|
attack = not not attacker.attack_reason and attacker.attack_reason or action:GetActionDisplayName({attacker}), |
|
retaliation = not not attacker.attack_reason and T(425058684346, "(<em>Interrupt</em>) ") or "", |
|
weapon = weapon.DisplayName, |
|
cth = results.chance_to_hit or 100, |
|
stealth_kill_chance = attack_args.stealth_kill_chance or 0, |
|
num_attacks = IsKindOf(weapon, "Firearm") and results.fired or 1, |
|
mishap = results.mishap and T(899186217845, "(<em>Mishap</em>) ") or "", |
|
target_spot = spotname and T{345592247170, "(<target_spot>)",target_spot = spotname} or "" |
|
} |
|
|
|
if IsKindOfClasses(weapon, "Firearm", "MeleeWeapon") then |
|
local indent = " " |
|
context.indent = indent |
|
|
|
if context.target == "" then |
|
CombatLog("short", T{103704598522, "<mishap><em><retaliation><attack></em> by <em><attacker></em> <target_spot>", context}) |
|
else |
|
CombatLog("short", T{201907063671, "<mishap><retaliation><em><attack></em> at <target> by <em><attacker></em> <target_spot>", context}) |
|
CombatLog("debug", T{Untranslated("Attack CtH - <percent(cth)>"), context}) |
|
end |
|
|
|
local any_hit = true |
|
if IsKindOf(target, "CombatObject") then |
|
LogDirectDamage(results, attacker, target, context, indent) |
|
any_hit = false |
|
end |
|
|
|
if IsKindOf(weapon, "Firearm") then |
|
|
|
local processed = { [target] = true } |
|
for _, hit in ipairs(results) do |
|
if not processed[hit.obj] and IsKindOf(hit.obj, "Unit") and hit.damage > 0 then |
|
|
|
LogDirectDamage(results, attacker, hit.obj, context, indent) |
|
processed[hit.obj] = true |
|
any_hit = false |
|
end |
|
end |
|
|
|
LogAreaDamageHits(results.area_hits or empty_table, attacker, indent, any_hit, results) |
|
end |
|
|
|
if any_hit and results.stealth_attack and (not results.stealth_kill) and (attack_args.stealth_kill_chance or 0) > 0 then |
|
CombatLog("short",T{321216462186, "<indent><em>Stealth Kill</em> failed", indent = indent, stealth_chance = attack_args.stealth_kill_chance}) |
|
CombatLog("debug",T{Untranslated("<indent>Stealth Kill< chance (<percent(stealth_chance)>)"), indent = indent, stealth_chance = attack_args.stealth_kill_chance}) |
|
end |
|
|
|
elseif IsKindOf(weapon, "Grenade") then |
|
if attacker.attack_reason then |
|
CombatLog("short", T{604040871119, "<mishap>Interrupt attack - <em><weapon></em> thrown by <em><attacker></em>", context}) |
|
else |
|
CombatLog("short", T{339680683529, "<mishap><em><attacker></em> has thrown a <em><weapon></em>", context}) |
|
end |
|
if not results.trap_placed then |
|
LogAreaDamageHits(results, attacker, " ", T(233144990184, "No targets hit"),results) |
|
end |
|
elseif IsKindOf(weapon, "Ordnance") then |
|
CombatLog("short", T{539114035613, "<mishap><em><attacker></em> has launched a <em><weapon></em>", context}) |
|
LogAreaDamageHits(results, attacker, " ", T(233144990184, "No targets hit"),results) |
|
end |
|
end |
|
|
|
DefineClass.HidingCombatObject = { |
|
__parents = {"CombatObject", "EditorObject"}, |
|
|
|
properties = { |
|
{id = "is_destroyed", editor = "bool", default = false, no_edit = true, dont_save = true}, |
|
}, |
|
} |
|
|
|
function HidingCombatObject:Die() |
|
self:Destroy() |
|
CombatLog("debug", T{Untranslated(" <name> was destroyed"), name = self:GetLogName()}) |
|
Msg("CombatObjectDied", self, self:GetObjectBBox()) |
|
self:SetCommand("Dead") |
|
end |
|
|
|
function HidingCombatObject:Dead() |
|
self:SetVisible(false) |
|
self:SetCollision(false) |
|
end |
|
|
|
function HidingCombatObject:SetDynamicData(data) |
|
if self:IsDead() then |
|
collision.SetAllowedMask(self, 0) |
|
end |
|
end |
|
|
|
if FirstLoad then |
|
g_DbgExplosionDamage = false |
|
end |
|
|
|
function DbgIncendiaryExplosion(pos) |
|
if not pos then |
|
local eye = camera.GetEye() |
|
local cursor = ScreenToGame(terminal.GetMousePos()) |
|
local sp = eye |
|
local ep = (cursor - eye) * 1000 + cursor |
|
local closest = false |
|
local objs = IntersectObjectsSphereCast(sp, ep, guim/4, 0, "Slab", function(o) |
|
|
|
if o.isVisible and not o.is_destroyed then |
|
|
|
closest = not closest and o or IsCloser(sp, o, closest) and o or closest |
|
return true |
|
end |
|
end) |
|
if closest then |
|
local p1, p2 = ClipSegmentWithBox3D(sp, ep, closest) |
|
pos = p1 or closest:GetPos() |
|
end |
|
if not pos then |
|
RequestPixelWorldPos(terminal.GetMousePos()) |
|
WaitNextFrame(6) |
|
pos = ReturnPixelWorldPos() |
|
end |
|
end |
|
if not pos then return end |
|
|
|
local obj = PlaceParticles("Explosion_Barrel") |
|
obj:SetPos(pos) |
|
|
|
local origin = SnapToVoxel(pos):SetZ(pos:z()) |
|
local radius = 2*const.SlabSizeX |
|
local step = const.SlabSizeX |
|
local step = 70*guic |
|
local pos_noise = 20*guic |
|
local terrain1 = Presets.TerrainObj.Default.Dry_BurntGround_01 |
|
local terrain2 = Presets.TerrainObj.Default.Dry_BurntGround_02 |
|
local objs = MapGet(pos, radius, "Object", function(o) return o:GetEnumFlags(const.efVisible) ~= 0 end) |
|
for _, obj in ipairs(objs) do |
|
obj:SetColorModifier(RGBA(0, 0, 0, 255)) |
|
end |
|
for dy = -radius, radius, step do |
|
for dx = -radius, radius, step do |
|
local pt = origin + point(dx, dy, 0) |
|
local slab_obj, z = WalkableSlabByPoint(pt) |
|
pt = pt:SetZ(z) |
|
if IsCloser(pos, pt, radius) then |
|
CreateGameTimeThread(function(p, t) |
|
local obj = PlaceParticles("Env_Fire1x1") |
|
obj:SetPos(p) |
|
terrain.SetTypeCircle(p, step/2, t) |
|
Sleep(5000 + AsyncRand(1000)) |
|
StopParticles(obj) |
|
obj = PlaceParticles("Env_Fire1x1_Smoldering") |
|
obj:SetPos(p) |
|
Sleep(2000 + AsyncRand(1000)) |
|
StopParticles(obj) |
|
end, pt, (AsyncRand(100) < 50 and terrain1 or terrain2).idx) |
|
end |
|
end |
|
end |
|
|
|
obj = PlaceObject("DecExplosion_02") |
|
if obj then |
|
obj:SetPos(pos) |
|
end |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
end |
|
|
|
local ce_thread = false |
|
function DbgCarpetExplosionDamage(ztype) |
|
|
|
|
|
|
|
|
|
local stepx = const.SlabSizeX * 3 |
|
local stepy = const.SlabSizeY * 3 |
|
local stepz = const.SlabSizeZ * 3 |
|
local border = GetBorderAreaLimits():grow(stepx, stepy, 0) |
|
local bmin = border:min() |
|
local bmax = border:max() |
|
DbgClear() |
|
local x, y, z = 0, 0, 0 |
|
if IsValidThread(ce_thread) then |
|
DeleteThread(ce_thread) |
|
end |
|
ce_thread = CreateRealTimeThread(function() |
|
while true do |
|
local xx = bmin:x() + const.SlabSizeX / 2 + x * stepx |
|
if xx >= bmax:x() then |
|
x = 0 |
|
y = y + 1 |
|
xx = bmin:x() + const.SlabSizeX / 2 |
|
end |
|
local yy = bmin:y() + const.SlabSizeY / 2 + y * stepy |
|
if yy >= bmax:y() then |
|
break |
|
end |
|
local zz |
|
if not ztype or ztype == "grounded" then |
|
zz = terrain.GetHeight(xx, yy) |
|
x = x + 1 |
|
elseif type(ztype) == "number" then |
|
if ztype < 0 then |
|
zz = terrain.GetHeight(xx, yy) + (abs(ztype) - z) * stepz |
|
else |
|
zz = terrain.GetHeight(xx, yy) + z * stepz |
|
end |
|
z = z + 1 |
|
if z > abs(ztype) then |
|
z = 0 |
|
x = x + 1 |
|
end |
|
elseif ztype == "bomb" then |
|
local th = terrain.GetHeight(xx, yy) |
|
zz = th |
|
local sp = point(xx, yy, th + const.SlabSizeZ * 100) |
|
local ep = point(xx, yy, th) |
|
local closest = GetClosestRayObj(sp, ep, const.efVisible + const.efCollision) |
|
if closest then |
|
zz = closest:GetObjectBBox():maxz() |
|
end |
|
x = x + 1 |
|
end |
|
|
|
DbgExplosionDamage(point(xx, yy, zz)) |
|
Sleep(5) |
|
end |
|
end) |
|
end |
|
|
|
if FirstLoad then |
|
DbgExplosionFX_ShowRange = false |
|
end |
|
|
|
function DbgExplosionFX(pos) |
|
if not pos then |
|
local eye = camera.GetEye() |
|
local cursor = ScreenToGame(terminal.GetMousePos()) |
|
local sp = eye |
|
local ep = (cursor - eye) * 1000 + cursor |
|
local closest = false |
|
|
|
local objs = IntersectObjectsOnSegment(sp, ep, 0, "Slab", function(o) |
|
if o.isVisible and not o.is_destroyed then |
|
|
|
closest = not closest and o or IsCloser(sp, o, closest) and o or closest |
|
return true |
|
end |
|
end) |
|
if closest then |
|
local p1, p2 = ClipSegmentWithBox3D(sp, ep, closest) |
|
pos = p1 or closest:GetPos() |
|
end |
|
if not pos then |
|
RequestPixelWorldPos(terminal.GetMousePos()) |
|
WaitNextFrame(6) |
|
pos = ReturnPixelWorldPos() |
|
end |
|
end |
|
if not pos then return end |
|
|
|
local explosion_actor = DbgCycleExplosion(0) |
|
local surf_fx_type = GetObjMaterial(pos) |
|
pos = pos - point(0,0,255) |
|
local grenade = PlaceInventoryItem(explosion_actor) |
|
local aoe_params = grenade:GetAreaAttackParams(nil, nil, pos) |
|
local results = GetAreaAttackResults(aoe_params, 0, nil, false) |
|
results.burn_ground = grenade.BurnGround |
|
|
|
if DbgExplosionFX_ShowRange then |
|
ShowCircle(pos, results.range, RGB(128, 128, 128)) |
|
end |
|
|
|
if IsKindOf(grenade, "ThrowableTrapItem") then |
|
explosion_actor = explosion_actor .. "_OnGround" |
|
end |
|
if IsKindOf(grenade, "Flare") then |
|
local flare = PlaceObject("FlareOnGround", {fx_actor_class = grenade.class}) |
|
flare:SetPos(pos) |
|
PlayFX("Spawn", "start", flare) |
|
else |
|
if grenade.aoeType ~= "none" then |
|
PlayFX("ExplosionGas", "start", explosion_actor, surf_fx_type, pos) |
|
else |
|
PlayFX("Explosion", "start", explosion_actor, surf_fx_type, pos) |
|
end |
|
ApplyExplosionDamage(nil, nil, results, 0) |
|
end |
|
DoneCombatObject(grenade) |
|
end |
|
|
|
local DbgGrenadeIdx = 9 |
|
|
|
function DbgSetExplosionType(self, root, prop_id, ged) |
|
DbgCycleExplosion(self.id) |
|
end |
|
|
|
function DbgCycleExplosion(value) |
|
local explosion_list = GetWeaponsByType("Grenade") |
|
local grenade_id = table.values(explosion_list, true, "id") |
|
local mortar_ammo = GetAmmosWithCaliber("MortarShell") |
|
local _40mm_ammo = GetAmmosWithCaliber("40mmGrenade") |
|
local mortar_id = table.values(mortar_ammo, true, "id") |
|
local _40mm_id = table.values(_40mm_ammo, true, "id") |
|
local all = table.iappend(mortar_id, _40mm_id) |
|
all = table.iappend(all, grenade_id) |
|
|
|
if type(value) == "string" then |
|
DbgGrenadeIdx = table.find(all, value) or DbgGrenadeIdx |
|
value = 0 |
|
end |
|
|
|
if table.maxn(all) == DbgGrenadeIdx and value == 1 then |
|
DbgGrenadeIdx = 1 |
|
else |
|
DbgGrenadeIdx = DbgGrenadeIdx + value |
|
if DbgGrenadeIdx == 0 then |
|
DbgGrenadeIdx = table.maxn(all) |
|
end |
|
end |
|
|
|
return all[DbgGrenadeIdx] |
|
end |
|
|
|
|
|
function DbgExplosionDamage(pos, dmg) |
|
dmg = dmg or g_DbgExplosionDamage |
|
if not pos then |
|
local eye = camera.GetEye() |
|
local cursor = ScreenToGame(terminal.GetMousePos()) |
|
local sp = eye |
|
local ep = (cursor - eye) * 1000 + cursor |
|
local closest = false |
|
|
|
local objs = IntersectObjectsOnSegment(sp, ep, 0, "Slab", function(o) |
|
if o.isVisible and not o.is_destroyed then |
|
|
|
closest = not closest and o or IsCloser(sp, o, closest) and o or closest |
|
return true |
|
end |
|
end) |
|
if closest then |
|
local p1, p2 = ClipSegmentWithBox3D(sp, ep, closest) |
|
pos = p1 or closest:GetPos() |
|
end |
|
if not pos then |
|
RequestPixelWorldPos(terminal.GetMousePos()) |
|
WaitNextFrame(6) |
|
pos = ReturnPixelWorldPos() |
|
end |
|
end |
|
if not pos then return end |
|
|
|
local grenade = PlaceInventoryItem("Super_HE_Grenade") |
|
local aoe_params = grenade:GetAreaAttackParams(nil, nil, pos) |
|
aoe_params.prediction = false |
|
local results = GetAreaAttackResults(aoe_params, 0, nil, dmg) |
|
if dmg then |
|
DbgTestExplode(pos, "Explosion") |
|
else |
|
DbgAddVector(pos) |
|
end |
|
ApplyExplosionDamage(nil, nil, results, 0) |
|
DoneCombatObject(grenade) |
|
end |
|
|
|
function DbgBulletDamage(pos, dmg) |
|
if not CurrentThread() then |
|
return CreateGameTimeThread(DbgBulletDamage, pos, dmg) |
|
end |
|
|
|
if not pos then |
|
RequestPixelWorldPos(terminal.GetMousePos()) |
|
WaitNextFrame(6) |
|
pos = ReturnPixelWorldPos() |
|
if not pos then |
|
return |
|
end |
|
end |
|
local target_pos = pos |
|
local target = GetPreciseCursorObj() |
|
if IsKindOf(target, "Unit") then |
|
target = SelectionPropagate(target) |
|
end |
|
if not target then |
|
print("no target found") |
|
return |
|
else |
|
|
|
if target:GetEnumFlags(const.efCollision) == 0 then |
|
print(" target has no collision, try using normal attacks (F)") |
|
return |
|
end |
|
end |
|
|
|
|
|
local attacker = SelectedObj |
|
local attack_pos, collision_pos |
|
if IsKindOf(attacker, "Unit") then |
|
attack_pos = attacker:GetSpotLocPos(attacker:GetSpotBeginIndex("Head")) |
|
target_pos = attack_pos + (target_pos - attack_pos)*5/4 |
|
local any_hit, hit_pos, hit_objs = CollideSegmentsObjs({attack_pos, target_pos}) |
|
if any_hit then |
|
for i, obj in ipairs(hit_objs) do |
|
if obj == target then |
|
collision_pos = hit_pos[i] |
|
break |
|
end |
|
end |
|
end |
|
else |
|
for i = 1, 100 do |
|
local len = 3 * guim + AsyncRand(5 * guim) |
|
local origin = RotateRadius(len, AsyncRand(360*60), target_pos) |
|
for j = 0, 20 do |
|
attack_pos = SnapToPassSlab(origin:SetTerrainZ(j*guim)) |
|
if attack_pos then break end |
|
end |
|
if attack_pos then |
|
if not attack_pos:IsValidZ() then |
|
attack_pos = attack_pos:SetTerrainZ() |
|
end |
|
attack_pos = attack_pos + point(0, 0, guim) |
|
local tp = attack_pos + (target_pos - attack_pos)*5/4 |
|
local any_hit, hit_pos, hit_objs = CollideSegmentsObjs({attack_pos, tp}) |
|
if any_hit then |
|
for i, obj in ipairs(hit_objs) do |
|
if obj == target then |
|
collision_pos = hit_pos[i] |
|
break |
|
end |
|
end |
|
end |
|
end |
|
if collision_pos then break end |
|
end |
|
end |
|
|
|
if not attack_pos or not collision_pos then |
|
print("failed to find a suitable shot vector") |
|
return |
|
end |
|
|
|
local hit = { |
|
obj = target, |
|
pos = collision_pos, |
|
distance = collision_pos:Dist(attack_pos), |
|
} |
|
local dir = SetLen(target_pos - attack_pos, 4096) |
|
Firearm:ProjectileFly(nil, attack_pos, collision_pos, dir, const.Combat.BulletVelocity, {hit}) |
|
if dmg and IsKindOf(target, "CombatObject") then |
|
target:TakeDirectDamage(dmg) |
|
end |
|
end |
|
|
|
MapVar("g_PlacedDescendantObjects", false) |
|
|
|
function PlaceDescendantObjects(parent_classes, pt, width) |
|
if type(parent_classes) == "string" then |
|
parent_classes = { parent_classes } |
|
end |
|
local classes = {} |
|
for _, parent in ipairs(parent_classes) do |
|
ClassDescendants(parent, function(child, classdef, classes) |
|
classes[child] = true |
|
end, classes) |
|
end |
|
|
|
classes = table.keys2(classes) |
|
table.sort(classes) |
|
|
|
local n = sqrt(#classes)+1 |
|
local x, y = pt:xyz() |
|
local idx = 1 |
|
|
|
SuspendPassEdits("pdo") |
|
|
|
for _, obj in ipairs(g_PlacedDescendantObjects or empty_table) do |
|
DoneObject(obj) |
|
end |
|
|
|
local placed_objs = {} |
|
|
|
for j=1,#classes do |
|
x = pt:x() |
|
local maxr, sumr = 0, 0 |
|
for i=1,#classes do |
|
if idx < #classes then |
|
local obj = PlaceObject(classes[idx]) |
|
local r = Max(const.SlabSizeX, Min(obj:GetEntityBBox():size():Len2D()/2, obj:GetRadius())) |
|
obj:SetPos(point(x, y)) |
|
maxr = Max(maxr, r) |
|
sumr = sumr + r |
|
idx = idx + 1 |
|
x = x + r * 2 |
|
placed_objs[#placed_objs+1] = obj |
|
|
|
if sumr > width then break end |
|
end |
|
end |
|
y = y + maxr*2 |
|
end |
|
|
|
ResumePassEdits("pdo") |
|
g_PlacedDescendantObjects = placed_objs |
|
end |