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" }, }, -- Change in GridMarkerType editor AreaWidth = 5, AreaHeight = 5, BadgePosition = "average", Type = "ExitZoneInteractable", fake_visual_obj = false, -- Object to be used when one isn't explicitly placed 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) -- ExitZoneInteractable is never its own visual. local visuals = self.visuals_cache for i, obj in ipairs(visuals) do if IsKindOf(obj, "Interactable") then obj.visual_of_interactable = self end end --[[ if IsValid(self.fake_visual_obj) then self.visuals_cache[#self.visuals_cache + 1] = self.fake_visual_obj 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 -- Mark all sectors that can be accessed from this sector as discovered 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) -- NOTE: enumerating in the voxel may be faster than all GridMarkers 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) -- Underground sectors exits in world directions can only lead to -- other underground sectors 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 ") else text = T(500843659226, "Exit To ") 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 -- Original Spec: http://mantis.haemimontgames.com/view.php?id=147486 -- Cases -- 1. Conflict All Units -- 2. Conflict Partial Units -- 3. No Conflict All Units -- 4. No Conflict Partial Units -- Subcases, apply these to to each of the above cases. -- 1. Going towards a sector with travel time 0 (cities/roads) -- 2. Going towards a sector with travel time above 0 -- 3. To Underground -- 4. To Overground 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 -- Find which units can leave 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 -- Check if the squad has any tired units local exhausted = GetSquadTiredUnits(squad, "Exhausted") if exhausted then local exhausted_ids = ShowExhaustedUnitsQuestion(squad, exhausted) if exhausted_ids then -- This needs to be sync so that the split happens completely before we proceed with the exit. -- This function it self isn't sync due to various UI popups etc. SyncSplitSquad(squad.UniqueId, exhausted_ids) ObjModified("hud_squads") else goto continue end end -- Find which units can leave 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 -- All were filtered out. 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 - ?", 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 = 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) --creates a question box that has it's ok enabled only for localPlayer == true --cancel is enabled for all clients to evade failure states --cancel/ok on localPlayer == true closes box on all clients 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 and enter 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 -- This squad initiated retreat but all the non-retreating units died. 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 -- Check for busy squads 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 -- Map retreat from non-adjacent sectors is possible when -- an exit zone interactable has its destination overriden. -- In these cases travel instantly. 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} -- waypointify local time = GetSectorTravelTime(from_sector_id, to_sector_id, route, squad.units) local instant = not time or time <= 0 -- When the link is multiple sectors teleport between them (186068) 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 -- Tick needs to be considered passed in order for the squad to be considered travelling. 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") -- For player units we need to sync back to the unit as they will sync back to the unit data in the despawn function 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) -- Try to get the units in the direction they retreated to 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 -- Used for resuming retreat if the last units on the map die 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 -- When retreating in conflict force cancel operations of units. (219850) SectorOperation_CancelByGame(units) -- Record in case the rest of the units die. -- We need to call LeaveSectorExploration then. 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 -- check if there are new squads with all mercs retreated 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 --no guys left on map 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] -- Apply unaware to non-player units when leaving the map. -- This will be synced to the unit data inside MoveWholeSquad 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 -- travel or teleport to other sector instantly local satellite = false for i, squad in ipairs(squads) do -- stop operations if dest_sector_id ~= squad.CurrentSector then local units = squad.units SectorOperation_CancelByGame(units, false, true) end -- move 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 -- Clear retreat flag from units for _, u in ipairs(squad.units) do local ud = gv_UnitData[u] ud.retreat_to_sector = false end -- Overwrite arrival direction with special entrance if any. -- This will affect the deployment on the new sector. 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 -- Retreat triggered while in satellite mode, such as by a merc release. -- In this case don't force an explore as it can be jarring. 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) --pause wants to yield, but it can't end) end end -- Promote half-way retreating squads into full retreating squads should all their units have been released. 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