|
GameVar("gv_Deployment", false) |
|
GameVar("gv_DeploymentStarted", false) |
|
GameVar("gv_DeploymentDir", false) |
|
MapVar("gv_Redeployment", false) |
|
MapVar("RedeploymentThread", false) |
|
|
|
DefineClass.DeploymentMarker = { |
|
__parents = {"GridMarker"}, |
|
|
|
properties = { |
|
{ category = "Grid Marker", id = "Type", name = "Type", editor = "dropdownlist", items = {"DeployArea"}, default = "DeployArea", no_edit = true }, |
|
{ category = "Marker", id = "Reachable", name = "Reachable only", editor = "bool", default = false, help = "Area of marker includes only tiles reachable from marker position, not the entire rectangle"}, |
|
{ category = "Marker", id = "GroundVisuals", name = "Ground Visuals", editor = "bool", default = true, help = "Show ground mesh on the marker area"}, |
|
{ category = "Trigger Logic", id = "Trigger", name = "Trigger", editor = "dropdownlist", items = { "always", }, default = "always", no_edit = true}, |
|
{ category = "Trigger Logic", id = "TriggerEffects", name = "Effects", editor = "nested_list", base_class = "Effect", default = false, no_edit = true}, |
|
{ category = "Deployment", id = "AlternateEntrance", name = "Alternate Entrance", editor = "bool", default = false}, |
|
}, |
|
} |
|
|
|
function DeploymentMarker:Init() |
|
self:UpdateVisuals(self.Type, true) |
|
end |
|
|
|
function DeploymentMarker:TriggerThreadProc() |
|
end |
|
|
|
function DeploymentMarker:IsAreaVisible() |
|
return IsEditorActive() or gv_DeploymentStarted and self:IsMarkerEnabled() |
|
end |
|
|
|
local deploy_types = {"Entrance", "Defender", "DefenderPriority", "DeployArea"} |
|
function IsDeployMarker(marker) |
|
return not not table.find(deploy_types, marker.Type) |
|
end |
|
|
|
function GetAvailableEntranceMarkers(arrival_dir) |
|
|
|
|
|
local markers |
|
if gv_Deployment ~= "custom" then |
|
markers = MapGetMarkers("Entrance", g_GoingAboveground and "Underground" or arrival_dir, function(marker) |
|
return marker:IsMarkerEnabled() |
|
end) |
|
else |
|
markers = {} |
|
end |
|
if not g_GoingAboveground then |
|
markers = markers or {} |
|
|
|
|
|
local additionalEntrances = MapGetMarkers("DeployArea", arrival_dir, function(marker) |
|
return marker:IsMarkerEnabled() |
|
end) |
|
table.iappend(markers, additionalEntrances) |
|
|
|
MapForEach("map", "DeploymentMarker", function (marker, markers) |
|
if not marker.AlternateEntrance and marker:IsMarkerEnabled() then |
|
markers[#markers + 1] = marker |
|
end |
|
end, markers) |
|
end |
|
|
|
return markers |
|
end |
|
|
|
function GetAvailableDeploymentMarkers(some_unit) |
|
local markers = {} |
|
some_unit = some_unit or SelectedObj |
|
if gv_Deployment == "defend" then |
|
markers = MapGetMarkers("Defender", false, function(m) return m:IsMarkerEnabled() end) |
|
local player_side = NetPlayerSide() |
|
local non_blocked_markers = {} |
|
for _, marker in ipairs(markers) do |
|
local area = marker:GetAreaBox() |
|
local blocked = false |
|
for _, unit in ipairs(g_Units) do |
|
if SideIsEnemy(player_side, unit.team.side) and not unit:IsDead() and area:Point2DInside(unit) then |
|
blocked = true |
|
break |
|
end |
|
end |
|
if not blocked then |
|
table.insert(non_blocked_markers, marker) |
|
end |
|
end |
|
|
|
return #non_blocked_markers > 0 and non_blocked_markers or {markers[1]} |
|
elseif some_unit then |
|
markers = GetAvailableEntranceMarkers(some_unit.arrival_dir) |
|
end |
|
return markers |
|
end |
|
|
|
function GetEnemyDeploymentMarkers() |
|
local markers = {} |
|
local _, enemy_squads = GetSquadsInSector(gv_CurrentSectorId) |
|
for _, squad in ipairs(enemy_squads) do |
|
local dir = squad.units and squad.units[1] and gv_UnitData[squad.units[1]] and gv_UnitData[squad.units[1]].arrival_dir |
|
if dir then |
|
local available = GetAvailableEntranceMarkers(dir) |
|
for _, marker in ipairs(available) do |
|
table.insert_unique(markers, marker) |
|
end |
|
end |
|
end |
|
return markers |
|
end |
|
|
|
function UpdateAvailableDeploymentMarkers() |
|
if gv_DeploymentStarted then |
|
local enemy_markers = GetEnemyDeploymentMarkers() |
|
local available = GetAvailableDeploymentMarkers() |
|
MapForEachMarker("GridMarker", nil, function(marker) |
|
if IsDeployMarker(marker) then |
|
if not table.find_value(available, marker) then |
|
marker:HideArea() |
|
DeleteBadgesFromTargetOfPreset("DeploymentAreaBadge", marker) |
|
else |
|
if not TargetHasBadgeOfPreset("DeploymentAreaBadge", marker) then |
|
CreateBadgeFromPreset("DeploymentAreaBadge", marker) |
|
end |
|
if not marker:IsAreaShown() then |
|
marker:ShowArea() |
|
end |
|
end |
|
if gv_Deployment == "defend" then |
|
if not table.find_value(enemy_markers, marker) then |
|
DeleteBadgesFromTargetOfPreset("EnemyDeploymentAreaBadge", marker) |
|
else |
|
if not TargetHasBadgeOfPreset("EnemyDeploymentAreaBadge", marker) then |
|
CreateBadgeFromPreset("EnemyDeploymentAreaBadge", marker) |
|
end |
|
end |
|
end |
|
end |
|
end) |
|
else |
|
UpdateEntranceAreasVisibility() |
|
MapForEachMarker("GridMarker", nil, function(marker) |
|
if marker.Type == "DeployArea" then |
|
marker:HideArea() |
|
end |
|
DeleteBadgesFromTargetOfPreset("DeploymentAreaBadge", marker) |
|
DeleteBadgesFromTargetOfPreset("EnemyDeploymentAreaBadge", marker) |
|
end) |
|
end |
|
end |
|
|
|
function IsFirstSquadDeployment(squad_id) |
|
local team = GetCurrentTeam() |
|
if team then |
|
for i, u in ipairs(team.units) do |
|
if (not squad_id or u.Squad == squad_id) and u:IsLocalPlayerControlled() and IsUnitDeployed(u) then |
|
return false |
|
end |
|
end |
|
end |
|
return true |
|
end |
|
|
|
function IsDeploymentReady() |
|
local team = GetCurrentTeam() |
|
if team then |
|
for i, u in ipairs(team.units) do |
|
if not IsUnitDeployed(u) then |
|
return false |
|
end |
|
end |
|
end |
|
return true |
|
end |
|
|
|
function GetCurrentDeploymentSquadUnits(local_player_controlled_only) |
|
local units = {} |
|
local currentSquad = gv_Squads[g_CurrentSquad] |
|
for i, session_id in ipairs(currentSquad.units) do |
|
local u = g_Units[session_id] |
|
if not local_player_controlled_only or u:IsLocalPlayerControlled() then |
|
units[#units + 1] = u |
|
end |
|
end |
|
return units |
|
end |
|
|
|
if FirstLoad then |
|
DeployButtonVisible = true |
|
end |
|
|
|
function HideDeployButton() |
|
DeployButtonVisible = false |
|
end |
|
|
|
function ShowDeployButton() |
|
DeployButtonVisible = true |
|
end |
|
|
|
function ShouldHideDeployButton() |
|
return not DeployButtonVisible |
|
end |
|
|
|
function ShowUnitsOnDeployment(bShow, bLclPlayer) |
|
if bShow then |
|
local igi = GetInGameInterfaceModeDlg() |
|
if not IsKindOf(igi, "IModeDeployment") or not igi.units_deployed then |
|
igi = false |
|
end |
|
|
|
for _, t in ipairs(g_Teams) do |
|
if t.side == "player1" or t.side == "player2" then |
|
for i, unit in ipairs(t.units) do |
|
if bLclPlayer == unit:IsLocalPlayerControlled() then |
|
unit:SetVisible(true) |
|
|
|
|
|
if igi then |
|
igi.units_deployed[unit] = true |
|
igi.cursor_voxel = false |
|
end |
|
end |
|
end |
|
end |
|
end |
|
else |
|
for _, t in ipairs(g_Teams) do |
|
if t.side == "player1" or t.side == "player2" or t.side == "enemy1" or t.side == "enemy2" then |
|
for i, unit in ipairs(t.units) do |
|
unit:SetVisible(false) |
|
end |
|
end |
|
end |
|
end |
|
ObjModified("DeployUpdated") |
|
ObjModified("UpdateTacticalNotification") |
|
end |
|
|
|
function SkipDeployment(mode) |
|
if gv_Deployment then |
|
return false |
|
elseif not mode then |
|
return true |
|
end |
|
if g_TestCombat and g_TestCombat.skip_deployment then |
|
return true |
|
end |
|
|
|
|
|
|
|
|
|
|
|
local currentSector = gv_Sectors[gv_CurrentSectorId] |
|
local conflict = IsConflictMode(gv_CurrentSectorId) |
|
|
|
if not currentSector.enabled_auto_deploy or not conflict then |
|
return true |
|
end |
|
|
|
if g_GoingAboveground then |
|
return true |
|
end |
|
|
|
return false |
|
end |
|
|
|
function SetDeploymentMode(deploy) |
|
local defend_mode = deploy == "defend" or not deploy and gv_Deployment == "defend" |
|
gv_Deployment = deploy |
|
deploy = not not deploy |
|
local update_visuals = {} |
|
if defend_mode then |
|
if deploy then |
|
MapForEachMarker("GridMarker", nil, function(marker) |
|
if (marker.Type == "Defender" or marker.Type == "DefenderPriority") and marker:IsMarkerEnabled() then |
|
table.insert_unique(g_InteractableAreaMarkers, marker) |
|
update_visuals[#update_visuals + 1] = marker |
|
end |
|
end) |
|
else |
|
MapForEachMarker("GridMarker", nil, function(marker) |
|
if (marker.Type == "Defender" or marker.Type == "DefenderPriority") and marker:IsMarkerEnabled() then |
|
marker:RemoveFloatTxt() |
|
table.remove_value(g_InteractableAreaMarkers, marker) |
|
update_visuals[#update_visuals + 1] = marker |
|
end |
|
end) |
|
end |
|
|
|
else |
|
update_visuals = MapGetMarkers("Entrance", g_GoingAboveground and "Underground" or nil) |
|
table.iappend(update_visuals, MapGetMarkers("DeployArea")) |
|
if deploy then |
|
if not g_GoingAboveground then |
|
MapForEachMarker("GridMarker", nil, function(marker) |
|
if marker:IsKindOf("DeploymentMarker") then |
|
table.insert_unique(g_InteractableAreaMarkers, marker) |
|
end |
|
end) |
|
end |
|
else |
|
if not g_GoingAboveground then |
|
MapForEachMarker("GridMarker", nil, function(marker) |
|
if marker:IsKindOf("DeploymentMarker") then |
|
marker:RemoveFloatTxt() |
|
table.remove_value(g_InteractableAreaMarkers, marker) |
|
end |
|
end) |
|
end |
|
end |
|
end |
|
for _, marker in ipairs(update_visuals) do |
|
marker.Reachable = true |
|
if marker.area_ground_mesh then |
|
marker.area_ground_mesh:UpdateState() |
|
end |
|
marker:UpdateVisuals(deploy and "DeployArea" or marker.Type, "force") |
|
marker:RecalcAreaPositions() |
|
if not marker:IsAreaVisible() then |
|
marker:HideArea() |
|
end |
|
end |
|
if not deploy then |
|
UpdateAvailableDeploymentMarkers() |
|
HideTacticalNotification("deployMode") |
|
Msg("DeploymentModeDone") |
|
end |
|
Msg("DeploymentModeSet", deploy) |
|
end |
|
|
|
function TFormat.DeployModeNotif(context_obj) |
|
local non_deployed = 0 |
|
local non_deployed_lcl_player = 0 |
|
local deployed = 0 |
|
local team = GetCurrentTeam() |
|
local totalUnits = 0 |
|
if team then |
|
for i, u in ipairs(team.units) do |
|
if not IsUnitDeployed(u) then |
|
non_deployed = non_deployed + 1 |
|
if u:IsLocalPlayerControlled() then |
|
non_deployed_lcl_player = non_deployed_lcl_player + 1 |
|
end |
|
else |
|
deployed = deployed + 1 |
|
end |
|
end |
|
totalUnits = #team.units |
|
end |
|
local sector = gv_Sectors[gv_CurrentSectorId] |
|
if non_deployed_lcl_player <= 0 and non_deployed > 0 then |
|
local other_player_info = GetOtherNetPlayerInfo() |
|
return T{616675804493, "<other_player> Deploying", other_player = Untranslated(other_player_info and other_player_info.name or "N/A")} |
|
else |
|
return T{673244787391, "Deploy Merc(s) (<deployed>/<total>)", deployed = deployed, total = totalUnits} |
|
end |
|
end |
|
|
|
function TFormat.IntelForSector(context_obj) |
|
local sector = gv_Sectors[gv_CurrentSectorId] |
|
if sector and sector.Intel and not sector.intel_discovered then |
|
return T(244800584080, "No Intel for this sector") |
|
end |
|
return false |
|
end |
|
|
|
function GetDeploymentAreaRollover(marker) |
|
if marker.DeployRolloverText ~= "" then |
|
return marker.DeployRolloverText |
|
end |
|
|
|
if marker.Type == "Entrance" then |
|
if marker:IsInGroup("North") then |
|
return T(147747736813, "North Deployment Zone") |
|
elseif marker:IsInGroup("South") then |
|
return T(565574703512, "South Deployment Zone") |
|
elseif marker:IsInGroup("East") then |
|
return T(189571269539, "East Deployment Zone") |
|
elseif marker:IsInGroup("West") then |
|
return T(998300938139, "West Deployment Zone") |
|
end |
|
end |
|
return T(419061570457, "Deployment Area") |
|
end |
|
|
|
OnMsg.CustomInteractableEffectsDone = UpdateAvailableDeploymentMarkers |
|
|
|
function IsUnitSeenByAnyDeploymentMarker(unit, markers) |
|
markers = markers or GetAvailableDeploymentMarkers() |
|
|
|
|
|
|
|
local unitSightRadius = unit:GetSightRadius() |
|
local ux, uy = unit:GetPosXYZ() |
|
local half_slabsize = const.SlabSizeX / 2 |
|
for i, m in ipairs(markers) do |
|
local mx, my = m:GetPosXYZ() |
|
local distX = abs(ux - mx) |
|
local markerWidth = m.AreaWidth * half_slabsize |
|
local dx = distX - markerWidth |
|
if dx <= unitSightRadius then |
|
local distY = abs(uy - my) |
|
local markerHeight = m.AreaHeight * half_slabsize |
|
local dy = distY - markerHeight |
|
if dy <= unitSightRadius then |
|
if distX <= markerWidth / 2 or distY <= markerHeight / 2 then |
|
return true |
|
end |
|
|
|
if dx * dx + dy * dy <= unitSightRadius * unitSightRadius then |
|
return true |
|
end |
|
end |
|
end |
|
end |
|
return false |
|
end |
|
|
|
function IsStuckedMercPos(unit, pos, pfclass, destinations) |
|
if not pfclass then |
|
pfclass = CalcPFClass("player1") |
|
end |
|
if not destinations then |
|
destinations = {} |
|
local markers = GetAvailableEntranceMarkers(unit.arrival_dir) |
|
for i, marker in ipairs(markers) do |
|
local pos = GetPassSlab(marker) |
|
if pos then |
|
table.insert(destinations, pos) |
|
end |
|
end |
|
end |
|
if #destinations == 0 then |
|
return false |
|
end |
|
local has_path, closest_pos = pf.HasPosPath(pos, destinations, pfclass) |
|
if has_path and table.find(destinations, closest_pos) then |
|
return false |
|
end |
|
return true |
|
end |
|
|
|
function HasStuckedMercs() |
|
local destinations, dummy |
|
local pfclass = CalcPFClass("player1") |
|
for _, t in ipairs(g_Teams) do |
|
if t.side == "player1" or t.side == "player2" then |
|
for i, unit in ipairs(t.units) do |
|
if unit:IsLocalPlayerControlled() and unit:IsValidPos() and not unit:IsDead() then |
|
if not destinations then |
|
local markers = GetAvailableEntranceMarkers(unit.arrival_dir) |
|
if not markers then |
|
return |
|
end |
|
destinations = {} |
|
for i, marker in ipairs(markers) do |
|
local pos = GetPassSlab(marker) |
|
if pos then |
|
table.insert(destinations, pos) |
|
end |
|
end |
|
end |
|
if #destinations == 0 then |
|
return |
|
end |
|
|
|
local start_pos = unit.traverse_tunnel and unit.traverse_tunnel:GetExit() or GetPassSlab(unit) or unit:GetPos() |
|
local pfflags = const.pfmImpassableSource |
|
local has_path, closest_pos = pf.HasPosPath(start_pos, destinations, pfclass, 0, 0, nil, 0, nil, pfflags) |
|
if not has_path or not table.find(destinations, closest_pos) then |
|
return true |
|
end |
|
end |
|
end |
|
end |
|
end |
|
DoneObject(dummy) |
|
return false |
|
end |
|
|
|
function RedeploymentCheck() |
|
local redeploy = false |
|
if mapdata.GameLogic and Game and not g_Combat then |
|
if HasStuckedMercs() then |
|
redeploy = true |
|
end |
|
end |
|
gv_Redeployment = redeploy |
|
ObjModified("gv_Redeployment") |
|
end |
|
|
|
function RedeploymentCheckDelayed() |
|
if not mapdata.GameLogic or not Game then |
|
return |
|
elseif g_Combat then |
|
return |
|
elseif IsValidThread(RedeploymentThread) then |
|
return |
|
elseif GameState.disable_redeploy_check then |
|
return |
|
end |
|
RedeploymentThread = CreateGameTimeThread(function() |
|
Sleep(2000) |
|
RedeploymentCheck() |
|
end) |
|
end |
|
|
|
OnMsg.OnPassabilityChanged = RedeploymentCheckDelayed |
|
OnMsg.CombatEnd = RedeploymentCheckDelayed |
|
|