File size: 33,860 Bytes
b6a38d7 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 |
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
--= IsPoint(target) and target:IsValid() or IsValid(target) and target:IsValidPos()
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()
--for most objs this is the same as GetMaterialPreset
--due to naming collisions, for slabs this is different
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 -- by default is impenetrable
end
collision.SetPenetratingDefense(obj, 1) -- -1 is impenetrable
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
-- Temporary HitPoints are removed at the end of combat
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)
--local dmg_left = Max(0, dmg - self.TempHitPoints)
--self.TempHitPoints = Max(0, self.TempHitPoints - dmg)
--self.HitPoints = Max(0, self.HitPoints - dmg_left)
--self:OnHPLoss(dmg, dmg_left)
--self:NetUpdateHash("TakeDirectDamage", dmg, hit_descr.prev_hit_points, self.HitPoints, self.TempHitPoints, dmg_left)
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
-- log/report
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"))) -- can happen before or after reaching the intended target
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
-- basic damage logging for all other hit units (stray shots)
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
-- basic damage logging for this unit (stray)
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) --wip, causes collision assert atm
--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 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--]]
--[[
local grenade = PlaceInventoryItem("Super_HE_Grenade")
local aoe_params = grenade:GetAreaAttackParams(nil, nil, pos)
local results = GetAreaAttackResults(aoe_params, 0, nil, dmg)
if dmg then
DbgTestExplode(pos)
end
ApplyExplosionDamage(nil, nil, results, 0)
DoneCombatObject(grenade)--]]
end
local ce_thread = false
function DbgCarpetExplosionDamage(ztype)
--ztype:
--nil or "grounded" -> explosions on terrainz
--number -> terrain z + 'number' of explosions at terrain z + zstep * z, if negative goes from top pt to bot pt, if positie goest from bot pt to top pt.
--"bomb" -> raycast from the sky, first obj hit's box maxz
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 = IntersectObjectsSphereCast(sp, ep, guim/4, 0, "Slab", function(o)
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) --"FragGrenade"
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)) -- range dbg
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") --MortarShell
local _40mm_ammo = GetAmmosWithCaliber("40mmGrenade") --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
--print(DbgGrenadeIdx, all[DbgGrenadeIdx])
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 = IntersectObjectsSphereCast(sp, ep, guim/4, 0, "Slab", function(o)
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
-- find target object
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
-- printf("target found: %s (%s)", target.class, target:GetEntity())
if target:GetEnumFlags(const.efCollision) == 0 then
print(" target has no collision, try using normal attacks (F)")
return
end
end
-- find a suitable shot around the target
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 |