diff --git a/AStar.cs b/AStar.cs new file mode 100644 index 00000000..84586f18 --- /dev/null +++ b/AStar.cs @@ -0,0 +1,343 @@ +using System.Collections.Generic; +using System; + +// An algorithm using AStar for finding best route from start to finish when using "goto" command in CivOne. +// "Stolen" from https://stackoverflow.com/questions/38387646/concise-universal-a-search-in-c-sharp by kaalus +// and modified to my taste + + +public abstract class AStar +{ + #region Fields + + public struct sPosition { public int iX; public int iY; }; + + public class Node + { + public sPosition Position; + public sPosition PreviousPosition; + public float F, G, H; + public bool IsClosed; + } + + private int m_nodesCacheIndex; + private List m_nodesCache = new List(); + private List m_openHeap = new List(); + private List m_neighbors = new List(); + private Dictionary m_defaultStorage; + + #endregion + + #region Domain Definition + + // User must override Neighbors, Cost and Heuristic functions to define search domain. + // It is optional to override StorageClear, StorageGet and StorageAdd functions. + // Default implementation of these storage functions uses a Dictionary, this works for all possible search domains. + // A domain-specific storage algorihm may be significantly faster. + // For example if searching on a finite 2D or 3D grid, storage using fixed array with each element representing one world point benchmarks an order of magnitude faster. + + /// + /// Return all neighbors of the given point. + /// Must be overridden. + /// + /// Point to return neighbors for + /// Empty collection to fill with neighbors + protected abstract void Neighbors(Node node, List neighbors); + + /// + /// Return cost of making a step from p1 to p2 (which are neighbors). + /// Cost equal to float.PositiveInfinity indicates that passage from p1 to p2 is impossible. + /// Must be overridden. + /// + /// Start point + /// End point + /// Cost value + protected abstract float Cost(sPosition p1, sPosition p2); + + /// + /// Return an estimate of cost of moving from p to nearest goal. + /// Must return 0 when p is goal. + /// This is an estimate of sum of all costs along the best path between p and the nearest goal. + /// This should not overestimate the actual cost; if it does, the result of A* might not be an optimal path. + /// Underestimating the cost is allowed, but may cause the algorithm to check more positions. + /// Must be overridden. + /// + /// Point to estimate cost from + /// Point to estimate cost to + /// Cost value + protected abstract float Heuristic(sPosition p); + + /// + /// Clear A* storage. + /// This will be called every time before a search starts and before any other user functions are called. + /// Optional override when using domain-optimized storage. + /// + protected virtual void StorageClear() + { + if (m_defaultStorage == null) + { + m_defaultStorage = new Dictionary(); + } + else + { + m_defaultStorage.Clear(); + } + } + + /// + /// Retrieve data from storage at given point. + /// Optional override when using domain-optimized storage. + /// + /// Point to retrieve data at + /// Data stored for point p or null if nothing stored + protected virtual object StorageGet(sPosition position) + { + object data; + m_defaultStorage.TryGetValue(position, out data); + return data; + } + + /// + /// Add data to storage at given point. + /// There will never be any data already stored at that point. + /// Optional override when using domain-optimized storage. + /// + /// Point to add data at + /// Data to add + protected virtual void StorageAdd(sPosition position, object data) + { + m_defaultStorage.Add(position, data); + } + + #endregion + + #region Public Interface + + /// + /// Find best path from start to nearest goal. + /// Goal is any point for which Heuristic override returns 0. + /// If maxPositionsToCheck limit is reached, best path found so far is returned. + /// If there is no path to goal, path to a point nearest to goal is returned instead. + /// + /// Path will contain steps to reach goal from start in reverse order (first step at the end of collection) + /// Starting point to search for path + /// Maximum number of positions to check + /// True when path to goal was found, false if partial path only + public bool FindPath(ICollection path, sPosition StartPosition, int maxPositionsToCheck = int.MaxValue) + { + // Check arguments + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + // Reset cache and storage + path.Clear(); + m_nodesCacheIndex = 0; + m_openHeap.Clear(); + StorageClear(); + + // Put start node + Node startNode = NewNode(StartPosition, StartPosition, 0, 0); // Start node point to itself + StorageAdd(StartPosition, startNode); + HeapEnqueue(startNode); + + // Astar loop + Node bestNode = null; + int checkedPositions = 0; + while (true) + { + // Get next node from heap + Node currentNode = m_openHeap.Count > 0 ? HeapDequeue() : null; + + // Check end conditions + if (currentNode == null || checkedPositions >= maxPositionsToCheck) + { + // No more nodes or limit reached, path not found, return path to best node if possible + if (bestNode != null) + { + BuildPathFromEndNode(path, startNode, bestNode); + } + return false; + } + + else if (Heuristic(currentNode.Position) <= 0) + { + // Node is goal, return path + BuildPathFromEndNode(path, startNode, currentNode); + return true; + } + + // Remember node with best heuristic; ignore start node + if (currentNode != startNode && (bestNode == null || currentNode.H < bestNode.H)) + { + bestNode = currentNode; + } + + // Move current node from open to closed in the storage + currentNode.IsClosed = true; + ++checkedPositions; + + // Try all neighbors + m_neighbors.Clear(); + Neighbors(currentNode, m_neighbors); + for (int i = 0; i < m_neighbors.Count; ++i) + { + // Get a neighbour + Node NeighborNode = m_neighbors[i]; + + // Check if this node is already in list(Closed) + Node NodeInList = (Node)StorageGet(NeighborNode.Position); + + // If position was already analyzed, ignore step + if (NodeInList != null && NodeInList.IsClosed == true) // if alredy in "closed" list + { + continue; + } + + // If position is not passable, ignore step + float cost = Cost( currentNode.Position, NeighborNode.Position ); + if( cost == float.PositiveInfinity ) + { + continue; + } + + // Calculate A* values + float g = currentNode.G + cost; + float h = Heuristic(currentNode.Position ); + // Update or create new node at position + if( NodeInList != null ) + { + // Update existing node if better + if( g < NodeInList.G ) + { + NodeInList.G = g; + NodeInList.F = g + NodeInList.H; + NodeInList.PreviousPosition = currentNode.Position; + HeapUpdate( NodeInList ); + } + } + else + { + // Create new open node if not yet exists + Node node = NewNode(NeighborNode.Position, currentNode.Position, g, h); + StorageAdd(node.Position, node); + HeapEnqueue(node); + } + } + } + } + #endregion + + #region Internals + + private void BuildPathFromEndNode(ICollection path, Node startNode, Node endNode) + { + for (Node node = endNode; node != startNode; node = (Node)StorageGet(node.PreviousPosition)) + { + path.Add(node.Position); + } + } + + private void HeapEnqueue(Node node) + { + m_openHeap.Add(node); + HeapifyFromPosToStart(m_openHeap.Count - 1); + } + + // Return node at pos 0 in open heap + private Node HeapDequeue() + { + Node result = m_openHeap[0]; + if (m_openHeap.Count <= 1) + { + m_openHeap.Clear(); + } + else + { + m_openHeap[0] = m_openHeap[m_openHeap.Count - 1]; + m_openHeap.RemoveAt(m_openHeap.Count - 1); + HeapifyFromPosToEnd(0); + } + return result; + } + + private void HeapUpdate(Node node) + { + int pos = -1; + for (int i = 0; i < m_openHeap.Count; ++i) + { + if (m_openHeap[i] == node) + { + pos = i; + break; + } + } + HeapifyFromPosToStart(pos); + } + + // Locate the the open node with the lowest F ( heuristics + cost ) by moving it to position 0 + private void HeapifyFromPosToEnd(int pos) + { + while (true) + { + int smallest = pos; + int left = 2 * pos + 1; + int right = 2 * pos + 2; + if (left < m_openHeap.Count && m_openHeap[left].F < m_openHeap[smallest].F) + smallest = left; + if (right < m_openHeap.Count && m_openHeap[right].F < m_openHeap[smallest].F) + smallest = right; + if (smallest != pos) + { + Node tmp = m_openHeap[smallest]; + m_openHeap[smallest] = m_openHeap[pos]; + m_openHeap[pos] = tmp; + pos = smallest; + } + else + { + break; + } + } + } + + private void HeapifyFromPosToStart(int pos) + { + int childPos = pos; + while (childPos > 0) + { + int parentPos = (childPos - 1) / 2; + Node parentNode = m_openHeap[parentPos]; + Node childNode = m_openHeap[childPos]; + if (parentNode.F > childNode.F) + { + m_openHeap[parentPos] = childNode; + m_openHeap[childPos] = parentNode; + childPos = parentPos; + } + else + { + break; + } + } + } + + private Node NewNode(sPosition position, sPosition previousPosition, float g, float h) + { + while (m_nodesCacheIndex >= m_nodesCache.Count) + { + m_nodesCache.Add(new Node()); + } + Node node = m_nodesCache[m_nodesCacheIndex++]; + node.Position = position; + node.PreviousPosition = previousPosition; + node.F = g + h; + node.G = g; + node.H = h; + node.IsClosed = false; + return node; + } + + #endregion +} diff --git a/AStarInterface.cs b/AStarInterface.cs new file mode 100644 index 00000000..4ea523a7 --- /dev/null +++ b/AStarInterface.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System; +using System.Collections.ObjectModel; +using CivOne.Tiles; +using CivOne.Enums; +using CivOne; + + + +public class AStarInterface : AStar +{ + // Interface for use in CivOne + + sPosition GoalPosition; + + ICollection path = new Collection(); + + public sPosition FindPath(sPosition StartPosition, sPosition GoalPosition) + { + this.GoalPosition = GoalPosition; + bool o = FindPath(path, StartPosition, 500); // Find path + AStar.sPosition[] Positions = new sPosition[500]; // Get path + int iCount = path.Count; + path.CopyTo( Positions, 0 ); + if (!o) + { + Positions[ iCount - 1 ].iX = -1; // unable to find path + } + return Positions[ iCount - 1 ]; + } + + /// Return cost of making a step from Positition to NextPosition (which are neighbors). + /// Cost equal to float.PositiveInfinity indicates that passage from Positition to NextPosition is impossible. + /// Currently is cost of current position ignored + /// + protected override float Cost( sPosition Positition, sPosition NextPosition ) + { + float fCost; + + float fNextCost = Map.Instance.GetMoveCost( NextPosition.iX, NextPosition.iY ); + fCost = Map.Instance.GetMoveCost( Positition.iX, Positition.iY ); + if( fNextCost == 1f && fCost == 1f ) return fNextCost; // if going along a road/railroad + else if( fNextCost == 1f ) return 3f; // if moving from terrain to road/railroad ( dont know if this is correct ) + else return fNextCost; + } + + + /// Return an estimate of cost of moving from Positition to goal. + /// Return 0 when Positition is goal. + /// This is an estimate of sum of all costs along the path between Positition and the goal. + static float fGoalF = 1.0f; + protected override float Heuristic( sPosition Positition) + { + if( Math.Abs( Positition.iX - GoalPosition.iX ) > Math.Abs( Positition.iY - GoalPosition.iY )) + return Math.Abs(( Positition.iX - GoalPosition.iX ) * fGoalF ); + else + return Math.Abs(( Positition.iY - GoalPosition.iY ) * fGoalF ); + + } + + + protected override void Neighbors( Node CurrNode, List neighbors ) + { + int[,] aiRelPos = new int[,] { { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, -1 }, { 0, 1 }, { 1, -1 }, { 1, 0 }, { 1, 1 } }; + + for (int i = 0; i < 8; i++) + { + sPosition CurPosition = CurrNode.Position; + sPosition NewPosition; + + NewPosition.iX = CurPosition.iX + aiRelPos[ i, 0 ]; + NewPosition.iY = CurPosition.iY + aiRelPos[ i, 1 ]; + Node NewNode = new Node(); + NewNode.Position = NewPosition; + NewNode.IsClosed = false; + NewNode.PreviousPosition = CurPosition; + neighbors.Add( NewNode ); + } + } +} + + diff --git a/Game.cs b/Game.cs new file mode 100644 index 00000000..0d0eb721 --- /dev/null +++ b/Game.cs @@ -0,0 +1,524 @@ +// CivOne +// +// To the extent possible under law, the person who associated CC0 with +// CivOne has waived all copyright and related or neighboring rights +// to CivOne. +// +// You should have received a copy of the CC0 legalcode along with this +// work. If not, see . + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using CivOne.Advances; +using CivOne.Buildings; +using CivOne.Civilizations; +using CivOne.Enums; +using CivOne.IO; +using CivOne.Screens; +using CivOne.Tasks; +using CivOne.Tiles; +using CivOne.Units; +using CivOne.Wonders; + +namespace CivOne +{ + public partial class Game : BaseInstance + { + private readonly int _difficulty, _competition; + private readonly Player[] _players; + private readonly List _cities; + private readonly List _units; + private readonly Dictionary _advanceOrigin = new Dictionary(); + private readonly List _replayData = new List(); + + internal readonly string[] CityNames = Common.AllCityNames.ToArray(); + + private int _currentPlayer = 0; + private int _activeUnit; + + private ushort _anthologyTurn = 0; + + public bool Animations { get; set; } + public bool Sound { get; set; } + public bool CivilopediaText { get; set; } + public bool EndOfTurn { get; set; } + public bool InstantAdvice { get; set; } + public bool AutoSave { get; set; } + public bool EnemyMoves { get; set; } + public bool Palace { get; set; } + + public void SetAdvanceOrigin(IAdvance advance, Player player) + { + if (_advanceOrigin.ContainsKey(advance.Id)) + return; + byte playerNumber = 0; + if (player != null) + playerNumber = PlayerNumber(player); + _advanceOrigin.Add(advance.Id, playerNumber); + } + public bool GetAdvanceOrigin(IAdvance advance, Player player) + { + if (_advanceOrigin.ContainsKey(advance.Id)) + return (_advanceOrigin[advance.Id] == PlayerNumber(player)); + return false; + } + + public int Difficulty => _difficulty; + + public bool HasUpdate => false; + + private ushort _gameTurn; + internal ushort GameTurn + { + get + { + return _gameTurn; + } + set + { + _gameTurn = value; + Log($"Turn {_gameTurn}: {GameYear}"); + if (_anthologyTurn >= _gameTurn) + { + //TODO: Show anthology + _anthologyTurn = (ushort)(_gameTurn + 20 + Common.Random.Next(40)); + } + } + } + + internal string GameYear => Common.YearString(GameTurn); + + internal Player HumanPlayer { get; set; } + + internal Player CurrentPlayer => _players[_currentPlayer]; + + internal ReplayData[] GetReplayData() => _replayData.ToArray(); + internal T[] GetReplayData() where T : ReplayData => _replayData.Where(x => x is T).Select(x => (x as T)).ToArray(); + + private void PlayerDestroyed(object sender, EventArgs args) + { + Player player = (sender as Player); + + ICivilization destroyed = player.Civilization; + ICivilization destroyedBy = Game.CurrentPlayer.Civilization; + if (destroyedBy == destroyed) destroyedBy = Game.GetPlayer(0).Civilization; + + _replayData.Add(new ReplayData.CivilizationDestroyed(_gameTurn, destroyed.Id, destroyedBy.Id)); + + if (player.IsHuman) + { + // TODO: Move Game Over code here + return; + } + + GameTask.Insert(Message.Advisor(Advisor.Defense, false, destroyed.Name, "civilization", "destroyed", $"by {destroyedBy.NamePlural}!")); + } + + internal byte PlayerNumber(Player player) + { + byte i = 0; + foreach (Player p in _players) + { + if (p == player) + return i; + i++; + } + return 0; + } + + internal Player GetPlayer(byte number) + { + if (_players.Length < number) + return null; + return _players[number]; + } + + internal IEnumerable Players => _players; + + public void EndTurn() + { + foreach (Player player in _players.Where(x => !(x.Civilization is Barbarian))) + { + player.IsDestroyed(); + } + + if (++_currentPlayer >= _players.Length) + { + _currentPlayer = 0; + GameTurn++; + if (GameTurn % 50 == 0 && AutoSave) + { + GameTask.Enqueue(Show.AutoSave); + } + + IEnumerable disasterCities = _cities.OrderBy(o => Common.Random.Next(0,1000)).Take(2).AsEnumerable(); + foreach (City city in disasterCities) + city.Disaster(); + + if (Barbarian.IsSeaSpawnTurn) + { + ITile tile = Barbarian.SeaSpawnPosition; + if (tile != null) + { + foreach (UnitType unitType in Barbarian.SeaSpawnUnits) + CreateUnit(unitType, tile.X, tile.Y, 0, false); + } + } + } + + if (!_players.Any(x => Game.PlayerNumber(x) != 0 && x != Human && !x.IsDestroyed())) + { + PlaySound("wintune"); + + GameTask conquest; + GameTask.Enqueue(Message.Newspaper(null, "Your civilization", "has conquered", "the entire planet!")); + GameTask.Enqueue(conquest = Show.Screen()); + conquest.Done += (s, a) => Runtime.Quit(); + } + + foreach (IUnit unit in _units.Where(u => u.Owner == _currentPlayer)) + { + GameTask.Enqueue(Turn.New(unit)); + } + foreach (City city in _cities.Where(c => c.Owner == _currentPlayer).ToArray()) + { + GameTask.Enqueue(Turn.New(city)); + } + GameTask.Enqueue(Turn.New(CurrentPlayer)); + + if (CurrentPlayer != HumanPlayer) return; + + if (Game.InstantAdvice && (Common.TurnToYear(Game.GameTurn) == -3600 || Common.TurnToYear(Game.GameTurn) == -2800)) + GameTask.Enqueue(Message.Help("--- Civilization Note ---", TextFile.Instance.GetGameText("HELP/HELP1"))); + else if (Game.InstantAdvice && (Common.TurnToYear(Game.GameTurn) == -3200 || Common.TurnToYear(Game.GameTurn) == -2400)) + GameTask.Enqueue(Message.Help("--- Civilization Note ---", TextFile.Instance.GetGameText("HELP/HELP2"))); + } + + public void Update() + { + IUnit unit = ActiveUnit; + if (CurrentPlayer == HumanPlayer) + { + if (unit != null && !unit.Goto.IsEmpty) + { + + ITile[] tiles = (unit as BaseUnit).MoveTargets.OrderBy(x => x.DistanceTo(unit.Goto)).ThenBy(x => x.Movement).ToArray(); + + if (unit.Class == UnitClass.Land && Settings.Instance.PathFinding ) + { + /* Use AStar */ + AStar.sPosition Destination, Pos; + Destination.iX = unit.Goto.X; + Destination.iY = unit.Goto.Y; + Pos.iX = unit.X; + Pos.iY = unit.Y; + + AStarInterface AStarInterface = new AStarInterface(); + + AStarInterface.sPosition NextPosition = AStarInterface.FindPath(Pos, Destination); + if (NextPosition.iX < 0) + { // if no path found + unit.Goto = Point.Empty; + return; + } + unit.MoveTo(NextPosition.iX - Pos.iX, NextPosition.iY - Pos.iY); + return; + + } + else + { + + int distance = unit.Tile.DistanceTo(unit.Goto); + if (tiles.Length == 0 || tiles[0].DistanceTo(unit.Goto) > distance) + { + // No valid tile to move to, cancel goto + unit.Goto = Point.Empty; + return; + } + else if (tiles[0].DistanceTo(unit.Goto) == distance) + { + // Distance is unchanged, 50% chance to cancel goto + if (Common.Random.Next(0, 100) < 50) + { + unit.Goto = Point.Empty; + return; + } + } + } + unit.MoveTo(tiles[0].X - unit.X, tiles[0].Y - unit.Y); + return; + } + return; + } + if (unit != null && (unit.MovesLeft > 0 || unit.PartMoves > 0)) + { + GameTask.Enqueue(Turn.Move(unit)); + return; + } + GameTask.Enqueue(Turn.End()); + } + + internal int CityNameId(Player player) + { + ICivilization civilization = player.Civilization; + ICivilization[] civilizations = Common.Civilizations; + int startIndex = Enumerable.Range(1, civilization.Id - 1).Sum(i => civilizations[i].CityNames.Length); + int spareIndex = Enumerable.Range(1, Common.Civilizations.Length - 1).Sum(i => civilizations[i].CityNames.Length); + int[] used = _cities.Select(c => c.NameId).ToArray(); + int[] available = Enumerable.Range(0, CityNames.Length) + .Where(i => !used.Contains(i)) + .OrderBy(i => (i >= startIndex && i < startIndex + civilization.CityNames.Length) ? 0 : 1) + .ThenBy(i => (i >= spareIndex) ? 0 : 1) + .ThenBy(i => i) + .ToArray(); + if (player.CityNamesSkipped >= available.Length) + return 0; + return available[player.CityNamesSkipped]; + } + + internal City AddCity(Player player, int nameId, int x, int y) + { + if (_cities.Any(c => c.X == x && c.Y == y)) + return null; + + City city = new City(PlayerNumber(player)) + { + X = (byte)x, + Y = (byte)y, + NameId = nameId, + Size = 1 + }; + if (!_cities.Any(c => c.Size > 0 && c.Owner == city.Owner)) + { + Palace palace = new Palace(); + palace.SetFree(); + city.AddBuilding(palace); + } + if ((Map[x, y] is Desert) || (Map[x, y] is Grassland) || (Map[x, y] is Hills) || (Map[x, y] is Plains) || (Map[x, y] is River)) + { + Map[x, y].Irrigation = true; + } + if (!Map[x, y].RailRoad) + { + Map[x, y].Road = true; + } + _cities.Add(city); + Game.UpdateResources(city.Tile); + return city; + } + + public void DestroyCity(City city) + { + foreach (IUnit unit in _units.Where(u => u.Home == city).ToArray()) + _units.Remove(unit); + city.X = 255; + city.Y = 255; + city.Owner = 0; + } + + internal City GetCity(int x, int y) + { + while (x < 0) x += Map.WIDTH; + while (x >= Map.WIDTH) x-= Map.WIDTH; + if (y < 0) return null; + if (y >= Map.HEIGHT) return null; + return _cities.Where(c => c.X == x && c.Y == y && c.Size > 0).FirstOrDefault(); + } + + private static IUnit CreateUnit(UnitType type, int x, int y) + { + IUnit unit; + switch (type) + { + case UnitType.Settlers: unit = new Settlers(); break; + case UnitType.Militia: unit = new Militia(); break; + case UnitType.Phalanx: unit = new Phalanx(); break; + case UnitType.Legion: unit = new Legion(); break; + case UnitType.Musketeers: unit = new Musketeers(); break; + case UnitType.Riflemen: unit = new Riflemen(); break; + case UnitType.Cavalry: unit = new Cavalry(); break; + case UnitType.Knights: unit = new Knights(); break; + case UnitType.Catapult: unit = new Catapult(); break; + case UnitType.Cannon: unit = new Cannon(); break; + case UnitType.Chariot: unit = new Chariot(); break; + case UnitType.Armor: unit = new Armor(); break; + case UnitType.MechInf: unit = new MechInf(); break; + case UnitType.Artillery: unit = new Artillery(); break; + case UnitType.Fighter: unit = new Fighter(); break; + case UnitType.Bomber: unit = new Bomber(); break; + case UnitType.Trireme: unit = new Trireme(); break; + case UnitType.Sail: unit = new Sail(); break; + case UnitType.Frigate: unit = new Frigate(); break; + case UnitType.Ironclad: unit = new Ironclad(); break; + case UnitType.Cruiser: unit = new Cruiser(); break; + case UnitType.Battleship: unit = new Battleship(); break; + case UnitType.Submarine: unit = new Submarine(); break; + case UnitType.Carrier: unit = new Carrier(); break; + case UnitType.Transport: unit = new Transport(); break; + case UnitType.Nuclear: unit = new Nuclear(); break; + case UnitType.Diplomat: unit = new Diplomat(); break; + case UnitType.Caravan: unit = new Caravan(); break; + default: return null; + } + unit.X = x; + unit.Y = y; + unit.MovesLeft = unit.Move; + return unit; + } + + public IUnit CreateUnit(UnitType type, int x, int y, byte owner, bool endTurn = false) + { + IUnit unit = CreateUnit((UnitType)type, x, y); + if (unit == null) return null; + + unit.Owner = owner; + if (unit.Class == UnitClass.Water) + { + Player player = GetPlayer(owner); + if ((player.HasWonder() && !WonderObsolete()) || + (player.HasWonder() && !WonderObsolete())) + { + unit.MovesLeft++; + } + } + if (endTurn) + unit.SkipTurn(); + _instance._units.Add(unit); + return unit; + } + + internal IUnit[] GetUnits(int x, int y) + { + while (x < 0) x += Map.WIDTH; + while (x >= Map.WIDTH) x-= Map.WIDTH; + if (y < 0) return null; + if (y >= Map.HEIGHT) return null; + return _units.Where(u => u.X == x && u.Y == y).OrderBy(u => (u == ActiveUnit) ? 0 : (u.Fortify || u.FortifyActive ? 1 : 2)).ToArray(); + } + + internal IUnit[] GetUnits() => _units.ToArray(); + + internal void UpdateResources(ITile tile, bool ownerCities = true) + { + for (int relY = -3; relY <= 3; relY++) + for (int relX = -3; relX <= 3; relX++) + { + if (tile[relX, relY] == null) continue; + City city = tile[relX, relY].City; + if (city == null) continue; + if (!ownerCities && CurrentPlayer == city.Owner) continue; + city.UpdateResources(); + } + } + + public City[] GetCities() => _cities.ToArray(); + + public IWonder[] BuiltWonders => _cities.SelectMany(c => c.Wonders).ToArray(); + + public bool WonderBuilt() where T : IWonder => BuiltWonders.Any(w => w is T); + + public bool WonderBuilt(IWonder wonder) => BuiltWonders.Any(w => w.Id == wonder.Id); + + public bool WonderObsolete() where T : IWonder, new() => WonderObsolete(new T()); + + public bool WonderObsolete(IWonder wonder) => (wonder.ObsoleteTech != null && _players.Any(x => x.HasAdvance(wonder.ObsoleteTech))); + + public void DisbandUnit(IUnit unit) + { + IUnit activeUnit = ActiveUnit; + + if (unit == null) return; + if (!_units.Contains(unit)) return; + if (unit.Tile is Ocean && unit is IBoardable) + { + int totalCargo = unit.Tile.Units.Where(u => u is IBoardable).Sum(u => (u as IBoardable).Cargo) - (unit as IBoardable).Cargo; + while (unit.Tile.Units.Count(u => u.Class != UnitClass.Water) > totalCargo) + { + IUnit subUnit = unit.Tile.Units.First(u => u.Class != UnitClass.Water); + subUnit.X = 255; + subUnit.Y = 255; + _units.Remove(subUnit); + } + } + unit.X = 255; + unit.Y = 255; + _units.Remove(unit); + + GetPlayer(unit.Owner).IsDestroyed(); + + if (_units.Contains(activeUnit)) + { + _activeUnit = _units.IndexOf(activeUnit); + } + } + + public void UnitWait() => _activeUnit++; + + public IUnit ActiveUnit + { + get + { + if (_units.Count(u => u.Owner == _currentPlayer && !u.Busy) == 0) + return null; + + // If the unit counter is too high, return to 0 + if (_activeUnit >= _units.Count) + _activeUnit = 0; + + // Does the current unit still have moves left? + if (_units[_activeUnit].Owner == _currentPlayer && (_units[_activeUnit].MovesLeft > 0 || _units[_activeUnit].PartMoves > 0) && !_units[_activeUnit].Sentry && !_units[_activeUnit].Fortify) + return _units[_activeUnit]; + + // Task busy, don't change the active unit + if (GameTask.Any()) + return _units[_activeUnit]; + + // Check if any units are still available for this player + if (!_units.Any(u => u.Owner == _currentPlayer && (u.MovesLeft > 0 || u.PartMoves > 0) && !u.Busy)) + { + if (CurrentPlayer == HumanPlayer && !EndOfTurn && !GameTask.Any() && (Common.TopScreen is GamePlay)) + { + GameTask.Enqueue(Turn.End()); + } + return null; + } + + // Loop through units + while (_units[_activeUnit].Owner != _currentPlayer || (_units[_activeUnit].MovesLeft == 0 && _units[_activeUnit].PartMoves == 0) || (_units[_activeUnit].Sentry || _units[_activeUnit].Fortify)) + { + _activeUnit++; + if (_activeUnit >= _units.Count) + _activeUnit = 0; + } + return _units[_activeUnit]; + } + internal set + { + if (value == null || value.MovesLeft == 0 && value.PartMoves == 0) + return; + value.Sentry = false; + value.Fortify = false; + _activeUnit = _units.IndexOf(value); + } + } + + public IUnit MovingUnit => _units.FirstOrDefault(u => u.Moving); + + public static bool Started => (_instance != null); + + private static Game _instance; + public static Game Instance + { + get + { + if (_instance == null) + { + Log("ERROR: Game instance does not exist"); + } + return _instance; + } + } + } +} \ No newline at end of file diff --git a/Settings.cs b/Settings.cs new file mode 100644 index 00000000..df7ebaec --- /dev/null +++ b/Settings.cs @@ -0,0 +1,445 @@ +// CivOne +// +// To the extent possible under law, the person who associated CC0 with +// CivOne has waived all copyright and related or neighboring rights +// to CivOne. +// +// You should have received a copy of the CC0 legalcode along with this +// work. If not, see . + + +using System; +using System.IO; +using CivOne.Enums; +using CivOne.Graphics; +using CivOne.Graphics.Sprites; + + +namespace CivOne +{ + public class Settings + { + private static IRuntime Runtime => RuntimeHandler.Runtime; + private static void Log(string text, params object[] parameters) => RuntimeHandler.Runtime.Log(text, parameters); + + // Set default settings + private string _windowTitle = "CivOne"; + private GraphicsMode _graphicsMode = GraphicsMode.Graphics256; + private bool _fullScreen = false; + private bool _rightSideBar = false; + private int _scale = 2; + private AspectRatio _aspectRatio = AspectRatio.Auto; + private int _expandWidth, _expandHeight; + private bool _revealWorld = false; + private bool _debugMenu = false; + private bool _deityEnabled = false; + private bool _arrowHelper = false; + private bool _customMapSize = false; +#if true + private bool _pathFinding = false; +#else + private bool _pathFinding = true; +#endif + private CursorType _cursorType = CursorType.Default; + private DestroyAnimation _destroyAnimation = DestroyAnimation.Sprites; + private GameOption _instantAdvice, _autoSave, _endOfTurn, _animations, _sound, _enemyMoves, _civilopediaText, _palace; + + internal string StorageDirectory => Runtime.StorageDirectory; + internal string CaptureDirectory => Path.Combine(StorageDirectory, "capture"); + internal string DataDirectory => Path.Combine(StorageDirectory, "data"); + internal string PluginsDirectory => Path.Combine(StorageDirectory, "plugins"); + internal string SavesDirectory => Path.Combine(StorageDirectory, "saves"); + internal string SoundsDirectory => Path.Combine(StorageDirectory, "sounds"); + + // Settings + + internal string WindowTitle + { + get => _windowTitle; + set + { + _windowTitle = value; + SetSetting("WindowTitle", _windowTitle); + Common.ReloadSettings = true; + } + } + + internal GraphicsMode GraphicsMode + { + get => _graphicsMode; + set + { + _graphicsMode = value; + string saveValue = _graphicsMode == GraphicsMode.Graphics256 ? "1" : "2"; + SetSetting("GraphicsMode", saveValue); + Common.ReloadSettings = true; + + Resources.ClearInstance(); + } + } + + public bool FullScreen + { + get => _fullScreen; + set + { + _fullScreen = value; + SetSetting("FullScreen", _fullScreen ? "1" : "0"); + Common.ReloadSettings = true; + } + } + + public int Scale + { + get => _scale; + set + { + if (value < 1 || value > 4) return; + _scale = value; + SetSetting("Scale", _scale.ToString()); + Common.ReloadSettings = true; + } + } + + public AspectRatio AspectRatio + { + get => _aspectRatio; + set + { + _aspectRatio = value; + string saveValue = ((int)_aspectRatio).ToString(); + SetSetting("AspectRatio", saveValue); + Common.ReloadSettings = true; + } + } + + public int ExpandWidth + { + get => _expandWidth; + set + { + _expandWidth = value; + string saveValue = ((int)_expandWidth).ToString(); + SetSetting("ExpandWidth", saveValue); + Common.ReloadSettings = true; + } + } + + public int ExpandHeight + { + get => _expandHeight; + set + { + _expandHeight = value; + string saveValue = ((int)_expandHeight).ToString(); + SetSetting("ExpandHeight", saveValue); + Common.ReloadSettings = true; + } + } + + // Patches + + internal bool RevealWorld + { + get => _revealWorld; + set + { + _revealWorld = value; + SetSetting("RevealWorld", _revealWorld ? "1" : "0"); + Common.ReloadSettings = true; + } + } + + internal bool RightSideBar + { + get => _rightSideBar; + set + { + _rightSideBar = value; + SetSetting("SideBar", _rightSideBar ? "1" : "0"); + Common.ReloadSettings = true; + } + } + + internal bool DebugMenu + { + get => _debugMenu; + set + { + _debugMenu = value; + SetSetting("DebugMenu", _debugMenu ? "1" : "0"); + Common.ReloadSettings = true; + } + } + + internal bool DeityEnabled + { + get => _deityEnabled; + set + { + _deityEnabled = value; + SetSetting("DeityEnabled", _deityEnabled ? "1" : "0"); + Common.ReloadSettings = true; + } + } + + internal bool ArrowHelper + { + get => _arrowHelper; + set + { + _arrowHelper = value; + SetSetting("ArrowHelper", _arrowHelper ? "1" : "0"); + Common.ReloadSettings = true; + } + } + + internal bool CustomMapSize + { + get => _customMapSize; + set + { + _customMapSize = value; + SetSetting("CustomMapSize", _customMapSize ? "1" : "0"); + Common.ReloadSettings = true; + } + } + + internal bool PathFinding + { + get => _pathFinding; + set + { + _pathFinding = value; + SetSetting("PathFindingAlgorithm", _pathFinding ? "1" : "0"); + Common.ReloadSettings = true; + } + } + + + public CursorType CursorType + { + get + { + if (Runtime.Settings.Free && _cursorType == CursorType.Default) + return CursorType.Builtin; + return _cursorType; + } + internal set + { + _cursorType = value; + string saveValue = ((int)_cursorType).ToString(); + SetSetting("CursorType", saveValue); + Cursor.ClearCache(); + Common.ReloadSettings = true; + } + } + + internal DestroyAnimation DestroyAnimation + { + get => _destroyAnimation; + set + { + _destroyAnimation = value; + string saveValue = ((int)_destroyAnimation).ToString(); + SetSetting("DestroyAnimation", saveValue); + Common.ReloadSettings = true; + } + } + + // Game options + public GameOption InstantAdvice + { + get => _instantAdvice; + set + { + _instantAdvice = value; + string saveValue = ((int)_instantAdvice).ToString(); + SetSetting("GameInstantAdvice", saveValue); + Common.ReloadSettings = true; + } + } + + public GameOption AutoSave + { + get => _autoSave; + set + { + _autoSave = value; + string saveValue = ((int)_autoSave).ToString(); + SetSetting("GameAutoSave", saveValue); + Common.ReloadSettings = true; + } + } + + public GameOption EndOfTurn + { + get => _endOfTurn; + set + { + _endOfTurn = value; + string saveValue = ((int)_endOfTurn).ToString(); + SetSetting("GameEndOfTurn", saveValue); + Common.ReloadSettings = true; + } + } + + public GameOption Animations + { + get => _animations; + set + { + _animations = value; + string saveValue = ((int)_animations).ToString(); + SetSetting("GameAnimations", saveValue); + Common.ReloadSettings = true; + } + } + + public GameOption Sound + { + get => _sound; + set + { + _sound = value; + string saveValue = ((int)_sound).ToString(); + SetSetting("GameSound", saveValue); + Common.ReloadSettings = true; + } + } + + public GameOption EnemyMoves + { + get => _enemyMoves; + set + { + _enemyMoves = value; + string saveValue = ((int)_enemyMoves).ToString(); + SetSetting("GameEnemyMoves", saveValue); + Common.ReloadSettings = true; + } + } + + public GameOption CivilopediaText + { + get => _civilopediaText; + set + { + _civilopediaText = value; + string saveValue = ((int)_civilopediaText).ToString(); + SetSetting("GameCivilopediaText", saveValue); + Common.ReloadSettings = true; + } + } + + public GameOption Palace + { + get => _palace; + set + { + _palace = value; + string saveValue = ((int)_palace).ToString(); + SetSetting("GamePalace", saveValue); + Common.ReloadSettings = true; + } + } + + public string[] DisabledPlugins + { + get => GetSetting("DisabledPlugins")?.Split(';') ?? new string[0]; + set => SetSetting("DisabledPlugins", string.Join(";", value)); + } + + internal void RevealWorldCheat() => _revealWorld = !_revealWorld; + + internal int ScaleX => _scale; + internal int ScaleY => _scale; + + private string GetSetting(string settingName) => Runtime.GetSetting(settingName); + + private bool GetSetting(string settingName, ref T output) where T: struct, IConvertible + { + if (!Int32.TryParse(GetSetting(settingName), out int value)) return false; + if (!Enum.IsDefined(typeof(T), value)) return false; + output = (T)Enum.Parse(typeof(T), value.ToString()); + return true; + } + + private void GetSetting(string settingName, ref string output) => output = GetSetting(settingName) ?? output; + + private void GetSetting(string settingName, ref bool output) => output = (GetSetting(settingName) == "1"); + + private bool GetSetting(string settingName, ref int output, int minValue = int.MinValue, int maxValue = int.MaxValue) + { + if (!Int32.TryParse(GetSetting(settingName), out int value)) return false; + if (value < minValue || value > maxValue) return false; + output = value; + return true; + } + + private void SetSetting(string settingName, string value) => Runtime.SetSetting(settingName, value); + + private void CreateDirectories() + { + foreach (string dir in new[] { StorageDirectory, CaptureDirectory, DataDirectory, PluginsDirectory, SavesDirectory, SoundsDirectory }) + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + for (char c = 'a'; c <= 'z'; c++) + { + string dir = Path.Combine(SavesDirectory, c.ToString()); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + } + } + + private static Settings _instance; + public static Settings Instance + { + get + { + if (_instance == null) + _instance = new Settings(); + return _instance; + } + } + + private Settings() + { + CreateDirectories(); + + // Read settings + GetSetting("WindowTitle", ref _windowTitle); + GetSetting("GraphicsMode", ref _graphicsMode); + GetSetting("FullScreen", ref _fullScreen); + GetSetting("SideBar", ref _rightSideBar); + GetSetting("Scale", ref _scale, 1, 4); + GetSetting("AspectRatio", ref _aspectRatio); + GetSetting("Sound", ref _sound); + if (!GetSetting("ExpandWidth", ref _scale, 320, 512) || !GetSetting("ExpandHeight", ref _scale, 200, 384)) + { + _expandWidth = -1; + _expandHeight = -1; + } + GetSetting("RevealWorld", ref _revealWorld); + GetSetting("DebugMenu", ref _debugMenu); + GetSetting("DeityEnabled", ref _deityEnabled); + GetSetting("ArrowHelper", ref _arrowHelper); + GetSetting("CustomMapSize", ref _customMapSize); + GetSetting("CursorType", ref _cursorType); + GetSetting("DestroyAnimation", ref _destroyAnimation); + GetSetting("GameInstantAdvice", ref _instantAdvice); + GetSetting("GameAutoSave", ref _autoSave); + GetSetting("GameEndOfTurn", ref _endOfTurn); + GetSetting("GameAnimations", ref _animations); + GetSetting("GameSound", ref _sound); + GetSetting("GameEnemyMoves", ref _enemyMoves); + GetSetting("GameCivilopediaText", ref _civilopediaText); + GetSetting("GamePalace", ref _palace); + } + } +} \ No newline at end of file diff --git a/Setup.cs b/Setup.cs new file mode 100644 index 00000000..78494e18 --- /dev/null +++ b/Setup.cs @@ -0,0 +1,324 @@ +// CivOne +// +// To the extent possible under law, the person who associated CC0 with +// CivOne has waived all copyright and related or neighboring rights +// to CivOne. +// +// You should have received a copy of the CC0 legalcode along with this +// work. If not, see . + +using System; +using System.Linq; +using CivOne.Enums; +using CivOne.Events; +using CivOne.Graphics; +using CivOne.IO; +using CivOne.UserInterface; + +using static CivOne.Enums.AspectRatio; +using static CivOne.Enums.CursorType; +using static CivOne.Enums.DestroyAnimation; +using static CivOne.Enums.GraphicsMode; + +namespace CivOne.Screens +{ + [Break, Expand] + internal class Setup : BaseScreen + { + private const int MenuFont = 6; + + private bool _update = true; + + protected override bool HasUpdate(uint gameTick) + { + if (!_update) return false; + _update = false; + + if (!HasMenu) + { + MainMenu(); + } + + return false; + } + + private void BrowseForSoundFiles(object sender, MenuItemEventArgs args) + { + string path = Runtime.BrowseFolder("Location of Civilization for Windows sound files"); + if (path == null) + { + // User pressed cancel + return; + } + + FileSystem.CopySoundFiles(path); + } + + private void BrowseForPlugins(object sender, MenuItemEventArgs args) + { + string path = Runtime.BrowseFolder("Location of CivOne plugin(s)"); + if (path == null) + { + // User pressed cancel + return; + } + + CloseMenus(); + MainMenu(2); + FileSystem.CopyPlugins(path); + } + + private void CreateMenu(string title, int activeItem, MenuItemEventHandler always, params MenuItem[] items) => + AddMenu(new Menu("Setup", Palette) + { + Title = $"{title.ToUpper()}:", + TitleColour = 15, + ActiveColour = 11, + TextColour = 5, + DisabledColour = 8, + FontId = MenuFont, + IndentTitle = 2 + } + .Items(items) + .Always(always) + .Center(this) + .SetActiveItem(activeItem) + ); + private void CreateMenu(string title, MenuItemEventHandler always, params MenuItem[] items) => CreateMenu(title, -1, always, items); + private void CreateMenu(string title, int activeItem, params MenuItem[] items) => CreateMenu(title, activeItem, null, items); + private void CreateMenu(string title, params MenuItem[] items) => CreateMenu(title, -1, null, items); + + private MenuItemEventHandler GotoMenu(Action action, int selectedItem = 0) => (s, a) => + { + CloseMenus(); + action(selectedItem); + }; + + private MenuItemEventHandler GotoMenu(Action action) => (s, a) => + { + CloseMenus(); + action(); + }; + + private MenuItemEventHandler GotoScreen(Action doneAction) where T : IScreen, new() => (s, a) => + { + CloseMenus(); + T screen = new T(); + screen.Closed += (sender, args) => doneAction(); + Common.AddScreen(screen); + }; + + private MenuItemEventHandler CloseScreen(Action action = null) => (s, a) => + { + Destroy(); + if (action != null) action(); + }; + + private void ChangeWindowTitle() + { + RuntimeHandler.Runtime.WindowTitle = Settings.WindowTitle; + SettingsMenu(0); + } + + private void MainMenu(int activeItem = 0) => CreateMenu("CivOne Setup", activeItem, + MenuItem.Create("Settings").OnSelect(GotoMenu(SettingsMenu)), + MenuItem.Create("Patches").OnSelect(GotoMenu(PatchesMenu)), + MenuItem.Create("Plugins").OnSelect(GotoMenu(PluginsMenu)), + MenuItem.Create("Game Options").OnSelect(GotoMenu(GameOptionsMenu)), + MenuItem.Create("Launch Game").OnSelect(CloseScreen()), + MenuItem.Create("Quit").OnSelect(CloseScreen(Runtime.Quit)) + ); + + private void SettingsMenu(int activeItem = 0) => CreateMenu("Settings", activeItem, + MenuItem.Create($"Window Title: {Settings.WindowTitle}").OnSelect(GotoScreen(ChangeWindowTitle)), + MenuItem.Create($"Graphics Mode: {Settings.GraphicsMode.ToText()}").OnSelect(GotoMenu(GraphicsModeMenu)), + MenuItem.Create($"Aspect Ratio: {Settings.AspectRatio.ToText()}").OnSelect(GotoMenu(AspectRatioMenu)), + MenuItem.Create($"Full Screen: {Settings.FullScreen.YesNo()}").OnSelect(GotoMenu(FullScreenMenu)), + MenuItem.Create($"Window Scale: {Settings.Scale}x").OnSelect(GotoMenu(WindowScaleMenu)), + MenuItem.Create("In-game sound").OnSelect(GotoMenu(SoundMenu)), + MenuItem.Create($"Back").OnSelect(GotoMenu(MainMenu, 0)) + ); + + private void GraphicsModeMenu() => CreateMenu("Graphics Mode", GotoMenu(SettingsMenu, 1), + MenuItem.Create($"{Graphics256.ToText()} (default)").OnSelect((s, a) => Settings.GraphicsMode = Graphics256).SetActive(() => Settings.GraphicsMode == Graphics256), + MenuItem.Create(Graphics16.ToText()).OnSelect((s, a) => Settings.GraphicsMode = Graphics16).SetActive(() => Settings.GraphicsMode == Graphics16), + MenuItem.Create("Back") + ); + + private void AspectRatioMenu() => CreateMenu("Aspect Ratio", GotoMenu(SettingsMenu, 2), + MenuItem.Create($"{Auto.ToText()} (default)").OnSelect((s, a) => Settings.AspectRatio = Auto).SetActive(() => Settings.AspectRatio == Auto), + MenuItem.Create(Fixed.ToText()).OnSelect((s, a) => Settings.AspectRatio = Fixed).SetActive(() => Settings.AspectRatio == Fixed), + MenuItem.Create(Scaled.ToText()).OnSelect((s, a) => Settings.AspectRatio = Scaled).SetActive(() => Settings.AspectRatio == Scaled), + MenuItem.Create(ScaledFixed.ToText()).OnSelect((s, a) => Settings.AspectRatio = ScaledFixed).SetActive(() => Settings.AspectRatio == ScaledFixed), + MenuItem.Create(AspectRatio.Expand.ToText()).OnSelect((s, a) => Settings.AspectRatio = AspectRatio.Expand).SetActive(() => Settings.AspectRatio == AspectRatio.Expand), + MenuItem.Create("Back") + ); + + private void FullScreenMenu() => CreateMenu("Full Screen", GotoMenu(SettingsMenu, 3), + MenuItem.Create($"{false.YesNo()} (default)").OnSelect((s, a) => Settings.FullScreen = false).SetActive(() => !Settings.FullScreen), + MenuItem.Create(true.YesNo()).OnSelect((s, a) => Settings.FullScreen = true).SetActive(() => Settings.FullScreen), + MenuItem.Create("Back") + ); + + private void WindowScaleMenu() => CreateMenu("Window Scale", GotoMenu(SettingsMenu, 4), + MenuItem.Create("1x").OnSelect((s, a) => Settings.Scale = 1).SetActive(() => Settings.Scale == 1), + MenuItem.Create("2x (default)").OnSelect((s, a) => Settings.Scale = 2).SetActive(() => Settings.Scale == 2), + MenuItem.Create("3x").OnSelect((s, a) => Settings.Scale = 3).SetActive(() => Settings.Scale == 3), + MenuItem.Create("4x").OnSelect((s, a) => Settings.Scale = 4).SetActive(() => Settings.Scale == 4), + MenuItem.Create("Back") + ); + + private void SoundMenu() => CreateMenu("In-game sound", GotoMenu(SettingsMenu, 5), + MenuItem.Create("Browse for files...").OnSelect(BrowseForSoundFiles).SetEnabled(!FileSystem.SoundFilesExist()), + MenuItem.Create("Back") + ); + + private void PatchesMenu(int activeItem = 0) => CreateMenu("Patches", activeItem, + MenuItem.Create($"Reveal world: {Settings.RevealWorld.YesNo()}").OnSelect(GotoMenu(RevealWorldMenu)), + MenuItem.Create($"Side bar location: {(Settings.RightSideBar ? "right" : "left")}").OnSelect(GotoMenu(SideBarMenu)), + MenuItem.Create($"Debug menu: {Settings.DebugMenu.YesNo()}").OnSelect(GotoMenu(DebugMenuMenu)), + MenuItem.Create($"Cursor type: {Settings.CursorType.ToText()}").OnSelect(GotoMenu(CursorTypeMenu)), + MenuItem.Create($"Destroy animation: {Settings.DestroyAnimation.ToText()}").OnSelect(GotoMenu(DestroyAnimationMenu)), + MenuItem.Create($"Enable Deity difficulty: {Settings.DeityEnabled.YesNo()}").OnSelect(GotoMenu(DeityEnabledMenu)), + MenuItem.Create($"Enable (no keypad) arrow helper: {Settings.ArrowHelper.YesNo()}").OnSelect(GotoMenu(ArrowHelperMenu)), + MenuItem.Create($"Custom map sizes (experimental): {Settings.CustomMapSize.YesNo()}").OnSelect(GotoMenu(CustomMapSizeMenu)), + MenuItem.Create($"Use smart PathFinding for \"goto\": {Settings.PathFinding.YesNo()}").OnSelect(GotoMenu(PathFindingeMenu)), + MenuItem.Create("Back").OnSelect(GotoMenu(MainMenu, 1)) + ); + + private void RevealWorldMenu() => CreateMenu("Reveal world", GotoMenu(PatchesMenu, 0), + MenuItem.Create($"{false.YesNo()} (default)").OnSelect((s, a) => Settings.RevealWorld = false).SetActive(() => !Settings.RevealWorld), + MenuItem.Create(true.YesNo()).OnSelect((s, a) => Settings.RevealWorld = true).SetActive(() => Settings.RevealWorld), + MenuItem.Create("Back") + ); + + private void SideBarMenu() => CreateMenu("Side bar location", GotoMenu(PatchesMenu, 1), + MenuItem.Create("Left (default)").OnSelect((s, a) => Settings.RightSideBar = false).SetActive(() => !Settings.RightSideBar), + MenuItem.Create("Right").OnSelect((s, a) => Settings.RightSideBar = true).SetActive(() => Settings.RightSideBar), + MenuItem.Create("Back") + ); + + private void DebugMenuMenu() => CreateMenu("Show debug menu", GotoMenu(PatchesMenu, 2), + MenuItem.Create($"{false.YesNo()} (default)").OnSelect((s, a) => Settings.DebugMenu = false).SetActive(() => !Settings.DebugMenu), + MenuItem.Create(true.YesNo()).OnSelect((s, a) => Settings.DebugMenu = true).SetActive(() => Settings.DebugMenu), + MenuItem.Create("Back") + ); + + private void CursorTypeMenu() => CreateMenu("Mouse cursor type", GotoMenu(PatchesMenu, 3), + MenuItem.Create(Default.ToText()).OnSelect((s, a) => Settings.CursorType = Default).SetActive(() => Settings.CursorType == Default && FileSystem.DataFilesExist(FileSystem.MouseCursorFiles)).SetEnabled(FileSystem.DataFilesExist(FileSystem.MouseCursorFiles)), + MenuItem.Create(Builtin.ToText()).OnSelect((s, a) => Settings.CursorType = Builtin).SetActive(() => Settings.CursorType == Builtin || (Settings.CursorType == Default && !FileSystem.DataFilesExist(FileSystem.MouseCursorFiles))), + MenuItem.Create(Native.ToText()).OnSelect((s, a) => Settings.CursorType = Native).SetActive(() => Settings.CursorType == Native), + MenuItem.Create("Back") + ); + + private void DestroyAnimationMenu() => CreateMenu("Destroy animation", GotoMenu(PatchesMenu, 4), + MenuItem.Create(Sprites.ToText()).OnSelect((s, a) => Settings.DestroyAnimation = Sprites).SetActive(() => Settings.DestroyAnimation == Sprites), + MenuItem.Create(Noise.ToText()).OnSelect((s, a) => Settings.DestroyAnimation = Noise).SetActive(() => Settings.DestroyAnimation == Noise), + MenuItem.Create("Back") + ); + + private void DeityEnabledMenu() => CreateMenu("Enable Deity difficulty", GotoMenu(PatchesMenu, 5), + MenuItem.Create($"{false.YesNo()} (default)").OnSelect((s, a) => Settings.DeityEnabled = false).SetActive(() => !Settings.DeityEnabled), + MenuItem.Create(true.YesNo()).OnSelect((s, a) => Settings.DeityEnabled = true).SetActive(() => Settings.DeityEnabled), + MenuItem.Create("Back") + ); + + private void ArrowHelperMenu() => CreateMenu("Enable (no keypad) arrow helper", GotoMenu(PatchesMenu, 6), + MenuItem.Create($"{false.YesNo()} (default)").OnSelect((s, a) => Settings.ArrowHelper = false).SetActive(() => !Settings.ArrowHelper), + MenuItem.Create(true.YesNo()).OnSelect((s, a) => Settings.ArrowHelper = true).SetActive(() => Settings.ArrowHelper), + MenuItem.Create("Back") + ); + + private void CustomMapSizeMenu() => CreateMenu("Custom map sizes (experimental)", GotoMenu(PatchesMenu, 7), + MenuItem.Create($"{false.YesNo()} (default)").OnSelect((s, a) => Settings.CustomMapSize = false).SetActive(() => !Settings.CustomMapSize), + MenuItem.Create(true.YesNo()).OnSelect((s, a) => Settings.CustomMapSize = true).SetActive(() => Settings.CustomMapSize), + MenuItem.Create("Back") + ); + +#if true + private void PathFindingeMenu() => CreateMenu("Use smart PathFinding for \"goto\"", GotoMenu(PatchesMenu, 8), + MenuItem.Create($"{false.YesNo()} (default)").OnSelect((s, a) => Settings.PathFinding = false).SetActive(() => !Settings.PathFinding), + MenuItem.Create(true.YesNo()).OnSelect((s, a) => Settings.PathFinding = true).SetActive(() => Settings.PathFinding), + MenuItem.Create("Back") + ); +#else + private void PathFindingeMenu() => CreateMenu("Use smart PathFinding for \"goto\"", GotoMenu(PatchesMenu, 8), + MenuItem.Create($"{true.YesNo()} (default)").OnSelect((s, a) => Settings.PathFinding = true).SetActive(() => !Settings.PathFinding), + MenuItem.Create(false.YesNo()).OnSelect((s, a) => Settings.PathFinding = false).SetActive(() => Settings.PathFinding), + MenuItem.Create("Back") + ); +#endif + private void PluginsMenu(int activeItem = 0) => CreateMenu("Plugins", activeItem, + new MenuItem[0] + .Concat( + Reflect.Plugins().Any() ? + Reflect.Plugins().Select(x => MenuItem.Create(x.ToString()).SetEnabled(!x.Deleted).OnSelect(GotoMenu(PluginMenu(x.Id, x)))) : + new [] { MenuItem.Create("No plugins installed").Disable() } + ) + .Concat(new [] + { + MenuItem.Create(null).Disable(), + MenuItem.Create("Add plugins").OnSelect(BrowseForPlugins), + MenuItem.Create("Back").OnSelect(GotoMenu(MainMenu, 2)) + }).ToArray() + ); + + private Action PluginMenu(int item, Plugin plugin) => () => CreateMenu(plugin.Name, 0, + MenuItem.Create($"Version: {plugin.Version}").Disable(), + MenuItem.Create($"Author: {plugin.Author}").Disable(), + MenuItem.Create($"Status: {plugin.Enabled.EnabledDisabled()}").OnSelect(GotoMenu(PluginStatusMenu(item, plugin))), + MenuItem.Create($"Delete plugin").OnSelect(GotoMenu(PluginDeleteMenu(item, plugin))), + MenuItem.Create("Back").OnSelect(GotoMenu(PluginsMenu, item)) + ); + + private Action PluginStatusMenu(int item, Plugin plugin) => () => CreateMenu($"{plugin.Name} Status", (plugin.Enabled ? 1 : 0), GotoMenu(PluginMenu(item, plugin)), + MenuItem.Create(false.EnabledDisabled()).OnSelect((s, a) => plugin.Enabled = false), + MenuItem.Create(true.EnabledDisabled()).OnSelect((s, a) => plugin.Enabled = true), + MenuItem.Create("Back") + ); + + private Action PluginDeleteMenu(int item, Plugin plugin) => () => CreateMenu($"Delete {plugin.Name} from disk?", 0, + MenuItem.Create(false.YesNo()).OnSelect(GotoMenu(PluginsMenu, item)), + MenuItem.Create(true.YesNo()).OnSelect((s, a) => plugin.Delete()).OnSelect(GotoMenu(PluginsMenu, item)) + ); + + private void GameOptionsMenu(int activeItem = 0) => CreateMenu("Game Options", activeItem, + MenuItem.Create($"Instant Advice: {Settings.InstantAdvice.ToText()}").OnSelect(GotoMenu(GameOptionMenu(0, "Instant Advice", () => Settings.InstantAdvice, (GameOption option) => Settings.InstantAdvice = option))), + MenuItem.Create($"AutoSave: {Settings.AutoSave.ToText()}").OnSelect(GotoMenu(GameOptionMenu(1, "AutoSave", () => Settings.AutoSave, (GameOption option) => Settings.AutoSave = option))), + MenuItem.Create($"End of Turn: {Settings.EndOfTurn.ToText()}").OnSelect(GotoMenu(GameOptionMenu(2, "End of Turn", () => Settings.EndOfTurn, (GameOption option) => Settings.EndOfTurn = option))), + MenuItem.Create($"Animations: {Settings.Animations.ToText()}").OnSelect(GotoMenu(GameOptionMenu(3, "Animations", () => Settings.Animations, (GameOption option) => Settings.Animations = option))), + MenuItem.Create($"Sound: {Settings.Sound.ToText()}").OnSelect(GotoMenu(GameOptionMenu(4, "Sound", () => Settings.Sound, (GameOption option) => Settings.Sound = option))), + MenuItem.Create($"Enemy Moves: {Settings.EnemyMoves.ToText()}").OnSelect(GotoMenu(GameOptionMenu(5, "Enemy Moves", () => Settings.EnemyMoves, (GameOption option) => Settings.EnemyMoves = option))), + MenuItem.Create($"Civilopedia Text: {Settings.CivilopediaText.ToText()}").OnSelect(GotoMenu(GameOptionMenu(6, "Civilopedia Text", () => Settings.CivilopediaText, (GameOption option) => Settings.CivilopediaText = option))), + MenuItem.Create($"Palace: {Settings.Palace.ToText()}").OnSelect(GotoMenu(GameOptionMenu(7, "Palace", () => Settings.Palace, (GameOption option) => Settings.Palace = option))), + MenuItem.Create("Back").OnSelect(GotoMenu(MainMenu, 3)) + ); + + private Action GameOptionMenu(int item, string title, Func getOption, Action setOption) => () => CreateMenu(title, GotoMenu(GameOptionsMenu, item), + MenuItem.Create(GameOption.Default.ToText()).OnSelect((s, a) => setOption(GameOption.Default)).SetActive(() => getOption() == GameOption.Default), + MenuItem.Create(GameOption.On.ToText()).OnSelect((s, a) => setOption(GameOption.On)).SetActive(() => getOption() == GameOption.On), + MenuItem.Create(GameOption.Off.ToText()).OnSelect((s, a) => setOption(GameOption.Off)).SetActive(() => getOption() == GameOption.Off), + MenuItem.Create("Back") + ); + + private void Resize(object sender, ResizeEventArgs args) + { + this.Clear(3); + + foreach (Menu menu in Menus["Setup"]) + { + menu.Center(this).ForceUpdate(); + } + + _update = true; + } + + public Setup() : base(MouseCursor.Pointer) + { + OnResize += Resize; + + Palette = Common.GetPalette256; + this.Clear(3); + } + } +} \ No newline at end of file diff --git a/src/AStar.cs b/src/AStar.cs new file mode 100644 index 00000000..84586f18 --- /dev/null +++ b/src/AStar.cs @@ -0,0 +1,343 @@ +using System.Collections.Generic; +using System; + +// An algorithm using AStar for finding best route from start to finish when using "goto" command in CivOne. +// "Stolen" from https://stackoverflow.com/questions/38387646/concise-universal-a-search-in-c-sharp by kaalus +// and modified to my taste + + +public abstract class AStar +{ + #region Fields + + public struct sPosition { public int iX; public int iY; }; + + public class Node + { + public sPosition Position; + public sPosition PreviousPosition; + public float F, G, H; + public bool IsClosed; + } + + private int m_nodesCacheIndex; + private List m_nodesCache = new List(); + private List m_openHeap = new List(); + private List m_neighbors = new List(); + private Dictionary m_defaultStorage; + + #endregion + + #region Domain Definition + + // User must override Neighbors, Cost and Heuristic functions to define search domain. + // It is optional to override StorageClear, StorageGet and StorageAdd functions. + // Default implementation of these storage functions uses a Dictionary, this works for all possible search domains. + // A domain-specific storage algorihm may be significantly faster. + // For example if searching on a finite 2D or 3D grid, storage using fixed array with each element representing one world point benchmarks an order of magnitude faster. + + /// + /// Return all neighbors of the given point. + /// Must be overridden. + /// + /// Point to return neighbors for + /// Empty collection to fill with neighbors + protected abstract void Neighbors(Node node, List neighbors); + + /// + /// Return cost of making a step from p1 to p2 (which are neighbors). + /// Cost equal to float.PositiveInfinity indicates that passage from p1 to p2 is impossible. + /// Must be overridden. + /// + /// Start point + /// End point + /// Cost value + protected abstract float Cost(sPosition p1, sPosition p2); + + /// + /// Return an estimate of cost of moving from p to nearest goal. + /// Must return 0 when p is goal. + /// This is an estimate of sum of all costs along the best path between p and the nearest goal. + /// This should not overestimate the actual cost; if it does, the result of A* might not be an optimal path. + /// Underestimating the cost is allowed, but may cause the algorithm to check more positions. + /// Must be overridden. + /// + /// Point to estimate cost from + /// Point to estimate cost to + /// Cost value + protected abstract float Heuristic(sPosition p); + + /// + /// Clear A* storage. + /// This will be called every time before a search starts and before any other user functions are called. + /// Optional override when using domain-optimized storage. + /// + protected virtual void StorageClear() + { + if (m_defaultStorage == null) + { + m_defaultStorage = new Dictionary(); + } + else + { + m_defaultStorage.Clear(); + } + } + + /// + /// Retrieve data from storage at given point. + /// Optional override when using domain-optimized storage. + /// + /// Point to retrieve data at + /// Data stored for point p or null if nothing stored + protected virtual object StorageGet(sPosition position) + { + object data; + m_defaultStorage.TryGetValue(position, out data); + return data; + } + + /// + /// Add data to storage at given point. + /// There will never be any data already stored at that point. + /// Optional override when using domain-optimized storage. + /// + /// Point to add data at + /// Data to add + protected virtual void StorageAdd(sPosition position, object data) + { + m_defaultStorage.Add(position, data); + } + + #endregion + + #region Public Interface + + /// + /// Find best path from start to nearest goal. + /// Goal is any point for which Heuristic override returns 0. + /// If maxPositionsToCheck limit is reached, best path found so far is returned. + /// If there is no path to goal, path to a point nearest to goal is returned instead. + /// + /// Path will contain steps to reach goal from start in reverse order (first step at the end of collection) + /// Starting point to search for path + /// Maximum number of positions to check + /// True when path to goal was found, false if partial path only + public bool FindPath(ICollection path, sPosition StartPosition, int maxPositionsToCheck = int.MaxValue) + { + // Check arguments + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + // Reset cache and storage + path.Clear(); + m_nodesCacheIndex = 0; + m_openHeap.Clear(); + StorageClear(); + + // Put start node + Node startNode = NewNode(StartPosition, StartPosition, 0, 0); // Start node point to itself + StorageAdd(StartPosition, startNode); + HeapEnqueue(startNode); + + // Astar loop + Node bestNode = null; + int checkedPositions = 0; + while (true) + { + // Get next node from heap + Node currentNode = m_openHeap.Count > 0 ? HeapDequeue() : null; + + // Check end conditions + if (currentNode == null || checkedPositions >= maxPositionsToCheck) + { + // No more nodes or limit reached, path not found, return path to best node if possible + if (bestNode != null) + { + BuildPathFromEndNode(path, startNode, bestNode); + } + return false; + } + + else if (Heuristic(currentNode.Position) <= 0) + { + // Node is goal, return path + BuildPathFromEndNode(path, startNode, currentNode); + return true; + } + + // Remember node with best heuristic; ignore start node + if (currentNode != startNode && (bestNode == null || currentNode.H < bestNode.H)) + { + bestNode = currentNode; + } + + // Move current node from open to closed in the storage + currentNode.IsClosed = true; + ++checkedPositions; + + // Try all neighbors + m_neighbors.Clear(); + Neighbors(currentNode, m_neighbors); + for (int i = 0; i < m_neighbors.Count; ++i) + { + // Get a neighbour + Node NeighborNode = m_neighbors[i]; + + // Check if this node is already in list(Closed) + Node NodeInList = (Node)StorageGet(NeighborNode.Position); + + // If position was already analyzed, ignore step + if (NodeInList != null && NodeInList.IsClosed == true) // if alredy in "closed" list + { + continue; + } + + // If position is not passable, ignore step + float cost = Cost( currentNode.Position, NeighborNode.Position ); + if( cost == float.PositiveInfinity ) + { + continue; + } + + // Calculate A* values + float g = currentNode.G + cost; + float h = Heuristic(currentNode.Position ); + // Update or create new node at position + if( NodeInList != null ) + { + // Update existing node if better + if( g < NodeInList.G ) + { + NodeInList.G = g; + NodeInList.F = g + NodeInList.H; + NodeInList.PreviousPosition = currentNode.Position; + HeapUpdate( NodeInList ); + } + } + else + { + // Create new open node if not yet exists + Node node = NewNode(NeighborNode.Position, currentNode.Position, g, h); + StorageAdd(node.Position, node); + HeapEnqueue(node); + } + } + } + } + #endregion + + #region Internals + + private void BuildPathFromEndNode(ICollection path, Node startNode, Node endNode) + { + for (Node node = endNode; node != startNode; node = (Node)StorageGet(node.PreviousPosition)) + { + path.Add(node.Position); + } + } + + private void HeapEnqueue(Node node) + { + m_openHeap.Add(node); + HeapifyFromPosToStart(m_openHeap.Count - 1); + } + + // Return node at pos 0 in open heap + private Node HeapDequeue() + { + Node result = m_openHeap[0]; + if (m_openHeap.Count <= 1) + { + m_openHeap.Clear(); + } + else + { + m_openHeap[0] = m_openHeap[m_openHeap.Count - 1]; + m_openHeap.RemoveAt(m_openHeap.Count - 1); + HeapifyFromPosToEnd(0); + } + return result; + } + + private void HeapUpdate(Node node) + { + int pos = -1; + for (int i = 0; i < m_openHeap.Count; ++i) + { + if (m_openHeap[i] == node) + { + pos = i; + break; + } + } + HeapifyFromPosToStart(pos); + } + + // Locate the the open node with the lowest F ( heuristics + cost ) by moving it to position 0 + private void HeapifyFromPosToEnd(int pos) + { + while (true) + { + int smallest = pos; + int left = 2 * pos + 1; + int right = 2 * pos + 2; + if (left < m_openHeap.Count && m_openHeap[left].F < m_openHeap[smallest].F) + smallest = left; + if (right < m_openHeap.Count && m_openHeap[right].F < m_openHeap[smallest].F) + smallest = right; + if (smallest != pos) + { + Node tmp = m_openHeap[smallest]; + m_openHeap[smallest] = m_openHeap[pos]; + m_openHeap[pos] = tmp; + pos = smallest; + } + else + { + break; + } + } + } + + private void HeapifyFromPosToStart(int pos) + { + int childPos = pos; + while (childPos > 0) + { + int parentPos = (childPos - 1) / 2; + Node parentNode = m_openHeap[parentPos]; + Node childNode = m_openHeap[childPos]; + if (parentNode.F > childNode.F) + { + m_openHeap[parentPos] = childNode; + m_openHeap[childPos] = parentNode; + childPos = parentPos; + } + else + { + break; + } + } + } + + private Node NewNode(sPosition position, sPosition previousPosition, float g, float h) + { + while (m_nodesCacheIndex >= m_nodesCache.Count) + { + m_nodesCache.Add(new Node()); + } + Node node = m_nodesCache[m_nodesCacheIndex++]; + node.Position = position; + node.PreviousPosition = previousPosition; + node.F = g + h; + node.G = g; + node.H = h; + node.IsClosed = false; + return node; + } + + #endregion +} diff --git a/src/AStarInterface.cs b/src/AStarInterface.cs new file mode 100644 index 00000000..4ea523a7 --- /dev/null +++ b/src/AStarInterface.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System; +using System.Collections.ObjectModel; +using CivOne.Tiles; +using CivOne.Enums; +using CivOne; + + + +public class AStarInterface : AStar +{ + // Interface for use in CivOne + + sPosition GoalPosition; + + ICollection path = new Collection(); + + public sPosition FindPath(sPosition StartPosition, sPosition GoalPosition) + { + this.GoalPosition = GoalPosition; + bool o = FindPath(path, StartPosition, 500); // Find path + AStar.sPosition[] Positions = new sPosition[500]; // Get path + int iCount = path.Count; + path.CopyTo( Positions, 0 ); + if (!o) + { + Positions[ iCount - 1 ].iX = -1; // unable to find path + } + return Positions[ iCount - 1 ]; + } + + /// Return cost of making a step from Positition to NextPosition (which are neighbors). + /// Cost equal to float.PositiveInfinity indicates that passage from Positition to NextPosition is impossible. + /// Currently is cost of current position ignored + /// + protected override float Cost( sPosition Positition, sPosition NextPosition ) + { + float fCost; + + float fNextCost = Map.Instance.GetMoveCost( NextPosition.iX, NextPosition.iY ); + fCost = Map.Instance.GetMoveCost( Positition.iX, Positition.iY ); + if( fNextCost == 1f && fCost == 1f ) return fNextCost; // if going along a road/railroad + else if( fNextCost == 1f ) return 3f; // if moving from terrain to road/railroad ( dont know if this is correct ) + else return fNextCost; + } + + + /// Return an estimate of cost of moving from Positition to goal. + /// Return 0 when Positition is goal. + /// This is an estimate of sum of all costs along the path between Positition and the goal. + static float fGoalF = 1.0f; + protected override float Heuristic( sPosition Positition) + { + if( Math.Abs( Positition.iX - GoalPosition.iX ) > Math.Abs( Positition.iY - GoalPosition.iY )) + return Math.Abs(( Positition.iX - GoalPosition.iX ) * fGoalF ); + else + return Math.Abs(( Positition.iY - GoalPosition.iY ) * fGoalF ); + + } + + + protected override void Neighbors( Node CurrNode, List neighbors ) + { + int[,] aiRelPos = new int[,] { { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, -1 }, { 0, 1 }, { 1, -1 }, { 1, 0 }, { 1, 1 } }; + + for (int i = 0; i < 8; i++) + { + sPosition CurPosition = CurrNode.Position; + sPosition NewPosition; + + NewPosition.iX = CurPosition.iX + aiRelPos[ i, 0 ]; + NewPosition.iY = CurPosition.iY + aiRelPos[ i, 1 ]; + Node NewNode = new Node(); + NewNode.Position = NewPosition; + NewNode.IsClosed = false; + NewNode.PreviousPosition = CurPosition; + neighbors.Add( NewNode ); + } + } +} + + diff --git a/src/Game.cs b/src/Game.cs index 6cb506d6..41481072 100644 --- a/src/Game.cs +++ b/src/Game.cs @@ -203,24 +203,50 @@ public void Update() { if (unit != null && !unit.Goto.IsEmpty) { - int distance = unit.Tile.DistanceTo(unit.Goto); - ITile[] tiles = (unit as BaseUnit).MoveTargets.OrderBy(x => x.DistanceTo(unit.Goto)).ThenBy(x => x.Movement).ToArray(); - if (tiles.Length == 0 || tiles[0].DistanceTo(unit.Goto) > distance) - { - // No valid tile to move to, cancel goto - unit.Goto = Point.Empty; - return; - } - else if (tiles[0].DistanceTo(unit.Goto) == distance) - { - // Distance is unchanged, 50% chance to cancel goto - if (Common.Random.Next(0, 100) < 50) - { + + ITile[] tiles = (unit as BaseUnit).MoveTargets.OrderBy(x => x.DistanceTo(unit.Goto)).ThenBy(x => x.Movement).ToArray(); + + if (unit.Class == UnitClass.Land) + { + /* Try AStar */ + AStar.sPosition Destination, Pos; + Destination.iX = unit.Goto.X; + Destination.iY = unit.Goto.Y; + Pos.iX = unit.X; + Pos.iY = unit.Y; + + AStarInterface AStarInterface = new AStarInterface(); + + AStarInterface.sPosition NextPosition = AStarInterface.FindPath(Pos, Destination); + if (NextPosition.iX < 0) + { // if no path found + unit.Goto = Point.Empty; + return; + } + unit.MoveTo(NextPosition.iX - Pos.iX, NextPosition.iY - Pos.iY); + return; + + } + else + { + + int distance = unit.Tile.DistanceTo(unit.Goto); + if (tiles.Length == 0 || tiles[0].DistanceTo(unit.Goto) > distance) + { + // No valid tile to move to, cancel goto unit.Goto = Point.Empty; return; } + else if (tiles[0].DistanceTo(unit.Goto) == distance) + { + // Distance is unchanged, 50% chance to cancel goto + if (Common.Random.Next(0, 100) < 50) + { + unit.Goto = Point.Empty; + return; + } + } } - unit.MoveTo(tiles[0].X - unit.X, tiles[0].Y - unit.Y); return; } diff --git a/src/Map.cs b/src/Map.cs index 5274b715..5b9d1cb9 100644 --- a/src/Map.cs +++ b/src/Map.cs @@ -168,5 +168,31 @@ private Map() Log("Map instance created"); } + + public float GetMoveCost( int x, int y ) // Used by AStar + { + float fCost = 3; // plain etc... + + if ( x < 0 || x >= WIDTH ) return float.PositiveInfinity; ; + if ( y < 0 || y >= HEIGHT ) return float.PositiveInfinity; + + bool road = _tiles[ x, y ].Road; + bool railRoad = _tiles[ x, y ].RailRoad; + if( road || railRoad ) return 1f ; + + switch( _tiles[ x, y ].Type ) + { + case Terrain.Forest: fCost = 6; break; + case Terrain.Swamp: fCost = 6; break; + case Terrain.Jungle: fCost = 6; break; + case Terrain.Hills: fCost = 6; break; + case Terrain.Mountains: fCost = 9; break; + case Terrain.Arctic: fCost = 6; break; + case Terrain.Ocean: fCost = float.PositiveInfinity; break; + } + + return fCost; + + } } } \ No newline at end of file