myspace / Lua /Tactical /Visibility.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
44 kB
const.LOFIgnoreHitDistance = const.SlabSizeX
const.MaxLOFRange = 141 * const.SlabSizeX -- limit by playable map size (100 tiles x sqrt(2))
const.CombatObjectMaxRadius = 2 * const.SlabSizeX
const.UnitHitRadius = const.SlabSizeX / 3
const.LOSCoverMaxConeAngle = 120 * 60
const.LOSSlabMaxConeAngle = 160 * 60
const.LOSProneHeight = 40*guic
const.LOSCrouchHeight = 90*guic
const.LOSStandingHeight = 130*guic
const.LOSPointsDistForward = const.SlabSizeX / 3
const.LOSPointsDistAside = const.SlabSizeX / 10
const.AreaAttackStandingSpots = { "Head", "Torso", "Elbowl", "Elbowr" }
const.AreaAttackProneSpots = { "Head" }
const.ConeAttackGroundMin = 20*guic -- hit prone units (do not hit floor slabs)
const.ConeAttackGroundMax = 200*guic -- 3 slabs height (do not hit ceiling slabs)
const.DefaultTargetSpots = { "Hit" }
const.uvVisible = 1
const.uvNPC = 2
const.uvRevealed = 4
-- sight condition consts
const.usObscured = 1
const.usConcealed = 2
MapVar("TargetDummies", {}, weak_keys_meta)
DefineClass.HittableObject= {
__parents = {"CObject"},
}
local function SetDefaultTargetSpots()
-- Prone area attack targets
local prone_points = {}
local prone_heights = {30*guic}
local prone_radius = 30*guic
local x = 40*guic
for i, h in ipairs(prone_heights) do
table.insert(prone_points, point(x, 0, h))
table.insert(prone_points, point(x, prone_radius, h))
table.insert(prone_points, point(x, -prone_radius, h))
end
SetAreaAttackProneHitPos(prone_points)
-- Standing area attack targets
local standing_points = {}
local standing_heights = {80*guic,130*guic}
local standing_radius = 30*guic
for i, h in ipairs(standing_heights) do
table.insert(standing_points, point(x, 0, h))
table.insert(standing_points, point(x, standing_radius, h))
table.insert(standing_points, point(x, -standing_radius, h))
end
SetAreaAttackStandingHitPos(standing_points)
end
SetDefaultTargetSpots()
OnMsg.EntitiesLoaded = UpdateUnitColliders
OnMsg.DataLoaded = UpdateUnitColliders
OnMsg.PresetSave = UpdateUnitColliders
local immune_to_half_area_damage_classes = {"Landmine"}
function GetAreaAttackHitModifier(obj, los_value)
if (los_value or 0) == 0 then
return 0
elseif obj:IsInvulnerable() then
return 0
elseif los_value == 1 then
if IsKindOf(obj, "Unit") then
if obj.stance == "Prone" then
return 0
end
elseif IsKindOfClasses(obj, immune_to_half_area_damage_classes) then
return 0
end
return 50
end
return 100
end
function GetAreaAttackHitModifiers(action_id, attack_args, targets)
local action = CombatActions[action_id]
local cone_angle = action.AimType == "cone" and attack_args.cone_angle or -1
if not attack_args.distance then
attack_args.distance = attack_args.max_range and attack_args.max_range * const.SlabSizeX or -1
end
local maxvalue, los_values = CheckLOS(targets, attack_args.step_pos, attack_args.distance, attack_args.stance, cone_angle, attack_args.target, false, attack_args.min_distance_2d)
local modifiers = {}
for i, target in ipairs(targets) do
modifiers[i] = GetAreaAttackHitModifier(target, los_values and los_values[i])
end
return modifiers
end
function GetAOETiles(step_pos, stance, distance, cone_angle, target, force2d)
local step_positions, step_objs = GetStepPositionsInArea(step_pos, distance, 0, cone_angle, target, force2d)
local maxvalue, los_values = CheckLOS(step_positions, step_pos, -1, stance, -1, false, false)
return step_positions, step_objs, los_values or empty_table
end
function OnMsg.ChangeMapDone()
MapForEach("map", "CombatObject", function(o)
if o:GetDetailClass() ~= "Essential" then
o:SetDetailClass("Essential") --non essential combat objects will change attack results.
end
end)
end
function GetAreaAttackResults(aoe_params, damage_bonus, applied_status, damage_override)
local prediction = aoe_params.prediction
local attacker = aoe_params.attacker
local step_pos = aoe_params.step_pos or IsValid(attacker) and attacker:GetPos()
local occupied_pos = aoe_params.occupied_pos or IsKindOf(attacker, "Unit") and attacker:GetOccupiedPos()
local stance = aoe_params.stance or IsKindOf(attacker, "Unit") and attacker.stance or "Standing"
local target_pos = aoe_params.target_pos or step_pos
local explosion = aoe_params.explosion
local cone_angle = aoe_params.cone_angle or -1
local range
if aoe_params.max_range and aoe_params.min_range and aoe_params.max_range ~= aoe_params.min_range then
range = Clamp(attacker:GetDist(target_pos), aoe_params.min_range * const.SlabSizeX, aoe_params.max_range * const.SlabSizeX)
else
range = aoe_params.max_range and aoe_params.max_range * const.SlabSizeX or -1
end
local min_range_2d = 0
local weapon = aoe_params.weapon
local dont_destroy_covers = aoe_params.dont_destroy_covers
if not prediction then
NetUpdateHash("GetAreaAttackResults", step_pos, stance, range, min_range_2d, cone_angle, target_pos, occupied_pos, dont_destroy_covers)
end
local targets, los_values = GetAreaAttackTargets(step_pos, stance, prediction, range, min_range_2d, cone_angle, target_pos, occupied_pos, dont_destroy_covers)
targets = table.ifilter(targets, function(idx, target) return not IsKindOf(target, "Landmine") end)
if not prediction then
NetUpdateHash("GetAreaAttackResults_Results", #targets)
end
if IsValid(attacker) and not aoe_params.can_be_damaged_by_attack then
local idx = table.find(targets, attacker)
if idx then
table.remove(targets, idx)
table.remove(los_values, idx)
end
end
local results = { start_pos = step_pos, target_pos = target_pos, range = range, cone_angle = cone_angle, aoe_type = aoe_params.aoe_type, explosion = explosion}
if #targets == 0 then
return results, 0, 0, {}
end
local total_damage, friendly_fire_dmg = 0, 0
if not step_pos:IsValidZ() then
step_pos = step_pos:SetTerrainZ()
end
local impact_force = weapon:GetImpactForce()
for i, obj in ipairs(targets) do
local dmg_mod = aoe_params.damage_mod
local nominal_dmg = (attacker and IsKindOf(attacker, "Unit")) and attacker:GetBaseDamage(weapon, obj) or weapon.BaseDamage
if aoe_params.damage_override then
nominal_dmg = aoe_params.damage_override
end
if not prediction then
nominal_dmg = RandomizeWeaponDamage(nominal_dmg)
end
local hit = {}
results[i] = hit
hit.obj = obj
hit.aoe = true
hit.area_attack_modifier = GetAreaAttackHitModifier(obj, los_values[i])
hit.aoe_type = aoe_params.aoe_type
if explosion then
local center_range = aoe_params.center_range or 1
if center_range > 1 then
hit.explosion_center = obj:GetDist(target_pos) <= (center_range * const.SlabSizeX)
else
hit.explosion_center = GetPassSlab(target_pos) == GetPassSlab(obj)
end
end
if hit.area_attack_modifier > 0 then
local dmg = 0
if dmg_mod ~= "no damage" then
dmg_mod = dmg_mod + aoe_params.attribute_bonus
dmg = MulDivRound(nominal_dmg, Max(0, dmg_mod), 100)
end
if dmg > 0 and not explosion and IsValid(attacker) and aoe_params.falloff_damage and aoe_params.falloff_start then
local dist = attacker:GetDist(obj)
local falloff_factor = Clamp(0, 100, MulDivRound(dist, 100, range) - aoe_params.falloff_start)
if falloff_factor > 0 then
local damage_start, damage_end = dmg, MulDivRound(dmg, aoe_params.falloff_damage, 100)
dmg = Max(1, MulDivRound(damage_start, 100 - falloff_factor, 100) + MulDivRound(damage_end, falloff_factor, 100))
end
end
weapon:PrecalcDamageAndStatusEffects(attacker, obj, step_pos, dmg, hit, applied_status, nil, nil, nil, prediction)
local damage
if damage_override then
damage = damage_override
else
damage = MulDivRound(hit.damage, 100 + (damage_bonus or 0), 100)
end
local dmg_mod = hit.area_attack_modifier
if explosion and IsKindOf(obj, "Unit") then
if obj.stance == "Prone" then
dmg_mod = dmg_mod + const.Combat.ExplosionProneDamageMod
if HasPerk(obj, "HitTheDeck") then
local mod = CharacterEffectDefs.HitTheDeck:ResolveValue("explosiveLessDamage")
dmg_mod = dmg_mod - mod
end
elseif obj.stance == "Crouch" then
dmg_mod = dmg_mod + const.Combat.ExplosionCrouchDamageMod
end
end
damage = MulDivRound(damage, Max(0, dmg_mod), 100)
if aoe_params.stealth_attack_roll and IsKindOf(attacker, "Unit") and IsKindOf(obj, "Unit") and not obj.villain and not obj:IsDead() then
if aoe_params.stealth_attack_roll < attacker:CalcStealthKillChance(weapon, obj) then
damage = MulDivRound(obj:GetTotalHitPoints(), 100 + obj:Random(50), 100)
hit.stealth_kill = true
end
hit.stealth_kill_chance = attacker:CalcStealthKillChance(weapon, obj)
end
hit.damage = damage
if IsKindOf(attacker, "Unit") and IsKindOf(obj, "Unit") then
total_damage = total_damage + damage
if not obj:IsOnEnemySide(attacker) then
friendly_fire_dmg = friendly_fire_dmg + damage
end
end
hit.impact_force = impact_force + weapon:GetDistanceImpactForce(obj:GetDist(step_pos))
else
hit.damage = 0
hit.stuck = true
hit.armor_decay = empty_table
hit.effects = empty_table
end
if aoe_params.explosion_fly and IsKindOf(hit.obj, "Unit") and hit.damage >= const.Combat.GrenadeMinDamageForFly then
hit.explosion_fly = true
end
end
results.total_damage = total_damage
results.friendly_fire_dmg = friendly_fire_dmg
results.hit_objs = targets
return results, total_damage, friendly_fire_dmg, targets
end
if FirstLoad then
g_InvisibleUnitOpacity = 0
g_ExperimentalModeLOS = "slab block only"
end
-- TODO: remove this when experimenting with LOS is finished
config.SlabEntityList = ""
function DbgCycleExperimentalLOS()
config.SlabEntityList = ""
if not g_ExperimentalModeLOS then
g_ExperimentalModeLOS = "all visible"
print("LOS: All enemies are visibles")
elseif g_ExperimentalModeLOS == "all visible" then
g_ExperimentalModeLOS = "slab block only"
config.SlabEntityList = "Floor,Stairs,WallExt,WallInt,Door,TallDoor,Window,WindowBig,WindowVent,Roof"
print("LOS: Only Slab objects block vision")
else
g_ExperimentalModeLOS = false
print("LOS: Normal mode.")
end
end
local function IsVisibleTo(self, other)
if g_ExperimentalModeLOS == "all visible" then
return true
end
if not other.team:IsEnemySide(self.team) then
return true
end
if self:CanSee(other) then
return true
end
return false
end
MapVar("g_Visibility", {})
MapVar("g_SightConditions", {})
MapVar("g_RevealedUnits", {}) -- [team] -> list of units known to the team regardless of visibility (lasts until the end of team's turn)
MapVar("g_VisibilityUpdated", false)
MapVar("g_SetpieceFullVisibility", false)
function NetSyncEvents.RevealToTeam(unit, teamId)
unit:RevealTo(g_Teams[teamId])
end
function Unit:RevealTo(obj, combat)
combat = combat or g_Combat
if not combat then return end
-- Dont reveal traps
if not IsKindOfClasses(obj, "Unit", "CombatTeam") then return end
local team = IsValid(obj) and obj.team or obj
-- add ourselves to g_RevealedUnits for the sake of consequent visibility/diplomacy updates
g_RevealedUnits[team] = g_RevealedUnits[team] or {}
table.insert_unique(g_RevealedUnits[team], self)
self:RemoveStatusEffect("Spotted")
self:RemoveStatusEffect("Hidden")
self:AddStatusEffect("Revealed")
-- add ourselves to target team visibility
if not HasVisibilityTo(team, self) then
g_Visibility[team] = g_Visibility[team] or {}
table.insert(g_Visibility[team], self)
end
g_Visibility[team][self] = bor(g_Visibility[team][self] or 0, const.uvRevealed)
InvalidateDiplomacy() -- update unit enemy lists
if g_Combat then
g_Combat:ApplyVisibility()
end
-- trigger unaware units who can now see us
for _, unit in ipairs(team.units) do
if VisibilityCheckAll(unit, self, nil, const.uvVisible) and unit:HasStatusEffect("Unaware") then
PushUnitAlert("sight", unit, self)
end
end
AlertPendingUnits()
end
function VisibilityCheckAll(observer, other, visibility, mask)
if IsKindOf(other, "Unit") then
local vis = (visibility or g_Visibility)[observer]
return band(vis and vis[other] or 0, mask) == mask
elseif IsKindOf(other, "Trap") then
assert((observer.team and observer.team.side == "player1") or observer.side == "player1")
local trapVis = g_AttackableVisibility[observer]
trapVis = trapVis and trapVis[other] and const.uvVisible or 0
return band(trapVis, mask) == mask
end
return true
end
function VisibilityCheckAny(observer, other, visibility, mask)
if IsKindOf(other, "Unit") then
local vis = (visibility or g_Visibility)[observer]
return band(vis and vis[other] or 0, mask) ~= 0
elseif IsKindOf(other, "Trap") then
assert((observer.team and observer.team.side == "player1") or observer.side == "player1")
local trapVis = g_AttackableVisibility[observer]
trapVis = trapVis and trapVis[other] and const.uvVisible or 0
return band(trapVis, mask) ~= 0
end
return true
end
function HasVisibilityTo(observer, other, visibility)
if IsKindOf(other, "Unit") then
local vis = (visibility or g_Visibility)[observer]
return (vis and vis[other] or 0) >= const.uvVisible
elseif IsKindOf(other, "Trap") then
assert((observer.team and observer.team.side == "player1") or observer.side == "player1")
local trapVis = g_AttackableVisibility[observer]
return trapVis and trapVis[other]
end
return true
end
function VisibilityGetValue(observer, other, visibility)
if IsKindOf(other, "Unit") then
local vis = (visibility or g_Visibility)[observer]
return vis and vis[other] or 0
elseif IsKindOf(other, "Trap") then
assert((observer.team and observer.team.side == "player1") or observer.side == "player1")
local trapVis = g_AttackableVisibility[observer]
return trapVis and trapVis[other] and const.uvVisible or 0
end
return const.uvVisible
end
function IsFullVisibility()
return CheatEnabled("FullVisibility")
end
function CheckSightCondition(observer, other, condition)
local value = (g_SightConditions[observer] or empty_table)[other] or 0
return band(value, condition) == condition
end
local function HandleSortFunction(a, b)
return a.handle < b.handle
end
function Unit:ComputeVisibleUnits()
local unit_visibility = {}
local uvVisible = const.uvVisible
local uvVisibleNPC = bor(uvVisible, const.uvNPC)
local team = self.team
for i, other in ipairs(g_UnitsLOS[self]) do
local vis_value = uvVisible
if not other.team:IsEnemySide(team) then
if other:IsNPC() and not other:HasStatusEffect("HiddenNPC") then
vis_value = uvVisibleNPC
end
end
unit_visibility[i] = other
unit_visibility[other] = vis_value
end
return unit_visibility
end
-- called from C++
function GetMaxSightRadius()
return MulDivRound(const.Combat.AwareSightRange, const.SlabSizeX * const.Combat.SightModMaxValue, 100)
end
local function UpdateUnitsLOS(unitsLOS)
local player_units = {}
local enemy_units = {}
local neutral_units = {} -- units that do not need LOS info (only players could check LOS to them for free aim attack)
local dead_units = {} -- enemies alarm checks
local enemyNeutral_Side = GameState.Conflict and "enemy1" or "neutral"
local script_target_groups
local table_iappend = table.iappend
for group, mods in pairs(gv_AITargetModifiers) do
for target_group, value in pairs(mods) do
if not script_target_groups then script_target_groups = {} end
script_target_groups[target_group] = true
end
end
for _, team in ipairs(g_Teams) do
if #team.units > 0 then
local side = team.side
if side == "enemyNeutral" then
side = enemyNeutral_Side
end
if side == "neutral" then
for _, unit in ipairs(team.units) do
local los_tbl
if not unit:IsDead() then
local is_script_target
if script_target_groups then
for _, group in ipairs(unit.Groups) do
if script_target_groups[group] then
is_script_target = true
break
end
end
end
if is_script_target then
local units_list = enemy_units[side]
if not units_list then
units_list = {}
enemy_units[side] = units_list
enemy_units[#enemy_units + 1] = side
end
units_list[#units_list + 1] = unit
los_tbl = unitsLOS[unit]
if not los_tbl or #los_tbl > 1 then
los_tbl = {}
end
los_tbl[1] = unit
los_tbl[unit] = 2
else
neutral_units[#neutral_units + 1] = unit
end
end
unitsLOS[unit] = los_tbl
end
elseif team.player_team then
for _, unit in ipairs(team.units) do
if unit.HitPoints <= 0 then
unitsLOS[unit] = nil
else
player_units[#player_units + 1] = unit
local los_tbl = unitsLOS[unit]
if not los_tbl or #los_tbl > 1 then
los_tbl = {}
unitsLOS[unit] = los_tbl
end
los_tbl[1] = unit
los_tbl[unit] = 2
end
end
else
local units_list = enemy_units[side]
if not units_list then
units_list = {}
enemy_units[side] = units_list
enemy_units[#enemy_units + 1] = side
end
local dead_list = dead_units[side]
if not dead_list then
dead_list = {}
dead_units[side] = dead_list
end
for _, unit in ipairs(team.units) do
if unit.HitPoints <= 0 then
dead_list[#dead_list + 1] = unit
unitsLOS[unit] = nil
else
units_list[#units_list + 1] = unit
local los_tbl = unitsLOS[unit]
if not los_tbl or #los_tbl > 1 then
los_tbl = {}
unitsLOS[unit] = los_tbl
end
los_tbl[1] = unit
los_tbl[unit] = 2
end
end
end
end
end
local src_units, target_units = {}, {}
-- player targets
local players_count = #player_units
local last_player_unit = player_units[players_count]
for i, unit1 in ipairs(player_units) do
local idx = #target_units
table_iappend(target_units, player_units)
target_units[idx + i] = last_player_unit
target_units[idx + players_count] = nil
for j, side in ipairs(enemy_units) do
table_iappend(target_units, enemy_units[side])
end
table_iappend(target_units, neutral_units)
for j = idx + 1, #target_units do
src_units[j] = unit1
end
end
-- enemies targets
for i, side in ipairs(enemy_units) do
for _, unit1 in ipairs(enemy_units[side]) do
local idx = #target_units
table_iappend(target_units, player_units)
for k, side2 in ipairs(enemy_units) do
if k ~= i then
table_iappend(target_units, enemy_units[side2])
end
end
table_iappend(target_units, dead_units[side])
for j = idx + 1, #target_units do
src_units[j] = unit1
end
end
end
if #src_units > 0 then
local los_any, result = CheckLOS(target_units, src_units)
if los_any then
for i, target in ipairs(target_units) do
if result[i] then
local los_tbl = unitsLOS[src_units[i]]
los_tbl[#los_tbl + 1] = target
los_tbl[target] = result[i]
end
end
end
end
end
function ComputeUnitsVisibility()
UpdateUnitsLOS(g_UnitsLOS)
local visibility = {}
local visual_contact_change = {}
local sight_conditions_change = {}
local uvVisible = const.uvVisible
local uvRevealed = const.uvRevealed
local usConcealed = const.usConcealed
local usObscured = const.usObscured
local usConcealedAndObscured = bor(usConcealed, usObscured)
local insert = table.insert
local pov_team = GetPoVTeam()
local innerInfo--= gv_CurrentSectorId and g_Units.Livewire and g_Units.Livewire.team == GetPoVTeam() and gv_Sectors[gv_CurrentSectorId].intel_discovered -- Livewire's perk enabled
for _, unit in ipairs(pov_team and pov_team.units) do
innerInfo = innerInfo or unit:CallReactions_Or("OnCheckIntelVisible")
end
-- init team visibility
for _, team in ipairs(g_Teams) do
if team.side == "neutral" then -- neutral units don't care about combat visibility
-- update visibility for the script units
for _, unit in ipairs(team.units) do
if g_UnitsLOS[unit] then
local unit_visibility = unit:ComputeVisibleUnits()
visibility[unit] = unit_visibility
end
end
else
local team_visibility = {}
visibility[team] = team_visibility
if g_Combat then
for i, ru in ipairs(g_RevealedUnits[team]) do
if not ru:IsDead() then
insert(team_visibility, ru)
team_visibility[ru] = uvRevealed
end
end
end
for _, unit in ipairs(team.units) do
if unit.enemy_visual_contact then
insert(visual_contact_change, unit)
visual_contact_change[unit] = 1
end
unit.enemy_visual_contact = false
if unit:IsValidPos() and not unit:IsDead() then
local unit_visibility = unit:ComputeVisibleUnits()
visibility[unit] = unit_visibility
-- build team visibility
for i, other in ipairs(unit_visibility) do
--NetUpdateHash("CompVis1", other)
local prev_val = team_visibility[other] or 0
local tval = bor(prev_val, unit_visibility[other])
if tval ~= prev_val then
if prev_val == 0 then
insert(team_visibility, other)
end
team_visibility[other] = tval
end
end
end
end
if team.player_team then
for _, unit in ipairs(g_Units) do
if unit:IsValidPos() and not unit:IsDead() and (team_visibility[unit] or 0) < uvVisible then
if unit:HasStatusEffect("ForcedVisibleNPC") then
table.insert_unique(team_visibility, unit)
team_visibility[unit] = bor(team_visibility[unit] or 0, uvVisible)
elseif innerInfo then
table.insert_unique(team_visibility, unit)
team_visibility[unit] = bor(team_visibility[unit] or 0, uvRevealed)
end
end
end
end
end
end
-- share visibility to allies
for j, team in ipairs(g_Teams) do
local vis = visibility[team]
if vis and #vis > 0 then
for k, team2 in ipairs(g_Teams) do
if team ~= team2 and team:IsAllySide(team2) then
local vis2 = visibility[team2]
for i, other in ipairs(vis) do
if not vis2[other] then
insert(vis2, other)
vis2[other] = const.uvRevealed
end
end
end
end
end
end
-- update visual contact & sight conditions
local prevSightConditions = g_SightConditions
g_SightConditions = {}
local FogUnkownFoeDistance = GameState.Fog and const.EnvEffects.FogUnkownFoeDistance
local DustStormUnkownFoeDistance = GameState.DustStorm and const.EnvEffects.DustStormUnkownFoeDistance
local current_player_team = g_Combat and g_Teams[g_CurrentTeam] and g_Teams[g_CurrentTeam].player_team and g_Teams[g_CurrentTeam]
for _, team in ipairs(g_Teams) do
if team.side ~= "neutral" then
for _, unit in ipairs(team.units) do
if unit:IsAware("pending") then -- visual contact
for _, other in ipairs(visibility[unit]) do
if other.team:IsEnemySide(team) then
if not other.enemy_visual_contact then
other.enemy_visual_contact = true
local prev = visual_contact_change[other]
visual_contact_change[other] = (prev or 0) | 2
if not prev then
insert(visual_contact_change, other)
end
end
if other.team == current_player_team and other.in_combat_movement and other:HasStatusEffect("Hidden") then
other:AddStatusEffect("Spotted")
other:SetEffectValue("Spotted-" .. team.side, true)
end
end
end
end
if FogUnkownFoeDistance or DustStormUnkownFoeDistance then
local unit_sight_conditions
local prev_sight_conditions = prevSightConditions[unit] or empty_table
for _, other in ipairs(visibility[team]) do
local value
if not other.indoors then
if FogUnkownFoeDistance and not IsCloser(unit, other, FogUnkownFoeDistance) then
value = usConcealed
end
if DustStormUnkownFoeDistance and not IsCloser(unit, other, DustStormUnkownFoeDistance) then
value = value and usConcealedAndObscured or usObscured
end
if value then
if not unit_sight_conditions then unit_sight_conditions = {} end
unit_sight_conditions[other] = value
end
end
if value ~= prev_sight_conditions[other] then
if not sight_conditions_change[other] then
sight_conditions_change[other] = true
sight_conditions_change[#sight_conditions_change + 1] = other
end
end
end
g_SightConditions[unit] = unit_sight_conditions
end
end
end
end
-- update HiddenNPC status
for _, team in ipairs(g_Teams) do
if team.player_team then
for _, unit in ipairs(visibility[team]) do
if unit:HasStatusEffect("HiddenNPC") then
if VisibilityCheckAll(team, unit, visibility, uvVisible) then
unit:RemoveStatusEffect("HiddenNPC")
end
end
end
end
end
for _, unit in ipairs(visual_contact_change) do
if visual_contact_change[unit] ~= 3 then
unit:UpdateHidden()
Msg("UnitStealthChanged", unit)
end
end
for _, unit in ipairs(sight_conditions_change) do
ObjModified(unit)
end
g_VisibilityUpdated = true
return visibility
end
local function Visibility_UnitsHash()
local hash
for _, unit in ipairs(g_Units) do
local sight = unit:GetSightRadius()
local stance_idx = StancesList[unit.stance]
if unit:IsValidPos() then
hash = xxhash(stance_pos_pack(unit, stance_idx), sight, hash)
end
if unit.visibility_override then
hash = xxhash(stance_pos_pack(unit.visibility_override.pos, stance_idx), sight, hash)
end
end
for _, list in ipairs(g_RevealedUnits) do
for _, unit in ipairs(list) do
hash = xxhash(unit:GetHandle(), hash)
end
end
return hash
end
local function Visibility_ResultsHash()
local hash
for _, unit in ipairs(g_Units) do
local uvis = g_Visibility[unit]
hash = xxhash(unit.handle, hash)
for _, target in ipairs(uvis) do
hash = xxhash(target.handle, uvis[target], hash)
end
end
return hash
end
function Combat:UpdateVisibility()
local hash = Visibility_UnitsHash()
if hash == self.visibility_update_hash then return end
self.visibility_update_hash = hash --includes only unit changes
--DbgClearVectors()
local prev_visibility = g_Visibility
g_Visibility = ComputeUnitsVisibility()
for ti, team in ipairs(g_Teams) do
-- notify the team for visibility changes
if prev_visibility then
local prev = prev_visibility[team] or empty_table
for _, unit in ipairs(prev) do
if IsValid(unit) and not unit:IsDead() and unit.team ~= team and unit.team:IsEnemySide(team) then
if not HasVisibilityTo(team, unit) then
team:OnEnemyLost(unit)
end
end
end
for _, unit in ipairs(g_Visibility[team]) do
if unit.team:IsEnemySide(team) then
if not HasVisibilityTo(team, unit, prev_visibility) then
if unit:HasStatusEffect("Spotted") then
unit:SetEffectValue("Spotted-" .. team.side, true)
else
team:OnEnemySighted(unit)
unit:RevealTo(team)
end
end
end
end
end
end
if prev_visibility then
for _, unit in ipairs(g_Units) do
for _, other in ipairs(g_Visibility[unit]) do
if unit:IsOnEnemySide(other) and not HasVisibilityTo(unit, other, prev_visibility) then
unit:OnEnemySighted(other)
end
end
end
end
Msg("CombatComputedVisibility")
end
local function IsInsideClosedVolume(unit)
local volume = EnumVolumes(unit, "smallest")
local building = volume and volume.building
if building then
local open_floor = VT2TouchedBuildings and VT2TouchedBuildings[building]
return not open_floor or (open_floor ~= volume.floor)
end
end
function IsOnFadedSlab(obj)
local uz
if IsValid(obj) then
uz = select(3, obj:GetPosXYZ())
elseif IsPoint(obj) then
uz = obj:z()
end
local slab = uz and MapGetFirst(obj, const.SlabSizeX/2, "FloorSlab", "RoofSlab", const.efVisible,
function(slab, uz)
local sz = select(3, slab:GetPosXYZ())
if sz and abs(uz - sz) < const.SlabSizeZ / 2 then
local cmt_state = C_CCMT_GetObjCMTState(slab)
if cmt_state == const.cmtHidden or cmt_state == const.cmtFadingOut then
return true
end
end
end, uz)
if slab then
return true
end
return false
end
local CameraObscureSpots = {
-- list of spots to check on different settings
["High"] = {"Head", "Torso", "Elbowl", "Elbowr", "Kneel", "Kneer"},
["Medium"] = {"Head", "Torso", "Elbowl", "Elbowr"},
-- fallback list if none of the above applies
[false] = { "Torso" },
}
function OnMsg.SetObjectDetail(action, params)
if action == "done" then
SetCameraObscureSpots(CameraObscureSpots[EngineOptions.ObjectDetail] or CameraObscureSpots[false])
end
end
function GetCameraObscureSpots()
return CameraObscureSpots[EngineOptions.ObjectDetail] or CameraObscureSpots[false]
end
function ApplyUnitVisibility(active_units, pov_team, visibility, force)
active_units = IsKindOf(active_units, "Unit") and {active_units} or active_units
local observers = g_Combat and {SelectedObj or nil} or (Selection or {})
local full_visibility = IsFullVisibility() or (IsSetpiecePlaying() and g_SetpieceFullVisibility)
local sector = (gv_DeploymentStarted or gv_Deployment) and gv_Sectors[gv_CurrentSectorId]
local pov_team_hidden = sector and sector.enabled_auto_deploy and pov_team.control == "UI"
local is_current_team_pov_team = g_Teams[g_CurrentTeam] == pov_team
local uvVisible = const.uvVisible
local deployment_markers
local camera_visibility_check_list = {}
for i, unit in ipairs(g_Units) do
if not IsValid(unit) then
elseif unit:HasStatusEffect("SetpieceHidden") or unit:HasStatusEffect("ScriptingHidden") or IsValid(unit.death_fx_object) then
unit:SetVisible(false, "force")
unit:SetHighlightReason("visibility", nil)
elseif gv_Deployment and IsMerc(unit) then
elseif full_visibility then
unit:SetVisible(true)
unit:SetHighlightReason("visibility", false)
unit:SetHighlightReason("concealed", false)
unit:SetHighlightReason("obscured", false)
unit:SetHighlightReason("faded", false)
elseif not IsSetpieceActor(unit) then
if IsValid(unit.prepared_attack_obj) then
if unit.team:IsEnemySide(pov_team) then
unit.prepared_attack_obj:SetColorFromTextStyle("PreparedAttackEnemy")
else
unit.prepared_attack_obj:SetColorFromTextStyle("PreparedAttackFriendly")
end
end
if unit.team == pov_team then
if unit.on_die_hit_descr and unit.on_die_hit_descr.death_explosion then
unit:SetVisible(false)
unit:SetHighlightReason("visibility", nil)
else
unit:SetVisible(not pov_team_hidden) --sync state
if IsOnFadedSlab(unit) then --async check!
unit:SetHighlightReason("visibility", true)
unit:SetHighlightReason("faded", true)
else
table.insert(camera_visibility_check_list, unit)
unit:SetHighlightReason("faded", nil)
end
end
elseif unit:IsDead() then
if unit.on_die_hit_descr and unit.on_die_hit_descr.death_explosion then
unit:SetVisible(false, "force")
unit:SetHighlightReason("visibility", nil)
else
unit:SetVisible(true) --sync state
if IsOnFadedSlab(unit) then --async check!
local interaction
for _, au in ipairs(active_units) do
if unit:GetInteractionCombatAction(au) then
interaction = true
break
end
end
if interaction then
unit:SetHighlightReason("visibility", true)
else
unit:SetHighlightReason("visibility", nil)
end
else
unit:SetHighlightReason("visibility", nil)
end
end
-- weather fx
unit:SetHighlightReason("concealed", unit:UIConcealed("skip"))
unit:SetHighlightReason("obscured", unit:UIObscured())
else
local seen_by_player = HasVisibilityTo(pov_team, unit)
-- Ensure that enemies the pov team has visibility to (livewire perk for instance) are
-- not seen if hidden.
if seen_by_player and unit.team and unit.team:IsEnemySide(pov_team) then
seen_by_player = not unit:HasStatusEffect("Hidden")
end
if not seen_by_player then
if deployment_markers == nil then
deployment_markers = (gv_DeploymentStarted or gv_Deployment) and GetAvailableDeploymentMarkers() or empty_table
if #deployment_markers == 0 then deployment_markers = false end
end
if deployment_markers and IsUnitSeenByAnyDeploymentMarker(unit, deployment_markers) then
seen_by_player = true
end
end
if seen_by_player then
-- actually seen by one of the player units
unit:SetVisible(true)
local los_active
local on_faded_slab = IsOnFadedSlab(unit)
if not on_faded_slab then
if is_current_team_pov_team then
for _, observer in ipairs(active_units) do
if VisibilityCheckAll(observer, unit, nil, uvVisible) then
los_active = true
break
end
end
else
-- out of player turn do not use unit-based highlights
los_active = true
end
end
-- weather fx
unit:SetHighlightReason("concealed", unit:UIConcealed("skip"))
unit:SetHighlightReason("obscured", unit:UIObscured())
if on_faded_slab or not los_active then
unit:SetHighlightReason("visibility", true)
else
table.insert(camera_visibility_check_list, unit)
end
unit:SetHighlightReason("faded", on_faded_slab)
elseif unit:HasStatusEffect("DiamondCarrier") then
unit:SetVisible(true)
unit:SetHighlightReason("visibility", true)
else
-- not seen at all
unit:SetVisible(false)
end
end
end
end
if #camera_visibility_check_list > 0 then
local camera_visibility = IsVisibleFromCamera(camera_visibility_check_list)
for i, unit in ipairs(camera_visibility_check_list) do
unit:SetHighlightReason("visibility", not camera_visibility[i])
end
end
end
function Combat:ApplyVisibility(active_unit)
active_unit = active_unit or SelectedObj or self.starting_unit
local pov_team = GetPoVTeam()
local playerControlled = g_Teams[g_CurrentTeam]:IsPlayerControlled() -- if player controlled but no selected obj, apply visibility for all as it is an edge case on turn end/start that causes flicker of object markings
if pov_team ~= g_Teams[g_CurrentTeam] or (playerControlled and not SelectedObj) then -- during the turn of other teams use all units from PoV team to decide visibility
active_unit = pov_team.units
end
ApplyUnitVisibility(active_unit, pov_team, g_Visibility)
NetUpdateHash("CombatApplyVisibility", GameTime())
Msg("CombatApplyVisibility", pov_team)
end
function Combat:ShouldEndDueToNoVisibility()
if Game and Game.game_type == "PvP" then
return
end
-- If the combat was started via script there is no guarantee that it wont
-- end instantly due to no visibility. (Such as H4U)
if gv_CombatStartFromConversation then
return false
end
self.turns_no_visibility = self.turns_no_visibility + #g_Teams
for _, t in ipairs(g_Teams) do
for _, obj in ipairs(g_Visibility[t]) do
if IsKindOf(obj, "Unit") and obj.team and obj.team:IsEnemySide(t) then
self.turns_no_visibility = 0
return false
end
end
end
return self.turns_no_visibility > 2 * #g_Teams
end
MapVar("g_VisibilityUpdateThread", false)
MapVar("g_UnitsLOS", {}, weak_keys_meta)
function InvalidateUnitLOS(unit)
g_UnitsLOS[unit] = nil
for u, los_tbl in pairs(g_UnitsLOS) do
if los_tbl[unit] then
los_tbl[unit] = nil
table.remove_value(los_tbl, unit)
end
end
VisibilityUpdate()
end
function InvalidateVisibility(force)
g_UnitsLOS = setmetatable({}, weak_keys_meta)
VisibilityUpdate(force)
end
MapVar("g_VisiblityUpdatesCount", 0)
MapVar("g_VisiblityUpdatesTime", 0)
MapVar("g_VisiblityUpdatesReportTime", GetPreciseTicks()) -- For debug
MapVar("g_VisibilityUpdateSuspendReasons", {})
MapVar("ReportVisibilityUpdates", false)
-- Visibility in exploration can be a big performance hit,
-- so it is throttled to be invalidated in specific increments.
MapVar("g_VisibilityExplorationTick", false)
MapVar("g_VisibilityExplorationDirty", false)
MapGameTimeRepeat("ExplorationVisibilityUpdate", 500, function()
if not g_VisibilityExplorationDirty then
return
end
g_VisibilityExplorationTick = true
InvalidateVisibility()
g_VisibilityExplorationDirty = false
g_VisibilityExplorationTick = false
end)
function SuspendVisibiltyUpdates(reason)
g_VisibilityUpdateSuspendReasons[reason] = true
end
function ResumeVisibiltyUpdates(reason)
g_VisibilityUpdateSuspendReasons[reason] = nil
if next(g_VisibilityUpdateSuspendReasons) == nil then
VisibilityUpdate()
return true
end
end
function OnMsg.CombatActionStart(unit)
if not g_Combat then return end
SuspendVisibiltyUpdates(unit)
end
function OnMsg.CombatActionEnd(unit)
if not g_Combat then return end
CreateGameTimeThread(function()
if ResumeVisibiltyUpdates(unit) then
WaitMsg("VisibilityUpdate")
end
if g_Combat then
g_Combat:EndCombatCheck()
end
end)
end
local function lExplorationVisibilityApply()
if g_Combat or g_StartingCombat or IsSetpiecePlaying() then
return
end
local prev_visibility = g_Visibility or empty_table
g_Visibility = ComputeUnitsVisibility()
local pov_team = GetPoVTeam()
if not pov_team then return end
local active_units = Selection
if not active_units or (#active_units == 0) then
active_units = SelectedObj
end
ApplyUnitVisibility(active_units, pov_team, g_Visibility)
local sees_enemy
for _, seen in ipairs(g_Visibility[pov_team]) do
if seen.team and pov_team:IsEnemySide(seen.team) and not seen:IsDead() then
if not HasVisibilityTo(pov_team, seen, prev_visibility) then
Msg("EnemySightedExploration", seen)
end
sees_enemy = true
end
end
if g_TestCombat and sees_enemy then
NetSyncEvent("ExplorationStartCombat")
end
end
function VisibilityUpdate(force)
local suspend_reasons_count = 0
for reason, _ in pairs(g_VisibilityUpdateSuspendReasons) do
if IsKindOf(reason, "Unit") and reason:IsDead() then
g_VisibilityUpdateSuspendReasons[reason] = nil
else
suspend_reasons_count = suspend_reasons_count + 1
end
end
NetUpdateHash("VisibilityUpdate()", GameTime(), suspend_reasons_count, force)
if suspend_reasons_count > 0 and not force then
return
end
-- run in a thread to avoid overly aggressive updates
if not IsValidThread(g_VisibilityUpdateThread) then
if not g_Combat then
g_VisibilityExplorationDirty = true
if not g_VisibilityExplorationTick then
return
end
end
g_VisibilityUpdateThread = CreateGameTimeThread(function(force)
local tStart = GetPreciseTicks()
if g_Combat then
if force then
g_Combat.visibility_update_hash = false
end
g_Combat:UpdateVisibility()
g_Combat:ApplyVisibility()
else
lExplorationVisibilityApply()
end
NetUpdateHash("VisibilityUpdate", GameTime(), Visibility_UnitsHash(), Visibility_ResultsHash())
Msg("VisibilityUpdate")
ObjModified("VisibilityUpdate")
g_VisibilityUpdateThread = false
if ReportVisibilityUpdates then
local time_now = GetPreciseTicks()
local update_time = time_now - tStart
g_VisiblityUpdatesCount = g_VisiblityUpdatesCount + 1
g_VisiblityUpdatesTime = g_VisiblityUpdatesTime + update_time
if time_now - g_VisiblityUpdatesReportTime > 1000 then
printf("%d visibility updates in the last second, %d ms spent updating in total, %d ms per call",
g_VisiblityUpdatesCount, g_VisiblityUpdatesTime, g_VisiblityUpdatesTime / g_VisiblityUpdatesCount)
g_VisiblityUpdatesCount = 0
g_VisiblityUpdatesTime = 0
g_VisiblityUpdatesReportTime = time_now
end
end
end, force)
end
end
function NetSyncEvents.RecalcVisibility()
WaitRecalcVisibility = false
if g_Combat then
g_Combat:UpdateVisibility()
g_Combat:ApplyVisibility()
end
end
MapVar("WaitRecalcVisibility", false) --flag used to stop flickering of objects on selection change by early out in UpdateHighlightMarking
function OnMsg.SelectionChange()
if g_Combat and g_Combat.combat_started then
WaitRecalcVisibility = true
NetSyncEvent("RecalcVisibility")
end
end
function OnMsg.UnitMovementDone(unit)
-- update .indoors first so ApplyVisibility -> Obscured/ConcealedCheck work on correct data
UpdateIndoors(unit)
InvalidateUnitLOS(unit)
end
OnMsg.CombatGotoStep = InvalidateUnitLOS
OnMsg.UnitStanceChanged = InvalidateUnitLOS
function OnMsg.UnitDieStart(...)
if g_Combat then
g_Combat.visibility_update_hash = false
end
InvalidateUnitLOS(...)
-- Used by the "FullVisibility" cheat. We dont want dead units in visibility
g_VisibilityUpdated = false
end
function OnMsg.LoadSessionData()
CreateGameTimeThread(function()
g_VisibilityExplorationTick = true
InvalidateVisibility("force")
g_VisibilityExplorationTick = false
end)
end
function OnMsg.OnPassabilityChanged()
-- Doors opening etc
if IsEditorActive() then return end
InvalidateVisibility("force")
end
OnMsg.OverwatchChanged = function() NetUpdateHash("VU_OverwatchChanged"); VisibilityUpdate(); end
OnMsg.GameExitEditor = InvalidateVisibility
OnMsg.UnitAwarenessChanged = InvalidateUnitLOS
OnMsg.UnitStealthChanged = InvalidateVisibility
OnMsg.GroupChangeSide = function() NetUpdateHash("VU_GroupChangeSide"); VisibilityUpdate(); end
OnMsg.DiplomacyInvalidated = function() NetUpdateHash("VU_DiplomacyInvalidated"); VisibilityUpdate(); end
function OnMsg.TurnStart(team)
team = g_Teams[team]
-- clear Revealed for our units
for _, unit in ipairs(team.units) do
for _, list in pairs(g_RevealedUnits) do
table.remove_value(list, unit)
end
unit:RemoveStatusEffect("Revealed")
end
-- mark all visible units as revealed until the end of their turn to make sure they don't disappear or gain Hidden passively
local units = g_Visibility[team]
g_RevealedUnits[team] = g_RevealedUnits[team] or {}
for _, unit in ipairs(units) do
if team:IsEnemySide(unit.team) then
assert(not unit:HasStatusEffect("Hidden"))
unit:RevealTo(team)
end
end
end
function OnMsg.CombatEnd(combat)
g_RevealedUnits = {}
end
function ReapplyUnitVisibility(force)
local pov_team = GetPoVTeam()
if not pov_team then return end
local active_units = Selection
if not active_units or (#active_units == 0) then
active_units = SelectedObj or pov_team.units
end
ApplyUnitVisibility(active_units, pov_team, g_Visibility, force)
end
function OnMsg.WallVisibilityChanged()
NetSyncEvent("ReapplyUnitVisibility")
end
function OnMsg.SetObjectDetail(stage)
if stage == "done" then
NetSyncEvent("ReapplyUnitVisibility")
end
end
local last_camera_hash = 0
MapRealTimeRepeat("unit_visibility", 250, function()
if not cameraTac.IsActive() then return end
local eye, lookat = cameraTac.GetPosLookAt()
local hash = xxhash(GetMap(), eye, lookat)
if hash ~= last_camera_hash then
NetSyncEvent("ReapplyUnitVisibility")
last_camera_hash = hash
end
end)
function NetSyncEvents.ReapplyUnitVisibility()
ReapplyUnitVisibility()
end
function OnMsg.EntitiesLoaded()
SetupEntityObstructionMasks()
end
AppendClass.EntitySpecProperties = {
properties = {
{ id = "obstruction", name = "Blocks line of sight", editor = "bool", category = "Misc", default = false, entitydata = true, },
{ id = "provide_cover", name = "Provide cover", editor = "bool", default = true, entitydata = true, },
},
}
function SetupEntityObstructionMasks()
local obstruction_entities = {}
local cover_entities = {}
local materials = Presets.ObjMaterial.Default
for k in pairs(GetAllEntities()) do
local t = EntityData[k]
if t then
local entity = t.entity
local material = entity and materials[entity.material_type]
if t.editor_category == "Slab" and t.editor_subcategory ~= "Window" and t.editor_subcategory ~= "Door" or
t.editor_category == "Rock" or
entity and entity.obstruction or
material and material.impenetrable or
k:find("WallExt") or
k:find("WallInt") or
k:find("Floor") or
k:find("Roof") or
k:find("Stairs") or
k:find("Vehicle") or
k:find("WaterPlane")
then
obstruction_entities[#obstruction_entities+1] = k
end
if entity and entity.provide_cover ~= false then
cover_entities[#cover_entities + 1] = k
end
end
end
SetEntityObstructionMasks(obstruction_entities, cover_entities)
end