myspace / Lua /Tactical /Grenade.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
71.1 kB
local dirPX = 0
local dirNX = 1
local dirPY = 2
local dirNY = 3
local dirPZ = 4
local dirNZ = 5
local sizex = const.SlabSizeX
local sizey = const.SlabSizeY
local sizez = const.SlabSizeZ
MapVar("MapIncendiaryGrenadeZones", {})
MapVar("MapSmokeZones", {})
MapVar("MapFlareOnGround", {})
DefineClass.Grenade = {
__parents = { "QuickSlotItem", "GrenadeProperties", "InventoryStack", "BaseWeapon", "BobbyRayShopOtherProperties" },
RolloverClassTemplate = "Throwables",
base_skill = "Dexterity",
}
function Grenade:GetRolloverType()
return self.ItemType or "Throwables"
end
function Grenade:GetAccuracy(dist)
return 100
end
function Grenade:GetMaxAimRange(unit)
local str = IsKindOf(unit, "Unit") and unit.Strength or 100
local range = MulDivRound(self.BaseRange, Max(0, 100 - str), 100) + MulDivRound(self.ThrowMaxRange, Min(100, str), 100)
return Max(1, range)
end
function Grenade:GetMaxRange()
return self.AreaOfEffect * const.SlabSizeX * 3 / 2
end
function Grenade:IsGasGrenade(aoeType)
aoeType = aoeType or self.aoeType
return aoeType == "smoke" or aoeType == "teargas" or aoeType == "toxicgas"
end
function Grenade:IsFireGrenade(aoeType)
return (aoeType or self.aoeType) == "fire"
end
function Grenade:OnPrepareThrow(thrower, visual)
if InventoryItemDefs[self.class] and InventoryItemDefs[self.class].group == "Grenade - Explosive" then
ShowThrowingTutorial()
end
end
function Grenade:OnFinishThrow(thrower)
if GetCurrentOpenedTutorialId() == "Throwing" then
TutorialHintsState.Throwing = true
end
end
function Grenade:OnThrow(thrower, visual_objs)
for _, obj in ipairs(visual_objs) do
if obj:HasAnim("fly") then
obj:SetAnim(1, "fly")
end
end
if GetCurrentOpenedTutorialId() == "Throwing" then
TutorialHintsState.Throwing = true
end
end
function Grenade:OnLand(thrower, attackResults, visual_obj)
if not CurrentThread() then
CreateGameTimeThread(Grenade.OnLand, self, thrower, attackResults, visual_obj)
return
end
Sleep(160)
if self.ThrowNoise > 0 then
-- <Unit> Heard a thud
PushUnitAlert("noise", visual_obj, self.ThrowNoise, Presets.NoiseTypes.Default.ThrowableLandmine.display_name)
end
if self.Noise > 0 then
PushUnitAlert("noise", visual_obj, self.Noise, Presets.NoiseTypes.Default.Grenade.display_name)
end
attackResults.aoe_type = self.aoeType
attackResults.burn_ground = self.BurnGround
ApplyExplosionDamage(thrower, visual_obj, attackResults)
if IsValid(visual_obj) and not IsKindOf(visual_obj, "GridMarker") then
DoneObject(visual_obj)
end
end
Grenade.PrecalcDamageAndStatusEffects = ExplosionPrecalcDamageAndStatusEffects
function GetGrenadeDamageBonus(unit)
return MulDivRound(const.Combat.GrenadeStatBonus, unit.Explosives, 100)
end
function Grenade:GetAreaAttackParams(action_id, attacker, target_pos, step_pos)
target_pos = target_pos or self:GetPos()
local aoeType = self.aoeType
local max_range = self.AreaOfEffect
if aoeType == "fire" then
max_range = 2
end
local params = {
attacker = false,
weapon = self,
target_pos = target_pos,
step_pos = target_pos,
stance = "Prone",
min_range = self.AreaOfEffect,
max_range = max_range,
center_range = self.CenterAreaOfEffect,
damage_mod = 100,
attribute_bonus = 0,
can_be_damaged_by_attack = true,
aoe_type = aoeType,
explosion = true, -- damage dealt depends on target stance
explosion_fly = self.DeathType == "BlowUp",
}
if self.coneShaped then
params.cone_length = self.AreaOfEffect * const.SlabSizeX
params.cone_angle = self.coneAngle * 60
params.target_pos = RotateRadius(params.cone_length, CalcOrientation(step_pos or attacker, target_pos), target_pos)
if not params.target_pos:IsValidZ() or params.target_pos:z() - terrain.GetHeight(params.target_pos) <= 10*guic then
params.target_pos = params.target_pos:SetTerrainZ(10*guic)
end
end
if IsKindOf(attacker, "Unit") then
params.attacker = attacker
--params.attribute_bonus = GetGrenadeDamageBonus(attacker) -- already applied in GetBaseDamage
end
return params
end
local GrenadeCustomDescriptions = {
["fire"] = T(933586341461, "Sets an area on fire and inflicts <em>Burning</em>."),
["smoke"] = T(487595290755, "Ranged attacks passing through gas become <em>grazing</em> hits."),
["teargas"] = T(411101296173, "Inflicts <em>Blinded</em>, making affected characters less accurate. Ranged attacks passing through gas become <em>grazing</em> hits."),
["toxicgas"] = T(298305721189, "Inflicts <em>Choking</em>, forcing affected characters to take damage and lose energy, eventually fall <em>unconscious</em>. Ranged attacks passing through gas become <em>grazing</em> hits."),
}
function Grenade:GetCustomActionDescription(units)
return GrenadeCustomDescriptions[self.aoeType] -- if the table value is nil the default action description will be used
end
function ExplosionDamage(attacker, weapon, pos, fx_actor, force_flyoff, disableBurnFx, ignore_targets)
local aoe_params = weapon:GetAreaAttackParams(nil, attacker, pos)
local effects = {}
if not aoe_params.aoe_type or aoe_params.aoe_type == "none" then
effects[1] = "CancelShot"
end
if (weapon.AppliedEffect or "") ~= "" then
effects[#effects + 1] = weapon.AppliedEffect
end
aoe_params.prediction = false
local original_mask
if attacker and attacker.spawned_by_explosive_object then
original_mask = collision.GetAllowedMask(attacker.spawned_by_explosive_object)
local ignore_self = 0
collision.SetAllowedMask(attacker.spawned_by_explosive_object, ignore_self)
end
local results = GetAreaAttackResults(aoe_params, 0, effects)
CompileKilledUnits(results)
if original_mask then
collision.SetAllowedMask(attacker.spawned_by_explosive_object, original_mask)
end
if force_flyoff then
for i, h in ipairs(results) do
h.explosion_fly = true
end
end
if IsKindOf(weapon, "ExplosiveProperties") then
results.burn_ground = weapon.BurnGround
end
results.weapon = weapon
ApplyExplosionDamage(attacker, fx_actor, results, weapon.Noise, disableBurnFx, ignore_targets)
end
function GetExplosionDecalPos(pos)
local offset = guim
local lowest_z = pos:z()
for dx = -offset, offset, offset do
for dy = -offset, offset, offset do
local z = terrain.GetHeight(pos + point(dx, dy, 0))
lowest_z = (z < lowest_z) and z or lowest_z
end
end
lowest_z = Max(lowest_z, pos:z() - guim)
return pos:SetZ(lowest_z - 25 * guic)
end
MapVar("g_LastExplosionTime", false)
local function igMolotovDestroyOrBurn(obj, dist2)
if dist2 < sizex*sizex then
return "destroy"
end
if dist2 > 2*sizex*sizex then
return "impact"
end
return InteractionRand(100, "Explosion") < 50 and "destroy" or "burn"
end
local function igExplosionDestroyOrBurn(obj, dist2, range)
if dist2 > range*range then
return "impact"
end
return "destroy"
end
local function GetExplosionFXPos(visual_obj_or_pos)
local pos = IsPoint(visual_obj_or_pos) and visual_obj_or_pos or visual_obj_or_pos:GetPos() --light fx use this pos, alternatively we could make them not affect stealth
if not pos:IsValidZ() then
pos = pos:SetTerrainZ() -- only when pos is on invalidz so we can still get walkable slabs that are over the terrain if the position is right
end
local slab, slab_z = WalkableSlabByPoint(pos)
local z = pos:z()
if slab_z then
if slab_z <= z and slab_z >= z - guim then
pos = pos:SetZ(slab_z)
end
else
pos = GetExplosionDecalPos(pos)
end
return pos
end
function ApplyExplosionDamage(attacker, fx_actor, results, noise, disableBurnFx, ignore_targets)
if not CurrentThread() then
CreateGameTimeThread(ApplyExplosionDamage, attacker, fx_actor, results, noise, disableBurnFx)
return
end
local gas = results.aoe_type == "smoke" or results.aoe_type == "teargas" or results.aoe_type == "toxicgas"
local fire = results.aoe_type == "fire"
local pos
local surf_fx_type
local fx_action
if fx_actor then
pos = GetExplosionFXPos(fx_actor)
surf_fx_type = GetObjMaterial(pos)
if fire then
fx_action = "ExplosionFire"
elseif gas then
fx_action = "ExplosionGas"
else
fx_action = "Explosion"
end
PlayFX(fx_action, "start", fx_actor, surf_fx_type, pos)
end
Sleep(100)
if fx_actor and not disableBurnFx then
PlayFX(fx_action, "end", fx_actor, surf_fx_type, pos)
end
local burn_ground = true
if results.burn_ground ~= nil then
burn_ground = results.burn_ground
end
if not fire and not gas and burn_ground and results.target_pos then
--PlaceWindModifierExplosion(results.target_pos, results.range)
if not disableBurnFx then
local fDestroyOrBurn = (results.aoe_type == "fire") and igMolotovDestroyOrBurn or igExplosionDestroyOrBurn
Explosion_ProcessGrassAndShrubberies(results.target_pos, results.range/2, fDestroyOrBurn)
end
end
local attack_reaction, weapon
if IsKindOf(attacker, "DynamicSpawnLandmine") and IsKindOf(attacker.attacker, "Unit") then
attack_reaction = true
weapon = attacker
attacker = attacker.attacker
elseif IsKindOf(attacker, "Unit") then
attack_reaction = true
weapon = results.weapon
end
local hit_units = {}
ignore_targets = ignore_targets or empty_table
for _, hit in ipairs(results) do
local obj = hit.obj
hit.explosion = true
if IsValid(obj) then --after fx sleep this obj can no longer be valid
if not ignore_targets[obj] then
if IsKindOf(obj, "Unit") then
if not hit_units[obj] then
hit_units[obj] = hit
else
hit_units[obj].damage = hit_units[obj].damage + hit.damage
end
elseif IsKindOf(obj, "CombatObject") then
obj:TakeDamage(hit.damage, attacker, hit)
elseif IsKindOf(obj, "Destroyable") and hit.damage > 0 then
obj:Destroy()
end
end
end
end
--pp units so they know wheather they would need to fall down before dying
for u, hit in sorted_handled_obj_key_pairs(hit_units or empty_table) do
if not ignore_targets or not ignore_targets[u] then
u:ApplyDamageAndEffects(attacker, hit.damage, hit, hit.armor_decay)
end
end
if fx_actor and attacker and not attacker.spawned_by_explosive_object then
PushUnitAlert("thrown", fx_actor, attacker)
end
if fx_actor and noise then
PushUnitAlert("noise", fx_actor, noise, Presets.NoiseTypes.Default.Explosion.display_name)
end
if fire then
ExplosionCreateFireAOE(attacker, results, fx_actor)
elseif gas then
ExplosionCreateSmokeAOE(attacker, results, fx_actor)
end
if attack_reaction then
AttackReaction("ThrowGrenadeA", {obj = attacker, weapon = weapon}, results)
end
NetUpdateHash("g_LastExplosionTime_set")
g_LastExplosionTime = GameTime()
end
function GrenadesComboItems(items)
items = items or {}
ForEachPresetInGroup("InventoryItemCompositeDef", "Grenade", function(o) items[#items+1] = o.id end)
return items
end
function Grenade:CalcTrajectory(attack_args, target_pos, angle, max_bounces)
local attacker = attack_args.obj
local anim_phase = attacker:GetAnimMoment(attack_args.anim, "hit") or 0
local attack_offset = attacker:GetRelativeAttachSpotLoc(attack_args.anim, anim_phase, attacker, attacker:GetSpotBeginIndex("Weaponr"))
local step_pos = attack_args.step_pos
if not step_pos:IsValidZ() then
step_pos = step_pos:SetTerrainZ()
end
local pos0 = step_pos:SetZ(step_pos:z() + attack_offset:z())
if not angle then
if target_pos:z() - pos0:z() > const.SlabSizeZ / 2 then
angle = const.Combat.GrenadeLaunchAngle_Incline
else
angle = const.Combat.GrenadeLaunchAngle
end
end
local sina, cosa = sincos(angle)
local aim_pos = pos0 + Rotate(point(cosa, 0, sina), CalcOrientation(pos0, target_pos))
local grenade_pos = GetAttackPos(attack_args.obj, step_pos, axis_z, attack_args.angle, aim_pos, attack_args.anim, anim_phase, attack_args.weapon_visual)
if grenade_pos:Equal2D(target_pos) then
return empty_table
end
local dir = target_pos - grenade_pos
local bounce_diminish = 40
local vec
local can_bounce = self.CanBounce
if attack_args.can_bounce ~= nil then
can_bounce = attack_args.can_bounce
end
max_bounces = can_bounce and max_bounces or 0
if can_bounce then
max_bounces = 1
end
if max_bounces > 0 then
local coeff = 1000
local d = 10 * bounce_diminish
for i = 1, max_bounces do
coeff = coeff + d
d = MulDivRound(d, bounce_diminish, 100)
end
local bounce_target_pos = grenade_pos + MulDivRound(dir, 1000, coeff)
vec = CalcLaunchVector(grenade_pos, bounce_target_pos, angle, const.Combat.Gravity)
else
vec = CalcLaunchVector(grenade_pos, target_pos, angle, const.Combat.Gravity)
end
local time = MulDivRound(grenade_pos:Dist2D(target_pos), 1000, Max(vec:Len2D(), 1))
if time == 0 then
return empty_table
end
local trajectory = CalcBounceParabolaTrajectory(grenade_pos, vec, const.Combat.Gravity, time, 20, max_bounces, bounce_diminish)
return trajectory
end
function Grenade:ValidatePos(explosion_pos)
return explosion_pos
end
function Grenade:GetTrajectory(attack_args, attack_pos, target_pos, mishap)
if not attack_pos and attack_args.lof then
local lof_idx = table.find(attack_args.lof, "target_spot_group", attack_args.target_spot_group)
local lof_data = attack_args.lof[lof_idx or 1]
attack_pos = lof_data.attack_pos
end
if not attack_pos then
return {}
end
-- sanity-check the target pos
local pass = SnapToPassSlab(target_pos)
local valid_target = true
if pass then
pass = pass:IsValidZ() and pass or pass:SetTerrainZ()
if abs(pass:z() - target_pos:z()) >= 2*const.SlabSizeZ then
valid_target = false
end
else
valid_target = false
end
-- try the different trajectories to pick a suitable one
local angles = {}
if valid_target or mishap then
if target_pos:z() - attack_pos:z() >= 2*const.SlabSizeZ then
angles[1] = const.Combat.GrenadeLaunchAngle_Incline
angles[2] = const.Combat.GrenadeLaunchAngle
else
-- throwing down/level, prefer low arc
if target_pos:z() - attack_pos:z() <= const.SlabSizeZ / 2 then
angles[1] = const.Combat.GrenadeLaunchAngle_Low
end
angles[#angles+1] = const.Combat.GrenadeLaunchAngle
if not GameState.Underground then
angles[#angles+1] = const.Combat.GrenadeLaunchAngle_Incline
end
end
end
local attacker = attack_args.obj
local best_dist, best_trajectory = attacker:GetDist(target_pos), {}
for _, angle in ipairs(angles) do
local trajectory = self:CalcTrajectory(attack_args, target_pos, angle, (angle == const.Combat.GrenadeLaunchAngle_Low) and 1 or 0)
local hit_pos = (#trajectory > 0) and trajectory[#trajectory].pos
if hit_pos and (hit_pos:Dist(trajectory[1].pos) > 0) then
if IsCloser(hit_pos, target_pos, const.SlabSizeX) then
return trajectory
end
local dist = hit_pos:Dist(target_pos)
if dist < best_dist then
best_dist, best_trajectory = dist, trajectory
end
end
end
return best_trajectory
end
function Grenade:GetAttackResults(action, attack_args)
local attacker = attack_args.obj
local explosion_pos = attack_args.explosion_pos
local trajectory = {}
local mishap
if not explosion_pos and attack_args.lof then
local lof_idx = table.find(attack_args.lof, "target_spot_group", attack_args.target_spot_group)
local lof_data = attack_args.lof[lof_idx or 1]
local attack_pos = lof_data.attack_pos
local target_pos = lof_data.target_pos
-- mishap & stealth kill checks
if not attack_args.prediction and IsKindOf(self, "MishapProperties") then
local chance = self:GetMishapChance(attacker, target_pos)
if CheatEnabled("AlwaysMiss") or attacker:Random(100) < chance then
mishap = true
-- Try a couple of times to get a valid deviated position
local validPositionTries = 0
local maxPositionTries = 5
while validPositionTries < maxPositionTries do
local dv = self:GetMishapDeviationVector(attacker, target_pos)
local deviatePosition = target_pos + dv
local trajectory = self:GetTrajectory(attack_args, attack_pos, deviatePosition, "mishap")
local finalPos = #trajectory > 0 and trajectory[#trajectory].pos
if finalPos and self:ValidatePos(finalPos, attack_args) then
target_pos = deviatePosition
break
end
validPositionTries = validPositionTries + 1
end
attacker:ShowMishapNotification(action)
end
end
trajectory = self:GetTrajectory(attack_args, attack_pos, target_pos, mishap)
if #trajectory > 0 then
explosion_pos = trajectory[#trajectory].pos
end
explosion_pos = self:ValidatePos(explosion_pos, attack_args)
end
local results
if explosion_pos then
local aoe_params = self:GetAreaAttackParams(action.id, attacker, explosion_pos, attack_args.step_pos)
if attack_args.stealth_attack then
aoe_params.stealth_attack_roll = not attack_args.prediction and attacker:Random(100) or 100
end
aoe_params.prediction = attack_args.prediction
if aoe_params.aoe_type ~= "none" or IsKindOf(self, "Flare") then
aoe_params.damage_mod = "no damage"
end
results = GetAreaAttackResults(aoe_params)
CompileKilledUnits(results)
local radius = aoe_params.max_range * const.SlabSizeX
local explosion_voxel_pos = SnapToVoxel(explosion_pos) + point(0, 0, const.SlabSizeZ / 2)
local impact_force = self:GetImpactForce()
local unit_damage = {}
for _, hit in ipairs(results) do
local obj = hit.obj
if not obj or hit.damage == 0 then goto continue end
local dist = hit.obj:GetDist(explosion_voxel_pos)
if IsKindOf(obj, "Unit") then
if not obj:IsDead() then
unit_damage[obj] = (unit_damage[obj] or 0) + hit.damage
if unit_damage[obj] >= obj:GetTotalHitPoints() then
results.killed_units = results.killed_units or {}
table.insert_unique(results.killed_units, obj)
end
end
end
hit.impact_force = impact_force + self:GetDistanceImpactForce(dist)
hit.explosion = true
::continue::
end
else
results = {}
end
results.trajectory = trajectory
results.explosion_pos = explosion_pos
results.weapon = self
results.mishap = mishap
results.no_damage = IsKindOf(self, "Flare")
return results
end
function Grenade:CreateVisualObj(owner)
local grenade = owner:AttachGrenade(self)
return grenade
end
function Grenade:GetVisualObj(attacker, force)
local grenade = attacker:GetAttach("GrenadeVisual")
if not grenade or force then
-- AI has skipped the UI part
grenade = attacker:AttachGrenade(self)
end
return grenade
end
---[[
function DbgAddVoxel(pt, color)
pt = SnapToVoxel(pt)
local x, y, z = pt:xyz()
local bx = box(x - const.SlabSizeX/2, y - const.SlabSizeY/2, z, x + const.SlabSizeX/2, y + const.SlabSizeY/2, z + const.SlabSizeZ)
DbgAddBox(bx, color)
end
function CalcLaunchVector(start_pt, end_pt, angle, gravity)
if not start_pt or not end_pt then
return
end
assert(start_pt:IsValidZ() and end_pt:IsValidZ())
angle = angle or const.Combat.GrenadeLaunchAngle
local dist = start_pt:Dist2D(end_pt)
local dir = SetLen((end_pt - start_pt):SetZ(0), guim)
local y0 = start_pt:z()
local yt = end_pt:z()
local sina, cosa = sin(angle), cos(angle)
local t1 = MulDivRound(gravity or const.Combat.Gravity, dist*dist, guim) -- scale guim^2
local t2 = 2*cosa*cosa -- scale 4k^2
local t3 = (yt - y0) * guim -- scale guim^2
local t4 = MulDivRound(dist, sina * guim, cosa) -- scale guim^2
local v0 = MulDivRound(sqrt(t1), guim, Max(1, MulDivRound(sqrt(t2), sqrt(Max(0, -t3+t4)), 4096)))
local v0x = MulDivRound(v0, cosa, 4096)
local v0y = MulDivRound(v0, sina, 4096)
return SetLen(dir, v0x) + point(0, 0, v0y)
end
function IsBreakableWindow(obj)
return IsKindOf(obj, "SlabWallWindow") and obj:IsWindow() and obj:IsBreakable() and not obj:IsBroken()
end
--[[
function CalcBounceParabolaTrajectory(start_pt, launch_speed, gravity, time, tStep, max_bounces, diminish, no_collision)
local t = 0
local pos = start_pt
local v = launch_speed
local vg = point(0, 0, -(gravity or const.Combat.Gravity))
local trajectory = {}
local bounce_flags = const.efVisible + const.efCollision
if not v:IsValidZ() then
v = v:SetZ(0)
end
while terrain.IsPointInBounds(pos) and pos:z() >= terrain.GetHeight(pos) do
table.insert(trajectory, { pos = pos, t = t })
t = t + tStep
local vnext = v + MulDivRound(vg, tStep, 1000)
local posnext = pos + MulDivRound(vnext, tStep, 1000)
local hit_water
local function bounce(pt, normal, obj)
if hit_water then
return
end
if normal == point30 then
normal = axis_z
end
local len = Dot(normal, v) / normal:Len()
-- reflect the velocity vector
local proj = SetLen(normal, len)
local velocity = vnext:Len()
vnext = SetLen(vnext - (2 * proj), velocity)
if diminish then
vnext = MulDivRound(vnext, diminish, 100)
end
-- correct the position
local move_dist = posnext:Dist(pos)
local hit_dist = pos:Dist(pt)
local refl_dist = move_dist - hit_dist
if refl_dist > 0 and vnext ~= point30 then
table.insert(trajectory, {pos = pt, t = t - tStep + MulDivRound(tStep, hit_dist, move_dist), obj = obj, bounce = true})
pos = pt
posnext = pt + SetLen(vnext, refl_dist)
end
max_bounces = max_bounces - 1
end
-- window detection
local ipt = terrain.IntersectSegment(pos, posnext)
if not no_collision then
local obj, pt, normal = GetClosestRayObj(pos, ipt or posnext, bounce_flags, 0, function(obj, x, y, z)
local window = IsBreakableWindow(obj) or nil
local water = IsKindOf(obj, "WaterPlane") or nil
if window or water then
local pt = point(x, y, z)
local move_dist = posnext:Dist(pos)
local hit_dist = pos:Dist(pt)
if hit_dist > 0 and move_dist > 0 and vnext ~= point30 then
local time = t - tStep + MulDivRound(tStep, hit_dist, move_dist)
local step = { pos = pt, t = time, obj = obj }
if window then
step.window = true
end
if water then
hit_water = #trajectory
step.water = true
end
table.insert(trajectory, step)
pos = pt
posnext = pt + SetLen(vnext, hit_dist)
t = time + tStep
end
return false
end
return true
end, 0, const.cmDefaultObject)
if obj and (not obj:IsKindOf("SlabWallDoor") or obj:IsDead() or obj.pass_through_state ~= "open") then
bounce(pt, normal, obj)
if t >= time or max_bounces < 0 then
-- end before collision pos to avoid detonating inside collision geometry
local dist = pos:Dist(pt)
local adj_dist = dist - 10*guic
if adj_dist <= 0 then
break
end
local end_pt = pos + SetLen(pt - pos, adj_dist)
local time = t - tStep + MulDivRound(tStep, adj_dist, dist)
table.insert(trajectory, { pos = end_pt, obj = obj, t = time })
break
end
end
end
if ipt then
bounce(ipt, terrain.GetTerrainNormal(ipt), "terrain")
if max_bounces < 0 or t >= time then
break
end
end
pos, v = posnext, vnext
end
-- before returning make sure the trajectory stays above ground & has contents
if #trajectory == 0 then
table.insert(trajectory, { pos = start_pt, t = 0 })
end
if #trajectory == 1 then
table.insert(trajectory, { pos = start_pt + SetLen(launch_speed, 10*guic), t = 1 })
end
for _, step in ipairs(trajectory) do
if step.pos:z() < terrain.GetHeight(step.pos) then
step.pos = step.pos:SetTerrainZ()
end
end
return trajectory
end
--]]
function AnimateThrowTrajectory(visual_obj, trajectory, rotation_axis, rpm, surf_fx, explosion_fx)
local time = trajectory[#trajectory].t
local angle = MulDivTrunc(rpm * 360, time, 1000)
local hit_water
if rpm ~= 0 then
visual_obj:SetAxis(rotation_axis)
end
local t = 0
local pos, dir
for i, step in ipairs(trajectory) do
local step_time = step.t - t
local dist = pos and step.pos:Dist(pos) or 0
if dist > 0 then
dir = step.pos - pos
--[[if i == 1 and step_time == 0 then
local idx = i
local dt, d = 0, 0
while dt <= 0 or d <= 0 do
idx = idx + 1
d = trajectory[idx].pos:Dist(trajectory[i].pos)
dt = trajectory[idx].t - trajectory[i].t
end
step_time = MulDivRound(dt, dist, d)
end--]]
end
step_time = Max(0, step_time)
pos = step.pos
visual_obj:SetPos(pos, step_time)
local st = 0
while st < step_time do
local dt = Min(20, step_time - st)
if rpm ~= 0 then
visual_obj:SetAngle(visual_obj:GetAngle() + MulDivTrunc(angle, dt, time), dt)
end
st = st + dt
Sleep(dt)
end
t = step.t
local obj = IsValid(step.obj) and step.obj or nil
if IsBreakableWindow(obj) then
obj:SetWindowState("broken")
obj = nil
end
if surf_fx and not hit_water and (obj or i == #trajectory or step.bounce) then
local surf_fx_type = GetObjMaterial(pos, obj)
local fx_target = surf_fx_type or obj
PlayFX(surf_fx, "start", visual_obj, fx_target, pos)
end
if step.water then
hit_water = true
end
end
visual_obj:SetAxis(axis_z)
visual_obj:SetAngle(dir and dir:Len2D() > 0 and CalcOrientation(dir) or 0)
if explosion_fx then
local pos = GetExplosionFXPos(visual_obj)
local surf_fx_type = GetObjMaterial(pos)
dir = dir and dir:SetZ(0)
if dir and dir:Len() > 0 then
dir = SetLen(dir, 4096)
else
dir = axis_z
end
PlayFX(explosion_fx, "start", visual_obj, surf_fx_type, pos, dir)
end
Msg("GrenadeDoneThrow")
end
function ResolveGrenadeTargetPos(target, startPos, grenade)
target = IsValid(target) and target:GetPos() or target
if not IsPoint(target) then return false end
-- Move target pos a bit behind cursor for cone shaped explosions
if grenade and grenade.coneShaped and startPos:Dist(target) > 0 then
local z = target:z()
target = target + SetLen(startPos - target, const.SlabSizeX/2)
if z and target:z() ~= z then
target = target:SetZ(z)
end
end
if target:IsValidZ() then
target = target:SetZ(target:z() + const.SlabSizeZ / 2)
else
target = target:SetTerrainZ(const.SlabSizeZ / 2)
end
return target
end
DefineClass.FXGrenade = {
__parents = { "SpawnFXObject", "ComponentCustomData" },
flags = { efCollision = false, efApplyToGrids = false, gofRealTimeAnim = false }
}
DefineClass.GrenadeVisual = {
__parents = { "FXGrenade" },
custom_equip = false,
equip_index = 0,
}
MapGameTimeRepeat("ExplosionFiresUpdate", 500, function()
if g_Combat or g_StartingCombat then return end -- handled on NewCombatTurn msg
local change
for _, fire in ipairs(MapIncendiaryGrenadeZones) do
if fire:UpdateRemainingTime(500) then
change = true
end
end
if change then
UpdateMapBurningState(GameState.FireStorm)
end
for _, zone in ipairs(MapSmokeZones) do
zone:UpdateRemainingTime(500)
end
for _, flare in ipairs(MapFlareOnGround) do
flare:UpdateRemainingTime(500)
end
end)
local function EnvEffectReaction(zone_type, attacker, target, damage)
if not attacker then return end
local attack_args = {obj = attacker, target = target}
local results = {}
results.area_hits = { {obj = target, aoe = true, damage = damage} }
results.hit_objs = { target }
results.hit_objs[target] = true
results.killed_units = {}
results.env_effect = true
if target:IsDead() then
table.insert(results.killed_units, target)
if target:IsCivilian() and target.Affiliation == "Civilian" and attacker then
CivilianDeathPenalty()
end
end
AttackReaction(zone_type, attack_args, results)
end
function EnvEffectBurningTick(unit, voxels, combat_moment)
local inside
if next(g_DistToFire) ~= nil then
if not voxels then
voxels = unit:GetVisualVoxels()
end
local fire, dist = AreVoxelsInFireRange(voxels)
inside = fire and dist < const.SlabSizeX
end
local effect = unit:GetStatusEffect("Burning")
if effect then
if not inside then
local x, y, z = unit:GetVisualPosXYZ()
if terrain.IsWater(x, y, z) then
local water_z = terrain.GetWaterHeight(x, y)
local dz = (z or terrain.GetHeight(x, y)) - water_z
if dz >= const.FXWaterMinOffsetZ and dz <= const.FXWaterMaxOffsetZ then
unit:RemoveStatusEffect("Burning")
return
end
end
end
local start_time
if combat_moment ~= "end turn" then
if g_Combat or g_StartingCombat then
return
end
start_time = effect:ResolveValue("burning_start_time")
if GameTime() - start_time < 5000 then
return
end
end
local damage = Burning:ResolveValue("damage")
unit:TakeDirectDamage(damage, T{529212078060, "<damage> (Burning)", damage = damage})
if inside then
if g_Combat or g_StartingCombat then
start_time = GameTime()
else
start_time = (start_time or effect:GetParameter("burning_start_time")) + 5000
end
effect:SetParameter("burning_start_time", start_time)
else
unit:RemoveStatusEffect("Burning")
end
elseif inside then
unit:AddStatusEffect("Burning")
end
end
function EnvEffectToxicGasTick(unit, voxels, combat_moment)
local inside, protected, attacker
if next(g_SmokeObjs) ~= nil then
if not voxels then
voxels = unit:GetVisualVoxels()
end
local smoke
for _, voxel in ipairs(voxels) do
local smoke_obj = g_SmokeObjs[voxel]
if smoke_obj and smoke_obj:GetGasType() == "toxicgas" then
smoke = smoke_obj
inside = true
break
end
end
if inside then
local mask = unit:GetItemInSlot("Head", "GasMaskBase")
protected = mask and mask.Condition > 0
for _, zone in ipairs(smoke.zones) do
if zone.owner then
attacker = zone.owner
break
end
end
end
end
if inside and protected and attacker then
-- awareness reactions (there will be no damage/negative effects)
PushUnitAlert("attack", attacker, unit)
end
inside = inside and not protected
local effect = unit:GetStatusEffect("Choking")
if effect then
local start_time = effect:ResolveValue("choking_start_time")
if (combat_moment == "end turn") or (not g_Combat and not g_StartingCombat and GameTime() >= start_time + 5000) then
local damage = Choking:ResolveValue("damage")
unit:TakeDirectDamage(damage, T{698692719911, "<damage> (Choking)", damage = damage})
EnvEffectReaction("toxicgas", attacker, unit, damage)
if g_Combat or g_StartingCombat then
start_time = GameTime()
else
start_time = start_time + 5000
end
if inside then
effect:SetParameter("choking_start_time", start_time)
else
unit:RemoveStatusEffect("Choking")
end
local tiredness = RollSkillCheck(unit, "Health") and 1 or 2
unit:ChangeTired(tiredness)
end
elseif inside then
unit:AddStatusEffect("Choking")
EnvEffectReaction("toxicgas", attacker, unit, 0)
end
end
function EnvEffectTearGasTick(unit, voxels, combat_moment)
local inside, protected, attacker
if next(g_SmokeObjs) ~= nil then
if not voxels then
voxels = unit:GetVisualVoxels()
end
local smoke
for _, voxel in ipairs(voxels) do
local smoke_obj = g_SmokeObjs[voxel]
if smoke_obj and smoke_obj:GetGasType() == "teargas" then
smoke = smoke_obj
inside = true
break
end
end
if inside then
local mask = unit:GetItemInSlot("Head", "GasMaskBase")
protected = mask and mask.Condition > 0
for _, zone in ipairs(smoke.zones) do
if zone.owner then
attacker = zone.owner
break
end
end
end
end
if inside and protected and attacker then
-- awareness reactions (there will be no damage/negative effects)
PushUnitAlert("attack", attacker, unit)
end
inside = inside and not protected
local effect = unit:GetStatusEffect("Blinded")
if effect then
local start_time = effect:ResolveValue("blinded_start_time")
if (combat_moment == "end turn") or (not g_Combat and not g_StartingCombat and GameTime() >= start_time + 5000) then
-- choking damage/end will happen on end turn in combat
if g_Combat or g_StartingCombat then
start_time = GameTime()
else
start_time = start_time + 5000
end
if inside then
effect:SetParameter("blinded_start_time", start_time)
else
unit:RemoveStatusEffect("Blinded")
end
elseif combat_moment == "start turn" and inside then
if not RollSkillCheck(unit, "Health") then
unit:AddStatusEffect("Panicked")
end
end
elseif inside then
unit:AddStatusEffect("Blinded")
EnvEffectReaction("teargas", attacker, unit, 0)
end
end
function EnvEffectSmokeTick(unit, voxels, combat_moment)
local inside
if next(g_SmokeObjs) ~= nil then
if not voxels then
voxels = unit:GetVisualVoxels()
end
for _, voxel in ipairs(voxels) do
local smoke = g_SmokeObjs[voxel]
if smoke and (smoke:GetGasType() == "smoke") then
inside = true
break
end
end
end
local effect = unit:GetStatusEffect("Smoked")
if effect then
local start_time = effect:ResolveValue("smoked_start_time")
if (combat_moment == "end turn") or (not g_Combat and not g_StartingCombat and GameTime() >= start_time + 5000) then
if g_Combat or g_StartingCombat then
start_time = GameTime()
else
start_time = start_time + 5000
end
if inside then
effect:SetParameter("smoked_start_time", start_time)
else
unit:RemoveStatusEffect("Smoked")
end
end
elseif inside then
unit:AddStatusEffect("Smoked")
end
end
function EnvEffectDarknessTick(unit, voxels)
if IsIlluminated(unit, voxels, "sync") then
if unit:HasStatusEffect("Darkness") then
unit:RemoveStatusEffect("Darkness")
end
else
if not unit:HasStatusEffect("Darkness") then
unit:AddStatusEffect("Darkness")
end
end
end
function EnvEffectsUpdate(side_type, time)
if IsSetpiecePlaying() then
Sleep(time)
return
end
local units = {}
for _, team in ipairs(g_Teams) do
if side_type == "player" then
if team.player_team then
table.iappend(units, team.units)
end
else
local team_side = team.side
if team_side == "enemyNeutral" then
team_side = GameState.Conflict and "enemy1" or "neutral"
end
if side_type == "neutral" then
if team_side == "neutral" then
table.iappend(units, team.units)
end
else
if team_side ~= "neutral" then
table.iappend(units, team.units)
end
end
end
end
local count = #units
if count == 0 then
Sleep(time)
return
end
local iclear = table.iclear
local voxels = {}
for i = 1, count do
Sleep(i * time / count - (i - 1) * time / count)
local unit = units[i]
if IsValid(unit) and not unit:IsDead() then
iclear(voxels)
unit:GetVisualVoxels(nil, nil, voxels)
EnvEffectBurningTick(unit, voxels)
EnvEffectToxicGasTick(unit, voxels)
EnvEffectTearGasTick(unit, voxels)
EnvEffectSmokeTick(unit, voxels)
EnvEffectDarknessTick(unit, voxels)
end
end
end
MapGameTimeRepeat("EnvEffectsPlayer", 0, function() return EnvEffectsUpdate("player", 100) end)
MapGameTimeRepeat("EnvEffectsNeutral", 0, function() return EnvEffectsUpdate("neutral", 500) end)
MapGameTimeRepeat("EnvEffectsOther", 0, function() return EnvEffectsUpdate("other", 300) end)
function OnMsg.NewCombatTurn()
local change
for _, fire in ipairs(MapIncendiaryGrenadeZones) do
if fire:UpdateRemainingTime(5000) then
change = true
end
end
if change then
UpdateMapBurningState(GameState.FireStorm)
end
for _, zone in ipairs(MapSmokeZones) do
zone:UpdateRemainingTime(5000)
end
for _, flare in ipairs(MapFlareOnGround) do
flare:UpdateRemainingTime(5000)
end
end
MapVar("g_FireAreas", {})
function OnMsg.DoneMap()
for _, fire in ipairs(g_FireAreas) do
DoneObject(fire)
end
end
DefineClass.MolotovFireObj = {
__parents = { "SpawnFXObject" },
}
DefineClass.IncendiaryGrenadeZone = {
__parents = { "GameDynamicSpawnObject", "SpawnFXObject" },
remaining_time = false,
fire_positions = false,
campaign_time = false,
owner = false,
visuals = false,
}
function IncendiaryGrenadeZone:GameInit()
table.insert(MapIncendiaryGrenadeZones, self)
self.campaign_time = self.campaign_time or Game.CampaignTime
PlayFX("FireZone", "start", self)
end
function IncendiaryGrenadeZone:UpdateRemainingTime(delta)
local remaining = self.remaining_time - delta
if remaining <= 0 or self.campaign_time < Game.CampaignTime then
DoneObject(self)
return true
elseif self.remaining_time > 5000 and remaining <= 5000 then
-- update particles
PlayFX("FireZone", "subside", self)
for i, obj in ipairs(self.visuals) do
if IsKindOf(obj, "MolotovFireObj") then
PlayFX("MolotovFire", "subside", obj)
end
end
end
self.remaining_time = remaining
end
function IncendiaryGrenadeZone:Init()
table.insert_unique(g_FireAreas, self)
end
function IncendiaryGrenadeZone:Done()
table.remove_value(MapIncendiaryGrenadeZones, self)
for _, obj in ipairs(self.visuals) do
DoneObject(obj)
end
self.visuals = nil
table.remove_value(g_FireAreas, self)
PlayFX("FireZone", "end", self)
end
function IncendiaryGrenadeZone:GetDynamicData(data)
data.remaining_time = self.remaining_time or nil
data.fire_positions = self.fire_positions and table.copy(self.fire_positions) or nil
data.campaign_time = self.campaign_time or nil
data.owner = IsValid(self.owner) and self.owner:GetHandle() or nil
end
function IncendiaryGrenadeZone:SetDynamicData(data)
self.remaining_time = data.remaining_time
self.campaign_time = data.campaign_time or Game.CampaignTime
self.owner = HandleToObject[data.owner or false]
if self.campaign_time < Game.CampaignTime then
return
end
if data.fire_positions then
self.fire_positions = table.copy(data.fire_positions)
self:CreateVisuals("instant")
end
CreateGameTimeThread(function()
Sleep(1) -- delay until loading is done and game has started for real, otherwise PlaySound can fail due to actor being too far
PlayFX("FireZone", "start", self)
end)
end
function IncendiaryGrenadeZone:CreateVisuals(instant)
for _, obj in ipairs(self.visuals) do
DoneObject(obj)
end
self.visuals = {}
local pos = GetExplosionFXPos(self)
PlayFX("MolotovExplosion", "start", self, false, pos)
if not instant then
Sleep(150)
end
for i, pos in ipairs(self.fire_positions) do
local fire = PlaceObject("MolotovFireObj")
fire:SetPos(pos)
PlayFX("MolotovFire", "start", fire)
table.insert(self.visuals, fire)
if not instant then
for _, unit in ipairs(g_Units) do
if not unit:IsDead() and unit:GetDist(pos) < const.SlabSizeX / 2 and not unit:HasStatusEffect("Burning") then
unit:AddStatusEffect("Burning")
end
end
Sleep(100)
end
end
end
function ExplosionCreateFireAOE(attacker, attackResults, fx_actor)
local trajectory = attackResults.trajectory or empty_table
local pos = #trajectory > 0 and trajectory[#trajectory].pos or attackResults.target_pos
if not pos then return end
local water = terrain.IsWater(pos) and terrain.GetWaterHeight(pos)
if water and (not pos:IsValidZ() or water >= pos:z()) then
pos = pos:IsValidZ() and pos or pos:SetTerrainZ()
PlayFX("GrenadeSplash", "start", fx_actor, false, pos)
CreateFloatingText(pos:SetZ(water), T(760226092433, "Sunk"))
return
end
local radius = const.SlabSizeX + const.SlabSizeX / 2
local x, y, z = VoxelToWorld(WorldToVoxel(pos))
pos = GetExplosionFXPos(point(x, y, z))
-- possible locations are terrain + floor slabs in range
local zoffs = point(0, 0, const.SlabSizeZ / 2)
local target_locs = MapGet(pos, radius + const.SlabSizeX, "FloorSlab") or {}
table.imap_inplace(target_locs, function(slab) return slab:GetPos() end)
target_locs = table.ifilter(target_locs, function(idx, loc, pos)
return pos:Dist2D(loc) / const.SlabSizeX <= 1
end, pos)
local limit = DivCeil(radius, const.SlabSizeX) * const.SlabSizeX
for dx = -limit, limit, const.SlabSizeX do
for dy = -limit, limit, const.SlabSizeY do
local epos = GetExplosionFXPos(point(x+dx, y+dy, z))
if epos and epos:Dist2D(pos) / const.SlabSizeX <= 1 then
target_locs[#target_locs + 1] = epos
end
end
end
target_locs = table.ifilter(target_locs, function(idx, loc)
return not terrain.IsWater(loc) or terrain.GetWaterHeight(loc) < loc:z()
end)
table.sort(target_locs, function(a, b) return a:Dist(pos) < b:Dist(pos) end)
local los_targets = table.map(target_locs, function(loc)
if loc:IsValidZ() then
return loc + zoffs
end
return loc:SetTerrainZ(const.SlabSizeZ / 2)
end)
local los_any, los_to_targets = CheckLOS(los_targets, pos)
if not los_any then
return
end
local fire_locs = {}
local explosion_pos, min_dist
for i, loc in ipairs(los_targets) do
if los_to_targets[i] then
fire_locs[#fire_locs + 1] = target_locs[i]
local dist = target_locs[i]:Dist(pos)
if not explosion_pos or dist < min_dist then
explosion_pos, min_dist = target_locs[i], dist
end
end
end
if #fire_locs > 0 then
local num_turns = 4
local step_pos = SnapToPassSlab(explosion_pos) or point(VoxelToWorld(WorldToVoxel(explosion_pos)))
local volume = EnumVolumes(step_pos, "smallest")
if not volume then
if GameState.RainHeavy or GameState.DustStorm then
num_turns = 2
elseif GameState.FireStorm then
num_turns = 6
end
end
local fire = IncendiaryGrenadeZone:new{
fire_positions = fire_locs,
remaining_time = num_turns * 5000,
}
if IsKindOf(attacker, "Unit") then
fire.owner = attacker
end
fire:SetPos(explosion_pos)
CreateGameTimeThread(IncendiaryGrenadeZone.CreateVisuals, fire)
UpdateMapBurningState(GameState.FireStorm)
end
end
local function IsOutsideSmokeArea(x, y, z)
if x < -2 or x > 3 or y < -2 or y > 3 then
return true
end
if (x == -2 or x == 3) and (y == -2 or y == 3) then
return true
end
if x > -1 and x < 2 and y > -1 and y < 2 then
return abs(z) >= 3
elseif x > -2 and x < 3 and y > -2 and y < 3 then
return abs(z) >= 2
end
return z ~= 0
end
function calc_voxelblocking(iter)
iter = iter or 1
ic("start")
local slabs = MapGet("map", "Slab")
ic("map enum done")
local block_slabs = {}
local function set_block_slab(gx, gy, gz, dir)
local key = bor(band(gx, 0xffff), shift(band(gy, 0xffff), 16), shift(band(gz, 0xffff), 32), shift(dir, 48))
block_slabs[key] = true
end
local tStart = GetPreciseTicks()
local g0x, g0y, g0z = 0, 0, 0
for i = 1, iter do
block_slabs = {}
for _, slab in ipairs(slabs) do
local gx, gy, gz, side = slab:GetGridCoords()
if IsKindOf(slab, "FloorSlab") then
set_block_slab(gx-g0x, gy-g0y, gz-g0z, dirNZ)
set_block_slab(gx-g0x, gy-g0y, gz-1-g0z, dirPZ)
elseif IsKindOf(slab, "CeilingSlab") then
set_block_slab(gx-g0x, gy-g0y, gz-g0z, dirPZ)
set_block_slab(gx-g0x, gy-g0y, gz+1-g0z, dirNZ)
elseif IsKindOf(slab, "WallSlab") then
local blocked = slab.isVisible
local height = 1
if not blocked and slab.wall_obj then
height = slab.wall_obj.height
if IsKindOf(slab.wall_obj, "SlabWallDoor") then
blocked = not slab.wall_obj:IsDead() and slab.wall_obj.pass_through_state == "closed"
elseif IsKindOf(slab.wall_obj, "SlabWallWindow") then
blocked = slab.wall_obj.pass_through_state == "intact"
end
end
if blocked then
local dir = Rotate(point(1, 0, 0), slab:GetAngle())
local x, y = dir:xy()
local neg
if x > 0 then
dir = dirPX
neg = dirNX
elseif x < 0 then
dir = dirNX
neg = dirPX
elseif y > 0 then
dir = dirPY
neg = dirNY
else -- y < 0
dir = dirNY
neg = dirPY
end
for i = 1, height do
set_block_slab(gx-g0x, gy+i-1-g0y, gz-g0z, dir)
set_block_slab(gx+x-g0x, gy+y+i-1-g0y, gz-g0z, neg)
end
end
end
end
end
local time = GetPreciseTicks() - tStart
printf("%d recalcs on %d slabs done in %d ms, %d ms/recalc", iter, #slabs, time, time/iter)
return block_slabs
end
MapVar("g_VoxelBlock", false)
function IsVoxelBlocked(gx, gy, gz, dir)
local key = bor(band(gx, 0xffff), shift(band(gy, 0xffff), 16), shift(band(gz, 0xffff), 32), shift(dir, 48))
return g_VoxelBlock[key]
end
function VoxelCanReach(x1, y1, z1, x2, y2, z2)
if x1 == x2 and y1 == y2 and z1 == z2 then
return true, false
end
local reached, blocked = false, false
if x2 > x1 then
if IsVoxelBlocked(x1, y1, z1, dirPX) then
blocked = true
else
local r, b = VoxelCanReach(x1+1, y1, z1, x2, y2, z2)
reached = reached or r
blocked = blocked or b
end
end
if x2 < x1 then
if IsVoxelBlocked(x1, y1, z1, dirNX) then
blocked = true
else
local r, b = VoxelCanReach(x1-1, y1, z1, x2, y2, z2)
reached = reached or r
blocked = blocked or b
end
end
if y2 > y1 then
if IsVoxelBlocked(x1, y1, z1, dirPY) then
blocked = true
else
local r, b = VoxelCanReach(x1, y1+1, z1, x2, y2, z2)
reached = reached or r
blocked = blocked or b
end
end
if y2 < y1 then
if IsVoxelBlocked(x1, y1, z1, dirNY) then
blocked = true
else
local r, b = VoxelCanReach(x1, y1-1, z1, x2, y2, z2)
reached = reached or r
blocked = blocked or b
end
end
if z2 > z1 then
if IsVoxelBlocked(x1, y1, z1, dirPZ) then
blocked = true
else
local r, b = VoxelCanReach(x1, y1, z1+1, x2, y2, z2)
reached = reached or r
blocked = blocked or b
end
end
if z2 < z1 then
if IsVoxelBlocked(x1, y1, z1, dirNZ) then
blocked = true
else
local r, b = VoxelCanReach(x1, y1, z1-1, x2, y2, z2)
reached = reached or r
blocked = blocked or b
end
end
return reached, blocked
--[[
if x1 == x2 and y1 == y2 and z1 == z2 then
return true
end
if x2 < x1 and not IsVoxelBlocked(x1, y1, z1, dirPX) and VoxelCanReach(x1+1, y1, z1, x2, y2, z2) then
return true
end
if x2 < x1 and not IsVoxelBlocked(x1, y1, z1, dirNX) and VoxelCanReach(x1-1, y1, z1, x2, y2, z2) then
return true
end
if y2 > y1 and not IsVoxelBlocked(x1, y1, z1, dirPY) and VoxelCanReach(x1, y1+1, z1, x2, y2, z2) then
return true
end
if y2 < y1 and not IsVoxelBlocked(x1, y1, z1, dirNY) and VoxelCanReach(x1, y1-1, z1, x2, y2, z2) then
return true
end
if z2 > z1 and not IsVoxelBlocked(x1, y1, z1, dirPZ) and VoxelCanReach(x1, y1, z1+1, x2, y2, z2) then
return true
end
if z2 < z1 and not IsVoxelBlocked(x1, y1, z1, dirNZ) and VoxelCanReach(x1, y1, z1+1, x2, y2, z2) then
return true
end
return false--]]
end
function VoxelLOS(unit1, unit2)
local head1 = select(2, unit1:GetVisualVoxels())
local head2 = select(2, unit2:GetVisualVoxels())
if not g_VoxelBlock then
g_VoxelBlock = calc_voxelblocking()
end
local voxel_box = box(point(-const.SlabSizeX/2, -const.SlabSizeY/2, 0), point(const.SlabSizeX/2, const.SlabSizeY/2, const.SlabSizeZ))
--clear_smoke_dbg_visuals()
local gx1, gy1, gz1 = point_unpack(head1)
local gx2, gy2, gz2 = point_unpack(head2)
--local mesh = PlaceBox(voxel_box + point(VoxelToWorld(gx1, gy1, gz1)), const.clrGreen, nil, false)
--table.insert(g_dbgSmokeVisuals, mesh)
--mesh = PlaceBox(voxel_box + point(VoxelToWorld(gx2, gy2, gz2)), const.clrBlue, nil, false)
--table.insert(g_dbgSmokeVisuals, mesh)
local dx, dy, dz = abs(gx2-gx1), abs(gy2-gy1), abs(gz2-gz1)
local sx = (gx2 > gx1) and 1 or -1
local sy = (gy2 > gy1) and 1 or -1
local sz = (gz2 > gz1) and 1 or -1
local p1, p2
local x, y, z = gx1, gy1, gz1
local voxels = {}
if dx > dy and dx > dz then
p1 = 2*dy-dx
p2 = 2*dz-dx
while x ~= gx2 do
x = x + sx
if p1 >= 0 then
y = y + sy
p1 = p1 - 2*dx
end
if p2 >= 0 then
z = z + sz
p2 = p2 - 2*dx
end
p1 = p1 + 2*dy
p2 = p2 + 2*dz
table.insert(voxels, point_pack(x, y, z))
end
elseif dy > dz then
p1 = 2*dx-dy
p2 = 2*dz-dy
while y ~= gy2 do
y = y + sy
if p1 >= 0 then
x = x + sx
p1 = p1 - 2*dy
end
if p2 >= 0 then
z = z + sz
p2 = p2 - 2*dy
end
p1 = p1 + 2*dx
p2 = p2 + 2*dz
table.insert(voxels, point_pack(x, y, z))
end
else
p1 = 2*dx-dz
p2 = 2*dy-dz
while z ~= gz2 do
z = z + sz
if p1 >= 0 then
x = x + sx
p1 = p1 - 2*dz
end
if p2 >= 0 then
y = y + sy
p2 = p2 - 2*dz
end
p1 = p1 + 2*dx
p2 = p2 + 2*dy
table.insert(voxels, point_pack(x, y, z))
end
end
x, y, z = gx1, gy1, gz1
local los_reached, los_blocked
for _, pt in ipairs(voxels) do
local nx, ny, nz = point_unpack(pt)
local reached, blocked = VoxelCanReach(x, y, z, nx, ny, nz)
los_reached = los_reached or reached
los_blocked = los_blocked or blocked
if los_reached and los_blocked then
break
end
--[[local color
if reached and not blocked then
color = const.clrWhite
elseif reached then
color = const.clrYellow
else
color = const.clrRed
end
mesh = PlaceBox(voxel_box + point(VoxelToWorld(nx, ny, nz)), color, nil, false)
table.insert(g_dbgSmokeVisuals, mesh)--]]
x, y, z = nx, ny, nz
end
if los_reached and los_blocked then
return "partial"
elseif los_reached then
return "clear"
end
return "blocked"
end
function test_voxel_los()
g_VoxelBlock = calc_voxelblocking()
local t = GetPreciseTicks()
local num = 10
local total = 0
local stats = {}
for i = 1, num do
for _, unit in ipairs(g_Units) do
if not IsValid(unit) or unit:IsDead() then
goto continue
end
for _, other in ipairs(g_Units) do
if IsValid(other) and not other:IsDead() then
local result = VoxelLOS(unit, other)
stats[result] = (stats[result] or 0) + 1
total = total + 1
end
end
::continue::
end
end
local time = GetPreciseTicks() - t
printf("%d recalcs done in %d ms, %d ms/cycle", num, time, time / num)
printf("results received: %d", total)
if total > 0 then
for k, n in sorted_pairs(stats) do
printf(" %s: %d (%d%%)", k, n, MulDivRound(n, 100, total))
end
end
end
function PropagateSmokeInGrid(g0x, g0y, g0z, gdx, gdy)
local smoke = {}
local queue = { point_pack(0, 0, 0) }
local blocked = {}
local block_slabs = {}
local function set_block_slab(gx, gy, gz, dir)
local key = bor(band(gx, 0xffff), shift(band(gy, 0xffff), 16), shift(band(gz, 0xffff), 32), shift(dir, 48))
block_slabs[key] = true
end
local function is_blocked(gx, gy, gz, dir)
local key = bor(band(gx, 0xffff), shift(band(gy, 0xffff), 16), shift(band(gz, 0xffff), 32), shift(dir, 48))
return block_slabs[key]
end
local wpt = point(VoxelToWorld(g0x, g0y, g0z))
local slabs = MapGet(wpt, 5*const.SlabSizeX, "Slab")
for _, slab in ipairs(slabs) do
local gx, gy, gz, side = slab:GetGridCoords()
if IsKindOf(slab, "FloorSlab") then
set_block_slab(gx-g0x, gy-g0y, gz-g0z, dirNZ)
set_block_slab(gx-g0x, gy-g0y, gz-1-g0z, dirPZ)
elseif IsKindOfClasses(slab, "CeilingSlab", "RoofSlab") then
set_block_slab(gx-g0x, gy-g0y, gz-g0z, dirPZ)
set_block_slab(gx-g0x, gy-g0y, gz+1-g0z, dirNZ)
elseif IsKindOf(slab, "WallSlab") then
local blocked = slab.isVisible
local height = 1
if not blocked and slab.wall_obj then
height = slab.wall_obj.height
if IsKindOf(slab.wall_obj, "SlabWallDoor") then
blocked = not slab.wall_obj:IsDead() and slab.wall_obj.pass_through_state == "closed"
elseif IsKindOf(slab.wall_obj, "SlabWallWindow") then
blocked = slab.wall_obj.pass_through_state == "intact"
end
end
if blocked then
local dir = Rotate(point(1, 0, 0), slab:GetAngle())
local x, y = dir:xy()
local neg
if x > 0 then
dir = dirPX
neg = dirNX
elseif x < 0 then
dir = dirNX
neg = dirPX
elseif y > 0 then
dir = dirPY
neg = dirNY
else -- y < 0
dir = dirNY
neg = dirPY
end
for i = 1, height do
set_block_slab(gx-g0x, gy+i-1-g0y, gz-g0z, dir)
set_block_slab(gx+x-g0x, gy+y+i-1-g0y, gz-g0z, neg)
end
end
end
end
local function try_spill(px, py, pz, dx, dy, dz)
local vx, vy, vz = px + dx, py + dy, pz + dz
local prevpack = point_pack(px, py, pz)
local packed = point_pack(vx, vy, vz)
if IsOutsideSmokeArea(vx*gdx, vy*gdy, vz) then
local dir
if dx > 0 then
dir = dirPX
elseif dx < 0 then
dir = dirNX
elseif dy > 0 then
dir = dirPY
elseif dy < 0 then
dir = dirNY
elseif dz > 0 then
dir = dirPZ
else
dir = dirNZ
end
blocked[prevpack] = bor(blocked[prevpack] or 0, shift(1, dir))
return
end
local pt = point(VoxelToWorld(g0x, g0y, g0z))
local wpt = pt + point(vx * const.SlabSizeX, vy * const.SlabSizeY, vz * const.SlabSizeZ)
smoke[point_pack(0, 0, 0)] = wpt
-- check terrain
if dz < 0 and terrain.GetHeight(wpt) > wpt:z() + sizez/2 then
--DbgAddVector(pt + point(px * sizex, py * sizey, pz * sizez + sizez/4), point(0, 0, -sizez/2), const.clrRed)
blocked[prevpack] = bor(blocked[prevpack] or 0, shift(1, dirNZ))
return
end
if dz > 0 then
-- check the floor of the new tile
if is_blocked(px, py, pz, dirPZ) then
--DbgAddVector(pt + point(px * sizex, py * sizey, vz * sizez - sizez/4), point(0, 0, sizez/2), const.clrRed)
blocked[prevpack] = bor(blocked[prevpack] or 0, shift(1, dirPZ))
return
end
elseif dz < 0 then
-- check the floor of the previous tile
if is_blocked(px, py, pz, dirNZ) then
--DbgAddVector(pt + point(px * sizex, py * sizey, pz * sizez + sizez/4), point(0, 0, -sizez/2), const.clrRed)
blocked[prevpack] = bor(blocked[prevpack] or 0, shift(1, dirNZ))
return
end
else
-- check for wall slabs (at the respective edge)
local side
local dirShift
if dx > 0 then
side = "E" -- 0
dirShift = dirPX
elseif dx < 0 then
side = "W" -- 180
dirShift = dirNX
elseif dy > 0 then
side = "S" -- 90
dirShift = dirPY
else -- dy < 0
side = "N" -- 270
dirShift = dirNY
end
if is_blocked(px, py, pz, dirShift) then
--DbgAddVector(pt + point(px*sizex + dx*sizex/4, py*sizey + dy*sizey/4, pz*sizez + sizez/2), point(dx*sizex/2, dy*sizey/2, 0), const.clrRed)
blocked[prevpack] = bor(blocked[prevpack] or 0, shift(1, dirShift))
return
end
end
if smoke[packed] then return end -- needs to be after the blocked checks to make sure we're not missing information about blocked voxel to voxel transition
smoke[packed] = wpt
queue[#queue + 1] = packed
end
local idx = 1
while idx <= #queue do
local x, y, z = point_unpack(queue[idx])
idx = idx + 1
try_spill(x, y, z, 1, 0, 0)
try_spill(x, y, z, -1, 0, 0)
try_spill(x, y, z, 0, 1, 0)
try_spill(x, y, z, 0, -1, 0)
try_spill(x, y, z, 0, 0, 1)
try_spill(x, y, z, 0, 0, -1)
end
return smoke, blocked
end
local function FindSmokeObjPos(smoke, blocked)
-- find a place for the smoke grenade obj: nearest voxel to original target (0, 0, 0)
local smoke_obj_ppos, min_dist2
local mask = bor(shift(1, dirPX), shift(1, dirPY), shift(1, dirNX), shift(1, dirNY))
for packed, _ in pairs(smoke) do
-- only consider voxels that are blocked downwards and do not have blocked x/y sides
local value = blocked[packed] or 0
if band(value, shift(1, dirNZ)) ~= 0 and band(value, mask) == 0 then
local x, y, z = point_unpack(packed)
local dist2 = (x*x*guim*guim) + (y*y*guim*guim) + (z*z*guim*guim)
if not smoke_obj_ppos or min_dist2 > dist2 then
smoke_obj_ppos, min_dist2 = packed, dist2
end
end
end
return smoke_obj_ppos
end
MapVar("g_dbgSmokeVisuals", {})
function clear_smoke_dbg_visuals()
DbgClearVectors()
DbgClearTexts()
for _, obj in ipairs(g_dbgSmokeVisuals) do
DoneObject(obj)
end
end
function test_smoke(target)
if not CurrentThread() then
CreateRealTimeThread(test_smoke, target)
return
end
local target = target or GetTerrainCursor()
local pt = SnapToPassSlab(target) or target
local voxel_box = box(point(-const.SlabSizeX/2, -const.SlabSizeY/2, 0), point(const.SlabSizeX/2, const.SlabSizeY/2, const.SlabSizeZ))
local g0x, g0y, g0z = WorldToVoxel(pt)
target = target:SetTerrainZ()
if not pt:IsValidZ() then
pt = pt:SetTerrainZ()
end
clear_smoke_dbg_visuals()
DbgAddVector(target, point(0, 0, 2*guim), const.clrGreen)
local smoke_box, mesh
smoke_box = voxel_box + pt
mesh = PlaceBox(smoke_box, const.clrGreen, nil, false)
table.insert(g_dbgSmokeVisuals, mesh)
local cx, cy = pt:xy()
local tx, ty = target:xy()
local ax, ay
if SelectedObj then
ax, ay = SelectedObj:GetPosXYZ()
end
-- pick directions to form the 2x2 block
local gdx = tx - cx
if gdx == 0 and ax then gdx = tx - ax end
gdx = (gdx ~= 0) and (gdx / abs(gdx)) or 1
local gdy = ty - cy
if gdy == 0 and ay then gdy = ty - ay end
gdy = (gdy ~= 0) and (gdy / abs(gdy)) or 1
-- check for walls in the direction of the spill
local sizex = const.SlabSizeX
local sizey = const.SlabSizeY
local sizez = const.SlabSizeZ
local midpt = pt + point(gdx*sizex/2, gdy*sizey/2, 0)
DbgAddVector(midpt, point(0, 0, 5*guim), const.clrMagenta)
DbgAddVector(pt + point(gdx * sizex / 4, 0, sizez / 2), point(gdx*2*guim, 0, 0), const.clrGreen)
DbgAddVector(pt + point(0, gdy * sizey / 4, sizez / 2), point(0, gdy*2*guim, 0), const.clrGreen)
local smoke, blocked = PropagateSmokeInGrid(g0x, g0y, g0z, gdx, gdy)
local voffset = {
point(sizex/4, 0, sizez/2), point(-sizex/4, 0, sizez/2),
point(0, sizey/4, sizez/2), point(0, -sizey/4, sizez/2),
point(0, 0, 3*sizez/4), point(0, 0, sizez/4),
}
local vbody = {
point(sizex/2, 0, 0), point(-sizex/2, 0, 0),
point(0, sizey/2, 0), point(0, -sizey/2, 0),
point(0, 0, sizez/2), point(0, 0, -sizez/2),
}
for packed, wpt in pairs(smoke) do
local smoke_box = voxel_box + wpt
assert(IsPoint(wpt))
local mesh = PlaceBox(smoke_box, const.clrGray, nil, false)
table.insert(g_dbgSmokeVisuals, mesh)
local block_mask = blocked[packed]
for i = dirPX, dirNZ do
local color = band(block_mask, shift(1, i)) == 0 and const.clrGreen or const.clrRed
DbgAddVector(wpt + voffset[i+1], vbody[i+1], color)
end
end
local smoke_obj_ppos = FindSmokeObjPos(smoke, blocked)
if smoke_obj_ppos then
local x, y, z = point_unpack(smoke_obj_ppos)
local pos = pt + point(x*sizex, y*sizex, z*sizez)
local obj = PlaceObject("GrenadeVisual", {fx_actor_class = "SmokeGrenadeSpinner"} )
obj:SetPos(pos)
table.insert(g_dbgSmokeVisuals, obj)
end
end
function ExplosionCreateSmokeAOE(attacker, results, fx_actor)
local trajectory = results.trajectory or empty_table
local pos = #trajectory > 0 and trajectory[#trajectory].pos or results.target_pos
if not pos then return end
local water = terrain.IsWater(pos) and terrain.GetWaterHeight(pos)
if water and (not pos:IsValidZ() or water >= pos:z()) then
pos = pos:IsValidZ() and pos or pos:SetTerrainZ()
PlayFX("GrenadeSplash", "start", fx_actor, false, pos)
CreateFloatingText(pos:SetZ(water), T(760226092433, "Sunk"))
return
end
pos = SnapToPassSlab(pos) or pos
local dx, dy = 1, 1
for i = #trajectory - 1, 1, -1 do
local step = trajectory[i]
if step.pos:Dist2D(pos) > 0 then
local px, py = step.pos:xy()
local x, y = pos:xy()
dx = (px == x) and 1 or ((x - px) / abs(x - px))
dy = (py == y) and 1 or ((y - py) / abs(y - py))
break
end
end
local turns = 4
local step_pos = SnapToPassSlab(pos) or point(VoxelToWorld(WorldToVoxel(pos)))
local volume = EnumVolumes(step_pos, "smallest")
if not volume then
if GameState.RainHeavy or GameState.DustStorm then
turns = 2
else
turns = 3
end
end
local zone = SmokeZone:new{smoke_dx = dx, smoke_dy = dy, remaining_time = turns * 5000, gas_type = results.aoe_type}
if IsKindOf(attacker, "Unit") then
zone.owner = attacker
end
zone:SetPos(pos)
zone:PropagateSmoke()
end
MapVar("g_SmokeObjs", {}) -- ppos -> smoke obj
DefineClass.SmokeObj = {
__parents = { "SpawnFXObject" },
flags = { efVisible = false, efApplyToGrids = false, cfSmokeObj = true },
entity = "SolidVoxel",
zones = false, -- zone refs
}
function SmokeObj:GetGasType()
return self.zones and self.zones[1] and self.zones[1].gas_type
end
function SmokeObj:Init()
self.zones = {}
InvalidateVisibility()
end
function SmokeObj:Done()
InvalidateVisibility()
end
DefineClass.SmokeZone = {
__parents = { "GameDynamicSpawnObject", "SpawnFXObject" },
remaining_time = false,
gas_type = false,
smoke_positions = false,
owner = false,
smoke_dx = 1,
smoke_dy = 1,
spinner = false,
campaign_time = false,
}
function SmokeZone:GameInit()
table.insert(MapSmokeZones, self)
self.campaign_time = self.campaign_time or Game.CampaignTime
PlayFX("SmokeZone", "start", self, self.gas_type)
end
function SmokeZone:Done()
table.remove_value(MapSmokeZones, self)
for _, wpt in pairs(self.smoke_positions) do
self:RemoveSmokeFromPos(point_pack(WorldToVoxel(wpt)))
end
if IsValid(self.spinner) then
DoneObject(self.spinner)
end
self.spinner = nil
PlayFX("SmokeZone", "end", self, self.gas_type)
end
function SmokeZone:RemoveSmokeFromPos(ppos)
local obj = g_SmokeObjs[ppos]
if not IsValid(obj) then return end
table.remove_value(obj.zones, self)
PlayFX("VoxelGas", "end", obj, self.gas_type)
if #obj.zones == 0 then
DoneObject(obj)
g_SmokeObjs[ppos] = nil
else
PlayFX("VoxelGas", "start", obj, obj:GetGasType())
end
end
function SmokeZone:PropagateSmoke()
local gx, gy, gz = WorldToVoxel(self)
local smoke, blocked = PropagateSmokeInGrid(gx, gy, gz, self.smoke_dx, self.smoke_dy)
-- remove existing smoke if it no longer propagates there (e.g. a fire started there, door closed, etc)
for ppos, wpt in pairs(self.smoke_positions) do
local gppos = point_pack(WorldToVoxel(wpt))
if not smoke[ppos] then
self:RemoveSmokeFromPos(gppos)
end
end
-- update self.smoke_positions from smoke & create smoke objs if necessary
for _, wpt in pairs(smoke) do
local ppos = point_pack(WorldToVoxel(wpt))
local obj = g_SmokeObjs[ppos]
local gas_type = obj and obj:GetGasType()
if not obj then
obj = PlaceObject("SmokeObj")
obj:SetPos(wpt)
g_SmokeObjs[ppos] = obj
end
table.insert_unique(obj.zones, self)
if not gas_type then
PlayFX("VoxelGas", "start", obj, self.gas_type)
end
end
self.smoke_positions = smoke
if not self.spinner then
local ppos = FindSmokeObjPos(smoke, blocked)
if ppos then
local x, y, z = point_unpack(ppos)
local pos = self:GetPos() + point(x*sizex, y*sizex, z*sizez)
local fx_class = "SmokeGrenadeSpinner"
if self.gas_type == "teargas" then
fx_class = "TearGasGrenadeSpinner"
elseif self.gas_type == "toxicgas" then
fx_class = "ToxicGasGrenadeSpinner"
end
local obj = PlaceObject("GrenadeVisual", {fx_actor_class = fx_class} )
obj:SetPos(pos)
obj:SetAnim(1, "rotating")
PlayFX("SmokeGrenadeSpin", "start", obj)
self.spinner = obj
else
self.spinner = "none" -- non-false/nil value to indicate we couldn't find a place for the object
end
end
end
function SmokeZone:UpdateRemainingTime(delta)
local remaining = self.remaining_time - delta
if remaining <= 0 or self.campaign_time < Game.CampaignTime then
DoneObject(self)
return true
end
self.remaining_time = remaining
self:PropagateSmoke()
end
function SmokeZone:GetDynamicData(data)
data.remaining_time = self.remaining_time or nil
data.gas_type = self.gas_type or nil
data.campaign_time = self.campaign_time or nil
data.owner = self.owner and self.owner:GetHandle() or nil
data.smoke_positions = self.smoke_positions and table.copy(self.smoke_positions) or nil
if self.spinner and not IsValid(self.spinner) then
data.spinner = self.spinner
end
end
function SmokeZone:SetDynamicData(data)
self.remaining_time = data.remaining_time
self.gas_type = data.gas_type
self.spinner = data.spinner
self.owner = HandleToObject[data.owner or false]
self.campaign_time = data.campaign_time or Game.CampaignTime
if self.campaign_time < Game.CampaignTime then
return
end
if data.smoke_positions then
self.smoke_positions = table.copy(data.smoke_positions)
self:PropagateSmoke()
end
CreateGameTimeThread(function()
Sleep(1) -- delay until loading is done and game has started for real, otherwise PlaySound can fail due to actor being too far
PlayFX("SmokeZone", "start", self)
end)
end
function ToggleGasDebug()
if g_dbgSmokeVisuals then
for _, obj in ipairs(g_dbgSmokeVisuals) do
DoneObject(obj)
end
g_dbgSmokeVisuals = false
return
end
g_dbgSmokeVisuals = {}
local voxel_box = box(point(-const.SlabSizeX/2, -const.SlabSizeY/2, 0), point(const.SlabSizeX/2, const.SlabSizeY/2, const.SlabSizeZ))
for ppos, obj in pairs(g_SmokeObjs) do
local pt = point(VoxelToWorld(point_unpack(ppos)))
if not pt:IsValidZ() then
pt = pt:SetTerrainZ()
end
local gas_box = voxel_box + pt
local color = const.clrGray
local gas = obj:GetGasType()
if gas == "teargas" then
color = const.clrYellow
elseif gas == "toxicgas" then
color = const.clrGreen
end
local mesh = PlaceBox(gas_box, color, nil, false)
table.insert(g_dbgSmokeVisuals, mesh)
end
for _, unit in ipairs(g_Units) do
local voxels, head = unit:GetVisualVoxels()
for _, voxel in ipairs(voxels) do
local pt = point(VoxelToWorld(point_unpack(voxel)))
local unit_box = MulDivRound(voxel_box, 9, 10) + pt
local color = const.clrWhite
if voxel == head then
if g_SmokeObjs[head] then
color = const.clrRed
else
color = const.clrBlue
end
end
local mesh = PlaceBox(unit_box, color, nil, false)
table.insert(g_dbgSmokeVisuals, mesh)
end
end
end
DefineClass.Flare = {
__parents = { "Grenade" },
properties = {
{ category = "Combat", id = "Expires", editor = "bool", default = true, template = true, },
{ category = "Combat", id = "ExpirationTurns", editor = "number", min = 1, default = 4, template = true, },
{ category = "Combat", id = "Despawn", editor = "bool", default = false, template = true },
},
}
function Flare:OnPrepareThrow(thrower, visual)
--this happens on one client only
PlayFX("Flare", "start", visual)
ResetVoxelStealthParamsCache()
end
function Flare:OnFinishThrow(thrower)
ResetVoxelStealthParamsCache()
end
function Flare:OnLand(thrower, attackResults, visual_obj)
local pos = attackResults.explosion_pos
local snapped = false
local sync = false
if pos then
snapped = SnapToPassSlab(pos:xyz()) or pos
if snapped then
sync = true
end
end
if not snapped then
snapped = IsValid(visual_obj) and visual_obj:GetPos()
end
if not snapped then
return
end
pos = pos or snapped
if snapped:IsValidZ() then
pos = pos:SetZ(snapped:z())
else
pos = pos:SetInvalidZ()
end
local flare = PlaceObject("FlareOnGround")
flare:SetPos(pos)
flare.item_class = self.class
flare.campaign_time = Game.CampaignTime
flare.Despawn = self.Despawn
if self.Expires then
flare.remaining_time = self.ExpirationTurns * 5000
end
flare:UpdateVisualObj()
PushUnitAlert("thrown", flare, thrower)
if self.ThrowNoise > 0 then
-- <Unit> Heard a thud
PushUnitAlert("noise", visual_obj, self.ThrowNoise, Presets.NoiseTypes.Default.ThrowableLandmine.display_name)
end
if IsValid(visual_obj) then
DoneObject(visual_obj)
end
end
DefineClass.FlareOnGround = {
__parents = { "GameDynamicSpawnObject" },
item_class = "FlareStick",
visual_obj = false,
remaining_time = -1,
campaign_time = false,
Despawn = true,
}
function FlareOnGround:GameInit()
table.insert(MapFlareOnGround, self)
end
function FlareOnGround:Done()
table.remove_value(MapFlareOnGround, self)
end
function FlareOnGround:UpdateVisualObj(start_fx)
if not IsValid(self.visual_obj) then
self.visual_obj = PlaceObject("GrenadeVisual", {fx_actor_class = self.item_class .. "_OnGround"})
end
self:Attach(self.visual_obj)
if start_fx then
PlayFX("Flare", "start", self.visual_obj)
end
ResetVoxelStealthParamsCache()
end
function FlareOnGround:UpdateRemainingTime(delta)
if Game.CampaignTime ~= self.campaign_time then
DoneObject(self)
return
end
if self.remaining_time < 0 then return end
self.remaining_time = Max(0, self.remaining_time - delta)
if self.remaining_time == 0 then
PlayFX("Flare", "end", self.visual_obj)
local attaches = self.visual_obj:GetAttaches("Weapon_FlareStick_OnGround")
for _, obj in ipairs(attaches) do
PlayFX("Flare", "end", obj)
end
if self.Despawn then
DoneObject(self)
else
self.remaining_time = -1
end
ResetVoxelStealthParamsCache()
end
end
function TestKillFlares()
for _, f in ipairs(MapFlareOnGround) do
f:UpdateRemainingTime(99999999)
end
end
function FlareOnGround:GetDynamicData(data)
data.item_class = self.item_class
data.remaining_time = self.remaining_time
data.campaign_time = self.campaign_time
end
function FlareOnGround:SetDynamicData(data)
self.item_class = data.item_class
self.remaining_time = data.remaining_time
self.campaign_time = data.campaign_time
self:UpdateVisualObj(self.remaining_time ~= 0)
end
DefineClass.Weapon_SmokeGrenade = { __parents = {"Weapon_SmokeGrenade_Base"}, entity = "Weapon_SmokeGrenade", }
DefineClass.Weapon_SmokeGrenade_Spinning = { __parents = {"Weapon_SmokeGrenade_Base"}, entity = "Weapon_SmokeGrenade", }
DefineClass.Weapon_TearGasGrenade = { __parents = {"Weapon_SmokeGrenade_Base"}, entity = "Weapon_SmokeGrenade", }
DefineClass.Weapon_TearGasGrenade_Spinning = { __parents = {"Weapon_SmokeGrenade_Base"}, entity = "Weapon_SmokeGrenade", }
DefineClass.Weapon_ToxicGasGrenade = { __parents = {"Weapon_SmokeGrenade_Base"}, entity = "Weapon_SmokeGrenade", }
DefineClass.Weapon_ToxicGasGrenade_Spinning = { __parents = {"Weapon_SmokeGrenade_Base"}, entity = "Weapon_SmokeGrenade", }
DefineClass.Weapon_PipeBomb = { __parents = {"GrenadeVisual"}, entity = "Weapon_PipeBomb", }
DefineClass.Weapon_PipeBomb_OnGround = { __parents = {"GrenadeVisual"}, entity = "Weapon_PipeBomb", }
DefineClass.Weapon_FlareStick = { __parents = {"GrenadeVisual"}, entity = "World_Flarestick_01", }
DefineClass.Weapon_FlareStick_OnGround = { __parents = {"GrenadeVisual"}, entity = "World_Flarestick_01", }
DefineClass.Weapon_GlowStick = { __parents = {"GrenadeVisual"}, entity = "Weapon_GlowStick", }
DefineClass.Weapon_GlowStick_OnGround = { __parents = {"GrenadeVisual"}, entity = "Weapon_GlowStick", }
DefineClass.Weapon_ProximityTNT = { __parents = {"GrenadeVisual"}, entity = "Explosive_TNT", }
DefineClass.Weapon_ProximityTNT_OnGround = { __parents = {"GrenadeVisual"}, entity = "Explosive_TNT", }
DefineClass.Weapon_RemoteTNT = { __parents = {"GrenadeVisual"}, entity = "Explosive_TNT", }
DefineClass.Weapon_RemoteTNT_OnGround = { __parents = {"GrenadeVisual"}, entity = "Explosive_TNT", }
DefineClass.Weapon_TimedTNT = { __parents = {"GrenadeVisual"}, entity = "Explosive_TNT", }
DefineClass.Weapon_TimedTNT_OnGround = { __parents = {"GrenadeVisual"}, entity = "Explosive_TNT", }
DefineClass.Weapon_ProximityC4 = { __parents = {"GrenadeVisual"}, entity = "Explosive_C4", }
DefineClass.Weapon_ProximityC4_OnGround = { __parents = {"GrenadeVisual"}, entity = "Explosive_C4", }
DefineClass.Weapon_RemoteC4 = { __parents = {"GrenadeVisual"}, entity = "Explosive_C4", }
DefineClass.Weapon_RemoteC4_OnGround = { __parents = {"GrenadeVisual"}, entity = "Explosive_C4", }
DefineClass.Weapon_TimedC4 = { __parents = {"GrenadeVisual"}, entity = "Explosive_C4", }
DefineClass.Weapon_TimedC4_OnGround = { __parents = {"GrenadeVisual"}, entity = "Explosive_C4", }
DefineClass.Weapon_ProximityPETN = { __parents = {"GrenadeVisual"}, entity = "Explosive_PETN", }
DefineClass.Weapon_ProximityPETN_OnGround = { __parents = {"GrenadeVisual"}, entity = "Explosive_PETN", }
DefineClass.Weapon_RemotePETN = { __parents = {"GrenadeVisual"}, entity = "Explosive_PETN", }
DefineClass.Weapon_RemotePETN_OnGround = { __parents = {"GrenadeVisual"}, entity = "Explosive_PETN", }
DefineClass.Weapon_TimedPETN = { __parents = {"GrenadeVisual"}, entity = "Explosive_PETN", }
DefineClass.Weapon_TimedPETN_OnGround = { __parents = {"GrenadeVisual"}, entity = "Explosive_PETN", }