--- /tmp/erri-hunt-before/ERR_SupplyConvoyTactics.c 2026-10-04 12:52:07.062390760 +0200 +++ addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c 2026-10-04 12:57:40.250476502 +0200 @@ -8,6 +8,9 @@ bool Registered; bool Released; vector OrderedContact; + vector SearchPosition; + int SearchStep; + bool SearchPlanned; ref ERR_ConvoyBoardingProgress BoardingProgress; float BoardingRetryAt; void Clear() @@ -202,6 +205,7 @@ protected vector m_Left; protected float m_LastContact = -1; protected float m_NextOrders; + protected float m_NextSearch; protected vector m_LastOrderContact; protected bool m_OrdersIssued; protected float m_BoardDeadline; @@ -280,7 +284,7 @@ // Only production native observations or a delivered HQ snapshot may call this. void Report(vector position, float observed, float now) { - if (m_Stopped || !ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(observed) || !ERR_ForceRules.FiniteVector(position) || observed < 0 || observed > now || now - observed > 60000 || observed < m_LastContact) return; + if (m_Stopped || !ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(observed) || !ERR_ForceRules.FiniteVector(position) || observed < 0 || observed > now || now - observed > 60000 || observed <= m_LastContact) return; m_LastContact = observed; if (m_State != 1) Start(position, now); else if (vector.DistanceXZ(position, m_Contact) > 40 && now >= m_NextOrders) { m_Contact = position; Orders(false, now); } @@ -299,19 +303,58 @@ } protected void Flank(int index, bool dismount) { + PlanFlank(index, dismount, 0); + } + protected bool PlanFlank(int index, bool dismount, int step) + { ERR_SupplyTeam team = m_Task.Teams[index]; - if (team.Released || !team.Living()) return; + if (team.Released || !team.Living() || step < 0 || step > 2) return false; // Preserve an in-progress native all-member dismount across contact refreshes. - if (!team.Aboard(m_Task.Vehicles[2].Entity, false) && SCR_BoardingWaypoint.Cast(team.Group.GetCurrentWaypoint())) return; - team.Clear(); team.OrderedContact = m_Contact; - if (dismount && !team.Aboard(m_Task.Vehicles[2].Entity, false)) team.Add(Board(m_Task.Vehicles[2].Entity, false)); + if (!team.Aboard(m_Task.Vehicles[2].Entity, false) && SCR_BoardingWaypoint.Cast(team.Group.GetCurrentWaypoint())) return false; float side = 1; if (index == 1) side = -1; + vector desired = m_Contact + m_Left * (70 * side); + if (step == 1) desired = m_Contact + m_Forward * 100 + m_Left * (140 * side); + if (step == 2) desired = m_Contact + m_Forward * 160 + m_Left * (50 * side); vector approach, sector; - if (!FootPosition(team, m_Contact - m_Forward * 80 + m_Left * (110 * side), approach) || !FootPosition(team, m_Contact + m_Left * (70 * side), sector)) return; - team.Add(Move(approach, 15)); + if (!FootPosition(team, desired, sector)) return false; + approach = sector; + if (step == 0 && !FootPosition(team, m_Contact - m_Forward * 80 + m_Left * (110 * side), approach)) return false; + AIWaypoint move = Move(approach, 15); AIWaypoint search = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), sector)); - if (search) search.SetCompletionRadius(30); + AIWaypoint exit; + bool needsExit = dismount && !team.Aboard(m_Task.Vehicles[2].Entity, false); + if (needsExit) exit = Board(m_Task.Vehicles[2].Entity, false); + // Failed placement/allocation must not erase the team's working orders. + if (!move || !search || (needsExit && !exit)) + { + if (move) SCR_EntityHelper.DeleteEntityAndChildren(move); + if (search) SCR_EntityHelper.DeleteEntityAndChildren(search); + if (exit) SCR_EntityHelper.DeleteEntityAndChildren(exit); + return false; + } + team.Clear(); team.OrderedContact = m_Contact; + team.SearchPosition = sector; team.SearchStep = step; team.SearchPlanned = true; + team.Add(exit); team.Add(move); + search.SetCompletionRadius(30); team.Add(search); + ERR_HQ.Event("CONVOY HUNT", string.Format("%1 team %2: search step %3/3 at %4 from last observed contact; formation suspended", m_Task.Receipt.SourceId, index + 1, step + 1, sector)); + return true; + } + protected void ContinueSearch(float now) + { + if (now < m_NextSearch) return; + m_NextSearch = now + 45000; + for (int i = 0; i < m_Task.Teams.Count(); i++) + { + ERR_SupplyTeam team = m_Task.Teams[i]; + if (team.Released || !team.Living() || !team.Aboard(m_Task.Vehicles[2].Entity, false)) continue; + // Retry refused initial orders, but never replace a team still travelling. + if (!team.SearchPlanned || vector.DistanceXZ(team.OrderedContact, m_Contact) > 40) { PlanFlank(i, false, 0); continue; } + if (team.SearchStep >= 2) continue; + IEntity leader = team.Group.GetLeaderEntity(); + if (!leader || vector.DistanceXZ(leader.GetOrigin(), team.SearchPosition) > 45) continue; + PlanFlank(i, false, team.SearchStep + 1); + } } protected bool SupportPosition(int index, vector desired, out vector position) { @@ -381,6 +424,8 @@ if (intercept.InterceptToken != 0) return; } Clear(); m_State = 1; m_Contact = contact; m_TruckWithdrawn = false; m_OrdersIssued = false; + m_NextSearch = now + 45000; + foreach (ERR_SupplyTeam team : m_Task.Teams) { team.SearchPlanned = false; team.SearchStep = 0; } if (m_Task.Column) m_Task.Column.Reset(now); m_Task.Waiting = false; vector origin; @@ -493,7 +538,11 @@ } } if (found && now >= m_NextOrders) Orders(!InfantryAboard(false), now); - if (now - m_LastContact >= 60000) + // Search only remembered positions. Real new sightings can redirect the + // teams; silence gives them time to reach and sweep both flanks, not a + // one-minute dash followed by forced reboarding. + if (!found && now - m_LastContact < 180000) ContinueSearch(now); + if (now - m_LastContact >= 180000) { Clear(); if (!Usable(2)) { foreach (ERR_SupplyTeam lost : m_Task.Teams) lost.Release(m_Mode); Finish(now); return false; }