Download Lua/Tactical/Visibility.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 44 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/Visibility.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/Lua/Tactical/Visibility.lua
-
curl -L -o Visibility.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/Visibility.lua
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 | |