|
DefineClass.ExitZoneInteractable = { |
|
__parents = { "EditorVisibleObject", "Interactable", "Object", "GridMarker" }, |
|
properties = { |
|
{ category = "Enabled Logic", editor = "bool", id = "HideVisualWhenDisabled", default = false }, |
|
{ category = "Travel", editor = "combo", id = "SectorOverride", items = function() return GetCampaignSectorsCombo() end, default = false }, |
|
{ category = "Travel", editor = "bool", id = "IsUnderground", name = "IsUndergroundExit", default = false }, |
|
{ category = "Travel", editor = "bool", id = "RetreatInConflictOnlyIfCameFromHere", default = false }, |
|
{ category = "Travel", editor = "choice", name = "Entity", id = "entity", items = function() return table.get(Presets, "EntityVariation", "Default", "TravelObject", "Entities") or { "UITravelObject_01" } end, default = "UITravelObject_01" }, |
|
}, |
|
|
|
|
|
AreaWidth = 5, |
|
AreaHeight = 5, |
|
|
|
BadgePosition = "average", |
|
Type = "ExitZoneInteractable", |
|
|
|
fake_visual_obj = false, |
|
discovered = true |
|
} |
|
|
|
function ExitZoneInteractable:InitFakeVO() |
|
if not self.fake_visual_obj then |
|
local obj = PlaceObject(self.entity) |
|
obj:SetColorizationPalette(g_DefaultColorsPalette) |
|
obj:SetEnumFlags(const.efSelectable) |
|
obj:ClearGameFlags(const.gofPermanent) |
|
obj:SetCollision(false) |
|
obj.spawner = self |
|
self.fake_visual_obj = obj |
|
if self.visuals_cache then |
|
self.visuals_cache[#self.visuals_cache + 1] = self.fake_visual_obj |
|
end |
|
end |
|
end |
|
|
|
function ExitZoneInteractable:GameInit() |
|
self:InitFakeVO() |
|
self:EvaluateNeedForFakeVisual() |
|
end |
|
|
|
function ExitZoneInteractable:Done() |
|
if IsValid(self.fake_visual_obj) then |
|
DoneObject(self.fake_visual_obj) |
|
self.fake_visual_obj = false |
|
end |
|
end |
|
|
|
function ExitZoneInteractable:Setentity(value) |
|
self:ChangeEntity(value) |
|
if IsValid(self.fake_visual_obj) then |
|
self.fake_visual_obj:ChangeEntity(value) |
|
self.fake_visual_obj:SetColorizationPalette(g_DefaultColorsPalette) |
|
end |
|
self:SetColorizationPalette(g_DefaultColorsPalette) |
|
self.entity = value |
|
end |
|
|
|
function ExitZoneInteractable:PopulateVisualCache() |
|
Interactable.PopulateVisualCache(self) |
|
table.remove_value(self.visuals_cache, self) |
|
|
|
local visuals = self.visuals_cache |
|
for i, obj in ipairs(visuals) do |
|
if IsKindOf(obj, "Interactable") then |
|
obj.visual_of_interactable = self |
|
end |
|
end |
|
|
|
|
|
|
|
|
|
end |
|
|
|
local function lUpdateVisualsOfExitZoneInteractables() |
|
FireNetSyncEventOnHost("UpdateVisualsOfExitZoneInteractables") |
|
end |
|
|
|
function NetSyncEvents.UpdateVisualsOfExitZoneInteractables() |
|
local sector = gv_Sectors[gv_CurrentSectorId] |
|
local inConflict = sector and sector.conflict |
|
|
|
MapForEach("map", "ExitZoneInteractable", function(o) |
|
o:EvaluateNeedForFakeVisual() |
|
|
|
if o.RetreatInConflictOnlyIfCameFromHere and inConflict then return end |
|
|
|
if o:IsMarkerEnabled() then |
|
|
|
local nextSector = o:GetNextSector() |
|
if nextSector then |
|
nextSector.discovered = true |
|
end |
|
end |
|
end) |
|
end |
|
|
|
OnMsg.ExplorationStart = lUpdateVisualsOfExitZoneInteractables |
|
OnMsg.DeploymentStarted = lUpdateVisualsOfExitZoneInteractables |
|
OnMsg.CombatEnd = lUpdateVisualsOfExitZoneInteractables |
|
|
|
function ExitZoneInteractable:EvaluateNeedForFakeVisual() |
|
self:InitFakeVO() |
|
|
|
local visualObjects = ResolveInteractableVisualObjects(self) |
|
local shouldHave |
|
if #visualObjects == 0 then |
|
shouldHave = true |
|
elseif #visualObjects <= 2 then |
|
local o1, o2 = visualObjects[1], visualObjects[2] |
|
if (not o1 or o1 == self or o1 == self.fake_visual_obj) and |
|
(not o2 or o2 == self or o2 == self.fake_visual_obj) then |
|
shouldHave = true |
|
end |
|
end |
|
|
|
if shouldHave and self.HideVisualWhenDisabled and not self:IsMarkerEnabled() then |
|
shouldHave = false |
|
end |
|
|
|
local nextSector = self:GetNextSector() |
|
if not nextSector then |
|
shouldHave = false |
|
end |
|
|
|
shouldHave = shouldHave or IsEditorActive() |
|
if shouldHave then |
|
if self.visuals_cache and not table.find(self.visuals_cache, self.fake_visual_obj) then |
|
self.visuals_cache[#self.visuals_cache + 1] = self.fake_visual_obj |
|
end |
|
self.fake_visual_obj:SetEnumFlags(const.efVisible) |
|
self.fake_visual_obj:SetCollision(true) |
|
else |
|
if self.visuals_cache then |
|
table.remove_value(self.visuals_cache, self.fake_visual_obj) |
|
end |
|
self.fake_visual_obj:ClearEnumFlags(const.efVisible) |
|
self.fake_visual_obj:SetCollision(false) |
|
end |
|
|
|
local dirs = { |
|
point(const.SlabSizeX, 0, 0), |
|
point(const.SlabSizeX, 0, 0), |
|
point(0, const.SlabSizeY, 0), |
|
point(0, -const.SlabSizeY, 0) |
|
} |
|
|
|
local spotForFakeInteractable = false |
|
for i, d in ipairs(dirs) do |
|
local voxel = self:GetPos() + d |
|
local bbox = GetVoxelBBox(voxel, false, true) |
|
local boxHasZ = bbox:minz() |
|
local any = MapGetFirst(bbox, "GridMarker", function(obj) |
|
if not boxHasZ then return true end |
|
|
|
local x, y, z = obj:GetPosXYZ() |
|
if not z then z = terrain.GetHeight(x, y) end |
|
|
|
return bbox:PointInside(x, y, z) |
|
end) |
|
if not any then |
|
spotForFakeInteractable = voxel |
|
break |
|
end |
|
end |
|
|
|
self.fake_visual_obj:SetPos(spotForFakeInteractable) |
|
self.fake_visual_obj:SetAngle(self:GetAngle()) |
|
end |
|
|
|
function ExitZoneInteractable:EditorEnter() |
|
EditorVisibleObject.EditorEnter(self) |
|
self:EvaluateNeedForFakeVisual() |
|
end |
|
|
|
function ExitZoneInteractable:EditorExit() |
|
EditorVisibleObject.EditorExit(self) |
|
Interactable.EditorExit(self) |
|
self:EvaluateNeedForFakeVisual() |
|
end |
|
|
|
function ExitZoneInteractable:EditorCallbackMove() |
|
self:EvaluateNeedForFakeVisual() |
|
end |
|
|
|
function ExitZoneInteractable:EditorCallbackRotate() |
|
self:EvaluateNeedForFakeVisual() |
|
end |
|
|
|
function ExitZoneInteractable:EditorCallbackPlace() |
|
|
|
end |
|
|
|
function ExitZoneInteractable:GetNextSector() |
|
if not gv_CurrentSectorId then return false end |
|
|
|
local sectorId, underground = false, false |
|
if self:IsUndergroundExit() then |
|
sectorId = gv_Sectors[gv_CurrentSectorId].GroundSector or (gv_CurrentSectorId .. "_Underground") |
|
underground = self.Groups[1] |
|
else |
|
local selfIsUnderground = not not gv_Sectors[gv_CurrentSectorId].GroundSector |
|
|
|
for _, dir in ipairs(const.WorldDirections) do |
|
if self:IsInGroup(dir) then |
|
local neighSectorId = GetNeighborSector(gv_CurrentSectorId, dir) |
|
|
|
|
|
|
|
if selfIsUnderground and not IsSectorUnderground(neighSectorId) then |
|
neighSectorId = false |
|
end |
|
|
|
if neighSectorId and |
|
not IsTravelBlocked(gv_CurrentSectorId, neighSectorId) and |
|
not GetDirectionProperty(neighSectorId, gv_CurrentSectorId, "BlockTravelRiver") and |
|
gv_Sectors[neighSectorId].Map then |
|
|
|
sectorId = neighSectorId |
|
break |
|
end |
|
end |
|
end |
|
end |
|
|
|
if self.SectorOverride then |
|
sectorId = self.SectorOverride |
|
end |
|
|
|
return gv_Sectors[sectorId], underground |
|
end |
|
|
|
function ExitZoneInteractable:IsUndergroundExit() |
|
return self:IsInGroup("Underground") or self.IsUnderground |
|
end |
|
|
|
function ExitZoneInteractable:BadgeTextUpdate() |
|
local withCursor = table.find(self.highlight_reasons, "cursor") |
|
local badgeInstance = self.interactable_badge |
|
if not badgeInstance or badgeInstance.ui.window_state == "destroying" then return end |
|
|
|
if IsUnitPartOfAnyActiveBanter(self) then |
|
badgeInstance.ui.idText:SetVisible(false) |
|
return |
|
end |
|
|
|
local unit = UIFindInteractWith(self) |
|
if unit then |
|
local currentSect = gv_Sectors[gv_CurrentSectorId] |
|
if not currentSect then return end |
|
|
|
local nextSect, underground = self:GetNextSector() |
|
local nextMapName = nextSect and GetSectorText(nextSect) |
|
local action = self:GetInteractionCombatAction(unit) |
|
badgeInstance.ui.idText:SetContext(unit) |
|
|
|
local text = action:GetActionDisplayName({unit, self}) |
|
if g_TestExploration then |
|
text = Untranslated("Cant Travel in Exploration Test") |
|
elseif underground then |
|
if IsSectorUnderground(gv_CurrentSectorId) then |
|
text = T(705526346094, "Exit") |
|
else |
|
text = T(749506366915, "Go Underground") |
|
end |
|
elseif currentSect.conflict then |
|
text = T(482029101969, "Retreat To <Map>") |
|
else |
|
text = T(500843659226, "Exit To <Map>") |
|
end |
|
|
|
badgeInstance.ui.idText:SetText(T{text, Map = nextMapName}) |
|
end |
|
badgeInstance.ui.idText:SetVisible(withCursor) |
|
end |
|
|
|
function ExitZoneInteractable:GetInteractionCombatAction(unit) |
|
if not self:IsMarkerEnabled() then return false end |
|
if gv_DeploymentStarted then return false end |
|
|
|
if self.RetreatInConflictOnlyIfCameFromHere then |
|
local group = self.Groups |
|
group = group and group[1] |
|
|
|
local sector = gv_Sectors[gv_CurrentSectorId] |
|
if sector and sector.conflict and unit and unit.arrival_dir ~= group then |
|
return false |
|
end |
|
end |
|
|
|
local sector = gv_Sectors[gv_CurrentSectorId] |
|
if sector and sector.conflict and sector.conflict.disable_travel then return false end |
|
|
|
local nextSector = self:GetNextSector() |
|
if not nextSector then |
|
return false |
|
end |
|
|
|
if unit and (unit:IsDowned() or unit:IsDead()) then |
|
return false |
|
end |
|
|
|
return CombatActions.Interact_Exit |
|
end |
|
|
|
MapVar("g_RetreatThread", false) |
|
|
|
function ExitZoneInteractable:UnitLeaveSector(unit) |
|
if IsValidThread(g_RetreatThread) then return end |
|
g_RetreatThread = CreateRealTimeThread(ExitZoneInteractable.UnitLeaveSectorInternal, self, unit) |
|
end |
|
|
|
function ExitZoneInteractable:IsUnitInside(u) |
|
local entranceMarker = MapGetMarkers("Entrance", self.Groups and self.Groups[1]) |
|
entranceMarker = entranceMarker and entranceMarker[1] or self |
|
return self:IsInsideArea(u) or entranceMarker:IsInsideArea(u) |
|
end |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function ExitZoneInteractable:UnitLeaveSectorInternal(unit) |
|
if not unit:CanBeControlled() then return end |
|
|
|
local sector, underground = self:GetNextSector() |
|
if not sector then return end |
|
local sector_id = sector.Id |
|
|
|
local playerSquads = GetSquadsOnMap() |
|
local leavingSquads = {} |
|
for i, squadId in ipairs(playerSquads) do |
|
local squad = gv_Squads[squadId] |
|
if not squad then goto continue end |
|
|
|
|
|
local thisSquadHasLeavingUnit = false |
|
for _, id in ipairs(squad.units or empty_table) do |
|
local u = g_Units[id] |
|
if u and u:IsLocalPlayerControlled() and self:IsUnitInside(u) and self:IsMarkerEnabled() then |
|
thisSquadHasLeavingUnit = true |
|
break |
|
end |
|
end |
|
if thisSquadHasLeavingUnit then |
|
leavingSquads[#leavingSquads + 1] = squadId |
|
end |
|
|
|
::continue:: |
|
end |
|
|
|
local leavingUnits = {} |
|
for i, squadId in ipairs(leavingSquads) do |
|
local squad = gv_Squads[squadId] |
|
if not squad then goto continue end |
|
|
|
|
|
local exhausted = GetSquadTiredUnits(squad, "Exhausted") |
|
if exhausted then |
|
local exhausted_ids = ShowExhaustedUnitsQuestion(squad, exhausted) |
|
if exhausted_ids then |
|
|
|
|
|
SyncSplitSquad(squad.UniqueId, exhausted_ids) |
|
ObjModified("hud_squads") |
|
else |
|
goto continue |
|
end |
|
end |
|
|
|
|
|
for _, id in ipairs(squad.units or empty_table) do |
|
local u = g_Units[id] |
|
if u and u:CanBeControlled() and self:IsUnitInside(u) and self:IsMarkerEnabled() then |
|
table.insert(leavingUnits, u.session_id) |
|
end |
|
end |
|
|
|
::continue:: |
|
end |
|
|
|
local spawned_units = 0 |
|
for i, squadId in ipairs(playerSquads) do |
|
local squad = gv_Squads[squadId] |
|
if not squad then goto continue end |
|
|
|
for _, id in ipairs(squad.units or empty_table) do |
|
local u = g_Units[id] |
|
if u then |
|
spawned_units = spawned_units + 1 |
|
end |
|
end |
|
|
|
::continue:: |
|
end |
|
|
|
|
|
if #leavingUnits == 0 then return end |
|
|
|
if gv_Sectors[gv_CurrentSectorId].conflict then |
|
LeaveSectorConflict(sector_id, leavingUnits, underground, spawned_units, unit) |
|
else |
|
LeaveSectorExploration(sector_id, leavingUnits, underground, nil, unit:IsLocalPlayerControlled()) |
|
end |
|
end |
|
|
|
function GetExitZoneInteractableFromMarker(marker) |
|
if not marker then return end |
|
local exitInteractable = MapGetMarkers("ExitZoneInteractable", marker.Groups and marker.Groups[1]) |
|
return exitInteractable and exitInteractable[1] |
|
end |
|
|
|
function LeaveSectorConflict(sectorId, units, underground, totalPlayerUnits, initiatingUnit) |
|
local names = {} |
|
for _, u in ipairs(units) do |
|
local unitData = gv_UnitData[u] |
|
names[#names + 1] = _InternalTranslate(unitData.Nick) |
|
end |
|
names = table.concat(names, ", ") |
|
|
|
local initiatedByLocalPlayer = initiatingUnit:IsLocalPlayerControlled() |
|
local state_func = nil |
|
if not initiatedByLocalPlayer then |
|
state_func = function() return "disabled" end |
|
end |
|
local three_choices = #units > 1 |
|
local res = WaitPopupChoice(GetInGameInterfaceModeDlg(), { |
|
translate = true, |
|
text = T{867511434762, "Do you want to retreat the following mercs - <u(names)>?", names = names}, |
|
choice1 = three_choices and T(288455844681, "Retreat All") or T(1138, "Yes"), |
|
choice1_state_func = state_func, |
|
choice1_gamepad_shortcut = "ButtonX", |
|
choice2 = three_choices and T{162642612318, "Retreat <merc>", merc = initiatingUnit.Nick} or T(967444875712, "Cancel"), |
|
choice2_state_func = three_choices and state_func or nil, |
|
choice2_gamepad_shortcut = three_choices and "ButtonY" or "ButtonB", |
|
choice3 = three_choices and T(1000246, "Cancel") or nil, |
|
choice3_gamepad_shortcut = "ButtonB", |
|
sync_close = initiatedByLocalPlayer, |
|
}) |
|
|
|
if res == 1 then |
|
|
|
elseif res == 2 and three_choices then |
|
units = {initiatingUnit.session_id} |
|
else |
|
return |
|
end |
|
|
|
if #units < totalPlayerUnits then |
|
NetSyncEvent("RetreatUnits", units, sectorId, underground, totalPlayerUnits - #units) |
|
else |
|
LeaveSectorExploration(sectorId, units, underground, true) |
|
end |
|
end |
|
|
|
function WaitQuestion_ZuluSync(parent, caption, text, ok_text, cancel_text, obj, localPlayer) |
|
|
|
|
|
|
|
assert(type(parent) == "table" and parent.IsKindOf and parent:IsKindOf("XWindow"), "The first argument must be a parent window. Don't just create 'global' messages, attach them to the correct parent so they'd share their lifetimes.", 1) |
|
local dialog |
|
if IsKindOf(caption, "XDialog") then |
|
dialog = caption |
|
else |
|
local func = nil |
|
if not localPlayer then |
|
func = function() return "disabled" end |
|
end |
|
dialog = CreateQuestionBox(parent, caption, text, ok_text, cancel_text, obj, func, nil, nil, localPlayer) |
|
end |
|
local result, dataset, xInputStateAtClose = dialog:Wait() |
|
return result, dataset, xInputStateAtClose |
|
end |
|
|
|
function LeaveSectorExploration(sectorId, units, underground, skipNotify, localPlayer) |
|
if not skipNotify then |
|
local popupText |
|
if underground then |
|
if IsSectorUnderground(gv_CurrentSectorId) then |
|
popupText = T(528652976882, "Are you sure you want to exit?") |
|
else |
|
popupText = T(261972368205, "Are you sure you want to go underground?") |
|
end |
|
else |
|
popupText = T{397573113952, "Are you sure you want to leave sector <SectorName(current_sector)> and enter sector <SectorName(next_sector)>?", |
|
current_sector = gv_Sectors[gv_CurrentSectorId], |
|
next_sector = gv_Sectors[sectorId], |
|
} |
|
end |
|
|
|
if WaitQuestion_ZuluSync(GetInGameInterfaceModeDlg(), T(814633909510, "Confirm"), popupText, T(689884995409, "Yes"), T(782927325160, "No"), nil, localPlayer) ~= "ok" then |
|
return |
|
end |
|
end |
|
|
|
local special_entrance = underground |
|
|
|
local squads = {} |
|
local playerSquads = GetSquadsOnMap() |
|
for i, squadId in ipairs(playerSquads) do |
|
local squad = gv_Squads[squadId] |
|
if not squad then goto continue end |
|
|
|
|
|
local thisSquadHasLeavingUnit = false |
|
for _, id in ipairs(squad.units or empty_table) do |
|
local u = g_Units[id] |
|
local ud = gv_UnitData[id] |
|
if not u and ud and ud.retreat_to_sector then |
|
thisSquadHasLeavingUnit = true |
|
end |
|
end |
|
if thisSquadHasLeavingUnit then |
|
table.insert_unique(squads, squadId) |
|
end |
|
|
|
::continue:: |
|
end |
|
|
|
for i, u in ipairs(units) do |
|
local unit = g_Units[u] or gv_UnitData[u] |
|
local squadId = unit.Squad |
|
table.insert_unique(squads, squadId) |
|
end |
|
|
|
|
|
local squadsToMove = {} |
|
for i, sqId in ipairs(squads) do |
|
local squad = gv_Squads[sqId] |
|
local squadToMove = CheckSquadBusy(sqId) |
|
if squadToMove then |
|
squadsToMove[#squadsToMove + 1] = squadToMove |
|
end |
|
end |
|
if #squadsToMove == 0 then return end |
|
|
|
NetSyncEvent("LeaveSectorMap", sectorId, false, special_entrance, squadsToMove) |
|
end |
|
|
|
|
|
|
|
|
|
function AreAdjacentSectors(s1Id, s2Id) |
|
return GetSectorDistance(s1Id, s2Id) <= 1 |
|
end |
|
|
|
function RetreatMoveWholeSquad(squad_id, to_sector_id, from_sector_id) |
|
local squad = gv_Squads[squad_id] |
|
|
|
local route = GenerateRouteDijkstra(from_sector_id, to_sector_id) |
|
if not route then route = {to_sector_id} end |
|
route = {route} |
|
|
|
local time = GetSectorTravelTime(from_sector_id, to_sector_id, route, squad.units) |
|
local instant = not time or time <= 0 |
|
|
|
|
|
if route and route[1] and #route[1] > 1 then instant = true end |
|
|
|
local from_sector = gv_Sectors[from_sector_id] |
|
local to_sector = gv_Sectors[to_sector_id] |
|
|
|
if not instant then |
|
|
|
route.satellite_tick_passed = true |
|
SetSatelliteSquadRetreatRoute(squad, route, "keepJoiningSquads", "from_map") |
|
else |
|
squad.Retreat = false |
|
if not gv_SatelliteView then SyncUnitProperties("map") end |
|
SetSatelliteSquadCurrentSector(squad, to_sector_id, "update_pos", "teleported") |
|
|
|
if not gv_SatelliteView then SyncUnitProperties("session") end |
|
end |
|
return instant |
|
end |
|
|
|
local function lMoveWholeSquadTacticalView(squad_id, sector_id) |
|
return RetreatMoveWholeSquad(squad_id, sector_id, gv_CurrentSectorId) |
|
end |
|
|
|
function RetreatUnit(unit, sector_id) |
|
Msg("UnitRetreat", unit) |
|
local team = unit.team |
|
unit:Despawn() |
|
gv_UnitData[unit.session_id].retreat_to_sector = sector_id |
|
if g_Combat then |
|
if g_Teams[g_CurrentTeam] == team then |
|
g_Combat:NextUnit(team, "force") |
|
end |
|
g_Combat:CheckEndTurn() |
|
end |
|
ObjModified(Game) |
|
ObjModified(Selection) |
|
ObjModified("hud_squads") |
|
end |
|
|
|
function CancelUnitRetreat(ud) |
|
|
|
if IsSectorUnderground(ud.retreat_to_sector) then |
|
ud.arrival_dir = "Underground" |
|
else |
|
local dirToRetreatSector = GetSectorDirection(gv_CurrentSectorId, ud.retreat_to_sector) |
|
ud.arrival_dir = dirToRetreatSector |
|
end |
|
|
|
ud.retreat_to_sector = false |
|
ud.already_spawned_on_map = false |
|
end |
|
|
|
|
|
GameVar("gv_LastRetreatedUnit", false) |
|
GameVar("gv_LastRetreatedEntrance", false) |
|
|
|
function NetSyncEvents.RetreatUnits(session_ids, sector_id, underground, remaining) |
|
local units = {} |
|
for _, id in ipairs(session_ids) do |
|
local unit = g_Units[id] |
|
if not unit then |
|
assert(false, "Trying to retreat non existent unit") |
|
return |
|
end |
|
units[#units + 1] = unit |
|
end |
|
|
|
|
|
SectorOperation_CancelByGame(units) |
|
|
|
|
|
|
|
gv_LastRetreatedUnit = #session_ids > 0 and session_ids[1] |
|
gv_LastRetreatedEntrance = { sector_id, underground } |
|
|
|
local check_squads = {} |
|
for _, unit in ipairs(units) do |
|
RetreatUnit(unit, sector_id) |
|
table.insert_unique(check_squads, unit.Squad) |
|
end |
|
|
|
|
|
local squadsToMove = {} |
|
for _, id in ipairs(check_squads) do |
|
local retreat_whole_squad = true |
|
local squad = gv_Squads[id] |
|
for _, unit in ipairs(squad.units or empty_table) do |
|
if not gv_UnitData[unit].retreat_to_sector then |
|
retreat_whole_squad = false |
|
break |
|
end |
|
end |
|
if retreat_whole_squad then |
|
lMoveWholeSquadTacticalView(squad.UniqueId, sector_id) |
|
table.insert(squadsToMove, squad.UniqueId) |
|
end |
|
end |
|
EnsureCurrentSquad() |
|
ShowTacticalNotification("allyRetreat", nil, T(312444150797, "Retreated successfully"), { number = remaining }) |
|
|
|
if #squadsToMove > 0 and #GetAllPlayerUnitsOnMap() <= 0 then |
|
NetSyncEvents.LeaveSectorMap(sector_id, false, underground, squadsToMove) |
|
end |
|
end |
|
|
|
function SyncSplitSquad(squad_id, available) |
|
assert(CanYield()) |
|
NetSyncEvent("SplitSquad", squad_id, available) |
|
local err, newSquad, oldSquad |
|
while oldSquad ~= squad_id do |
|
err, newSquad, oldSquad = WaitMsg("SyncSplitSquad", 1000) |
|
if err then |
|
break |
|
end |
|
end |
|
return newSquad |
|
end |
|
|
|
function CheckSquadBusy(squad_id) |
|
local busy, available = GetSquadBusyAvailable(squad_id) |
|
if next(busy) then |
|
local res = GetSplitMoveChoice(busy, available) |
|
if res == "split" then |
|
return SyncSplitSquad(squad_id, available) |
|
elseif res == "cancel" then |
|
return false |
|
end |
|
end |
|
return squad_id |
|
end |
|
|
|
function NetSyncEvents.LeaveSectorMap(dest_sector_id, spawn_mode, special_entrance, squad_ids) |
|
if g_Combat and not g_Combat.combat_started then return end |
|
if IsSetpiecePlaying() then return end |
|
|
|
SectorOperation_SquadOnMove(gv_CurrentSectorId, squad_ids) |
|
|
|
local squads = GetSquadsWithIds(squad_ids) |
|
local curSector = gv_Sectors[gv_CurrentSectorId] |
|
|
|
|
|
|
|
for _, unit in ipairs(g_Units) do |
|
if unit.team and not unit.team.player_team then |
|
unit:AddStatusEffect("Unaware") |
|
else |
|
if unit:HasStatusEffect("ManningEmplacement") then |
|
unit:LeaveEmplacement("instant") |
|
elseif unit:HasStatusEffect("StationedMachineGun") then |
|
unit:MGPack() |
|
end |
|
end |
|
end |
|
|
|
|
|
local satellite = false |
|
for i, squad in ipairs(squads) do |
|
|
|
if dest_sector_id ~= squad.CurrentSector then |
|
local units = squad.units |
|
SectorOperation_CancelByGame(units, false, true) |
|
end |
|
|
|
|
|
local instant = lMoveWholeSquadTacticalView(squad.UniqueId, dest_sector_id) |
|
satellite = satellite or not instant |
|
|
|
for _, id in ipairs(squad.units) do |
|
local unit = g_Units[id] |
|
if unit then |
|
RetreatUnit(unit, dest_sector_id) |
|
end |
|
end |
|
|
|
|
|
for _, u in ipairs(squad.units) do |
|
local ud = gv_UnitData[u] |
|
ud.retreat_to_sector = false |
|
end |
|
|
|
|
|
|
|
if special_entrance then |
|
for _, u in ipairs(squad.units) do |
|
local ud = gv_UnitData[u] |
|
ud.arrival_dir = special_entrance |
|
end |
|
end |
|
end |
|
|
|
local conflict = curSector.conflict |
|
local bRetreat = false |
|
for i, squad in ipairs(squads) do |
|
if conflict then |
|
if satellite then squad.Retreat = true end |
|
bRetreat = true |
|
end |
|
end |
|
|
|
if conflict then |
|
ResolveConflict(curSector, bRetreat and "no_voice", false, bRetreat) |
|
end |
|
|
|
|
|
|
|
if gv_SatelliteView then return end |
|
|
|
if satellite then |
|
ForceReloadSectorMap = true |
|
LocalCheckUnitsMapPresence() |
|
CreateRealTimeThread(function() |
|
OpenSatelliteView() |
|
SetCampaignSpeed(Game.CampaignTimeFactor, "UI") |
|
end) |
|
else |
|
if not spawn_mode then |
|
local destSector = gv_Sectors[dest_sector_id] |
|
spawn_mode = destSector and destSector.conflict and "attack" or "explore" |
|
end |
|
CreateGameTimeThread(function() |
|
LoadSector(dest_sector_id, spawn_mode) |
|
end) |
|
end |
|
end |
|
|
|
|
|
function OnMsg.MercReleased(_, squadId) |
|
local squad = gv_Squads[squadId] |
|
if not squad then return end |
|
|
|
local squadUnitsLeft = squad.units |
|
local allRetreating, retreatingTo = true, false |
|
for i, u in ipairs(squadUnitsLeft) do |
|
local ud = gv_UnitData[u] |
|
if not ud.retreat_to_sector then |
|
allRetreating = false |
|
elseif not retreatingTo then |
|
retreatingTo = ud.retreat_to_sector |
|
end |
|
end |
|
if allRetreating then |
|
local currentSector = squad.CurrentSector |
|
local underground = currentSector .. "_Underground" == retreatingTo or retreatingTo .. "_Underground" == currentSector |
|
LeaveSectorExploration(retreatingTo, squadUnitsLeft, underground, true) |
|
end |
|
end |
|
|
|
MapVar("gv_RetreatOrTravelOption", false) |
|
|
|
function CheckRetreatButtonVisibility() |
|
local selectedUnit = Selection and Selection[1] |
|
if not selectedUnit or (IsKindOf(selectedUnit, "Unit") and not selectedUnit:CanBeControlled()) then |
|
gv_RetreatOrTravelOption = false |
|
ObjModified("gv_RetreatOrTravelOption") |
|
return |
|
end |
|
|
|
local markers = MapGetMarkers("Entrance") |
|
for i, m in ipairs(markers) do |
|
if m:IsMarkerEnabled() and m:IsInsideArea(selectedUnit) then |
|
local exitInteractable = MapGetMarkers("ExitZoneInteractable", m.Groups and m.Groups[1]) |
|
exitInteractable = exitInteractable and exitInteractable[1] |
|
|
|
if exitInteractable and exitInteractable:GetInteractionCombatAction(selectedUnit) then |
|
gv_RetreatOrTravelOption = exitInteractable |
|
ObjModified("gv_RetreatOrTravelOption") |
|
return |
|
end |
|
end |
|
end |
|
|
|
gv_RetreatOrTravelOption = false |
|
ObjModified("gv_RetreatOrTravelOption") |
|
end |
|
|
|
OnMsg.ExplorationTick = CheckRetreatButtonVisibility |
|
OnMsg.CombatGotoStep = CheckRetreatButtonVisibility |
|
OnMsg.SelectedObjChange = CheckRetreatButtonVisibility |
|
OnMsg.SelectionChange = CheckRetreatButtonVisibility |
|
OnMsg.TurnStart = CheckRetreatButtonVisibility |
|
OnMsg.RepositionEnd = CheckRetreatButtonVisibility |
|
|
|
function GetClosestExitZoneInteractable(pos_or_obj) |
|
local closestExitZone = false |
|
MapForEach("map", "ExitZoneInteractable", function(o) |
|
if not closestExitZone then |
|
closestExitZone = o |
|
return |
|
end |
|
closestExitZone = closestExitZone and IsCloser(pos_or_obj, o, closestExitZone) and o or closestExitZone |
|
end) |
|
|
|
return closestExitZone |
|
end |
|
|
|
if Platform.developer then |
|
local function lCheckMapEntrances(campaign_preset, sector, errors) |
|
errors = errors or {} |
|
|
|
local sectors = campaign_preset.Sectors or empty_table |
|
local directions = { } |
|
for _, dir in ipairs(const.WorldDirections) do |
|
local neighSectorId = GetNeighborSector(sector.Id, dir, campaign_preset) |
|
if neighSectorId then |
|
local sector = table.find_value(sectors, "Id", neighSectorId) |
|
if sector and sector.Passability ~= "Blocked" then |
|
directions[#directions + 1] = dir |
|
end |
|
end |
|
end |
|
|
|
local blockedTravel = sector.BlockTravel or empty_table |
|
for i, dir in ipairs(directions) do |
|
if not blockedTravel[dir] and not next(MapGetMarkers("ExitZoneInteractable", dir)) then |
|
errors[#errors + 1] = string.format("No ExitZoneInteractable on map '%s' for direction '%s'", GetMapName(), dir) |
|
end |
|
end |
|
|
|
return errors |
|
end |
|
|
|
function OnMsg.SaveMap() |
|
local campaign = Game and Game.Campaign or rawget(_G, "DefaultCampaign") or "HotDiamonds" |
|
local campaign_presets = rawget(_G, "CampaignPresets") or empty_table |
|
local campaign_preset = campaign_presets[campaign] |
|
|
|
local sectors = campaign_preset and campaign_preset.Sectors or empty_table |
|
|
|
local sector = false |
|
for i, s in ipairs(sectors) do |
|
if s.Map == CurrentMap then |
|
sector = s |
|
break |
|
end |
|
end |
|
if not sector or sector.GroundSector then return end |
|
|
|
local errors = lCheckMapEntrances(campaign_preset, sector) |
|
for i, err in ipairs(errors) do |
|
StoreErrorSource(i, err) |
|
end |
|
end |
|
|
|
function CheckEntrancesOfAllMaps() |
|
if not CanYield() then |
|
CreateRealTimeThread(CheckEntrancesOfAllMaps) |
|
return |
|
end |
|
|
|
local campaign = Game and Game.Campaign or rawget(_G, "DefaultCampaign") or "HotDiamonds" |
|
local campaign_presets = rawget(_G, "CampaignPresets") or empty_table |
|
local campaign_preset = campaign_presets[campaign] |
|
|
|
local sectors = campaign_preset and campaign_preset.Sectors or empty_table |
|
local maps = {} |
|
local mapToSector = {} |
|
for i, s in ipairs(sectors) do |
|
if s.Map and not s.GroundSector then |
|
maps[#maps + 1] = s.Map |
|
mapToSector[s.Map] = s |
|
end |
|
end |
|
|
|
local errors = {} |
|
ForEachMap(maps, function() |
|
local sector = mapToSector[CurrentMap] |
|
errors = lCheckMapEntrances(campaign_preset, sector, errors) |
|
end) |
|
|
|
while IsChangingMap() do Sleep(100) end |
|
|
|
for i, err in ipairs(errors) do |
|
StoreErrorSource(false, err) |
|
end |
|
Inspect(errors) |
|
end |
|
end |