using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.InputSystem.HID;
using UnityEngine.Splines;
public class IndicatrixAPI : MonoBehaviour
{
public static IndicatrixAPI instance;
// =========================
// DIRECTION MASK
// =========================
///
/// Smery, ktorými môže koľaj/dlaždica prepojiť susedov.
/// Konvencia (zhodná s A* DIRECTIONS v TrainSystem):
/// Right = +X (sused na východ)
/// Left = -X (sused na západ)
/// Top = +Z (sused na sever)
/// Bottom = -Z (sused na juh)
///
[Flags]
public enum DirectionMask
{
None = 0,
Left = 1,
Right = 2,
Top = 4,
Bottom = 8
}
///
/// Vráti opačný smer ku zadanému jednoduchému smeru.
/// Pre kombinovaný mask vracia DirectionMask.None.
///
public static DirectionMask Opposite(DirectionMask d)
{
switch (d)
{
case DirectionMask.Left: return DirectionMask.Right;
case DirectionMask.Right: return DirectionMask.Left;
case DirectionMask.Top: return DirectionMask.Bottom;
case DirectionMask.Bottom: return DirectionMask.Top;
default: return DirectionMask.None;
}
}
// =========================
// TILE CATEGORY (RAIL vs ROAD)
// =========================
///
/// Kategória dlaždice – odlišuje železničné a cestné prvky pri rovnakom
/// tileID. Železnice a cesty zdieľajú jeden tile grid, ale TrainSystem
/// si vyberá iba kategóriu Rail a (budúci) RoadSystem iba kategóriu Road.
///
public enum TileCategory
{
None = 0,
Rail = 1,
Road = 2,
Factory = 3 // Spoločná kategória pre Factory (tileID 4) aj Processing (tileID 5)
}
///
/// Mapuje RailConstructionMode (uložený ako stateID) na DirectionMask.
/// Pre režimy, ktoré nie sú koľajové (None, LevelUp, ...) vracia None.
///
public static DirectionMask GetConnectionsForMode(GameManager.RailConstructionMode mode)
{
switch (mode)
{
// Priame koľaje
case GameManager.RailConstructionMode.RailHorizontal:
return DirectionMask.Left | DirectionMask.Right;
case GameManager.RailConstructionMode.RailVertical:
return DirectionMask.Top | DirectionMask.Bottom;
// Križovatka
case GameManager.RailConstructionMode.RailCrossroad:
return DirectionMask.Left | DirectionMask.Right | DirectionMask.Top | DirectionMask.Bottom;
// Krivky (uhlové, ostré – bez vyhladenia)
case GameManager.RailConstructionMode.RailCurveRightBottom:
return DirectionMask.Right | DirectionMask.Bottom;
case GameManager.RailConstructionMode.RailCurveLeftBottom:
return DirectionMask.Left | DirectionMask.Bottom;
case GameManager.RailConstructionMode.RailCurveRightTop:
return DirectionMask.Right | DirectionMask.Top;
case GameManager.RailConstructionMode.RailCurveLeftTop:
return DirectionMask.Left | DirectionMask.Top;
// Stanice
case GameManager.RailConstructionMode.StationHorizontal:
return DirectionMask.Left | DirectionMask.Right;
case GameManager.RailConstructionMode.StationVertical:
return DirectionMask.Top | DirectionMask.Bottom;
// Depá
case GameManager.RailConstructionMode.DepotHorizontalBottom:
case GameManager.RailConstructionMode.DepotHorizontalTop:
return DirectionMask.Left | DirectionMask.Right;
case GameManager.RailConstructionMode.DepotVerticalBottom:
case GameManager.RailConstructionMode.DepotVerticalTop:
return DirectionMask.Top | DirectionMask.Bottom;
// Výhybky (3-cestné, plne obojsmerné).
// Každá výhybka má 1 priamy smer (Left↔Right alebo Top↔Bottom)
// a 1 odbočný smer (kolmý). Všetky 3 spojenia môžu prechádzať
// medzi sebou navzájom (žiadne asymetrické turnout).
case GameManager.RailConstructionMode.RailSwitchHorizontalBottom:
return DirectionMask.Left | DirectionMask.Right | DirectionMask.Bottom;
case GameManager.RailConstructionMode.RailSwitchHorizontalTop:
return DirectionMask.Left | DirectionMask.Right | DirectionMask.Top;
case GameManager.RailConstructionMode.RailSwitchVerticalBottom:
return DirectionMask.Top | DirectionMask.Bottom | DirectionMask.Right;
case GameManager.RailConstructionMode.RailSwitchVerticalTop:
return DirectionMask.Top | DirectionMask.Bottom | DirectionMask.Left;
default:
return DirectionMask.None;
}
}
///
/// Mapuje RoadConstructionMode (uložený ako stateID) na DirectionMask.
/// Úplne analogické k RAIL verzii vyššie.
/// Pre režimy, ktoré nie sú cestné (None, LevelUp, ...) vracia None.
///
public static DirectionMask GetConnectionsForMode(GameManager.RoadConstructionMode mode)
{
switch (mode)
{
// Priame cesty
case GameManager.RoadConstructionMode.RoadHorizontal:
return DirectionMask.Left | DirectionMask.Right;
case GameManager.RoadConstructionMode.RoadVertical:
return DirectionMask.Top | DirectionMask.Bottom;
// Križovatka
case GameManager.RoadConstructionMode.RoadCrossroad:
return DirectionMask.Left | DirectionMask.Right | DirectionMask.Top | DirectionMask.Bottom;
// Krivky
case GameManager.RoadConstructionMode.RoadCurveRightBottom:
return DirectionMask.Right | DirectionMask.Bottom;
case GameManager.RoadConstructionMode.RoadCurveLeftBottom:
return DirectionMask.Left | DirectionMask.Bottom;
case GameManager.RoadConstructionMode.RoadCurveRightTop:
return DirectionMask.Right | DirectionMask.Top;
case GameManager.RoadConstructionMode.RoadCurveLeftTop:
return DirectionMask.Left | DirectionMask.Top;
// Stanice (autobusové / nákladné zastávky)
case GameManager.RoadConstructionMode.StationHorizontal:
return DirectionMask.Left | DirectionMask.Right;
case GameManager.RoadConstructionMode.StationVertical:
return DirectionMask.Top | DirectionMask.Bottom;
// Depá (garáže)
case GameManager.RoadConstructionMode.DepotHorizontalBottom:
case GameManager.RoadConstructionMode.DepotHorizontalTop:
return DirectionMask.Left | DirectionMask.Right;
case GameManager.RoadConstructionMode.DepotVerticalBottom:
case GameManager.RoadConstructionMode.DepotVerticalTop:
return DirectionMask.Top | DirectionMask.Bottom;
// T-križovatky / Y-rozdvojenia (3-cestné, plne obojsmerné)
case GameManager.RoadConstructionMode.RoadSwitchHorizontalBottom:
return DirectionMask.Left | DirectionMask.Right | DirectionMask.Bottom;
case GameManager.RoadConstructionMode.RoadSwitchHorizontalTop:
return DirectionMask.Left | DirectionMask.Right | DirectionMask.Top;
case GameManager.RoadConstructionMode.RoadSwitchVerticalBottom:
return DirectionMask.Top | DirectionMask.Bottom | DirectionMask.Right;
case GameManager.RoadConstructionMode.RoadSwitchVerticalTop:
return DirectionMask.Top | DirectionMask.Bottom | DirectionMask.Left;
default:
return DirectionMask.None;
}
}
///
/// Mapuje FactoryConstructionMode na DirectionMask.
///
/// POZN.: Továrne (Factory / Processing) NIE SÚ dopravný graf – nemajú
/// koľajové/cestné prepojenia so susedmi. Sú to staticky umiestnené
/// viac-tile objekty (footprint). Preto vždy vraciame DirectionMask.None.
/// Toto preťaženie existuje hlavne kvôli konzistentnosti API (Load,
/// SetTile, TileData konštruktor) – rovnako ako pri RAIL/ROAD.
///
public static DirectionMask GetConnectionsForMode(GameManager.FactoryConstructionMode mode)
{
// Továrne nemajú smerové spojenia.
return DirectionMask.None;
}
// =========================
// TILE ENGINE
// =========================
[System.Serializable]
public struct TileData
{
public int tileID;
public int stateID; // = (int)RailConstructionMode ALEBO (int)RoadConstructionMode
public DirectionMask connections;
public TileCategory category; // RAIL / ROAD / None
public TileData(int t, int s, DirectionMask c, TileCategory cat)
{
tileID = t;
stateID = s;
connections = c;
category = cat;
}
// Pôvodný konštruktor (3 parametre) – pre spätnú kompatibilitu predpokladá
// RAIL kategóriu (pretože pred zavedením ROAD bolo všetko rail).
public TileData(int t, int s, DirectionMask c)
{
tileID = t;
stateID = s;
connections = c;
category = (t == 0) ? TileCategory.None : TileCategory.Rail;
}
// Spätne kompatibilný konštruktor – connections sa odvodí zo stateID.
// Predpokladá RAIL kategóriu (zachovanie pôvodného správania).
public TileData(int t, int s)
{
tileID = t;
stateID = s;
connections = GetConnectionsForMode((GameManager.RailConstructionMode)s);
category = (t == 0) ? TileCategory.None : TileCategory.Rail;
}
}
// =========================
// FACTORY FOOTPRINT
// =========================
///
/// FactoryFootprint
/// ─────────────────────────────────────────────────────────────────────
/// Popisuje veľkosť (footprint) jednej továrne v tiloch a pozíciu
/// "kurzorového" (anchor) tilu v rámci tohto obdĺžnika.
///
/// width = počet tilov v smere X
/// depth = počet tilov v smere Z
/// anchorX, anchorZ = lokálny index tilu (0..width-1, 0..depth-1), nad
/// ktorým je snap kurzor myši. Zvyšok footprintu sa
/// odvodzuje relatívne od tohto tilu.
///
/// PREČO ANCHOR:
/// Pre nepárny rozmer (napr. 3×3) existuje presný stredový tile.
/// Pre párny rozmer (napr. 2×3 alebo 2×2) geometrický stred padne na
/// hranu medzi tilmi – stredový tile neexistuje. Anchor preto definuje
/// fixný, predvídateľný tile, ktorý hráč "drží" pod myšou, takže
/// umiestnenie je konzistentné pri každej veľkosti.
///
/// VÝPOČET ROHOV footprintu z anchor tilu (ax, az):
/// minX = ax - anchorX
/// minZ = az - anchorZ
/// maxX = minX + width - 1
/// maxZ = minZ + depth - 1
///
///
/// FactoryRotation
/// ─────────────────────────────────────────────────────────────────────
/// Otočenie továrne na tile mape v krokoch po 90°.
///
/// PREČO ENUM A NIE bool:
/// Bool (true/false) by stačil len na prepnutie 2×3 ↔ 3×2. Ale pri
/// nesymetrickom footprinte (2×3) chýba aj otočenie o 180° a 270°,
/// ktoré menia, KTORÝM smerom je továreň "otočená" (kde má vstup,
/// ktorý roh je anchor). Enum so 4 hodnotami pokrýva všetky prípady
/// a je ľahko rozšíriteľný.
///
/// VPLYV NA FOOTPRINT:
/// - Deg0 / Deg180 → rozmery zostávajú width×depth.
/// - Deg90 / Deg270 → rozmery sa PREHODIA na depth×width
/// (to je presne tvoja predstava "rotation XZ / ZX").
/// Anchor (kurzorový tile) sa transformuje spolu s obdĺžnikom, takže
/// kurzor vždy "drží" ten istý logický tile továrne.
///
public enum FactoryRotation
{
Deg0 = 0, // bez rotácie (natívna orientácia z GetFactoryFootprint)
Deg90 = 1, // 90° proti smeru hod. ručičiek – prehodí width↔depth
Deg180 = 2, // 180°
Deg270 = 3 // 270° proti smeru hod. ručičiek – prehodí width↔depth
}
[System.Serializable]
public struct FactoryFootprint
{
public int width;
public int depth;
public int anchorX;
public int anchorZ;
public FactoryFootprint(int width, int depth, int anchorX, int anchorZ)
{
this.width = width;
this.depth = depth;
this.anchorX = anchorX;
this.anchorZ = anchorZ;
}
public int TileCount => width * depth;
///
/// Vráti NOVÝ footprint otočený o zadaný uhol.
///
/// Transformácia lokálnej súradnice (lx, lz) v rámci obdĺžnika
/// width×depth do otočeného obdĺžnika:
/// Deg0: (lx, lz) rozmer width×depth
/// Deg90: (lz, width-1-lx) rozmer depth×width
/// Deg180: (width-1-lx, depth-1-lz) rozmer width×depth
/// Deg270: (depth-1-lz, lx) rozmer depth×width
/// Tá istá transformácia sa aplikuje na anchor, aby kurzor zostal
/// nad rovnakým logickým tilom továrne.
///
public FactoryFootprint Rotated(FactoryRotation rot)
{
switch (rot)
{
case FactoryRotation.Deg90:
// rozmer depth×width, anchor (anchorZ, width-1-anchorX)
return new FactoryFootprint(depth, width,
anchorZ, width - 1 - anchorX);
case FactoryRotation.Deg180:
// rozmer width×depth, anchor zrkadlený v oboch osiach
return new FactoryFootprint(width, depth,
width - 1 - anchorX, depth - 1 - anchorZ);
case FactoryRotation.Deg270:
// rozmer depth×width, anchor (depth-1-anchorZ, anchorX)
return new FactoryFootprint(depth, width,
depth - 1 - anchorZ, anchorX);
case FactoryRotation.Deg0:
default:
return this;
}
}
}
///
/// Vráti footprint (veľkosť + anchor) pre zadaný typ továrne.
///
/// VEĽKOSTI (podľa zadania):
/// Factory (tileID 4):
/// - Coal Mine 2×3
/// - Forest 3×3
/// - Iron Ore Mine 3×3
/// - Gold Mine 3×3
/// - Silver Mine 3×3
/// - Farm 3×3
/// - Oil Wells 3×3
/// Processing (tileID 5):
/// - Power Station 2×2
/// - Sawmill 2×2
/// - Oil Refinery 2×2
/// - Electronics Factory 3×3
/// - Furniture Factory 3×3
/// - Slaughterhouse 2×2
/// - Grain Factory 2×3
/// - Smelter 2×3
/// - Glass Factory 2×2
///
/// ANCHOR: zvolený tak, aby kurzor bol čo najbližšie stredu obdĺžnika.
/// - Pre nepárny rozmer → presný stred (3 → index 1).
/// - Pre párny rozmer → "ľavý/dolný zo stredovej dvojice"
/// (2 → index 0), čím je footprint deterministický.
///
/// Tieto hodnoty sú jediné miesto, kde sa veľkosti definujú – ide o
/// "nastaviteľný parameter". Pre zmenu veľkosti továrne stačí upraviť
/// príslušný riadok tu.
///
public static FactoryFootprint GetFactoryFootprint(GameManager.FactoryConstructionMode mode)
{
switch (mode)
{
// ── Factory (tileID 4) ──
// Coal Mine 2×3 → anchor (0,1)
case GameManager.FactoryConstructionMode.CoalMine:
return new FactoryFootprint(2, 3, 0, 1);
// Forest 3×3 → anchor (1,1) = presný stred
case GameManager.FactoryConstructionMode.Forest:
return new FactoryFootprint(3, 3, 1, 1);
// Iron Ore Mine 3×3 → anchor (1,1)
case GameManager.FactoryConstructionMode.IronOreMine:
return new FactoryFootprint(3, 3, 1, 1);
// Gold Mine 3×3 → anchor (1,1)
case GameManager.FactoryConstructionMode.GoldMine:
return new FactoryFootprint(3, 3, 1, 1);
// Silver Mine 3×3 → anchor (1,1)
case GameManager.FactoryConstructionMode.SilverMine:
return new FactoryFootprint(3, 3, 1, 1);
// Farm 3×3 → anchor (1,1)
case GameManager.FactoryConstructionMode.Farm:
return new FactoryFootprint(3, 3, 1, 1);
// Oil Wells 3×3 → anchor (1,1)
case GameManager.FactoryConstructionMode.OilWells:
return new FactoryFootprint(3, 3, 1, 1);
// ── Processing (tileID 5) ──
// Power Station 2×2 → anchor (0,0)
case GameManager.FactoryConstructionMode.PowerStation:
return new FactoryFootprint(2, 2, 0, 0);
// Sawmill 2×2 → anchor (0,0)
case GameManager.FactoryConstructionMode.SawMill:
return new FactoryFootprint(2, 2, 0, 0);
// Oil Refinery 2×2 → anchor (0,0)
case GameManager.FactoryConstructionMode.OilRefinery:
return new FactoryFootprint(2, 2, 0, 0);
// Electronics Factory 3×3 → anchor (1,1)
case GameManager.FactoryConstructionMode.ElectronicsFactory:
return new FactoryFootprint(3, 3, 1, 1);
// Furniture Factory 3×3 → anchor (1,1)
case GameManager.FactoryConstructionMode.FurnitureFactory:
return new FactoryFootprint(3, 3, 1, 1);
// Slaughterhouse 2×2 → anchor (0,0)
case GameManager.FactoryConstructionMode.Slaughterhouse:
return new FactoryFootprint(2, 2, 0, 0);
// Grain Factory 2×3 → anchor (0,1)
case GameManager.FactoryConstructionMode.GrainFactory:
return new FactoryFootprint(2, 3, 0, 1);
// Smelter 2×3 → anchor (0,1)
case GameManager.FactoryConstructionMode.Smelter:
return new FactoryFootprint(2, 3, 0, 1);
// Glass Factory 2×2 → anchor (0,0)
case GameManager.FactoryConstructionMode.GlassFactory:
return new FactoryFootprint(2, 2, 0, 0);
// None / neznáme → 1×1 (degeneruje na klasický single-tile)
default:
return new FactoryFootprint(1, 1, 0, 0);
}
}
///
/// Vráti footprint pre daný typ továrne UŽ OTOČENÝ o zadaný uhol.
/// Pohodlné preťaženie – spojí GetFactoryFootprint(mode) a .Rotated(rot).
///
public static FactoryFootprint GetFactoryFootprint(
GameManager.FactoryConstructionMode mode, FactoryRotation rotation)
{
return GetFactoryFootprint(mode).Rotated(rotation);
}
const int GRID_SIZE = 256;
TileData[,] tileGrid;
// =========================
// SNAP SYSTEM
// =========================
GameObject snapSphere;
GameObject lineFace01, lineFace02, lineFace03, lineFace04;
LineRenderer lineRenderer01, lineRenderer02, lineRenderer03, lineRenderer04;
Material lineFaceMat, snapFaceMatTex;
//Textúry pre RAIL
Texture2D snapFaceTex_Rail; //ID = 1
Texture2D snapFaceTex_RailStation; //ID = 2
Texture2D snapFaceTex_RailDepot; //ID = 3
//Textúry pre ROAD
Texture2D snapFaceTex_Road; //ID = 1
Texture2D snapFaceTex_RoadStation; //ID = 2
Texture2D snapFaceTex_RoadDepot; //ID = 3
//Textúry pre FACTORY
Texture2D snapFaceTex_Factory; //ID = 4
Texture2D snapFaceTex_Processing; //ID = 5
Mesh meshFace;
GameObject[,] tileMap;
// =========================
// INITIALIZATION
// =========================
private void Awake()
{
instance = this;
}
private void Start()
{
InitializeTileEngine();
InitializeSnapSystem();
}
void InitializeTileEngine()
{
tileGrid = new TileData[GRID_SIZE, GRID_SIZE];
tileMap = new GameObject[GRID_SIZE, GRID_SIZE];
for (int x = 0; x < GRID_SIZE; x++)
{
for (int z = 0; z < GRID_SIZE; z++)
{
tileGrid[x, z] = new TileData(0, 0, DirectionMask.None, TileCategory.None);
tileMap[x, z] = null;
}
}
}
void InitializeSnapSystem()
{
snapSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
Destroy(snapSphere.GetComponent());
snapSphere.transform.localScale = Vector3.one * 0.15f;
snapSphere.GetComponent().material.color = new Color(1, 1, 1, 0);
lineFaceMat = Resources.Load("Materials/LineMat");
snapFaceMatTex = Resources.Load("Materials/FaceMatTex");
//Načítanie textúr pre RAIL
snapFaceTex_Rail = Resources.Load("Textures/Rail/Rail_Tex");
snapFaceTex_RailStation = Resources.Load("Textures/Rail/RailStation_Tex");
snapFaceTex_RailDepot = Resources.Load("Textures/Rail/RailDepot_Tex");
//Načítanie textúr pre ROAD
snapFaceTex_Road = Resources.Load("Textures/Road/Road_Tex");
snapFaceTex_RoadStation = Resources.Load("Textures/Road/RoadStation_Tex");
snapFaceTex_RoadDepot = Resources.Load("Textures/Road/RoadDepot_Tex");
//Načítanie textúr pre FACTORY
snapFaceTex_Factory = Resources.Load("Textures/Factory/Factory_Tex");
snapFaceTex_Processing = Resources.Load("Textures/Factory/Processing_Tex");
meshFace = new Mesh();
CreateLineRenderer(ref lineFace01, ref lineRenderer01, "LineFace01");
CreateLineRenderer(ref lineFace02, ref lineRenderer02, "LineFace02");
CreateLineRenderer(ref lineFace03, ref lineRenderer03, "LineFace03");
CreateLineRenderer(ref lineFace04, ref lineRenderer04, "LineFace04");
HideSnapSphere();
HideSnapLines();
}
void HideSnapSphere()
{
if (snapSphere != null)
snapSphere.SetActive(false);
}
void ShowSnapSphere()
{
if (snapSphere != null)
snapSphere.SetActive(true);
}
void HideSnapLines()
{
if (lineFace01 != null) lineFace01.SetActive(false);
if (lineFace02 != null) lineFace02.SetActive(false);
if (lineFace03 != null) lineFace03.SetActive(false);
if (lineFace04 != null) lineFace04.SetActive(false);
}
void ShowSnapLines()
{
if (lineFace01 != null) lineFace01.SetActive(true);
if (lineFace02 != null) lineFace02.SetActive(true);
if (lineFace03 != null) lineFace03.SetActive(true);
if (lineFace04 != null) lineFace04.SetActive(true);
}
///
/// Skryje všetky snap vizuály (snap sphere aj línie). Volá sa pri prepnutí do vlakových režimov.
///
public void HideAllSnapVisuals()
{
HideSnapSphere();
HideSnapLines();
}
void CreateLineRenderer(ref GameObject obj, ref LineRenderer lr, string name)
{
obj = new GameObject(name);
lr = obj.AddComponent();
lr.material = lineFaceMat;
lr.startWidth = 0.1f;
lr.endWidth = 0.1f;
lr.positionCount = 2;
}
// =========================
// TILE ENGINE CORE
// =========================
public Vector2Int SnapTileIndex(Vector3 hitPoint)
{
int x = Mathf.FloorToInt(hitPoint.x);
int z = Mathf.FloorToInt(hitPoint.z);
return new Vector2Int(x, z);
}
///
/// Pôvodná signatúra – stateID 0 znamená "žiadny špecifický režim" (Demolish / clear).
/// Connections sa nastaví na None. Predpokladá RAIL kategóriu (spätná kompatibilita).
///
public void SetTile(Vector3 hitPoint, int tileID, int stateID)
{
SetTile(hitPoint, tileID, (GameManager.RailConstructionMode)stateID);
}
///
/// RAIL verzia – ukladá tileID + RailConstructionMode (ako stateID),
/// connections sa odvodí cez GetConnectionsForMode, category = Rail
/// (alebo None ak tileID == 0, t.j. Demolish/clear).
///
public void SetTile(Vector3 hitPoint, int tileID, GameManager.RailConstructionMode mode)
{
Vector2Int index = SnapTileIndex(hitPoint);
if (!IsValidIndex(index)) return;
DirectionMask conns = (tileID == 0) ? DirectionMask.None : GetConnectionsForMode(mode);
TileCategory cat = (tileID == 0) ? TileCategory.None : TileCategory.Rail;
tileGrid[index.x, index.y] = new TileData(tileID, (int)mode, conns, cat);
UpdateTileMap(index.x, index.y);
}
///
/// ROAD verzia – ukladá tileID + RoadConstructionMode (ako stateID),
/// connections sa odvodí cez GetConnectionsForMode preťaženie pre Road,
/// category = Road (alebo None ak tileID == 0).
///
/// POZN.: stateID pre RAIL a ROAD pochádza z odlišných enum-ov, preto
/// raw int by mohol kolidovať. Kategória je preto určujúca pri spätnej
/// rekonštrukcii (pri Load alebo iných operáciách, ktoré poznajú len
/// raw stateID).
///
public void SetTile(Vector3 hitPoint, int tileID, GameManager.RoadConstructionMode mode)
{
Vector2Int index = SnapTileIndex(hitPoint);
if (!IsValidIndex(index)) return;
DirectionMask conns = (tileID == 0) ? DirectionMask.None : GetConnectionsForMode(mode);
TileCategory cat = (tileID == 0) ? TileCategory.None : TileCategory.Road;
tileGrid[index.x, index.y] = new TileData(tileID, (int)mode, conns, cat);
UpdateTileMap(index.x, index.y);
}
///
/// FACTORY verzia – multi-tile umiestnenie továrne.
///
/// Na rozdiel od RAIL/ROAD (vždy 1×1) zaberá továreň obdĺžnik N×M tilov
/// podľa . Hit point určuje "anchor"
/// tile (kurzor myši); zvyšok footprintu sa dopočíta okolo neho.
///
/// Do KAŽDÉHO tilu footprintu sa zapíše tá istá TileData:
/// tileID = 4 (Factory) alebo 5 (Processing)
/// stateID = (int)FactoryConstructionMode (rozlíši CoalMine/Forest/...)
/// category = TileCategory.Factory
/// connections = None (továrne nie sú dopravný graf)
///
/// Vďaka tomu sa textúra (Factory_Tex / Processing_Tex) vykreslí na
/// všetkých tiloch footprintu (napr. 2×3 = 6 tilov).
///
/// VALIDÁCIA: celý footprint musí byť (a) vnútri gridu a (b) prázdny
/// (samé tileID == 0). Ak čokoľvek prekáža, NIČ sa nepoloží a metóda
/// vráti false – továreň sa nepoloží "čiastočne".
///
/// stateID == 0 (FactoryConstructionMode.None) sa berie ako "clear" –
/// vymaže 1×1 tile pod kurzorom (rovnaká sémantika ako Demolish).
///
/// PARAMETER rotation:
/// Otočenie továrne o 0/90/180/270°. Pri 90°/270° sa footprint
/// prehodí (width↔depth). Anchor sa transformuje spolu s obdĺžnikom,
/// takže kurzor drží stále ten istý logický tile. Default = Deg0
/// (bez rotácie) – staré volania bez tohto parametra fungujú ďalej.
///
public bool SetTile(Vector3 hitPoint, int tileID, GameManager.FactoryConstructionMode mode,
FactoryRotation rotation = FactoryRotation.Deg0)
{
// Deleguje na preťaženie s out parametrami; výsledný footprint sa
// tu zahodí (volajúci ho nepotrebuje).
return SetTile(hitPoint, tileID, mode, out _, out _, out _, out _, rotation);
}
///
/// FACTORY verzia s VÝSTUPOM footprintu – funkčne identická s preťažením
/// vyššie, navyše ale cez out parametre oznámi, KAM presne sa továreň
/// položila (ľavý-dolný roh + rozmery už po rotácii).
///
/// Vďaka tomu vie volajúci (GameManager) zaevidovať FactoryInstance bez
/// toho, aby duplikoval výpočet footprintu – jediný zdroj pravdy pre
/// pozíciu zostáva tu.
///
/// Pri neúspechu (mimo gridu / kolízia) sú out parametre nastavené na 0
/// a metóda vráti false. Pri None/clear vetve out parametre popisujú
/// vymazaný 1×1 tile (origin = anchor, width = depth = 1).
///
public bool SetTile(Vector3 hitPoint, int tileID, GameManager.FactoryConstructionMode mode,
out int originX, out int originZ, out int width, out int depth,
FactoryRotation rotation = FactoryRotation.Deg0)
{
originX = 0; originZ = 0; width = 0; depth = 0;
Vector2Int anchor = SnapTileIndex(hitPoint);
if (!IsValidIndex(anchor)) return false;
// None / clear → vymaž 1 tile pod kurzorom.
if (tileID == 0 || mode == GameManager.FactoryConstructionMode.None)
{
tileGrid[anchor.x, anchor.y] = new TileData(0, 0, DirectionMask.None, TileCategory.None);
UpdateTileMap(anchor.x, anchor.y);
originX = anchor.x; originZ = anchor.y; width = 1; depth = 1;
return true;
}
// Footprint UŽ otočený podľa rotation.
FactoryFootprint fp = GetFactoryFootprint(mode, rotation);
// Ľavý-dolný roh footprintu odvodený z anchor tilu.
int minX = anchor.x - fp.anchorX;
int minZ = anchor.y - fp.anchorZ;
int maxX = minX + fp.width - 1;
int maxZ = minZ + fp.depth - 1;
// (a) celý footprint musí byť vnútri gridu
if (minX < 0 || minZ < 0 || maxX >= GRID_SIZE || maxZ >= GRID_SIZE)
{
Debug.LogWarning($"[IndicatrixAPI] Factory '{mode}' ({fp.width}x{fp.depth}) " +
$"presahuje okraj mapy – neumiestnené.");
return false;
}
// (b) celý footprint musí byť prázdny
for (int x = minX; x <= maxX; x++)
{
for (int z = minZ; z <= maxZ; z++)
{
if (tileGrid[x, z].tileID != 0)
{
Debug.LogWarning($"[IndicatrixAPI] Factory '{mode}' – tile [{x},{z}] " +
$"je obsadený. Footprint musí byť celý prázdny – neumiestnené.");
return false;
}
}
}
// Zápis tej istej TileData do všetkých tilov footprintu.
DirectionMask conns = GetConnectionsForMode(mode); // None
for (int x = minX; x <= maxX; x++)
{
for (int z = minZ; z <= maxZ; z++)
{
tileGrid[x, z] = new TileData(tileID, (int)mode, conns, TileCategory.Factory);
UpdateTileMap(x, z);
}
}
// Výstup položeného footprintu (po rotácii).
originX = minX; originZ = minZ; width = fp.width; depth = fp.depth;
Debug.Log($"[IndicatrixAPI] Factory '{mode}' (rot {rotation}) umiestnená na footprint " +
$"[{minX},{minZ}]–[{maxX},{maxZ}] ({fp.TileCount} tilov).");
return true;
}
///
/// Vymaže JEDEN tile na danom indexe [x,z] (nastaví prázdnu TileData,
/// tileID 0, category None) a prekreslí jeho mesh cez UpdateTileMap.
///
/// Na rozdiel od SetTile(Vector3 ...) pracuje priamo s tile indexom, takže
/// volajúci (napr. GameManager.DemolishFactory) môže v cykle vyčistiť celý
/// footprint viac-tile objektu (továrne) bez prepočtu svetových súradníc.
/// Index mimo mriežky sa bezpečne ignoruje.
///
public void ClearTileByIndex(int x, int z)
{
if (!IsValidIndex(new Vector2Int(x, z))) return;
tileGrid[x, z] = new TileData(0, 0, DirectionMask.None, TileCategory.None);
UpdateTileMap(x, z);
}
///
/// Vypočíta (BEZ akéhokoľvek zápisu do mapy) obdĺžnik footprintu, ktorý by
/// zabrala továreň typu položená pod kurzorom
/// s rotáciou .
///
/// Slúži na PRED-validáciu v GameManager-i (kontrola obsadenosti footprintu
/// a ochrannej zóny okolo iných tovární) – footprintová matematika tak
/// zostáva na jedinom mieste (zhodná s tým, čo neskôr spraví SetTile).
///
/// Vráti true, ak je celý footprint vnútri gridu; out parametre potom držia
/// ľavý-dolný roh a rozmery (už po rotácii). Pri footprinte mimo mapy vráti
/// false a out parametre sú 0.
///
public bool TryGetFactoryFootprintBounds(
Vector3 hitPoint, GameManager.FactoryConstructionMode mode, FactoryRotation rotation,
out int originX, out int originZ, out int width, out int depth)
{
originX = 0; originZ = 0; width = 0; depth = 0;
Vector2Int anchor = SnapTileIndex(hitPoint);
if (!IsValidIndex(anchor)) return false;
if (mode == GameManager.FactoryConstructionMode.None) return false;
FactoryFootprint fp = GetFactoryFootprint(mode, rotation);
int minX = anchor.x - fp.anchorX;
int minZ = anchor.y - fp.anchorZ;
int maxX = minX + fp.width - 1;
int maxZ = minZ + fp.depth - 1;
if (minX < 0 || minZ < 0 || maxX >= GRID_SIZE || maxZ >= GRID_SIZE)
return false;
originX = minX; originZ = minZ; width = fp.width; depth = fp.depth;
return true;
}
///
/// Vráti tile dáta pre daný hit point.
///
/// KRITICKÉ: Táto metóda RETURNS ONLY RAIL tiles. Ak je na pozícii ROAD
/// tile, vráti prázdnu TileData (tileID=0). Týmto sa zabezpečuje, že
/// existujúci kód (predovšetkým TrainSystem.cs a jeho A* algoritmus)
/// vidí len RAIL graf a nemusí byť modifikovaný kvôli ROAD systému.
///
/// Pre prístup ku všetkým tile dátam (bez ohľadu na kategóriu) použite
/// .
///
public TileData GetTile(Vector3 hitPoint)
{
Vector2Int index = SnapTileIndex(hitPoint);
if (!IsValidIndex(index))
return new TileData(0, 0, DirectionMask.None, TileCategory.None);
var td = tileGrid[index.x, index.y];
if (td.category == TileCategory.Road)
return new TileData(0, 0, DirectionMask.None, TileCategory.None);
return td;
}
///
/// Vráti tile dáta pre daný hit point BEZ filtra kategórie. Použije sa
/// keď volajúci potrebuje vidieť všetky tile dáta (napr. GameManager
/// pri Demolish overuje kategóriu, alebo budúci RoadSystem).
///
public TileData GetTileAny(Vector3 hitPoint)
{
Vector2Int index = SnapTileIndex(hitPoint);
if (!IsValidIndex(index))
return new TileData(0, 0, DirectionMask.None, TileCategory.None);
return tileGrid[index.x, index.y];
}
///
/// Priamy prístup k tileGrid cez tile indexy (pre TrainSystem a A*).
///
/// KRITICKÉ: Vracia len RAIL tiles (rovnaká logika ako GetTile vyššie).
/// ROAD tiles sa javia ako prázdne (tileID=0). Týmto je TrainSystem.cs
/// úplne izolovaný od ROAD systému.
///
public TileData GetTileByIndex(int x, int z)
{
if (x < 0 || x >= GRID_SIZE || z < 0 || z >= GRID_SIZE)
return new TileData(0, 0, DirectionMask.None, TileCategory.None);
var td = tileGrid[x, z];
if (td.category == TileCategory.Road)
return new TileData(0, 0, DirectionMask.None, TileCategory.None);
return td;
}
///
/// Priamy prístup k tileGrid cez tile indexy BEZ filtra kategórie.
/// Použije sa keď volajúci potrebuje vidieť všetky tile dáta vrátane ROAD
/// (napr. GameManager kontroluje kategóriu pred Demolish, alebo budúci
/// RoadSystem.cs si bude takto čítať road graf).
///
public TileData GetTileByIndexAny(int x, int z)
{
if (x < 0 || x >= GRID_SIZE || z < 0 || z >= GRID_SIZE)
return new TileData(0, 0, DirectionMask.None, TileCategory.None);
return tileGrid[x, z];
}
// =========================
// TERÉNNE DOTAZY (rovinatosť face, vrchol vs. obsadené tiles)
//
// Pomocné read-only metódy pre GameManager error-guardy:
// • IsFaceFlat – je tile vodorovný (4 vertexy rovnaké Y)?
// • IsVertexOnOccupiedTile – patrí vrchol niektorému obsadenému tile?
// =========================
///
/// Vráti true, ak je face tile [tileX, tileZ] VODOROVNÝ – t.j. všetky
/// 4 rohové vertexy majú rovnaké Y. Používa FunctionalValueY (číta výšky
/// vertexov z TerrainManager.coordsF), rovnako ako SnapMeshFace.
///
/// Rohy tile [x,z]: (x,z), (x,z+1), (x+1,z+1), (x+1,z).
///
/// Slúži ako guard pre stavbu staníc/dep – tie sa smú stavať len na rovine.
/// Ak TerrainManager ešte nie je inicializovaný, vracia true (fail-open –
/// guard vtedy nebráni stavbe; reálne sa volá až počas hry, keď terén beží).
///
public bool IsFaceFlat(int tileX, int tileZ)
{
if (TerrainManager.instance == null) return true;
float y0 = FunctionalValueY(tileX, tileZ);
float y1 = FunctionalValueY(tileX, tileZ + 1);
float y2 = FunctionalValueY(tileX + 1, tileZ + 1);
float y3 = FunctionalValueY(tileX + 1, tileZ);
const float EPS = 0.0001f; // tolerancia na float nepresnosti
return Mathf.Abs(y0 - y1) < EPS
&& Mathf.Abs(y0 - y2) < EPS
&& Mathf.Abs(y0 - y3) < EPS;
}
///
/// Vráti true, ak je face tile [tileX, tileZ] ŠIKMÝ – čistá RAMPA: dve
/// susedné vertexy (jedna hrana) sú na jednej Y a protiľahlá hrana (druhé
/// dva susedné vertexy) je na inej Y. Sklon teda vedie buď pozdĺž osi X,
/// alebo pozdĺž osi Z.
///
/// Rohy tile [x,z] (poradie ako GetFaceVertices):
/// y0=(x,z), y1=(x,z+1), y2=(x+1,z+1), y3=(x+1,z).
///
/// NIE JE rampa: rovina (vylúčená podmienkou „hrany v rôznej výške“) ani
/// skrútený tile (napr. jeden roh hore) – tam neplatí ani jeden zo vzorov.
///
/// Slúži ako guard pre rovné koľaje/cesty, ktoré sa smú stavať aj na rampe.
///
public bool IsFaceRamp(int tileX, int tileZ)
{
if (TerrainManager.instance == null) return false;
float y0 = FunctionalValueY(tileX, tileZ); // (x, z)
float y1 = FunctionalValueY(tileX, tileZ + 1); // (x, z+1)
float y2 = FunctionalValueY(tileX + 1, tileZ + 1); // (x+1, z+1)
float y3 = FunctionalValueY(tileX + 1, tileZ); // (x+1, z)
const float EPS = 0.0001f;
// Sklon pozdĺž Z: hrana pri z je rovná (y0==y3), hrana pri z+1 je rovná
// (y1==y2), a navzájom sú v rôznej výške.
bool rampAlongZ = Mathf.Abs(y0 - y3) < EPS
&& Mathf.Abs(y1 - y2) < EPS
&& Mathf.Abs(y0 - y1) >= EPS;
// Sklon pozdĺž X: hrana pri x je rovná (y0==y1), hrana pri x+1 je rovná
// (y3==y2), a navzájom sú v rôznej výške.
bool rampAlongX = Mathf.Abs(y0 - y1) < EPS
&& Mathf.Abs(y3 - y2) < EPS
&& Mathf.Abs(y0 - y3) >= EPS;
return rampAlongZ || rampAlongX;
}
///
/// Vráti true, ak vrchol na grid pozícii [vertX, vertZ] je rohom aspoň
/// jedného OBSADENÉHO tile (tileID != 0).
///
/// Jeden vrchol je zdieľaný až 4 susednými tilmi:
/// (vertX-1, vertZ-1), (vertX, vertZ-1), (vertX-1, vertZ), (vertX, vertZ).
/// GetTileByIndexAny ošetrí indexy mimo mriežky (vráti tileID 0).
///
/// Slúži ako guard pre úpravu terénu (LevelUp/LevelDown): ak by sa
/// upravovaný vrchol dotýkal obsadeného tile, operácia sa zablokuje –
/// a to aj vtedy, keď hráč klikol „vedľa“ obsadeného tile, lebo vrchol
/// patrí aj jeho face.
///
public bool IsVertexOnOccupiedTile(int vertX, int vertZ)
{
for (int tx = vertX - 1; tx <= vertX; tx++)
for (int tz = vertZ - 1; tz <= vertZ; tz++)
if (GetTileByIndexAny(tx, tz).tileID != 0)
return true;
return false;
}
///
/// Vráti súradnice [x, z] VŠETKÝCH staníc danej kategórie na mape.
///
/// Stanica je dlaždica s tileID == 2. Železnice a cesty zdieľajú jeden
/// tile grid, líšia sa len kategóriou (TileCategory) – preto sa pri
/// vyhľadávaní filtruje aj podľa category:
/// • TileCategory.Rail → vlakové (Train) stanice,
/// • TileCategory.Road → vozidlové (Vehicle) stanice.
///
/// Metóda jednorázovo prejde celý tileGrid (GRID_SIZE × GRID_SIZE) a
/// vyzbiera zhodné dlaždice. tileGrid ostáva privátny – navonok dávame
/// len read-only zoznam súradníc, takže volajúci nemôže poškodiť mapu.
///
/// Slúži pre informačné UI (StatusStationsMenuUI).
///
/// Rail = Train stanice, Road = Vehicle stanice.
public List GetAllStationCoords(TileCategory category)
{
var result = new List();
// tileGrid ešte nemusí byť inicializovaný (InitializeTileEngine sa
// volá v Start()) – vtedy vrátime prázdny zoznam.
if (tileGrid == null)
return result;
for (int x = 0; x < GRID_SIZE; x++)
{
for (int z = 0; z < GRID_SIZE; z++)
{
TileData td = tileGrid[x, z];
if (td.tileID == 2 && td.category == category)
result.Add(new Vector2Int(x, z));
}
}
return result;
}
///
/// Celkový počet staníc danej kategórie na mape. Pohodlný getter –
/// vnútorne využíva GetAllStationCoords (počet = Count zoznamu).
///
/// Rail = Train stanice, Road = Vehicle stanice.
public int GetStationCount(TileCategory category)
{
return GetAllStationCoords(category).Count;
}
bool IsValidIndex(Vector2Int index)
{
return index.x >= 0 && index.x < GRID_SIZE &&
index.y >= 0 && index.y < GRID_SIZE;
}
void UpdateTileMap(int x, int z)
{
TileData data = tileGrid[x, z];
if (tileMap[x, z] != null)
{
Destroy(tileMap[x, z]);
tileMap[x, z] = null;
}
if (data.tileID == 0)
return;
GameObject go = new GameObject($"Tile_{x}_{z}");
tileMap[x, z] = go;
MeshFilter mf = go.AddComponent();
MeshRenderer mr = go.AddComponent();
Mesh mesh = CreateTileMesh(x, z);
mf.mesh = mesh;
Material mat = new Material(snapFaceMatTex);
// Výber textúry podľa kategórie (RAIL vs ROAD vs FACTORY) a tileID
// (1 = trať/cesta, 2 = stanica, 3 = depo, 4 = továreň, 5 = spracovateľská)
if (data.category == TileCategory.Road)
{
if (data.tileID == 1)
mat.mainTexture = snapFaceTex_Road;
else if (data.tileID == 2)
mat.mainTexture = snapFaceTex_RoadStation;
else if (data.tileID == 3)
mat.mainTexture = snapFaceTex_RoadDepot;
}
else if (data.category == TileCategory.Factory)
{
if (data.tileID == 4)
mat.mainTexture = snapFaceTex_Factory;
else if (data.tileID == 5)
mat.mainTexture = snapFaceTex_Processing;
}
else // Rail (alebo nezadefinované – default RAIL pre spätnú kompatibilitu)
{
if (data.tileID == 1)
mat.mainTexture = snapFaceTex_Rail;
else if (data.tileID == 2)
mat.mainTexture = snapFaceTex_RailStation;
else if (data.tileID == 3)
mat.mainTexture = snapFaceTex_RailDepot;
}
mr.material = mat;
}
Mesh CreateTileMesh(int x, int z)
{
Vector3[] verts = new Vector3[4];
float dy = 0.01f;
verts[0] = new Vector3(x, FunctionalValueY(x, z) + dy, z);
verts[1] = new Vector3(x, FunctionalValueY(x, z + 1) + dy, z + 1);
verts[2] = new Vector3(x + 1, FunctionalValueY(x + 1, z + 1) + dy, z + 1);
verts[3] = new Vector3(x + 1, FunctionalValueY(x + 1, z) + dy, z);
Mesh mesh = new Mesh();
mesh.vertices = verts;
mesh.triangles = new int[] { 0, 1, 2, 2, 3, 0 };
mesh.uv = new Vector2[]
{
new Vector2(0,0),
new Vector2(1,0),
new Vector2(1,1),
new Vector2(0,1)
};
mesh.RecalculateNormals();
return mesh;
}
// =========================
// LOAD / SAVE
// =========================
//
// KOMPLETNÁ uložená hra (binárne, VERZIOVANÉ). Jeden súbor SaveData.dat
// drží všetko, čo sa NEDÁ deterministicky znovu vyrobiť. Veci, ktoré sa
// dajú dopočítať z uloženého stavu, sa zámerne NEukladajú a po načítaní sa
// vygenerujú:
//
// ── UKLADÁ SA (zdroj pravdy, hráč to ovplyvnil) ─────────────────────
// • terén – reálne Y výšky všetkých vrcholov (coordsF), lebo hráč ich
// upravuje (TerrainVertexLevel). Náhodný seed by neobnovil
// ručné úpravy, preto ukladáme priamo výšky.
// • tile grid – tileID + stateID + category každého tilu (256×256).
// • konto (GameEconomy.Balance, vrátane mínusu),
// • herný čas (GameClock: Year/Month/Day/TotalDaysElapsed),
// • cenový násobiteľ (ResourcePricing.Multiplier),
// • ročná účtovná kniha (BudgetSystem – akumulátor prebiehajúceho roka),
// • TOVÁRNE (FactoryRegistry) – typ, footprint, rotácia + PREMENLIVÝ
// stav: množstvá/kapacity slotov, mzda, mzdový level, occupancy flag,
// progres výstavby,
// • VLAKY (TrainSystem) a VOZIDLÁ (VehicleSystem) viazané na depá –
// typ súpravy/vozidla, vek, náklad, priradené stanice, beh/stop.
//
// ── NEUKLADÁ SA (dopočíta sa pri LoadGame) ──────────────────────────
// • mesh terénu (prekreslí sa z výšok),
// • textúry/GameObjekty tilov (UpdateTileMap z tile gridu),
// • priradenie tovární staniciam – 9×9 catchment (RescanAll),
// • konkrétne trasy/sub-tile pozícia vlakov a vozidiel (A* sa prepočíta;
// po načítaní stoja v depe a ak bežali, znovu sa rozbehnú).
//
// Poradie blokov je ZHODNÉ pri zápise aj čítaní. Tile engine je orchestrátor:
// terén/tiles/továrne/skalárne stavy rieši sám, serializáciu vlakov,
// vozidiel a knihy delegujе na príslušné systémy (každý si vlastný stav
// serializuje sám – pozri WriteSave/ReadSave v TrainSystem/VehicleSystem/
// BudgetSystem v companion patchi).
private const int SAVE_MAGIC = 0x494E4458; // "INDX"
private const int SAVE_VERSION = 2;
public void SaveGame()
{
string path = Application.persistentDataPath + "/SaveData.dat";
using (BinaryWriter bw = new BinaryWriter(File.Open(path, FileMode.Create)))
{
// ── Hlavička ──
bw.Write(SAVE_MAGIC);
bw.Write(SAVE_VERSION);
// ── 1) TERÉN (reálne Y výšky vrcholov) ──
WriteTerrain(bw);
// ── 2) TILE GRID ──
for (int x = 0; x < GRID_SIZE; x++)
{
for (int z = 0; z < GRID_SIZE; z++)
{
bw.Write(tileGrid[x, z].tileID);
bw.Write(tileGrid[x, z].stateID);
bw.Write((int)tileGrid[x, z].category);
// connections sa neukladá – odvodí sa pri Load zo stateID a category.
}
}
// ── 3) KONTO / ČAS / CENOVÝ NÁSOBITEĽ ──
bw.Write(GameEconomy.instance != null ? GameEconomy.instance.Balance : 0L);
var clock = GameClock.instance;
bw.Write(clock != null ? clock.Year : 1950);
bw.Write(clock != null ? clock.Month : 1);
bw.Write(clock != null ? clock.Day : 1);
bw.Write(clock != null ? clock.TotalDaysElapsed : 0L);
bw.Write(ResourcePricing.Multiplier);
// ── 4) ROČNÁ ÚČTOVNÁ KNIHA ──
if (BudgetSystem.instance != null) BudgetSystem.instance.WriteSave(bw);
else WriteEmptyBudget(bw);
// ── 5) TOVÁRNE ──
WriteFactories(bw);
// ── 6) VLAKY a VOZIDLÁ ──
if (TrainSystem.instance != null) TrainSystem.instance.WriteSave(bw);
else bw.Write(0);
if (VehicleSystem.instance != null) VehicleSystem.instance.WriteSave(bw);
else bw.Write(0);
}
Debug.Log("[IndicatrixAPI] Hra uložená do " + path);
}
public void LoadGame()
{
string path = Application.persistentDataPath + "/SaveData.dat";
if (!File.Exists(path)) return;
using (BinaryReader br = new BinaryReader(File.Open(path, FileMode.Open)))
{
// ── Hlavička ──
int magic = br.ReadInt32();
int version = br.ReadInt32();
if (magic != SAVE_MAGIC)
{
Debug.LogError("[IndicatrixAPI] SaveData.dat má neznámy formát (magic) – " +
"načítanie zrušené.");
return;
}
if (version != SAVE_VERSION)
{
Debug.LogWarning($"[IndicatrixAPI] SaveData.dat má verziu {version} (očakávaná " +
$"{SAVE_VERSION}). Pokúsim sa načítať, no formát sa mohol zmeniť.");
}
// ── 1) TERÉN ──
ReadTerrain(br);
// ── 2) TILE GRID (+ prekreslenie meshu každého tilu) ──
for (int x = 0; x < GRID_SIZE; x++)
{
for (int z = 0; z < GRID_SIZE; z++)
{
int tileID = br.ReadInt32();
int stateID = br.ReadInt32();
TileCategory category = (TileCategory)br.ReadInt32();
DirectionMask conns = RebuildConnections(tileID, stateID, ref category);
tileGrid[x, z] = new TileData(tileID, stateID, conns, category);
UpdateTileMap(x, z);
}
}
// ── 3) KONTO / ČAS / NÁSOBITEĽ ──
long balance = br.ReadInt64();
int year = br.ReadInt32();
int month = br.ReadInt32();
int day = br.ReadInt32();
long totalDays = br.ReadInt64();
int multiplier = br.ReadInt32();
if (GameEconomy.instance != null) GameEconomy.instance.SetBalanceSigned(balance);
if (GameClock.instance != null) GameClock.instance.LoadDate(year, month, day, totalDays);
ResourcePricing.Multiplier = multiplier < 1 ? 1 : multiplier;
// EconomySystem si pri štarte zapamätal lastMonth/lastYear z PÔVODNÉHO
// (defaultného) dátumu. Po prepísaní hodín ich treba zladiť, inak by
// najbližší denný tik mohol omylom zúčtovať "nový mesiac/rok" navyše.
if (EconomySystem.instance != null) EconomySystem.instance.ResyncToClock();
// ── 4) ROČNÁ KNIHA ──
if (BudgetSystem.instance != null) BudgetSystem.instance.ReadSave(br);
else SkipBudget(br);
// ── 5) TOVÁRNE (vyprázdni register, potom obnov) ──
ReadFactories(br);
// ── 6) PRIRADENIE TOVÁRNÍ STANICIAM – dopočítané, NEukladá sa ──
// Tile grid + továrne sú už načítané, takže scan má z čoho zbierať.
RailStationRegistry.RescanAll();
RoadStationRegistry.RescanAll();
// ── 7) VLAKY a VOZIDLÁ ──
if (TrainSystem.instance != null) TrainSystem.instance.ReadSave(br);
else SkipCollection(br);
if (VehicleSystem.instance != null) VehicleSystem.instance.ReadSave(br);
else SkipCollection(br);
}
Debug.Log("[IndicatrixAPI] Hra načítaná z " + path);
}
// ------------------------------------------------------------------
// POMOCNÉ – TERÉN
// ------------------------------------------------------------------
//
// Ukladajú sa reálne Y výšky vrcholov (coordsF). x,z sú deterministicky
// dané indexom (CreateMap), preto ich netreba ukladať. Po načítaní výšok
// sa dopočíta coordsI (ConvertCoordsFI(true)) a prekreslia sa meshe
// elementov terénu.
private void WriteTerrain(BinaryWriter bw)
{
var tm = TerrainManager.instance;
if (tm == null || tm.coordsF == null)
{
bw.Write(0);
return;
}
bw.Write(tm.coordsF.Length);
for (int i = 0; i < tm.coordsF.Length; i++)
bw.Write(tm.coordsF[i].y);
}
private void ReadTerrain(BinaryReader br)
{
int count = br.ReadInt32();
var tm = TerrainManager.instance;
if (tm == null || tm.coordsF == null)
{
// Preskoč dáta (4 bajty na float), nech ostane stream zarovnaný.
for (int i = 0; i < count; i++) br.ReadSingle();
Debug.LogWarning("[IndicatrixAPI] TerrainManager nie je pripravený – výšky preskočené.");
return;
}
if (count != tm.coordsF.Length)
{
Debug.LogWarning($"[IndicatrixAPI] Veľkosť terénu v save ({count}) sa nezhoduje s " +
$"aktuálnou ({tm.coordsF.Length}). Načítam min. spoločnú časť.");
}
int n = Mathf.Min(count, tm.coordsF.Length);
for (int i = 0; i < n; i++)
{
float y = br.ReadSingle();
tm.coordsF[i].y = y;
}
// Zvyšok (ak bol save väčší) dočítaj, nech je stream zarovnaný.
for (int i = n; i < count; i++) br.ReadSingle();
// Dopočítaj celočíselné úrovne z výšok a prekresli meshe.
tm.ConvertCoordsFI(true);
if (tm.terrainElements != null)
{
foreach (var e in tm.terrainElements)
if (e != null) e.Rebuild();
}
}
// ------------------------------------------------------------------
// POMOCNÉ – TILE CONNECTIONS (odvodenie pri Load, ako v pôvodnej verzii)
// ------------------------------------------------------------------
private DirectionMask RebuildConnections(int tileID, int stateID, ref TileCategory category)
{
if (tileID == 0)
return DirectionMask.None;
if (category == TileCategory.Road)
return GetConnectionsForMode((GameManager.RoadConstructionMode)stateID);
if (category == TileCategory.Factory)
return GetConnectionsForMode((GameManager.FactoryConstructionMode)stateID); // None
// Rail (alebo None pri starších záznamoch – dorovnáme na Rail).
if (category == TileCategory.None) category = TileCategory.Rail;
return GetConnectionsForMode((GameManager.RailConstructionMode)stateID);
}
// ------------------------------------------------------------------
// POMOCNÉ – TOVÁRNE
// ------------------------------------------------------------------
//
// Ukladá sa typ (FactoryConstructionMode), footprint (origin + rozmery),
// rotácia a PREMENLIVÝ stav. Pri Load sa register vyprázdni a každá továreň
// sa znovu zaregistruje cez FactoryRegistry.Register (rovnaká cesta ako pri
// ručnom položení), následne sa obnovia premenlivé hodnoty.
private void WriteFactories(BinaryWriter bw)
{
var all = FactoryRegistry.All;
bw.Write(all.Count);
foreach (var f in all)
{
bw.Write((int)f.Definition.Mode);
bw.Write(f.OriginX);
bw.Write(f.OriginZ);
bw.Write(f.Width);
bw.Write(f.Depth);
bw.Write((int)f.Rotation);
// Premenlivý stav.
bw.Write(f.EmployeeSalary);
bw.Write(f.LevelSalary);
bw.Write(f.OccupancyFlag);
bw.Write(f.BuildElapsed); // progres výstavby (sekundy)
// Sloty (amount + capacity – kapacitu hráč mohol editovať).
WriteSlots(bw, f.Load);
WriteSlots(bw, f.UnLoad);
}
}
private void ReadFactories(BinaryReader br)
{
FactoryRegistry.Clear();
int count = br.ReadInt32();
for (int k = 0; k < count; k++)
{
var mode = (GameManager.FactoryConstructionMode)br.ReadInt32();
int originX = br.ReadInt32();
int originZ = br.ReadInt32();
int width = br.ReadInt32();
int depth = br.ReadInt32();
var rotation = (FactoryRotation)br.ReadInt32();
int employeeSalary = br.ReadInt32();
short levelSalary = br.ReadInt16();
bool occupancy = br.ReadBoolean();
float buildElapsed = br.ReadSingle();
// Sloty zo save (do dočasných zoznamov, aplikujeme po Register).
var savedLoad = ReadSlots(br);
var savedUnLoad = ReadSlots(br);
FactoryDefinition def = FactoryDatabase.GetDefinition(mode);
if (def == null)
{
Debug.LogWarning($"[IndicatrixAPI] Neznáma továreň '{mode}' v save – preskočená.");
continue;
}
var inst = FactoryRegistry.Register(def, originX, originZ, width, depth, rotation);
if (inst == null) continue;
inst.EmployeeSalary = employeeSalary;
inst.LevelSalary = levelSalary;
inst.OccupancyFlag = occupancy;
// Obnov množstvá/kapacity slotov (poradie je deterministické z definície).
ApplySavedSlots(inst.Load, savedLoad);
ApplySavedSlots(inst.UnLoad, savedUnLoad);
// Obnov progres výstavby. Register cez konštruktor začína na 0 a
// (ak má def BuildingTime>0) v stave "vo výstavbe". AdvanceConstruction
// dotiahne elapsed na uloženú hodnotu; pri dokončení sa flag zhodí sám.
inst.AdvanceConstruction(buildElapsed);
}
}
private static void WriteSlots(BinaryWriter bw, System.Collections.Generic.List slots)
{
bw.Write(slots.Count);
foreach (var s in slots)
{
bw.Write((int)s.type);
bw.Write(s.amount);
bw.Write(s.capacity);
}
}
private struct SavedSlot { public int type; public int amount; public int capacity; }
private static System.Collections.Generic.List ReadSlots(BinaryReader br)
{
int n = br.ReadInt32();
var list = new System.Collections.Generic.List(n);
for (int i = 0; i < n; i++)
list.Add(new SavedSlot { type = br.ReadInt32(), amount = br.ReadInt32(), capacity = br.ReadInt32() });
return list;
}
private static void ApplySavedSlots(System.Collections.Generic.List target,
System.Collections.Generic.List saved)
{
// Spárуj podľa typu (robustnejšie než podľa indexu, keby sa definícia
// časom rozšírila). amount aj capacity prepíšeme zo save.
foreach (var sv in saved)
{
var rt = (ResourceType)sv.type;
foreach (var slot in target)
{
if (slot.type == rt)
{
slot.capacity = Mathf.Max(0, sv.capacity);
slot.amount = Mathf.Clamp(sv.amount, 0, slot.capacity);
break;
}
}
}
}
// ------------------------------------------------------------------
// POMOCNÉ – PRÁZDNE / PRESKOČENIE BLOKOV (keď systém v scéne chýba)
// ------------------------------------------------------------------
private void WriteEmptyBudget(BinaryWriter bw)
{
// 10 položiek knihy (pozri BudgetSystem.WriteSave) – samé nuly.
bw.Write(0L); // Revenue
bw.Write(0); bw.Write(0L); // TrainsBought, TrainsCost
bw.Write(0); bw.Write(0L); // VehiclesBought, VehiclesCost
bw.Write(0); bw.Write(0L); // FactoriesBought, FactoriesCost
bw.Write(0L); // TrainOperating
bw.Write(0L); // VehicleOperating
bw.Write(0L); // EmployeeSalaries
}
private void SkipBudget(BinaryReader br)
{
br.ReadInt64();
br.ReadInt32(); br.ReadInt64();
br.ReadInt32(); br.ReadInt64();
br.ReadInt32(); br.ReadInt64();
br.ReadInt64();
br.ReadInt64();
br.ReadInt64();
}
// Vlaky/vozidlá serializujú prvý int = count, potom count záznamov. Ak
// systém v scéne nie je, nevieme bezpečne preskočiť premenlivú dĺžku –
// ošetríme len count==0 (čo zapisuje SaveGame, keď systém chýbal).
private void SkipCollection(BinaryReader br)
{
int count = br.ReadInt32();
if (count != 0)
Debug.LogWarning("[IndicatrixAPI] Save obsahuje vlaky/vozidlá, ale systém v scéne " +
"chýba – ich blok sa nedá preskočiť. Stream je odteraz nezarovnaný.");
}
// =========================
// ORIGINAL SNAP FUNCTIONS
// =========================
public Vector3 SnapVertex(Vector3 hitPoint)
{
HideSnapLines();
ShowSnapSphere();
int snapX = 0, snapZ = 0;
float snapY = 0;
int xx = (int)hitPoint.x; float x = hitPoint.x - xx;
int zz = (int)hitPoint.z; float z = hitPoint.z - zz;
if (x >= 0.0f && x <= 0.5f) { snapX = xx; }
else snapX = xx + 1;
if (z >= 0.0f && z <= 0.5f) { snapZ = zz; }
else snapZ = zz + 1;
snapY = FunctionalValueY(snapX, snapZ);
snapSphere.transform.position = new Vector3(snapX, snapY, snapZ);
return new Vector3(snapX, snapY, snapZ);
}
public Vector3 SnapLineFace(Vector3 hitPoint)
{
HideSnapSphere();
ShowSnapLines();
Vector3[] verts = GetFaceVertices(hitPoint);
lineRenderer01.SetPosition(0, verts[0]);
lineRenderer01.SetPosition(1, verts[1]);
lineRenderer02.SetPosition(0, verts[1]);
lineRenderer02.SetPosition(1, verts[2]);
lineRenderer03.SetPosition(0, verts[2]);
lineRenderer03.SetPosition(1, verts[3]);
lineRenderer04.SetPosition(0, verts[3]);
lineRenderer04.SetPosition(1, verts[0]);
return GetCenterFace(hitPoint);
}
public Vector3 SnapMeshFace(Vector3 hitPoint)
{
return GetCenterFace(hitPoint);
}
///
/// SnapAreaFace – snap vizuál pre VIAC-TILE footprint (továrne).
///
/// Analógia k SnapLineFace, ale namiesto jednej 1×1 plochy zvýrazní
/// obvod celého obdĺžnika N×M tilov, do ktorého sa továreň umiestni.
/// Hit point určuje anchor tile; obdĺžnik sa dopočíta cez FactoryFootprint
/// rovnako ako v SetTile (FACTORY verzia), takže náhľad presne zodpovedá
/// tomu, čo sa po kliknutí položí.
///
/// Používa tie isté 4 line renderery ako SnapLineFace – vykreslí nimi
/// 4 strany veľkého obdĺžnika namiesto malého tilu.
///
/// PARAMETER rotation: footprint sa zvýrazní UŽ otočený, takže náhľad
/// presne zodpovedá tomu, čo SetTile s rovnakou rotáciou položí.
/// Default = Deg0.
///
public Vector3 SnapAreaFace(Vector3 hitPoint, GameManager.FactoryConstructionMode mode,
FactoryRotation rotation = FactoryRotation.Deg0)
{
HideSnapSphere();
ShowSnapLines();
Vector2Int anchor = SnapTileIndex(hitPoint);
FactoryFootprint fp = GetFactoryFootprint(mode, rotation);
// Ľavý-dolný roh footprintu (rovnaký výpočet ako v SetTile).
int minX = anchor.x - fp.anchorX;
int minZ = anchor.y - fp.anchorZ;
int maxX = minX + fp.width; // +width → pravá hrana posledného tilu
int maxZ = minZ + fp.depth; // +depth → horná hrana posledného tilu
float dy = 0.02f;
// 4 rohy veľkého obdĺžnika (proti smeru hod. ručičiek).
Vector3 c0 = new Vector3(minX, FunctionalValueY(minX, minZ) + dy, minZ);
Vector3 c1 = new Vector3(minX, FunctionalValueY(minX, maxZ) + dy, maxZ);
Vector3 c2 = new Vector3(maxX, FunctionalValueY(maxX, maxZ) + dy, maxZ);
Vector3 c3 = new Vector3(maxX, FunctionalValueY(maxX, minZ) + dy, minZ);
lineRenderer01.SetPosition(0, c0);
lineRenderer01.SetPosition(1, c1);
lineRenderer02.SetPosition(0, c1);
lineRenderer02.SetPosition(1, c2);
lineRenderer03.SetPosition(0, c2);
lineRenderer03.SetPosition(1, c3);
lineRenderer04.SetPosition(0, c3);
lineRenderer04.SetPosition(1, c0);
// Geometrický stred footprintu (užitočné pre prípadné umiestnenie
// 3D modelu továrne neskôr).
float cx = (minX + maxX) * 0.5f;
float cz = (minZ + maxZ) * 0.5f;
return new Vector3(cx, FunctionalValueY(anchor.x, anchor.y), cz);
}
Vector3[] GetFaceVertices(Vector3 hitPoint)
{
int xx = Mathf.FloorToInt(hitPoint.x);
int zz = Mathf.FloorToInt(hitPoint.z);
float dy = 0.01f;
Vector3[] verts = new Vector3[4];
verts[0] = new Vector3(xx, FunctionalValueY(xx, zz) + dy, zz);
verts[1] = new Vector3(xx, FunctionalValueY(xx, zz + 1) + dy, zz + 1);
verts[2] = new Vector3(xx + 1, FunctionalValueY(xx + 1, zz + 1) + dy, zz + 1);
verts[3] = new Vector3(xx + 1, FunctionalValueY(xx + 1, zz) + dy, zz);
return verts;
}
public Vector3 GetCenterFace(Vector3 hitPoint)
{
int xx = Mathf.FloorToInt(hitPoint.x);
int zz = Mathf.FloorToInt(hitPoint.z);
float dx = xx + 0.5f;
float dz = zz + 0.5f;
float dy = FunctionalValueY(xx, zz);
return new Vector3(dx, dy, dz);
}
float FunctionalValueY(int snapX, int snapZ)
{
int width = TerrainManager.instance.terrainWidth + 1;
int index = snapZ * width + snapX;
return TerrainManager.instance.coordsF[index].y;
}
}
3D tile system
V tomto článku bude uvedený 3D tile systém, ktorý som realizoval v mojej hre s pracovným názvom Indicatrix. Pre tieto účely nebolo použité nejaké AI, napísal som to úplne sám, vrátane snapping systémov a ešte som ich nechal overiť AI (ChatGPT) a verdikt znel takto, že je to napísané dobre, on by to nenapísal lepšie, ale použil by funkciu Vector2int na niektoré prevody. Keď som povedal AI, že či volanie funkcie nie je náročná operácia, tak odpovedal, že nie, ale v každom prípade osobne som to napísal bez týchto funkcií, takže to bolo bázickejšie, než od AI. Samotné snapping AI najskôr riešilo spôsobom cez Unity, nenapísalo bázické funkcie také ako som napísal. Štandardne by sa mala používať funkcionalita z nejakého engine prostredia, pretože je to uspôsobené pre tento účel. Avšak zámerne som to napísal bez použitia Unity prvkov, aby to bolo bázickejšie riešenie a teda univerzálne. Ak by bola potreba prepísať tieto funkcionality do iného jazyka, tak by boli prepísateľné v štandardnom tvare, ale čím viac sa volajú funkcie daného API, tak tým horšie sa to potom refaktoruje, prepisuje do iného jazyka, alebo do iného API. Toto nebol môj hlavný zámer, ale vedľajší. Začal som v lete roku 2024 s problematikou tycoon terrain, kde som sám vymyslel spôsob (teda, znovuobjavil) ako realizovať vykreslenie terénu typu tycoon (z hry Transport Tycoon z roku 1994). Tam som použil Unity na riešenie základného Face (Quad), ktorý obsahoval 4 vertexy, vertex buffer a k tomu indices (trojuholníky). Tam som zistil, že vytvoriť niečo takéto je relatívne jednoduché pri zmene, posunutí vertexu. Kedysi dávno som to skúšal cez OpenGL, ale nefungovalo to podľa mojich predstáv, zabudol som na refresh slučku pri update vertexe a toto bola kardinálna chyba. Teraz sa mi to podarilo a na základe tejto funkcionality som skúšal realizovať ostatné snapping systémy jednoducho tak, že bod priesečníku v tile bude styčný bod od ktorého sa budú počítať ostatné súradnice. Toto je hlavná a jednoduchá metóda, ktorú som použil. Nič špeciálne tam nie je. Avšak stále som si myslel, dlhé roky, že je to inak a že je to komplikovanejšie. Stačilo veriť v jednoduchosť riešenia. Intuitívneho riešenia :)
IndicatrixAPI.cs:
0 comments:
Post a Comment