using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using Game.TrainStock;
///
/// TrainSystem.cs
///
/// PARALELIZMUS a DEBOUNCE:
/// ─────────────────────────────────────────────────────────────────────────
/// ✔ A* pathfinding beží na Thread Pool cez Task.Run (AStarPathThreaded).
/// Pracuje s int[] snapshotom tileGrid – žiadne Unity API z vlákna.
/// Výsledky sa prenášajú cez ConcurrentQueue, aplikujú sa v Update().
///
/// ✔ OnMapChanged() – DEBOUNCE (riešenie trhnutia vlakov):
/// Každé volanie len nastaví flag + resetuje timer (_mapChangedDebounceTimer).
/// Žiadna práca na hlavnom vlákne pri kliku (žiadny SnapshotTileGrid).
/// Update() odpočítava timer; až po uplynutí DEBOUNCE_DELAY od posledného
/// kliku spustí SnapshotTileGrid() + Task.Run A* pre bežiace vlaky.
/// Vlaky sa hýbu ďalej po starej ceste počas čakania na debounce –
/// žiadne zastavenie, žiadne trhnutie.
///
/// Prepočítavajú sa len vlaky, ktorých aktuálna cesta mohla byť dotknutá
/// (isRunning && !isComputingPath). Vlaky, ktoré práve čakajú na stanici
/// alebo pri prerušení, sa neprepočítavajú zbytočne.
///
/// ✘ UpdateTrain / pohyb – hlavné vlákno (Unity API, thread-unsafe).
/// ✘ CreateTrain / RemoveTrain – hlavné vlákno (GameObject API).
/// ─────────────────────────────────────────────────────────────────────────
///
/// VLAKOVÁ SÚPRAVA – „Distance-Based Path System":
/// ─────────────────────────────────────────────────────────────────────────
/// • Trasa je reprezentovaná ako PathData (List bodov + predpočítané
/// kumulatívne dĺžky segmentov). Zdroj: A* ako List → konvertovaný
/// na waypointy cez BuildWaypointPath (centrá + hrany + krivkové rohy).
/// • Lokomotíva má skalárnu hodnotu locomotiveDistance (vzdialenosť pozdĺž cesty).
/// Každý frame: locomotiveDistance += speed * deltaTime.
/// • Vagón i má offsetDist = i * wagonSpacing za lokomotívou.
/// wagonDistance = locomotiveDistance - (i * wagonSpacing), min 0.
/// • Pozícia = PathData.GetPositionAtDistance(d) → Vector3.Lerp na segmente.
/// • Rotácia = PathData.GetDirectionAtDistance(d) → Quaternion.LookRotation.
/// • ŽIADNA história, ŽIADNE oneskorenie, ŽIADNE posHistory / historyAccum.
/// • Deterministické: rovnaký vstup → rovnaký výstup každý frame.
///
/// WAYPOINT-BASED MOVEMENT (NOVÉ):
/// Trasa už nie je center→center. Pre každý prechod A→B sa generuje:
/// A.center → A.edge(smer A→B) → B.edge(smer B→A) → B.center
/// Pri krivkovej dlaždici sa center NEPOUŽÍVA – generuje sa:
/// B.entryEdge → B.exitEdge (priama diagonála cez vnútro dlaždice, ~45°)
/// Pri výhybkovej dlaždici (RailSwitch*) sa správanie líši podľa toho,
/// či ide o priamy prechod (proti-smery) alebo o odbočku (kolmé smery):
/// • PRIAMY: B.entryEdge → B.center → B.exitEdge (ako priama dlaždica)
/// • ODBOČKA: B.entryEdge → B.innerEntry → B.innerExit → B.exitEdge
/// (3 priame segmenty, stredný diagonálny ~45°; vizuálne železničný
/// turnout, BEZ bezier kriviek, BEZ splajnov, BEZ smoothingu.)
/// Pri križovatkovej dlaždici (RailCrossroad, 4 spojenia) platí to isté:
/// • PRIAMY (Top↔Bottom, Left↔Right): edge → center → edge (90°)
/// • ROHOVÝ (napr. Top↔Right): edge → innerEntry → innerExit → edge
/// (45° turnout, identický s odbočkou výhybky)
/// Krivka, odbočka výhybky aj rohový prechod križovatky používajú
/// VÝHRADNE priame čiarové segmenty spojené lineárnym Lerp-om medzi
/// waypointmi.
///
/// PREJAZD CEZ SVAH (LevelUp / LevelDown):
/// Terén je výškový raster v ROHOCH dlaždíc – jedna dlaždica je
/// naklonený quad medzi 4 rohovými vertexmi s rôznymi Y. Y-súradnica
/// každého waypointu sa získava biliniárnou interpoláciou výšok týchto
/// 4 rohov (GetTileSurfaceY), takže každý bod na trase leží presne na
/// šikmej ploche dlaždice. Vlak na svahu ide pod skutočným uhlom
/// stúpania – uhol je daný geometriou terénu, nie pevnou konštantou.
/// Hladkosť na hrane medzi dlaždicami je zaručená automaticky:
/// A.right-edge a B.left-edge zdieľajú tie isté dva rohové vertexy,
/// takže ich interpolovaná Y vychádza identická → žiadny schod, žiadny
/// 90° lom medzi dlaždicami. Logika svahu je transparentná pre všetky
/// typy dlaždíc – priame, krivky, výhybky, križovatky.
///
/// OBRAT SMERU NA STANICI:
/// path.Reverse() + locomotiveDistance = totalLength - locomotiveDistance
/// → okamžitý, presný, bez usadzovania.
///
/// PARAMETRIZÁCIA:
/// wagonCount – počet vagónov (1 až 10, predvolene 5).
/// wagonSpacing – rozostup členov súpravy pozdĺž trajektórie (predvolene 0.85).
/// ─────────────────────────────────────────────────────────────────────────
///
public class TrainSystem : MonoBehaviour
{
public static TrainSystem instance;
// =====================================================================
// KONFIGURÁCIA VLAKOVEJ SÚPRAVY
// =====================================================================
[Range(1, 10)]
public int wagonCount = 5;
public float wagonSpacing = 0.85f;
// =====================================================================
// POMOCNÁ TRIEDA – PathData
// Ukladá trasu ako Vector3 body + predpočítané kumulatívne vzdialenosti.
// =====================================================================
public class PathData
{
/// Body trasy (waypointy: centrá + hrany + krivkové rohy). Nemeniť po inicializácii.
public readonly List points;
///
/// cumulativeLengths[i] = vzdialenosť od points[0] po points[i].
/// cumulativeLengths[0] == 0 vždy.
///
public readonly float[] cumulativeLengths;
/// Celková dĺžka trasy = cumulativeLengths[Count-1].
public readonly float totalLength;
public int Count => points.Count;
public PathData(List pts)
{
points = pts;
cumulativeLengths = new float[pts.Count];
cumulativeLengths[0] = 0f;
for (int i = 1; i < pts.Count; i++)
cumulativeLengths[i] = cumulativeLengths[i - 1] + Vector3.Distance(pts[i - 1], pts[i]);
totalLength = pts.Count > 0 ? cumulativeLengths[pts.Count - 1] : 0f;
}
///
/// Vráti pozíciu na trase pre zadanú vzdialenosť od začiatku.
/// Vzdialenosť je upnutá na [0, totalLength].
/// Interpolácia je VÝHRADNE lineárna (Vector3.Lerp) medzi dvoma susednými bodmi.
///
public Vector3 GetPositionAtDistance(float dist)
{
if (points.Count == 0) return Vector3.zero;
if (points.Count == 1) return points[0];
dist = Mathf.Clamp(dist, 0f, totalLength);
// Binárne vyhľadávanie segmentu
int lo = 0, hi = points.Count - 1;
while (lo < hi - 1)
{
int mid = (lo + hi) >> 1;
if (cumulativeLengths[mid] <= dist) lo = mid;
else hi = mid;
}
float segStart = cumulativeLengths[lo];
float segEnd = cumulativeLengths[hi];
float segLen = segEnd - segStart;
if (segLen < 1e-6f) return points[hi];
float localT = (dist - segStart) / segLen;
return Vector3.Lerp(points[lo], points[hi], localT);
}
///
/// Vráti normalizovaný smer pohybu na zadanej vzdialenosti.
/// Smer = (nextPoint - currentPoint).normalized pre segment, v ktorom leží dist.
/// NIKDY neakumuluje rotácie, NIKDY nepoužíva dáta z predchádzajúceho snímku.
///
public Vector3 GetDirectionAtDistance(float dist)
{
if (points.Count < 2) return Vector3.forward;
dist = Mathf.Clamp(dist, 0f, totalLength);
int lo = 0, hi = points.Count - 1;
while (lo < hi - 1)
{
int mid = (lo + hi) >> 1;
if (cumulativeLengths[mid] <= dist) lo = mid;
else hi = mid;
}
Vector3 dir = points[hi] - points[lo];
return dir.sqrMagnitude > 1e-12f ? dir.normalized : Vector3.forward;
}
///
/// Vráti obrátenú kópiu PathData (pre obrat smeru na stanici).
/// Pôvodný objekt zostáva nezmenený.
///
public PathData Reversed()
{
var rev = new List(points);
rev.Reverse();
return new PathData(rev);
}
}
// =====================================================================
// DÁTOVÉ ŠTRUKTÚRY
// =====================================================================
public class TrainData
{
public int depotX, depotZ;
///
/// Dátový popis vlakovej súpravy – typ vlaku + jeho atribúty + vagóny
/// a ich atribúty (Name, Cost, Speed, kapacity vagónov atď.).
/// Definované v TrainStock.cs. Pohybový/pathfinding stav zostáva
/// v poliach TrainData nižšie; toto je oddelená "obsahová" časť.
///
/// Naplní sa pri CreateTrain podľa voľby z TrainTypeDropdown /
/// WagonTypeDropdown. UI okno s detailmi vlaku si odtiaľto len číta.
///
public TrainInstance consist;
/// Skrytá kocka (Renderer off) – pohybová logika (pozícia hlavy).
public GameObject trainObject;
/// Vizuálna lokomotíva (CYAN kváder).
public GameObject locomotive;
/// Vizuálne vagóny (GREY kvádre). Count = wagonCount pri vytvorení.
public List wagons = new List();
// ------------------------------------------------------------------
// DISTANCE-BASED PATH STATE
// ------------------------------------------------------------------
///
/// Aktuálna trasa ako PathData (waypointy + kumulatívne dĺžky).
/// null = žiadna aktívna trasa.
///
public PathData activePath;
///
/// Vzdialenosť lokomotívy od začiatku activePath.
/// Každý frame: locomotiveDistance += trainSpeed * Time.deltaTime.
///
public float locomotiveDistance;
/// Rýchlosť pohybu vlaku (jednotky/sekunda). Odvodená z MOVE_TIME.
public float trainSpeed;
// ------------------------------------------------------------------
// Pôvodné polia TrainData (nezmenené)
// ------------------------------------------------------------------
public List stations;
public int currentStationIndex;
public bool isRunning;
public bool isWaiting;
public bool isReturningToDepot;
public bool isAtDepot;
public bool reverseDirection;
public List currentPath;
public int pathIndex;
public Vector2Int currentTile;
public float moveTimer;
public float waitTimer;
public bool isComputingPath;
public List pendingPath;
public bool isGoingToDepotViaStation;
public bool isStoppedAwaitingDepotReturn;
public bool pendingReturnToDepot;
// ------------------------------------------------------------------
// OBCHOD NA STANICI (TradeSystem)
// ------------------------------------------------------------------
///
/// True, ak v AKTUÁLNEJ zastávke na stanici už prebehol pokus o
/// transakciu (výmenu tovaru). Bráni tomu, aby sa obchod spustil
/// každý frame – spustí sa práve raz, po 2 s čakania. Resetuje sa
/// pri každom novom príchode na stanicu (OnPathComplete).
///
public bool tradeDoneAtStation;
/// Index v currentPath kde activePath zacina. Pouziva sa v UpdateCurrentTile.
public int pathIndexAtPathStart;
///
/// Mapovanie waypoint-index → currentPath-index.
/// Pre activePath.points[i] hovorí, ku ktorej dlaždici v currentPath waypoint patrí.
/// Používa sa v UpdateCurrentTile pri waypoint-based pohybe.
///
public int[] waypointToTileIdx;
public TrainData(int dx, int dz)
{
depotX = dx; depotZ = dz;
stations = new List();
currentStationIndex = 0;
isRunning = false; isWaiting = false;
isReturningToDepot = false; isAtDepot = true;
reverseDirection = false;
currentPath = new List();
pathIndex = 0;
currentTile = new Vector2Int(dx, dz);
moveTimer = 0f; waitTimer = 0f;
isComputingPath = false;
pendingPath = null;
isGoingToDepotViaStation = false;
isStoppedAwaitingDepotReturn = false;
pendingReturnToDepot = false;
tradeDoneAtStation = false;
activePath = null;
locomotiveDistance = 0f;
trainSpeed = 0f;
pathIndexAtPathStart = 0;
waypointToTileIdx = null;
consist = null;
}
}
// =====================================================================
// VÝSLEDKOVÁ FRONTA
// =====================================================================
enum PathDestination { Station, Depot, ReturnViaStation }
struct PathResult
{
public int depotKey;
public List path;
public bool isReturnToDepot;
public PathDestination destination;
}
public enum ReturnToDepotResult { Dispatched, StoppedAwaitingSecondR, Error }
readonly System.Collections.Concurrent.ConcurrentQueue _pendingPathResults
= new System.Collections.Concurrent.ConcurrentQueue();
// =====================================================================
// DEBOUNCE
// =====================================================================
const float DEBOUNCE_DELAY = 0.4f;
bool _mapChangePending = false;
float _mapChangedDebounceTimer = 0f;
// =====================================================================
// SNAPSHOT TILE GRIDU
// =====================================================================
const int GRID_SIZE = 256;
///
/// Snapshot tileGrid. Obsahuje DVA paralelné polia:
/// tileIDs[i] – tileID dlaždice (0 = empty, 1 = rail, 2 = station, 3 = depot)
/// connections[i] – DirectionMask dlaždice (uložený ako int pre vlákno-bezpečné použitie)
/// Indexovanie: i = z * GRID_SIZE + x
///
struct TileGridSnapshot
{
public int[] tileIDs;
public int[] connections;
}
TileGridSnapshot SnapshotTileGrid()
{
var snap = new TileGridSnapshot
{
tileIDs = new int[GRID_SIZE * GRID_SIZE],
connections = new int[GRID_SIZE * GRID_SIZE]
};
for (int x = 0; x < GRID_SIZE; x++)
{
for (int z = 0; z < GRID_SIZE; z++)
{
var td = IndicatrixAPI.instance.GetTileByIndex(x, z);
int idx = z * GRID_SIZE + x;
snap.tileIDs[idx] = td.tileID;
snap.connections[idx] = (int)td.connections;
}
}
return snap;
}
// =====================================================================
// KONŠTANTY A STAV
// =====================================================================
Dictionary trains = new Dictionary();
/// Čas prechodu jednej dlaždice v sekundách (1.0 = 1 s/dlaždica).
const float MOVE_TIME = 2.0f;
const float STATION_WAIT = 10.0f;
const float BREAK_WAIT = 1.0f;
///
/// Po koľkých sekundách čakania na cieľovej stanici vlak realizuje
/// výmenu tovaru (TradeSystem). Zadanie: štandardné čakanie je 10 s
/// (STATION_WAIT), transakcia prebehne po 2 s od príchodu.
///
/// Trigger je naviazaný na UPLYNULÝ čas: spustí sa, keď
/// (STATION_WAIT - waitTimer) >= TRADE_DELAY, t.j. po 2 s čakania.
///
const float TRADE_DELAY = 2.0f;
/// Rozmery kvádra lokomotívy aj vagónov.
static readonly Vector3 CONSIST_SCALE = new Vector3(0.3f, 0.3f, 0.7f);
void Awake()
{
instance = this;
wagonCount = Mathf.Clamp(wagonCount, 1, 10);
}
// =====================================================================
// POMOCNÉ
// =====================================================================
int DepotKey(int x, int z) => x * 10000 + z;
Vector3 TileCenter(Vector2Int tile)
{
// Stred dlaždice: (u, v) = (0.5, 0.5) v lokálnych súradniciach.
// Y sa získa biliniárnou interpoláciou výšok 4 rohov dlaždice
// → priemer výšok všetkých 4 rohov (na rovine konštanta, na svahu
// sa správne škáluje).
float y = GetTileSurfaceY(tile, 0.5f, 0.5f) + TRACK_OFFSET_Y;
return new Vector3(tile.x + 0.5f, y, tile.y + 0.5f);
}
float GetTerrainY(int x, int z)
{
try
{
int width = TerrainManager.instance.terrainWidth + 1;
int index = z * width + x;
if (index >= 0 && index < TerrainManager.instance.coordsF.Length)
return TerrainManager.instance.coordsF[index].y;
}
catch { }
return 0f;
}
/// Vertikálny offset koľaje nad povrchom terénu (track height).
const float TRACK_OFFSET_Y = 0.3f;
///
/// Biliniárna interpolácia výšky terénu vnútri jednej dlaždice (tile)
/// medzi 4 rohovými vertexmi.
///
/// Lokálne súradnice (u, v) ∈ [0, 1] × [0, 1]:
/// (u=0, v=0) = ľavý-dolný roh dlaždice → vertex (tile.x, tile.y)
/// (u=1, v=0) = pravý-dolný roh dlaždice → vertex (tile.x+1, tile.y)
/// (u=0, v=1) = ľavý-horný roh dlaždice → vertex (tile.x, tile.y+1)
/// (u=1, v=1) = pravý-horný roh dlaždice → vertex (tile.x+1, tile.y+1)
/// kde u zodpovedá X-osi (Left→Right), v zodpovedá Z-osi (Bottom→Top).
///
/// Vzorec biliniárnej interpolácie:
/// y = (1-u)(1-v) * h00 + u*(1-v) * h10 + (1-u)*v * h01 + u*v * h11
///
/// PREČO TOTO POTREBUJEME (kopce, LevelUp / LevelDown):
/// Aktuálny terénny model ukladá výšku v ROHOCH dlaždice. Jedna
/// dlaždica je naklonený quad medzi 4 rohmi s rôznymi Y. Ak by sme
/// pre celý waypoint v rámci dlaždice používali iba výšku jedného
/// rohu (napr. ľavého-dolného), všetky waypointy v dlaždici by mali
/// rovnaké Y → vlak ide vodorovne a na hrane medzi dlaždicami
/// "vyskočí" na novú výšku → ostrý 90° schod namiesto 45° svahu.
///
/// Biliniárnou interpoláciou dostáva každý waypoint správnu výšku
/// podľa svojej polohy (u, v) na šikmej ploche, takže pohyb cez
/// svah je plynulý a uhol stúpania presne zodpovedá skutočnému
/// prevýšeniu medzi rohmi terénu.
///
/// HLADKOSŤ NA HRANÁCH MEDZI DLAŽDICAMI:
/// Pre dve susediace dlaždice A a B (B = A + Right) platí:
/// A.exitEdge(Right) → použije priemer výšok dvoch pravých rohov A
/// B.entryEdge(Left) → použije priemer výšok dvoch ľavých rohov B
/// Ľavé rohy B sú TOTOŽNÉ vertexy ako pravé rohy A → výška vychádza
/// identická. Prechod cez hranu je teda C0-spojitý automaticky.
/// Žiadna explicitná konštanta uhla ani manuálne dorovnávanie nie je
/// potrebné – uhol svahu je daný geometriou terénu.
///
/// Funkcia je deterministická a thread-safe pokiaľ ide o vstupy
/// (číta len TerrainManager.instance.coordsF, ktoré sa nemení mimo
/// hlavného vlákna v rámci jedného frame-u).
///
float GetTileSurfaceY(Vector2Int tile, float u, float v)
{
float h00 = GetTerrainY(tile.x, tile.y); // ľavý-dolný roh
float h10 = GetTerrainY(tile.x + 1, tile.y); // pravý-dolný roh
float h01 = GetTerrainY(tile.x, tile.y + 1); // ľavý-horný roh
float h11 = GetTerrainY(tile.x + 1, tile.y + 1); // pravý-horný roh
float omu = 1f - u;
float omv = 1f - v;
return omu * omv * h00
+ u * omv * h10
+ omu * v * h01
+ u * v * h11;
}
int GetTileID(int x, int z)
{
if (x < 0 || x >= GRID_SIZE || z < 0 || z >= GRID_SIZE) return -1;
return IndicatrixAPI.instance.GetTileByIndex(x, z).tileID;
}
bool IsPassable(int x, int z)
{
int id = GetTileID(x, z);
return id == 1 || id == 2 || id == 3;
}
// =====================================================================
// DIRECTION HELPERS – mapovanie Vector2Int <-> DirectionMask
// =====================================================================
///
/// Konvertuje 4-smerový vektor (zo step v A*) na DirectionMask.
/// (+1, 0) → Right
/// (-1, 0) → Left
/// ( 0,+1) → Top
/// ( 0,-1) → Bottom
///
static IndicatrixAPI.DirectionMask DirFromStep(Vector2Int step)
{
if (step.x == 1 && step.y == 0) return IndicatrixAPI.DirectionMask.Right;
if (step.x == -1 && step.y == 0) return IndicatrixAPI.DirectionMask.Left;
if (step.x == 0 && step.y == 1) return IndicatrixAPI.DirectionMask.Top;
if (step.x == 0 && step.y == -1) return IndicatrixAPI.DirectionMask.Bottom;
return IndicatrixAPI.DirectionMask.None;
}
///
/// Vráti smer pohybu z dlaždice 'from' na dlaždicu 'to' (musia byť susedia).
///
static IndicatrixAPI.DirectionMask GetDirection(Vector2Int from, Vector2Int to)
{
return DirFromStep(to - from);
}
///
/// Pre danú dlaždicu (x, z) a smer 'dir' vráti svetový bod uprostred danej hrany dlaždice.
/// Hrana je medzi vnútrom dlaždice a susedom v smere 'dir'.
/// Right → x = tile.x + 1.0, z = tile.z + 0.5
/// Left → x = tile.x + 0.0, z = tile.z + 0.5
/// Top → x = tile.x + 0.5, z = tile.z + 1.0
/// Bottom → x = tile.x + 0.5, z = tile.z + 0.0
/// Y: biliniárna interpolácia výšky šikmej plochy dlaždice v polohe
/// stredu danej hrany. Pre rovinu vychádza priemer 4 rohov; pre svah
/// (LevelUp/LevelDown) vychádza presne stred danej hrany dlaždice,
/// takže vlak na svahu ide pod skutočným uhlom stúpania – nie po
/// schodoch s 90° lomami.
/// Hladkosť na hrane medzi susednými dlaždicami je zaručená
/// automaticky: A.right-edge a B.left-edge zdieľajú tie isté dva
/// rohové vertexy → ich priemer je totožný.
///
Vector3 EdgePoint(Vector2Int tile, IndicatrixAPI.DirectionMask dir)
{
float fx = tile.x, fz = tile.y;
switch (dir)
{
case IndicatrixAPI.DirectionMask.Right:
return new Vector3(fx + 1.0f, GetTileSurfaceY(tile, 1.0f, 0.5f) + TRACK_OFFSET_Y, fz + 0.5f);
case IndicatrixAPI.DirectionMask.Left:
return new Vector3(fx + 0.0f, GetTileSurfaceY(tile, 0.0f, 0.5f) + TRACK_OFFSET_Y, fz + 0.5f);
case IndicatrixAPI.DirectionMask.Top:
return new Vector3(fx + 0.5f, GetTileSurfaceY(tile, 0.5f, 1.0f) + TRACK_OFFSET_Y, fz + 1.0f);
case IndicatrixAPI.DirectionMask.Bottom:
return new Vector3(fx + 0.5f, GetTileSurfaceY(tile, 0.5f, 0.0f) + TRACK_OFFSET_Y, fz + 0.0f);
default: return TileCenter(tile);
}
}
///
/// Detekcia, či je daná dlaždica krivkou.
/// Krivka = presne 2 nastavené smery, ktoré NIE SÚ navzájom opačné
/// (t.j. nie Left+Right ani Top+Bottom).
///
static bool IsCurveTile(IndicatrixAPI.DirectionMask conns)
{
// Počet bitov
int bits = 0;
if ((conns & IndicatrixAPI.DirectionMask.Left) != 0) bits++;
if ((conns & IndicatrixAPI.DirectionMask.Right) != 0) bits++;
if ((conns & IndicatrixAPI.DirectionMask.Top) != 0) bits++;
if ((conns & IndicatrixAPI.DirectionMask.Bottom) != 0) bits++;
if (bits != 2) return false;
bool horiz = (conns & (IndicatrixAPI.DirectionMask.Left | IndicatrixAPI.DirectionMask.Right))
== (IndicatrixAPI.DirectionMask.Left | IndicatrixAPI.DirectionMask.Right);
bool vert = (conns & (IndicatrixAPI.DirectionMask.Top | IndicatrixAPI.DirectionMask.Bottom))
== (IndicatrixAPI.DirectionMask.Top | IndicatrixAPI.DirectionMask.Bottom);
return !horiz && !vert;
}
///
/// Detekcia, či je daná dlaždica výhybkou (rail switch / turnout).
/// Výhybka má presne 3 nastavené smery:
/// • RailSwitchHorizontalBottom: Left + Right + Bottom
/// • RailSwitchHorizontalTop: Left + Right + Top
/// • RailSwitchVerticalBottom: Top + Bottom + Right
/// • RailSwitchVerticalTop: Top + Bottom + Left
///
/// Výhybka je plne obojsmerná – ktorékoľvek dva z troch smerov sa
/// môžu navzájom prepojiť. Pohyb cez výhybku má dva režimy:
/// 1. Priamy prechod (proti-smery, napr. Left↔Right) – správa sa ako
/// klasická priama koľaj: edge → center → edge.
/// 2. Odbočka (kolmé smery, napr. Left↔Bottom) – generuje sa diagonálny
/// vnútorný layout (~45° turnout), nie ostrý 90° roh ako pri krivke.
///
/// Všetky 4 križovatkové smery (crossroad) majú 4 bity, takže sú
/// odlíšené: switch == 3 bity, crossroad == 4 bity.
///
static bool IsSwitchTile(IndicatrixAPI.DirectionMask conns)
{
int bits = 0;
if ((conns & IndicatrixAPI.DirectionMask.Left) != 0) bits++;
if ((conns & IndicatrixAPI.DirectionMask.Right) != 0) bits++;
if ((conns & IndicatrixAPI.DirectionMask.Top) != 0) bits++;
if ((conns & IndicatrixAPI.DirectionMask.Bottom) != 0) bits++;
return bits == 3;
}
///
/// Detekcia, či je daná dlaždica plnou križovatkou (RailCrossroad).
/// Križovatka má všetky 4 smery (Left + Right + Top + Bottom) –
/// jediný typ s 4 bitmi v DirectionMask.
///
/// Pohyb cez križovatku má dva režimy podľa entry/exit smerov:
/// 1. Priamy prechod (proti-smery, 90°):
/// Top ↔ Bottom (vertikálny prejazd)
/// Left ↔ Right (horizontálny prejazd)
/// → edge → center → edge (klasická priama)
/// 2. Rohový prechod (kolmé smery, 45°):
/// Top ↔ Right, Top ↔ Left
/// Bottom ↔ Right, Bottom ↔ Left
/// → edge → innerEntry → innerExit → edge (45° turnout layout,
/// identický s odbočkou výhybky)
///
static bool IsCrossroadTile(IndicatrixAPI.DirectionMask conns)
{
const IndicatrixAPI.DirectionMask ALL =
IndicatrixAPI.DirectionMask.Left
| IndicatrixAPI.DirectionMask.Right
| IndicatrixAPI.DirectionMask.Top
| IndicatrixAPI.DirectionMask.Bottom;
return (conns & ALL) == ALL;
}
///
/// Vráti true, ak sú dva smery navzájom kolmé (jeden horizontálny + jeden
/// vertikálny). Vracia false ak sú totožné, alebo proti-smery, alebo None.
/// Používa sa na odlíšenie odbočky vs priameho prechodu cez výhybku.
///
static bool IsPerpendicular(IndicatrixAPI.DirectionMask a, IndicatrixAPI.DirectionMask b)
{
bool aHoriz = a == IndicatrixAPI.DirectionMask.Left || a == IndicatrixAPI.DirectionMask.Right;
bool aVert = a == IndicatrixAPI.DirectionMask.Top || a == IndicatrixAPI.DirectionMask.Bottom;
bool bHoriz = b == IndicatrixAPI.DirectionMask.Left || b == IndicatrixAPI.DirectionMask.Right;
bool bVert = b == IndicatrixAPI.DirectionMask.Top || b == IndicatrixAPI.DirectionMask.Bottom;
return (aHoriz && bVert) || (aVert && bHoriz);
}
///
/// Vnútorný bod výhybky pre 45° turnout geometriu.
///
/// Pre danú dlaždicu, smer (z perspektívy hrany dlaždice, t.j. v ktorej
/// hrane sa nachádzame) a parameter t ∈ (0, 0.5) vráti bod ležiaci na osi
/// danej hrany, ale posunutý dovnútra dlaždice o vzdialenosť t.
///
/// Príklady (tile na (0,0), t = 0.25):
/// dir = Left → edge je (0.0, y, 0.5), inner = (0.25, y, 0.5)
/// dir = Right → edge je (1.0, y, 0.5), inner = (0.75, y, 0.5)
/// dir = Top → edge je (0.5, y, 1.0), inner = (0.5, y, 0.75)
/// dir = Bottom → edge je (0.5, y, 0.0), inner = (0.5, y, 0.25)
///
/// Použitie: pre odbočkový prechod cez výhybku skladáme cestu ako
/// entryEdge → innerEntry(t) → innerExit(t) → exitEdge
/// Stredný segment (innerEntry → innerExit) je pri t = 0.25 a kolmých
/// smeroch presne 45° diagonálny – vizuálne pripomína skutočný turnout
/// koľajnice, bez ostrého 90° lomu cez stred dlaždice.
///
Vector3 SwitchInnerEdgePoint(Vector2Int tile, IndicatrixAPI.DirectionMask dir, float t)
{
float fx = tile.x, fz = tile.y;
switch (dir)
{
case IndicatrixAPI.DirectionMask.Right:
return new Vector3(fx + 1.0f - t,
GetTileSurfaceY(tile, 1.0f - t, 0.5f) + TRACK_OFFSET_Y,
fz + 0.5f);
case IndicatrixAPI.DirectionMask.Left:
return new Vector3(fx + 0.0f + t,
GetTileSurfaceY(tile, 0.0f + t, 0.5f) + TRACK_OFFSET_Y,
fz + 0.5f);
case IndicatrixAPI.DirectionMask.Top:
return new Vector3(fx + 0.5f,
GetTileSurfaceY(tile, 0.5f, 1.0f - t) + TRACK_OFFSET_Y,
fz + 1.0f - t);
case IndicatrixAPI.DirectionMask.Bottom:
return new Vector3(fx + 0.5f,
GetTileSurfaceY(tile, 0.5f, 0.0f + t) + TRACK_OFFSET_Y,
fz + 0.0f + t);
default: return TileCenter(tile);
}
}
// =====================================================================
// CAN MOVE – validácia smeru pre A* (thread-safe – pracuje so snapshotom)
// =====================================================================
///
/// Validácia A* prechodu z 'fromTile' na 'toTile' v smere 'direction'.
/// Pravidlá:
/// 1. toTile musí byť priechodná (rail/station/depot)
/// 2. fromTile musí mať connection v smere 'direction'
/// 3. toTile musí mať connection v opačnom smere (Opposite(direction))
/// 4. Výnimka pre štart: ak je fromTile depo (tileID == 3), neaplikujeme
/// pravidlo č. 2 striktne (depo má jeden výstup, ten musí ladiť so smerom);
/// táto výnimka NIE JE potrebná – depo má vlastný DirectionMask, takže
/// vlak môže opustiť depo iba povoleným smerom. Ak by toto bolo príliš
/// reštriktívne, dá sa relaxovať.
///
static bool CanMove(int[] tileIDs, int[] conns, Vector2Int fromTile, Vector2Int toTile,
IndicatrixAPI.DirectionMask direction)
{
if (toTile.x < 0 || toTile.x >= GRID_SIZE || toTile.y < 0 || toTile.y >= GRID_SIZE)
return false;
if (fromTile.x < 0 || fromTile.x >= GRID_SIZE || fromTile.y < 0 || fromTile.y >= GRID_SIZE)
return false;
int toIdx = toTile.y * GRID_SIZE + toTile.x;
int toID = tileIDs[toIdx];
if (!(toID == 1 || toID == 2 || toID == 3)) return false;
int fromIdx = fromTile.y * GRID_SIZE + fromTile.x;
IndicatrixAPI.DirectionMask fromConn = (IndicatrixAPI.DirectionMask)conns[fromIdx];
IndicatrixAPI.DirectionMask toConn = (IndicatrixAPI.DirectionMask)conns[toIdx];
// fromTile musí povoľovať odchod v smere direction
if ((fromConn & direction) == 0) return false;
// toTile musí povoľovať vstup z opačného smeru
IndicatrixAPI.DirectionMask oppositeDir = IndicatrixAPI.Opposite(direction);
if ((toConn & oppositeDir) == 0) return false;
return true;
}
// =====================================================================
// BUILD WAYPOINT PATH – konvertuje List na waypointy
// =====================================================================
///
/// Z tile-cesty vygeneruje waypoint cestu podľa pravidiel:
///
/// Pre každú dlaždicu B medzi predchádzajúcou A a nasledujúcou C:
/// • Ak je B priama (rail/station/depot s opačnými smermi):
/// waypoint sekvencia: B.entryEdge, B.center, B.exitEdge
/// • Ak je B krivka (RailCurveX):
/// waypoint sekvencia: B.entryEdge, B.exitEdge
/// (center sa NEPOUŽÍVA, vnútorný roh sa NEPOUŽÍVA – PathData
/// ich spojí jedným lineárnym Lerp segmentom, ktorý prechádza
/// vnútrom dlaždice diagonálne pod ~45°.)
/// • Ak je B výhybka (RailSwitch*, 3 spojenia):
/// - PRIAMY prechod (proti-smery): B.entryEdge, B.center, B.exitEdge
/// (rovnako ako priama dlaždica)
/// - ODBOČKA (kolmé smery): B.entryEdge, B.innerEntry,
/// B.innerExit, B.exitEdge
/// (4 waypointy → 3 priame segmenty, stredný 45° diagonálny;
/// vizuálne 45° turnout, NIE ostrý 90° roh.)
/// • Ak je B križovatka (RailCrossroad, 4 spojenia):
/// - PRIAMY prechod (proti-smery, Top↔Bottom alebo Left↔Right):
/// B.entryEdge, B.center, B.exitEdge (90° prejazd cez stred)
/// - ROHOVÝ prechod (kolmé smery, napr. Top↔Right):
/// B.entryEdge, B.innerEntry, B.innerExit, B.exitEdge
/// (45° turnout layout, identický s odbočkou výhybky)
///
/// Štart a koniec:
/// • Prvá dlaždica (štart): pridá sa A.center, potom A.exitEdge.
/// • Posledná dlaždica (cieľ): pridá sa B.entryEdge, potom B.center.
///
/// Pre jednodlaždičovú cestu (count==1): vráti len [center].
///
/// HRANIČNÉ WAYPOINTY (kritické):
/// Tile-prechod A→B vygeneruje DVA waypointy s identickou Vector3
/// pozíciou, ktoré sú VŽDY zachované ako dva samostatné body:
/// - A.exitEdge(dir) ← patrí dlaždici A
/// - B.entryEdge(opposite) ← patrí dlaždici B
/// Aj keď sú geometricky totožné, logicky reprezentujú prechod cez
/// hranu a poradie je dôležité: zachováva smerovosť a umožňuje
/// správne priradenie currentTile cez waypointToTileIdx. Krivka,
/// výhybka aj križovatka spotrebúvajú SVOJ vstupný edge waypoint pre
/// korektné odvodenie exit-smeru. Z toho dôvodu deduplikujeme len
/// waypointy z ROVNAKEJ dlaždice (pozri AddWaypoint).
///
/// VÝSTUP:
/// waypoints – List waypointov v poradí pohybu
/// tileIdxPerWaypoint – pre každý waypoint index do tilePath, ku ktorému patrí
/// (potrebné pre UpdateCurrentTile)
///
void BuildWaypointPath(List tilePath, out List waypoints,
out List tileIdxPerWaypoint)
{
// Pracujeme cez lokálne premenné, nie cez 'out' parametre. Lokálna
// funkcia AddWaypoint nemôže zachytávať 'out'/'ref' parametre
// (CS1628), preto vyplníme lokálne zoznamy a na konci ich priradíme
// do 'out' parametrov.
var wp = new List();
var tIdx = new List();
if (tilePath == null || tilePath.Count == 0)
{
waypoints = wp;
tileIdxPerWaypoint = tIdx;
return;
}
if (tilePath.Count == 1)
{
wp.Add(TileCenter(tilePath[0]));
tIdx.Add(0);
waypoints = wp;
tileIdxPerWaypoint = tIdx;
return;
}
// Pomocný local fn na pridanie waypointu.
//
// PRAVIDLO (kritické pre tile-boundary semantiku):
// Waypointy s rovnakou Vector3 pozíciou sa NESMÚ zlučovať,
// ak patria DVOM RÔZNYM dlaždiciam. Konkrétne A.exitEdge(dir) a
// B.entryEdge(opposite(dir)) sú v priestore identické, ale logicky
// ide o dva po sebe idúce waypointy:
// - A.exitEdge patrí dlaždici A (tileIdx = A)
// - B.entryEdge patrí dlaždici B (tileIdx = B)
// Toto poradie zachováva smerovosť cesty a umožňuje korektné
// priradenie currentTile pri prekročení hranice (UpdateCurrentTile
// používa waypointToTileIdx mapovanie). Bez toho sa hraničný
// waypoint krivkovej dlaždice "stratí" a UpdateCurrentTile by
// hlásil zlú dlaždicu pre frame priamo po prekročení hrany.
//
// Deduplikujeme IBA vtedy, keď sú obidva waypointy z TEJ ISTEJ
// dlaždice a v priestore sa kryjú – vtedy ide o degenerovaný
// intra-tile prípad bez dopadu na sémantiku.
void AddWaypoint(Vector3 pt, int tileIdx)
{
if (wp.Count > 0)
{
Vector3 last = wp[wp.Count - 1];
int lastTileIdx = tIdx[tIdx.Count - 1];
bool sameTile = (lastTileIdx == tileIdx);
bool samePos = (pt - last).sqrMagnitude < 1e-8f;
if (sameTile && samePos) return; // intra-tile duplikát → preskoč
}
wp.Add(pt);
tIdx.Add(tileIdx);
}
for (int i = 0; i < tilePath.Count; i++)
{
Vector2Int tile = tilePath[i];
var conns = IndicatrixAPI.instance.GetTileByIndex(tile.x, tile.y).connections;
bool isFirst = (i == 0);
bool isLast = (i == tilePath.Count - 1);
// Smer odchodu z aktuálnej dlaždice (do nasledujúcej)
IndicatrixAPI.DirectionMask outDir = IndicatrixAPI.DirectionMask.None;
if (!isLast) outDir = GetDirection(tile, tilePath[i + 1]);
// Smer príchodu do aktuálnej dlaždice (z predchádzajúcej)
IndicatrixAPI.DirectionMask inDir = IndicatrixAPI.DirectionMask.None;
if (!isFirst) inDir = IndicatrixAPI.Opposite(GetDirection(tilePath[i - 1], tile));
// ────────────────────────────────────────────────────────────────
// KRIVKA: entryEdge → exitEdge (BEZ centra, BEZ rohového bodu)
//
// PathData spojí dva po sebe idúce waypointy lineárnym Lerp-om.
// Keďže entryEdge je stred jednej hrany dlaždice a exitEdge je
// stred susediacej (kolmnej) hrany, priamka medzi nimi prechádza
// vnútrom dlaždice diagonálne — vizuálne pod ~45°. To je presne
// požadovaný "ostrý" angular pohyb cez krivku.
//
// Predtým sme sem vkladali aj rohový bod (vrchol bunky), čo
// produkovalo dva segmenty s ostrým 90° lomom v rohu — pohyb
// skákal cez vrchol dlaždice von z dráhy. Rohový bod sa preto
// už NEpridáva.
//
// Funkcia CurveCornerPoint zostáva v zdrojáku pre prípadné
// budúce vizuálne pomocníky, ale BuildWaypointPath ju NEvolá.
// ────────────────────────────────────────────────────────────────
if (!isFirst && !isLast && IsCurveTile(conns))
{
// entryEdge je geometricky totožný bod ako A.exitEdge predchádzajúcej
// dlaždice, ale logicky patrí TEJTO krivkovej dlaždici –
// AddWaypoint ho zachová ako samostatný waypoint, pretože tileIdx
// je iný od predchádzajúceho waypointu.
AddWaypoint(EdgePoint(tile, inDir), i);
AddWaypoint(EdgePoint(tile, outDir), i);
continue;
}
// ────────────────────────────────────────────────────────────────
// VÝHYBKA (RAIL SWITCH) – 3 spojenia, plne obojsmerná.
//
// Vnútorná logika výhybky závisí od toho, či ide o priamy prechod
// alebo o odbočku:
//
// 1. PRIAMY PRECHOD (proti-smery, napr. Left↔Right na
// RailSwitchHorizontalBottom alebo Top↔Bottom na
// RailSwitchVerticalBottom):
// entryEdge → center → exitEdge
// Identické správanie ako klasická priama koľaj. Stred
// dlaždice je súčasťou cesty.
//
// 2. ODBOČKA (kolmé smery, napr. Left↔Bottom alebo Right↔Top):
// entryEdge → innerEntry(t) → innerExit(t) → exitEdge
// kde t = SWITCH_TURNOUT_T (= 0.25). Vizuálne to vytvorí
// pohyb, ktorý:
// • vstúpi do dlaždice po hrane vstupu,
// • prejde krátky úsek pozdĺž osi vstupnej hrany,
// • diagonálne (45°) sa stočí dovnútra,
// • prejde krátky úsek pozdĺž osi výstupnej hrany,
// • opustí dlaždicu cez výstupnú hranu.
// Pohyb používa IBA priame čiarové segmenty (žiadne
// bezier krivky, žiadne splajny, žiadna interpolácia okrem
// lineárneho Lerp medzi dvoma susednými waypointmi).
// Vizuálne to pripomína skutočný železničný výhybkový
// úsek (turnout) namiesto ostrého 90° rohu cez stred.
//
// Tile-prechodové pravidlá zostávajú nezmenené:
// A.center → A.edge → B.edge (medzi-tile)
// entryEdge a exitEdge sú ŠTANDARDNÉ hranové waypointy zhodné
// s ostatnými tile-typmi → tile-boundary semantika je zachovaná.
// ────────────────────────────────────────────────────────────────
if (!isFirst && !isLast && IsSwitchTile(conns))
{
// ── ODBOČKA: entry a exit sú kolmé smery ─────────────────
if (IsPerpendicular(inDir, outDir))
{
const float SWITCH_TURNOUT_T = 0.25f;
AddWaypoint(EdgePoint(tile, inDir), i);
AddWaypoint(SwitchInnerEdgePoint(tile, inDir, SWITCH_TURNOUT_T), i);
AddWaypoint(SwitchInnerEdgePoint(tile, outDir, SWITCH_TURNOUT_T), i);
AddWaypoint(EdgePoint(tile, outDir), i);
continue;
}
// ── PRIAMY PRECHOD: entry a exit sú proti-smery ─────────
// (napr. Left ↔ Right na horizontálnej výhybke)
// → správa sa ako klasická priama dlaždica: edge → center → edge
AddWaypoint(EdgePoint(tile, inDir), i);
AddWaypoint(TileCenter(tile), i);
AddWaypoint(EdgePoint(tile, outDir), i);
continue;
}
// ────────────────────────────────────────────────────────────────
// KRIŽOVATKA (RAIL CROSSROAD) – 4 spojenia (všetky 4 smery).
//
// Plná 4-cestná križovatka. Pohyb cez ňu závisí od entry/exit
// smerov rovnako ako pri výhybke, len s jedným spojením naviac:
//
// 1. PRIAMY PRECHOD – 90° osový prejazd (proti-smery):
// • Top ↔ Bottom (vertikálny prejazd)
// • Left ↔ Right (horizontálny prejazd)
// → entryEdge → center → exitEdge
// (rovnako ako klasická priama dlaždica – cez stred)
//
// 2. ROHOVÝ PRECHOD – 45° turnout (kolmé smery):
// • Top ↔ Right • Top ↔ Left
// • Bottom ↔ Right • Bottom ↔ Left
// → entryEdge → innerEntry(t) → innerExit(t) → exitEdge
// (presne ten istý layout ako odbočka výhybky –
// stredný segment je 45° diagonálny, vstupný a výstupný
// segment idú pozdĺž osí príslušných hrán)
//
// Princíp je identický s odbočkou výhybky:
// • IBA priame čiarové segmenty (žiadne bezier krivky,
// žiadne splajny, žiadny smoothing).
// • Stred dlaždice sa NEPOUŽÍVA pre rohové prechody –
// pohyb sa stočí pred dosiahnutím stredu.
// • Tile-prechodové pravidlá zostávajú nezmenené:
// entryEdge a exitEdge sú štandardné hranové waypointy
// zhodné s ostatnými typmi.
//
// POZN: Crossroad detekcia musí prísť AŽ ZA switch detekciou,
// pretože ako oddelenie používame počet bitov:
// curve = 2, switch = 3, crossroad = 4.
// ────────────────────────────────────────────────────────────────
if (!isFirst && !isLast && IsCrossroadTile(conns))
{
// ── ROHOVÝ PRECHOD: entry a exit sú kolmé smery ──────────
if (IsPerpendicular(inDir, outDir))
{
const float CROSSROAD_TURNOUT_T = 0.25f;
AddWaypoint(EdgePoint(tile, inDir), i);
AddWaypoint(SwitchInnerEdgePoint(tile, inDir, CROSSROAD_TURNOUT_T), i);
AddWaypoint(SwitchInnerEdgePoint(tile, outDir, CROSSROAD_TURNOUT_T), i);
AddWaypoint(EdgePoint(tile, outDir), i);
continue;
}
// ── PRIAMY PRECHOD: entry a exit sú proti-smery ─────────
// Top↔Bottom alebo Left↔Right → klasický prejazd cez stred
AddWaypoint(EdgePoint(tile, inDir), i);
AddWaypoint(TileCenter(tile), i);
AddWaypoint(EdgePoint(tile, outDir), i);
continue;
}
// ────────────────────────────────────────────────────────────────
// PRIAMA / KONCOVÁ DLAŽDICA: edge → center → edge
// ────────────────────────────────────────────────────────────────
if (isFirst)
{
// Štart – začneme v centre
AddWaypoint(TileCenter(tile), i);
AddWaypoint(EdgePoint(tile, outDir), i);
}
else if (isLast)
{
// Cieľ – vstúpime cez hranu a skončíme v centre
AddWaypoint(EdgePoint(tile, inDir), i);
AddWaypoint(TileCenter(tile), i);
}
else
{
// Stredná priama dlaždica: entryEdge → center → exitEdge
AddWaypoint(EdgePoint(tile, inDir), i);
AddWaypoint(TileCenter(tile), i);
AddWaypoint(EdgePoint(tile, outDir), i);
}
}
// Priradíme lokálne zoznamy do 'out' parametrov.
waypoints = wp;
tileIdxPerWaypoint = tIdx;
}
///
/// Vráti vrchol bunky pre krivku (vrchol, kde by sa dve hrany krivky
/// stretli pri 90° lome). NEPOUŽÍVA sa v BuildWaypointPath – pohyb cez
/// krivku je priamy Lerp medzi entryEdge a exitEdge (45° diagonála).
/// Funkcia ostáva pre prípadné budúce vizuálne pomôcky / debug overlay.
///
/// Pre RailCurveRightBottom (Right + Bottom):
/// Right hrana je x = tile.x+1, Bottom hrana je z = tile.z
/// → vrchol bunky = (tile.x+1, y, tile.z)
///
Vector3 CurveCornerPoint(Vector2Int tile, IndicatrixAPI.DirectionMask conns)
{
float fx = tile.x, fz = tile.y;
bool right = (conns & IndicatrixAPI.DirectionMask.Right) != 0;
bool left = (conns & IndicatrixAPI.DirectionMask.Left) != 0;
bool top = (conns & IndicatrixAPI.DirectionMask.Top) != 0;
bool bottom = (conns & IndicatrixAPI.DirectionMask.Bottom) != 0;
// X súradnica rohu: ak Right → fx+1, ak Left → fx
float cx = right ? fx + 1.0f : fx + 0.0f;
// Z súradnica rohu: ak Top → fz+1, ak Bottom → fz
float cz = top ? fz + 1.0f : fz + 0.0f;
// Y v rohu = priamo výška vertexu terénu v tom rohu (biliniárna
// interpolácia v rohu sa zredukuje na hodnotu samotného rohového
// vertexu). Korektné aj na svahu.
float u = right ? 1f : 0f;
float v = top ? 1f : 0f;
float y = GetTileSurfaceY(tile, u, v) + TRACK_OFFSET_Y;
return new Vector3(cx, y, cz);
}
// =====================================================================
// (Pôvodný BuildPathData zachovaný pre spätnú kompatibilitu, ale už
// nepoužívaný v hlavnej ceste – ApplyNewPath používa BuildWaypointPath.)
// =====================================================================
PathData BuildPathData(List tilePath)
{
if (tilePath == null || tilePath.Count == 0) return null;
BuildWaypointPath(tilePath, out var pts, out _);
return pts.Count > 0 ? new PathData(pts) : null;
}
// =====================================================================
// VIZUÁLNA SÚPRAVA – pomocná metóda vytvorenia
// =====================================================================
GameObject CreateConsistPart(string name, Color color, Vector3 position)
{
GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube);
go.name = name;
go.transform.localScale = CONSIST_SCALE;
go.transform.position = position;
Destroy(go.GetComponent());
var rend = go.GetComponent();
rend.material = new Material(Shader.Find("Standard"));
rend.material.color = color;
go.SetActive(false);
return go;
}
// =====================================================================
// SPRÁVA VLAKOV
// =====================================================================
///
/// Vytvorí vlak v depe [dx,dz].
///
/// trainTypeIndex / wagonTypeIndex sú indexy zvolené v TrainTypeDropdown /
/// WagonTypeDropdown (viď DepotRailConstructionMenuUI). Podľa nich sa
/// z katalógov (TrainCatalog / WagonCatalog v TrainStock.cs) zostaví
/// dátová štruktúra súpravy (TrainInstance) a uloží do td.consist.
///
/// Všetky vagóny v súprave sú zatiaľ rovnakého zvoleného typu; ich počet
/// je daný wagonCount. UI sa nemení – mapovanie atribútov na konkrétne UI
/// prvky nie je potrebné, detailné okno si ich prečíta z td.consist neskôr.
///
public bool CreateTrain(int dx, int dz, int trainTypeIndex, int wagonTypeIndex)
{
int key = DepotKey(dx, dz);
if (trains.ContainsKey(key)) return false;
if (GetTileID(dx, dz) != 3) return false;
int count = Mathf.Clamp(wagonCount, 1, 10);
TrainData td = new TrainData(dx, dz);
Vector3 depotPos = TileCenter(new Vector2Int(dx, dz));
// -----------------------------------------------------------------
// DÁTOVÁ ŠTRUKTÚRA SÚPRAVY (TrainStock.cs)
// -----------------------------------------------------------------
TrainSpec trainSpec = TrainCatalog.ByIndex(trainTypeIndex);
WagonSpec wagonSpec = WagonCatalog.ByIndex(wagonTypeIndex);
if (trainSpec == null)
{
Debug.LogWarning($"[TrainSystem] Neznámy index typu vlaku ({trainTypeIndex}) – použijem prvý z katalógu.");
trainSpec = TrainCatalog.ByIndex(0);
}
if (wagonSpec == null)
{
Debug.LogWarning($"[TrainSystem] Neznámy index typu vagónu ({wagonTypeIndex}) – použijem prvý z katalógu.");
wagonSpec = WagonCatalog.ByIndex(0);
}
td.consist = new TrainInstance(trainSpec);
for (int i = 0; i < count; i++)
td.consist.Wagons.Add(new WagonInstance(wagonSpec));
// Skrytá kocka – pohybová logika
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube.transform.localScale = CONSIST_SCALE;
cube.transform.position = depotPos;
cube.name = $"TrainHead_{dx}_{dz}";
Destroy(cube.GetComponent());
cube.GetComponent().enabled = false;
td.trainObject = cube;
// Lokomotíva – CYAN
td.locomotive = CreateConsistPart($"Loco_{dx}_{dz}", new Color(0f, 1f, 1f), depotPos);
// Vagóny – GREY
for (int i = 0; i < count; i++)
{
var wagon = CreateConsistPart($"Wagon_{dx}_{dz}_{i}",
new Color(0.502f, 0.502f, 0.502f), depotPos);
td.wagons.Add(wagon);
}
trains[key] = td;
Debug.Log($"[TrainSystem] Vlak '{td.consist.Name}' vytvorený v depe [{dx},{dz}] – "
+ $"1 lokomotíva + {count}× '{wagonSpec.Name}' (spacing={wagonSpacing}).");
return true;
}
///
/// Spätne kompatibilný preťažený podpis – vytvorí vlak s prvým typom
/// vlaku aj vagónu z katalógu (index 0). Volá sa z miest, kde voľba
/// typu nie je k dispozícii (napr. klávesová skratka Q bez UI kontextu).
///
public bool CreateTrain(int dx, int dz)
{
return CreateTrain(dx, dz, 0, 0);
}
public TrainData GetTrain(int dx, int dz)
{
int key = DepotKey(dx, dz);
return trains.TryGetValue(key, out var td) ? td : null;
}
///
/// Vráti súradnice [depotX, depotZ] VŠETKÝCH aktuálne existujúcich
/// vlakových dep. Keďže platí "1 train depo = 1 train", počet prvkov
/// zoznamu = počet vlakov v hre.
///
/// Slúži pre informačné UI (StatusStationsMenuUI). Vnútorný slovník
/// `trains` ostáva privátny – navonok dávame len read-only kópiu
/// kľúčových údajov, takže volajúci nemôže poškodiť interný stav.
///
public List GetAllDepotCoords()
{
var result = new List(trains.Count);
foreach (var kvp in trains)
{
TrainData td = kvp.Value;
result.Add(new Vector2Int(td.depotX, td.depotZ));
}
return result;
}
///
/// Celkový počet vlakových dep (= počet vlakov). Pohodlný getter, aby
/// UI nemuselo kvôli počtu vytvárať celý zoznam cez GetAllDepotCoords().
///
public int GetDepotCount()
{
return trains.Count;
}
///
/// Vráti dátové štruktúry (TrainInstance) VŠETKÝCH aktuálne existujúcich
/// vlakov. Keďže platí "1 train depo = 1 train", počet prvkov zoznamu =
/// počet vlakov v hre.
///
/// Slúži pre informačné UI (StatusTrainsMenuUI), ktoré z každej
/// TrainInstance prečíta názov vlaku (Name), počet vagónov
/// (Wagons.Count) a typ suroviny (Wagons[0].Type.Name).
///
/// Vnútorný slovník `trains` ostáva privátny – navonok dávame len
/// read-only kópiu zoznamu referencií na TrainInstance. Volajúci tak
/// nemôže pridať/odobrať vlak (zmeniť `trains`), čítať atribúty
/// jednotlivých vlakov ale môže. Analógia k GetAllDepotCoords().
///
public List GetAllTrainConsists()
{
var result = new List(trains.Count);
foreach (var kvp in trains)
{
TrainData td = kvp.Value;
if (td != null && td.consist != null)
result.Add(td.consist);
}
return result;
}
///
/// EKONOMIKA (read-only snapshot): pre KAŽDÝ existujúci vlak spáruje jeho
/// DEPO súradnice [depotX, depotZ] (= kľúč pre )
/// s jeho dátovou inštanciou (OperatingCosts,
/// ServiceLife, ServicingInterval). Interný slovník `trains` ostáva privátny.
///
/// Slúži pre EconomySystem: mesačné prevádzkové náklady + automatické
/// poslanie vlaku do depa po uplynutí servisného intervalu alebo životnosti
/// (EconomySystem zavolá s vrátenými súradnicami).
///
public List GetActiveTrainInfos()
{
var result = new List(trains.Count);
foreach (var kvp in trains)
{
TrainData td = kvp.Value;
if (td != null && td.consist != null)
result.Add(new ActiveTrainInfo(td.depotX, td.depotZ, td.consist));
}
return result;
}
/// Read-only dvojica [depo súradnice + dátová inštancia vlaku] pre EconomySystem.
public readonly struct ActiveTrainInfo
{
public readonly int DepotX;
public readonly int DepotZ;
public readonly TrainInstance Consist;
public ActiveTrainInfo(int depotX, int depotZ, TrainInstance consist)
{
DepotX = depotX;
DepotZ = depotZ;
Consist = consist;
}
}
public bool AddStation(int depotX, int depotZ, int stX, int stZ)
{
TrainData td = GetTrain(depotX, depotZ);
if (td == null) return false;
if (GetTileID(stX, stZ) != 2) return false;
Vector2Int st = new Vector2Int(stX, stZ);
if (!td.stations.Contains(st))
{
td.stations.Add(st);
Debug.Log($"[TrainSystem] Stanica [{stX},{stZ}] pridaná pre vlak v depe [{depotX},{depotZ}]. Celkom: {td.stations.Count}");
}
return true;
}
public bool StartTrain(int dx, int dz)
{
TrainData td = GetTrain(dx, dz);
if (td == null) return false;
if (td.stations.Count < 2)
{
Debug.LogWarning($"[TrainSystem] Vlak v depe [{dx},{dz}] nemá aspoň 2 stanice!");
return false;
}
bool isResume = !td.isAtDepot && td.currentPath != null && td.currentPath.Count > 0;
td.isRunning = true;
td.isWaiting = false;
td.isReturningToDepot = false;
if (isResume)
{
Debug.Log($"[TrainSystem] Vlak z depa [{dx},{dz}] OBNOVENÝ (resume).");
}
else
{
td.isAtDepot = false;
td.currentStationIndex = 0;
td.reverseDirection = false;
td.moveTimer = 0f;
td.waitTimer = 0f;
td.pendingPath = null;
td.pendingReturnToDepot = false;
// Reset path state
td.activePath = null;
td.locomotiveDistance = 0f;
ComputeNextPathAsync(td);
Debug.Log($"[TrainSystem] Vlak z depa [{dx},{dz}] SPUSTENÝ (prvý štart).");
}
return true;
}
public bool StopTrain(int dx, int dz)
{
TrainData td = GetTrain(dx, dz);
if (td == null) return false;
td.isRunning = false;
td.isComputingPath = false;
td.pendingPath = null;
td.pendingReturnToDepot = false;
Debug.Log($"[TrainSystem] Vlak z depa [{dx},{dz}] ZASTAVENÝ.");
return true;
}
public ReturnToDepotResult ReturnToDepot(int dx, int dz)
{
TrainData td = GetTrain(dx, dz);
if (td == null) return ReturnToDepotResult.Error;
if (GetTileID(dx, dz) != 3)
{
Debug.LogWarning($"[TrainSystem] Depo [{dx},{dz}] neexistuje – vlak zastane.");
td.isRunning = false;
td.isStoppedAwaitingDepotReturn = false;
return ReturnToDepotResult.Error;
}
if (td.isStoppedAwaitingDepotReturn)
{
td.isStoppedAwaitingDepotReturn = false;
td.pendingReturnToDepot = false;
td.isRunning = true;
td.isWaiting = false;
td.waitTimer = 0f;
td.isGoingToDepotViaStation = false;
td.isComputingPath = false;
td.pendingPath = null;
DispatchToDepot(td);
Debug.Log($"[TrainSystem] Vlak z depa [{dx},{dz}] → depo priamo (R po zastavení bez staníc).");
return ReturnToDepotResult.Dispatched;
}
Vector2Int depotTile = new Vector2Int(dx, dz);
if (td.isWaiting)
{
td.pendingReturnToDepot = false;
td.isGoingToDepotViaStation = true;
td.isStoppedAwaitingDepotReturn = false;
Debug.Log($"[TrainSystem] Vlak z depa [{dx},{dz}] čaká na stanici → po odpočítaní pôjde do depa.");
return ReturnToDepotResult.Dispatched;
}
if (IsDepotOnCurrentPath(td, depotTile))
{
td.pendingReturnToDepot = false;
td.isGoingToDepotViaStation = false;
td.isStoppedAwaitingDepotReturn = false;
TrimPathToDepot(td, depotTile);
td.isReturningToDepot = true;
// Prebuduj PathData pre skrátenú cestu
RebuildActivePathFromCurrentPath(td);
Debug.Log($"[TrainSystem] Vlak z depa [{dx},{dz}] → depo PRIAMO (depo je na aktuálnej ceste).");
return ReturnToDepotResult.Dispatched;
}
td.pendingReturnToDepot = true;
td.isGoingToDepotViaStation = false;
td.isStoppedAwaitingDepotReturn = false;
Debug.Log($"[TrainSystem] Vlak z depa [{dx},{dz}]: depo nie je v smere jazdy → vlak dokončí cestu na plánovanú stanicu a potom pôjde do depa.");
return ReturnToDepotResult.Dispatched;
}
bool IsDepotOnCurrentPath(TrainData td, Vector2Int depotTile)
{
if (td.currentPath == null || td.pathIndex >= td.currentPath.Count) return false;
for (int i = td.pathIndex; i < td.currentPath.Count; i++)
if (td.currentPath[i] == depotTile) return true;
return false;
}
void TrimPathToDepot(TrainData td, Vector2Int depotTile)
{
if (td.currentPath == null) return;
for (int i = td.pathIndex; i < td.currentPath.Count; i++)
{
if (td.currentPath[i] == depotTile)
{
td.currentPath = td.currentPath.GetRange(0, i + 1);
return;
}
}
}
void DispatchToDepot(TrainData td)
{
int key = DepotKey(td.depotX, td.depotZ);
Vector2Int goal = new Vector2Int(td.depotX, td.depotZ);
Vector2Int start = td.currentTile;
var snap = SnapshotTileGrid();
td.isReturningToDepot = true;
td.isGoingToDepotViaStation = false;
td.isComputingPath = true;
Task.Run(() =>
{
var path = AStarPathThreaded(snap, start, goal);
_pendingPathResults.Enqueue(new PathResult
{
depotKey = key,
path = path,
isReturnToDepot = true,
destination = PathDestination.Depot
});
});
Debug.Log($"[TrainSystem] Vlak z depa [{td.depotX},{td.depotZ}] → depo.");
}
///
/// Odstráni vlak z hry. Vymazanie je povolené VÝLUČNE vtedy, keď je
/// vlak fyzicky v depe (currentTile == depot tile) a nie je v pohybe
/// (isRunning == false). Flag isAtDepot sa NEPOUŽÍVA – v praxi sa
/// dostal mimo synchronizáciu pri scenári, keď je vlak po stlačení S
/// zaregistrovaný ako "vyšiel z depa" (isAtDepot = false), ale A*
/// nenašiel cestu von, takže vlak zostal fyzicky stáť na depote.
/// Po následnom P (StopTrain) by stary guard zablokoval T (RemoveTrain),
/// hoci vlak depo nikdy neopustil.
///
/// Nový guard kontroluje:
/// 1. Vlak v evidencii existuje (td != null).
/// 2. Vlak nie je v pohybe (!td.isRunning).
/// 3. Vlak je fyzicky na tile depota (td.currentTile == (dx, dz)).
/// 4. Tile (dx, dz) je stále depo (GetTileID == 3).
///
/// Vyrieši oba scenáre používateľa:
/// • Q (CreateTrain) → T (RemoveTrain) bezprostredne:
/// vlak ešte nikdy nevyšiel, currentTile = (dx,dz), isRunning = false
/// → povolené.
/// • S (StartTrain) → A* nenájde cestu → P (StopTrain) → T:
/// vlak depo neopustil, currentTile = (dx,dz), isRunning = false
/// → povolené.
/// V iných stavoch (vlak na trati, hoci aj zastavený) vymazanie
/// zlyhá, čo je v súlade s požiadavkou.
///
public bool RemoveTrain(int dx, int dz)
{
TrainData td = GetTrain(dx, dz);
if (td == null) return false;
if (td.isRunning)
{
Debug.LogWarning($"[TrainSystem] Vlak v depe [{dx},{dz}] sa nedá odstrániť – stále beží. Najprv ho zastavte (P).");
return false;
}
Vector2Int depotTile = new Vector2Int(dx, dz);
if (td.currentTile != depotTile)
{
Debug.LogWarning($"[TrainSystem] Vlak v depe [{dx},{dz}] sa nedá odstrániť – nie je fyzicky v depe (aktuálne na [{td.currentTile.x},{td.currentTile.y}]).");
return false;
}
if (GetTileID(dx, dz) != 3)
{
Debug.LogWarning($"[TrainSystem] Tile [{dx},{dz}] už nie je depo – vlak sa nedá odstrániť.");
return false;
}
// Defenzívne: ak by ešte stále prebiehal asynchrónny A* výpočet,
// jeho výsledok bude zahodený (TryGetValue v ApplyPendingPathResults
// neuspeje, lebo sme záznam už odstránili z trains).
td.isComputingPath = false;
td.pendingPath = null;
// Uvoľníme VŠETKY štruktúry, ktoré vytvoril CreateTrain – symetricky.
DestroyTrainStructures(td);
trains.Remove(DepotKey(dx, dz));
Debug.Log($"[TrainSystem] Vlak z depa [{dx},{dz}] ODSTRÁNENÝ.");
return true;
}
///
/// Uvoľní všetky štruktúry vlakovej súpravy, ktoré boli vytvorené
/// v CreateTrain. Táto metóda je presným ZRKADLOM CreateTrain –
/// pre každú štruktúru vytvorenú pri vytvorení vlaku tu existuje
/// zodpovedajúce zrušenie:
///
/// CreateTrain vytvorí → DestroyTrainStructures uvoľní
/// ─────────────────────────────────────────────────────────────
/// td.trainObject (GameObject) → Destroy(td.trainObject)
/// td.locomotive (GameObject) → Destroy(td.locomotive)
/// td.wagons (GameObjecty) → Destroy(každý) + zoznam vyčistený
/// td.consist (TrainInstance → Wagons vyčistené + referencia null
/// + WagonInstance)
///
/// Týmto je zaručené, že ak sa pri vytvorení vlaku alokuje nejaká
/// štruktúra, tá istá štruktúra sa pri odstránení vlaku aj korektne
/// uvoľní – žiadne "visiace" GameObjecty ani mŕtve referencie
/// v td.wagons po Destroy().
///
void DestroyTrainStructures(TrainData td)
{
if (td == null) return;
// Skrytá kocka – pohybová logika (zrkadlí: cube → td.trainObject).
if (td.trainObject != null) Destroy(td.trainObject);
td.trainObject = null;
// Vizuálna lokomotíva (zrkadlí: CreateConsistPart → td.locomotive).
if (td.locomotive != null) Destroy(td.locomotive);
td.locomotive = null;
// Vizuálne vagóny (zrkadlí: CreateConsistPart slučka → td.wagons).
if (td.wagons != null)
{
foreach (var w in td.wagons)
if (w != null) Destroy(w);
// Vyčistíme zoznam, aby v td.wagons neostali mŕtve referencie
// na už zničené GameObjecty.
td.wagons.Clear();
}
// Dátová štruktúra súpravy (zrkadlí: new TrainInstance + WagonInstance
// slučka → td.consist). Uvoľníme vagóny a samotnú referenciu.
if (td.consist != null)
{
td.consist.Wagons?.Clear();
td.consist = null;
}
// Vyčistíme aj pohybový / pathfinding stav, aby objekt TrainData
// neostal v nekonzistentnom stave, ak naň ešte drží referenciu
// prebiehajúci asynchrónny výpočet.
td.activePath = null;
td.currentPath = null;
td.pendingPath = null;
}
// =====================================================================
// NOTIFIKÁCIA PRI ZMENE MAPY – DEBOUNCE
// =====================================================================
public void OnMapChanged()
{
_mapChangePending = true;
_mapChangedDebounceTimer = DEBOUNCE_DELAY;
}
// =====================================================================
// UPDATE
// =====================================================================
void Update()
{
if (_mapChangePending)
{
_mapChangedDebounceTimer -= Time.deltaTime;
if (_mapChangedDebounceTimer <= 0f)
{
_mapChangePending = false;
_mapChangedDebounceTimer = 0f;
TriggerMapChangedReroute();
}
}
ApplyPendingPathResults();
foreach (var kvp in trains)
{
TrainData td = kvp.Value;
if (td.trainObject == null) continue;
if (!td.isRunning) continue;
UpdateTrain(td);
}
}
// =====================================================================
// POHYB VLAKU – DISTANCE-BASED
// =====================================================================
void UpdateTrain(TrainData td)
{
// ── Čakanie (stanica / pauza) ─────────────────────────────────────
if (td.isWaiting)
{
td.waitTimer -= Time.deltaTime;
// ── OBCHOD: po 2 s čakania na cieľovej stanici ────────────────
// Spustí sa práve raz za zastávku. tradeDoneAtStation je nastavené
// na false iba pri príchode na cieľovú stanicu (OnPathComplete),
// takže pri BREAK_WAIT ani pri ceste do depa sa obchod nespustí.
if (!td.tradeDoneAtStation
&& (STATION_WAIT - td.waitTimer) >= TRADE_DELAY)
{
td.tradeDoneAtStation = true;
TryExecuteTradeAtStation(td);
}
if (td.waitTimer <= 0f)
{
td.isWaiting = false;
td.waitTimer = 0f;
if (td.isReturningToDepot)
DispatchToDepot(td);
else if (td.isGoingToDepotViaStation)
DispatchToDepot(td);
else
AdvanceStation(td);
}
return;
}
// ── Čakáme na výpočet novej trasy ────────────────────────────────
if (td.activePath == null)
{
if (td.isComputingPath) return;
// Ak niet aktívnej trasy a nie sme počítajú, skúsime spustiť prepočet
OnPathComplete(td);
return;
}
// ── Pohyb lokomotívy pozdĺž trasy ────────────────────────────────
td.locomotiveDistance += td.trainSpeed * Time.deltaTime;
// Aktualizujeme currentTile podľa najbližšieho bodu na trase
UpdateCurrentTile(td);
// Pohyb vizuálnej hlavy (skrytá kocka)
Vector3 headPos = td.activePath.GetPositionAtDistance(td.locomotiveDistance);
td.trainObject.transform.position = headPos;
// ── Aktualizácia vizuálnej súpravy ───────────────────────────────
UpdateConsistVisuals(td);
// ── Detekcia konca trasy ──────────────────────────────────────────
if (td.locomotiveDistance >= td.activePath.totalLength)
{
// Lokomotíva dorazila na koniec – zafixujeme na posledný bod
td.locomotiveDistance = td.activePath.totalLength;
td.currentTile = td.currentPath != null && td.currentPath.Count > 0
? td.currentPath[td.currentPath.Count - 1]
: td.currentTile;
// Aplicujeme pendingPath ak existuje
if (td.pendingPath != null)
{
List pending = td.pendingPath;
td.pendingPath = null;
ApplyNewPath(td, pending);
return;
}
td.activePath = null;
OnPathComplete(td);
}
}
///
/// Udržiava currentTile synchronizované s aktuálnou pozíciou lokomotívy na trase.
/// Použije waypointToTileIdx mapovanie pre presné určenie aktuálnej dlaždice
/// pri waypoint-based pohybe.
///
void UpdateCurrentTile(TrainData td)
{
if (td.currentPath == null || td.activePath == null) return;
if (td.waypointToTileIdx == null || td.waypointToTileIdx.Length == 0) return;
// Nájdeme index waypointu, kde sa lokomotíva práve nachádza
float[] cumLens = td.activePath.cumulativeLengths;
int lastPassedIdx = 0;
for (int i = 0; i < cumLens.Length; i++)
{
if (cumLens[i] <= td.locomotiveDistance)
lastPassedIdx = i;
else
break;
}
// Mapuj waypoint-index na sub-path tile-index, potom na currentPath-index
int subPathTileIdx = td.waypointToTileIdx[lastPassedIdx];
int tileIdx = td.pathIndexAtPathStart + subPathTileIdx;
if (tileIdx >= 0 && tileIdx < td.currentPath.Count)
td.currentTile = td.currentPath[tileIdx];
}
// =====================================================================
// VIZUÁLNA SÚPRAVA – DISTANCE-BASED
// =====================================================================
///
/// Aktualizuje pozíciu, rotáciu a viditeľnosť každého člena súpravy.
///
/// Člen i:
/// offsetDist = i * wagonSpacing
/// memberDist = locomotiveDistance - offsetDist (min 0)
/// pos = activePath.GetPositionAtDistance(memberDist)
/// rot = Quaternion.LookRotation(activePath.GetDirectionAtDistance(memberDist))
///
/// Zobrazenie: člen i sa zobrazí keď locomotiveDistance >= i * wagonSpacing.
/// Skrývanie pri návrate do depa: keď memberDist >= totalLength - DEPOT_HIDE_MARGIN.
///
void UpdateConsistVisuals(TrainData td)
{
if (td.activePath == null) return;
const float DEPOT_HIDE_RADIUS = 0.55f;
bool isReturning = td.isReturningToDepot;
Vector3 depotCenter = TileCenter(new Vector2Int(td.depotX, td.depotZ));
int totalMembers = 1 + td.wagons.Count;
for (int i = 0; i < totalMembers; i++)
{
float offsetDist = i * wagonSpacing;
GameObject member = (i == 0) ? td.locomotive : td.wagons[i - 1];
if (member == null) continue;
// ── Kritérium zobrazenia ───────────────────────────────────────
bool shouldBeVisible = td.locomotiveDistance >= offsetDist;
float memberDist = Mathf.Max(0f, td.locomotiveDistance - offsetDist);
// ── Skrývanie pri návrate do depa ─────────────────────────────
if (isReturning && shouldBeVisible)
{
Vector3 memberPos = td.activePath.GetPositionAtDistance(memberDist);
if (Vector3.Distance(memberPos, depotCenter) < DEPOT_HIDE_RADIUS)
shouldBeVisible = false;
}
if (member.activeSelf != shouldBeVisible)
member.SetActive(shouldBeVisible);
if (!shouldBeVisible) continue;
// ── Pozícia ───────────────────────────────────────────────────
Vector3 pos = td.activePath.GetPositionAtDistance(memberDist);
if (float.IsNaN(pos.x) || float.IsNaN(pos.y) || float.IsNaN(pos.z))
{
Debug.LogWarning($"[TrainSystem] NaN pozícia pre člen {i} – preskočené.");
continue;
}
// ── Rotácia – priamo zo smeru segmentu, bez akumulácie ────────
Vector3 dir = td.activePath.GetDirectionAtDistance(memberDist);
Quaternion rot = Quaternion.LookRotation(dir, Vector3.up);
member.transform.position = pos;
member.transform.rotation = rot;
}
}
// =====================================================================
// DOKONČENIE CESTY
// =====================================================================
void OnPathComplete(TrainData td)
{
if (td.isReturningToDepot)
{
td.isRunning = false;
td.isAtDepot = true;
td.isReturningToDepot = false;
td.isGoingToDepotViaStation = false;
td.currentTile = new Vector2Int(td.depotX, td.depotZ);
td.trainObject.transform.position = TileCenter(td.currentTile);
td.activePath = null;
td.locomotiveDistance = 0f;
HideAllConsist(td);
Debug.Log($"[TrainSystem] Vlak z depa [{td.depotX},{td.depotZ}] dorazil do DEPA – súprava skrytá.");
return;
}
if (td.isGoingToDepotViaStation)
{
bool onStation = td.stations.Contains(td.currentTile)
&& GetTileID(td.currentTile.x, td.currentTile.y) == 2;
if (onStation)
{
Debug.Log($"[TrainSystem] Vlak dorazil na stanicu [{td.currentTile.x},{td.currentTile.y}] pred depom – čaká {STATION_WAIT}s.");
td.trainObject.transform.position = TileCenter(td.currentTile);
td.isWaiting = true;
td.waitTimer = STATION_WAIT;
// Zastávka pred návratom do depa – obchod sa nerealizuje.
td.tradeDoneAtStation = true;
}
else
{
Debug.LogWarning($"[TrainSystem] Vlak nedosiahol stanicu pred depom. Zastane.");
td.isRunning = false;
td.isGoingToDepotViaStation = false;
}
return;
}
Vector2Int targetStation = td.stations[td.currentStationIndex];
if (td.currentTile == targetStation)
{
if (td.pendingReturnToDepot)
{
td.pendingReturnToDepot = false;
td.isGoingToDepotViaStation = true;
Debug.Log($"[TrainSystem] Vlak dorazil na stanicu [{targetStation.x},{targetStation.y}] (pending návrat do depa) – čaká {STATION_WAIT}s, potom depo.");
// Posledná zastávka pred depom – obchod sa nerealizuje.
td.tradeDoneAtStation = true;
}
else
{
Debug.Log($"[TrainSystem] Vlak dorazil na stanicu [{targetStation.x},{targetStation.y}] – čaká {STATION_WAIT}s.");
// Riadna zastávka na cieľovej stanici – po 2 s prebehne obchod.
td.tradeDoneAtStation = false;
}
td.trainObject.transform.position = TileCenter(td.currentTile);
td.isWaiting = true;
td.waitTimer = STATION_WAIT;
}
else
{
Debug.LogWarning($"[TrainSystem] Vlak nedosiahol stanicu [{targetStation.x},{targetStation.y}]. Čaká {BREAK_WAIT}s.");
td.isWaiting = true;
td.waitTimer = BREAK_WAIT;
// Núdzové prerušenie – nie je to zastávka na stanici, žiadny obchod.
td.tradeDoneAtStation = true;
}
}
// =====================================================================
// OBCHOD NA STANICI – TRADE (TradeSystem)
// =====================================================================
///
/// Vykoná výmenu tovaru medzi súpravou vlaku a továrňami stanice, na
/// ktorej vlak práve čaká. Volá sa z UpdateTrain po 2 s čakania
/// (TRADE_DELAY), práve raz za zastávku.
///
/// Postup:
/// 1) Zisti tile, na ktorom vlak stojí (cieľová stanica).
/// 2) Nájdi StationInstance pre tento tile v StationRegistry.
/// Ak ešte nie je zaregistrovaná (napr. stanica pribudla bez
/// následného scanu), dorovná sa cez RescanAll.
/// 3) Odovzdaj súpravu + stanicu do TradeSystem.Execute.
/// 4) Podľa výsledku vypíš Debug.Log (úspech / neúspech).
///
/// Výpisy presne zodpovedajú zadaniu:
/// úspech → "Predaj prebehol úspešne, cena {N} euro"
/// neúspech→ "Obchod neprebehol."
///
void TryExecuteTradeAtStation(TrainData td)
{
// Bezpečnostné kontroly – bez dátovej súpravy nemá obchod zmysel.
if (td == null || td.consist == null)
{
Debug.Log("Obchod neprebehol.");
return;
}
Vector2Int stationTile = td.currentTile;
// Nájdi stanicu v registri. Ak chýba, skús dorovnať register.
StationInstance station = RailStationRegistry.GetStationAt(stationTile.x, stationTile.y);
if (station == null)
{
RailStationRegistry.RescanAll();
station = RailStationRegistry.GetStationAt(stationTile.x, stationTile.y);
}
if (station == null)
{
Debug.Log("Obchod neprebehol.");
Debug.LogWarning($"[TrainSystem] Pre tile [{stationTile.x},{stationTile.y}] " +
$"sa nenašla StationInstance – obchod sa neuskutočnil.");
return;
}
// Vykonaj samotnú transakciu (čisto dátová operácia).
TrainTradeSystem.TradeResult result = TrainTradeSystem.Execute(td.consist, station);
if (result.Success)
{
// Pripíš zárobok na herné konto – preprava tovaru je jediný zdroj
// príjmu, ktorý drží konto v pluse (a umožní reset cenového
// násobiteľa po prepadnutí do mínusu).
if (result.Revenue > 0)
{
if (GameEconomy.instance != null)
GameEconomy.instance.AddCredits((uint)result.Revenue);
// EVIDENCIA pre ročnú uzávierku – tržba z úspešnej prepravy
// (jediný zdroj príjmu hry, rovnako pre vlaky aj vozidlá).
BudgetSystem.instance?.RecordRevenue(result.Revenue);
}
Debug.Log($"Obchodná transakcia bola vykonaná úspešne, suma {result.Revenue} CR.");
Debug.Log($"[TrainSystem] Obchod na stanici [{stationTile.x},{stationTile.y}]: " +
$"naložené {result.LoadedUnits}, vyložené {result.UnloadedUnits}, " +
$"zárobok {result.Revenue} CR.");
// Plávajúci cenový label nad vlakom na 3 s. Volá sa presne tu –
// teda po 2 s od príchodu na stanicu (keď prebehne transakcia).
if (td.locomotive != null)
SalePriceLabelManager.Instance.ShowPrice(
td.locomotive.transform.position, result.Revenue);
}
else
{
Debug.Log("Obchod neprebehol.");
Debug.Log($"[TrainSystem] Obchod na stanici [{stationTile.x},{stationTile.y}] " +
$"neprebehol – dôvod: {result.Reason}.");
}
}
void HideAllConsist(TrainData td)
{
if (td.locomotive != null) td.locomotive.SetActive(false);
foreach (var w in td.wagons)
if (w != null) w.SetActive(false);
}
// =====================================================================
// ADVANCE STATION – OBRAT SMERU
// =====================================================================
void AdvanceStation(TrainData td)
{
if (!td.reverseDirection)
{
td.currentStationIndex++;
if (td.currentStationIndex >= td.stations.Count)
{
td.reverseDirection = true;
td.currentStationIndex = td.stations.Count - 2;
if (td.currentStationIndex < 0) td.currentStationIndex = 0;
}
}
else
{
td.currentStationIndex--;
if (td.currentStationIndex < 0)
{
td.reverseDirection = false;
td.currentStationIndex = 1;
if (td.currentStationIndex >= td.stations.Count)
td.currentStationIndex = 0;
}
}
ComputeNextPathAsync(td);
}
// =====================================================================
// ASYNC A* WRAPPER
// =====================================================================
void ComputeNextPathAsync(TrainData td)
{
if (td.stations.Count == 0) return;
if (td.isComputingPath) return;
Vector2Int goal = td.stations[td.currentStationIndex];
Vector2Int start = td.currentTile;
int key = DepotKey(td.depotX, td.depotZ);
var snap = SnapshotTileGrid();
td.isComputingPath = true;
Task.Run(() =>
{
var path = AStarPathThreaded(snap, start, goal);
_pendingPathResults.Enqueue(new PathResult
{
depotKey = key,
path = path,
isReturnToDepot = false
});
});
}
// =====================================================================
// TRIGGER MAP CHANGED REROUTE
// =====================================================================
void TriggerMapChangedReroute()
{
// Mapa sa zmenila (pribudli/ubudli koľaje, stanice alebo továrne) –
// prepočítaj 9×9 zóny staníc a ich evidenciu tovární. Lacná operácia.
RailStationRegistry.RescanAll();
var snap = SnapshotTileGrid();
foreach (var kvp in trains)
{
TrainData td = kvp.Value;
if (!td.isRunning) continue;
if (td.isComputingPath) continue;
int key = kvp.Key;
if (td.isReturningToDepot)
{
Vector2Int start = td.currentTile;
Vector2Int goal = new Vector2Int(td.depotX, td.depotZ);
td.isComputingPath = true;
Task.Run(() =>
{
var path = AStarPathThreaded(snap, start, goal);
_pendingPathResults.Enqueue(new PathResult
{
depotKey = key,
path = path,
isReturnToDepot = true,
destination = PathDestination.Depot
});
});
}
else if (td.isGoingToDepotViaStation)
{
Vector2Int? nearest = FindNearestStation(td);
if (!nearest.HasValue) continue;
Vector2Int start = td.currentTile;
Vector2Int goal = nearest.Value;
td.isComputingPath = true;
Task.Run(() =>
{
var path = AStarPathThreaded(snap, start, goal);
_pendingPathResults.Enqueue(new PathResult
{
depotKey = key,
path = path,
isReturnToDepot = false,
destination = PathDestination.ReturnViaStation
});
});
}
else
{
if (td.stations.Count == 0) continue;
Vector2Int start = td.currentTile;
Vector2Int goal = td.stations[td.currentStationIndex];
td.isComputingPath = true;
Task.Run(() =>
{
var path = AStarPathThreaded(snap, start, goal);
_pendingPathResults.Enqueue(new PathResult
{
depotKey = key,
path = path,
isReturnToDepot = false,
destination = PathDestination.Station
});
});
}
}
}
// =====================================================================
// POMOCNÁ – Nearest Station
// =====================================================================
Vector2Int? FindNearestStation(TrainData td)
{
Vector2Int? nearest = null;
int bestDist = int.MaxValue;
foreach (var st in td.stations)
{
if (GetTileID(st.x, st.y) != 2) continue;
int dist = Math.Abs(td.currentTile.x - st.x) + Math.Abs(td.currentTile.y - st.y);
if (dist < bestDist) { bestDist = dist; nearest = st; }
}
return nearest;
}
// =====================================================================
// APLIKÁCIA VÝSLEDKOV A*
// =====================================================================
void ApplyPendingPathResults()
{
while (_pendingPathResults.TryDequeue(out PathResult result))
{
if (!trains.TryGetValue(result.depotKey, out TrainData td)) continue;
td.isComputingPath = false;
bool hasPath = result.path != null && result.path.Count > 0;
if (result.destination == PathDestination.Depot)
{
if (hasPath) { ApplyNewPath(td, result.path); td.isWaiting = false; }
else
{
td.isRunning = false; td.isWaiting = true; td.waitTimer = BREAK_WAIT;
Debug.LogWarning($"[TrainSystem] Vlak z depa [{td.depotX},{td.depotZ}] nemôže nájsť cestu do depa, čaká.");
}
continue;
}
if (result.destination == PathDestination.ReturnViaStation)
{
if (hasPath) { ApplyNewPath(td, result.path); td.isWaiting = false; }
else
{
Debug.LogWarning($"[TrainSystem] Vlak z depa [{td.depotX},{td.depotZ}] nemôže nájsť cestu na stanicu. Zastane.");
td.isRunning = false; td.isWaiting = false; td.isGoingToDepotViaStation = false;
}
continue;
}
if (result.isReturnToDepot)
{
if (hasPath) { ApplyNewPath(td, result.path); td.isWaiting = false; }
else
{
td.isRunning = false; td.isWaiting = true; td.waitTimer = BREAK_WAIT;
Debug.LogWarning($"[TrainSystem] Vlak nemôže nájsť cestu do depa, čaká.");
}
}
else
{
if (hasPath) { ApplyNewPath(td, result.path); td.isWaiting = false; }
else
{
bool anyStationExists = false;
foreach (var st in td.stations)
if (GetTileID(st.x, st.y) == 2) { anyStationExists = true; break; }
if (!anyStationExists)
{
Debug.Log($"[TrainSystem] Vlak z depa [{td.depotX},{td.depotZ}]: žiadne dostupné stanice – vlak zastane. Stlačte R pre návrat do depa.");
td.isRunning = false; td.isWaiting = false;
td.isStoppedAwaitingDepotReturn = true;
td.currentPath = new List(); td.pathIndex = 0;
td.activePath = null;
// Synchronizácia flag-u isAtDepot s reálnou polohou.
// StartTrain (ne-resume) ho už nastavil na false, ale
// vlak fyzicky depo neopustil → vraciame flag späť na
// true, aby zodpovedal skutočnosti.
if (td.currentTile.x == td.depotX && td.currentTile.y == td.depotZ)
td.isAtDepot = true;
}
else
{
if (td.stations.Count > 0)
{
var goal = td.stations[td.currentStationIndex];
Debug.LogWarning($"[TrainSystem] A* nenašiel cestu do [{goal.x},{goal.y}]. Čakám {BREAK_WAIT}s.");
}
td.isWaiting = true; td.waitTimer = BREAK_WAIT;
td.currentPath = new List(); td.pathIndex = 0;
td.activePath = null;
// Synchronizácia flag-u isAtDepot s reálnou polohou.
// Vlak po neúspešnom A* zostáva fyzicky na depe –
// flag musí zodpovedať tomuto stavu, aby StopTrain (P)
// a následný RemoveTrain (T) fungovali korektne.
if (td.currentTile.x == td.depotX && td.currentTile.y == td.depotZ)
td.isAtDepot = true;
}
}
}
}
}
// =====================================================================
// APPLY NEW PATH – konvertuje List na PathData (waypoint-based)
// =====================================================================
void ApplyNewPath(TrainData td, List newPath)
{
bool isMoving = td.activePath != null && td.locomotiveDistance < td.activePath.totalLength;
// Ak vlak práve prechádza medzi dvoma dlaždicami, aplikujeme nový
// path od dlaždice, na ktorej sa aktuálne nachádza
Vector2Int startTile = td.currentTile;
int startIdxInNew = -1;
for (int i = 0; i < newPath.Count; i++)
{
if (newPath[i] == startTile) { startIdxInNew = i; break; }
}
if (startIdxInNew < 0)
{
// Vlak nie je na novej ceste – odložíme na neskôr alebo aplikujeme od začiatku
if (isMoving)
{
td.pendingPath = newPath;
return;
}
startIdxInNew = 0;
}
// Subpath od aktuálneho tielu po koniec
List subPath = newPath.GetRange(startIdxInNew, newPath.Count - startIdxInNew);
td.currentPath = newPath;
td.pathIndex = startIdxInNew;
td.pathIndexAtPathStart = startIdxInNew;
td.moveTimer = 0f;
// Zostav PathData – WAYPOINT-BASED (centrá + hrany + krivkové rohy)
BuildWaypointPath(subPath, out var pts, out var tileIdxList);
var newPathData = new PathData(pts);
// Rýchlosť: 1 dlaždica / MOVE_TIME s.
// Pri waypoint-based ceste obsahuje newPathData viac segmentov ako len
// počet dlaždíc – upravíme rýchlosť tak, aby vlak prešiel celú cestu
// za rovnaký čas ako pri pôvodnom center-only systéme.
float expectedTime = (subPath.Count - 1) * MOVE_TIME;
td.trainSpeed = (newPathData.totalLength > 0f && expectedTime > 0f)
? newPathData.totalLength / expectedTime
: 1f / MOVE_TIME;
td.locomotiveDistance = 0f;
td.activePath = newPathData;
td.waypointToTileIdx = tileIdxList.ToArray();
}
///
/// Prebuduje activePath z currentPath (po TrimPathToDepot).
///
void RebuildActivePathFromCurrentPath(TrainData td)
{
if (td.currentPath == null || td.currentPath.Count == 0)
{
td.activePath = null;
return;
}
int startIdx = td.pathIndex;
if (startIdx >= td.currentPath.Count) { td.activePath = null; return; }
List sub = td.currentPath.GetRange(startIdx, td.currentPath.Count - startIdx);
BuildWaypointPath(sub, out var pts, out var tileIdxList);
var pd = new PathData(pts);
td.pathIndexAtPathStart = startIdx;
float expectedTime = (sub.Count - 1) * MOVE_TIME;
td.trainSpeed = (pd.totalLength > 0f && expectedTime > 0f)
? pd.totalLength / expectedTime
: 1f / MOVE_TIME;
td.locomotiveDistance = 0f;
td.activePath = pd;
td.waypointToTileIdx = tileIdxList.ToArray();
}
// =====================================================================
// A* PATHFINDING – THREAD-SAFE (modifikované – CanMove namiesto IsPassable)
// =====================================================================
class AStarNode
{
public Vector2Int pos;
public AStarNode parent;
public float g, h;
public float f => g + h;
public AStarNode(Vector2Int p, AStarNode par, float g, float h)
{
pos = p; parent = par; this.g = g; this.h = h;
}
}
static readonly Vector2Int[] DIRECTIONS =
{
new Vector2Int( 1, 0), // Right
new Vector2Int(-1, 0), // Left
new Vector2Int( 0, 1), // Top
new Vector2Int( 0,-1) // Bottom
};
static List AStarPathThreaded(TileGridSnapshot snap, Vector2Int start, Vector2Int goal)
{
if (start == goal) return new List();
var open = new List();
var closed = new HashSet();
var nodeMap = new Dictionary();
open.Add(new AStarNode(start, null, 0f, Heuristic(start, goal)));
nodeMap[start] = open[0];
int iterations = 0;
const int MAX_ITER = 10000;
while (open.Count > 0 && iterations < MAX_ITER)
{
iterations++;
int bestIdx = 0;
for (int i = 1; i < open.Count; i++)
if (open[i].f < open[bestIdx].f) bestIdx = i;
AStarNode current = open[bestIdx];
open.RemoveAt(bestIdx);
if (current.pos == goal)
return ReconstructPath(current);
closed.Add(current.pos);
foreach (var dir in DIRECTIONS)
{
Vector2Int neighbor = current.pos + dir;
if (closed.Contains(neighbor)) continue;
IndicatrixAPI.DirectionMask dirMask = DirFromStep(dir);
if (!CanMove(snap.tileIDs, snap.connections, current.pos, neighbor, dirMask))
continue;
float newG = current.g + 1f;
if (nodeMap.TryGetValue(neighbor, out AStarNode existing))
{
if (newG < existing.g) { existing.g = newG; existing.parent = current; }
}
else
{
var node = new AStarNode(neighbor, current, newG, Heuristic(neighbor, goal));
open.Add(node);
nodeMap[neighbor] = node;
}
}
}
return null;
}
static float Heuristic(Vector2Int a, Vector2Int b)
=> Math.Abs(a.x - b.x) + Math.Abs(a.y - b.y);
static List ReconstructPath(AStarNode node)
{
var path = new List();
var cur = node;
while (cur.parent != null) { path.Add(cur.pos); cur = cur.parent; }
path.Reverse();
return path;
}
public void WriteSave(System.IO.BinaryWriter bw)
{
bw.Write(trains.Count);
foreach (var kvp in trains)
{
TrainData td = kvp.Value;
var consist = td.consist;
bw.Write(td.depotX);
bw.Write(td.depotZ);
// Typy (indexy v katalógu – mapovanie cez Name, ktoré je unikátne).
bw.Write(TrainTypeIndexOf(consist));
bw.Write(WagonTypeIndexOf(consist));
// Počet vagónov + vek.
int wc = consist != null ? consist.Wagons.Count : 0;
bw.Write(wc);
bw.Write(consist != null ? consist.Age : 0);
// Náklad jednotlivých vagónov.
for (int i = 0; i < wc; i++)
bw.Write(consist.Wagons[i].CurrentCapacity);
// Priradené stanice.
bw.Write(td.stations != null ? td.stations.Count : 0);
if (td.stations != null)
foreach (var s in td.stations) { bw.Write(s.x); bw.Write(s.y); }
// Beh/stop.
bw.Write(td.isRunning);
}
}
public void ReadSave(System.IO.BinaryReader br)
{
int count = br.ReadInt32();
for (int k = 0; k < count; k++)
{
int dx = br.ReadInt32();
int dz = br.ReadInt32();
int trainTypeIndex = br.ReadInt32();
int wagonTypeIndex = br.ReadInt32();
int wagonCnt = br.ReadInt32();
int age = br.ReadInt32();
int[] cargo = new int[wagonCnt];
for (int i = 0; i < wagonCnt; i++) cargo[i] = br.ReadInt32();
int stationCnt = br.ReadInt32();
var stations = new System.Collections.Generic.List(stationCnt);
for (int i = 0; i < stationCnt; i++)
stations.Add(new Vector2Int(br.ReadInt32(), br.ReadInt32()));
bool wasRunning = br.ReadBoolean();
// CreateTrain použije pole `wagonCount` ako počet vagónov –
// nastavíme ho na uloženú hodnotu (CreateTrain si ho ešte oreže 1..10).
wagonCount = Mathf.Clamp(wagonCnt, 1, 10);
if (!CreateTrain(dx, dz, trainTypeIndex, wagonTypeIndex))
continue;
TrainData td = GetTrain(dx, dz);
if (td == null) continue;
if (td.consist != null)
{
td.consist.Age = age;
int n = Mathf.Min(cargo.Length, td.consist.Wagons.Count);
for (int i = 0; i < n; i++)
td.consist.Wagons[i].CurrentCapacity = cargo[i];
}
foreach (var s in stations)
AddStation(dx, dz, s.x, s.y);
if (wasRunning)
StartTrain(dx, dz);
}
}
/// Index typu vlaku v TrainCatalog (podľa unikátneho Name); 0 ak sa nenájde.
private int TrainTypeIndexOf(Game.TrainStock.TrainInstance consist)
{
if (consist == null) return 0;
var all = Game.TrainStock.TrainCatalog.All;
for (int i = 0; i < all.Count; i++)
if (all[i].Name == consist.Spec.Name) return i;
return 0;
}
/// Index typu vagónu v WagonCatalog (podľa Name 1. vagónu); 0 ak sa nenájde.
private int WagonTypeIndexOf(Game.TrainStock.TrainInstance consist)
{
if (consist == null || consist.Wagons.Count == 0) return 0;
string name = consist.Wagons[0].Spec.Name;
var all = Game.TrainStock.WagonCatalog.All;
for (int i = 0; i < all.Count; i++)
if (all[i].Name == name) return i;
return 0;
}
}
Herný železničný systém, algoritmus A*, debounce a paralelizmus
V tomto článku by som uviedol algoritmus pre herný železničný systém pre moju hru s pracovným názvom Indicatrix, ktorý bol vygenerovaný úplne prostredníctvom AI (zo začaitku ChatGPT a potom Claude AI). Obsahuje algoritmus A*(Star), debounce, paralelizmus výpočtu pre A* cesty, pre každý vlak separé (existuje aj verzia pre VehicleSystem.cs pre vozidlá) a systematiku pre generovanie vlakovej súpravy.
TrainSystem.cs:
0 comments:
Post a Comment