| | | 1 | | using System.ComponentModel; |
| | | 2 | | using System.Runtime.CompilerServices; |
| | | 3 | | using Godot; |
| | | 4 | | |
| | | 5 | | |
| | | 6 | | namespace Safarimacik.Model; |
| | | 7 | | |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Simulates a game of Safarimacik. Handles the input of the player, start and end of a game, and supplies several even |
| | | 11 | | /// </summary> |
| | | 12 | | public class GameModel : INotifyPropertyChanged, IDisposable { |
| | | 13 | | #region Constants |
| | | 14 | | |
| | | 15 | | public const int TicksADay = 120; |
| | | 16 | | public const float BalanceTouristSpawn = 10f; |
| | | 17 | | public const int JeepRange = 10; |
| | | 18 | | public const int BountyForPredators = 100; |
| | | 19 | | |
| | | 20 | | #endregion |
| | | 21 | | |
| | | 22 | | #region Fields |
| | | 23 | | |
| | | 24 | | private Difficulty _difficulty; |
| | | 25 | | private bool _calledGameOver; |
| | | 26 | | private int _money; |
| | | 27 | | private int _entryFee; |
| | | 28 | | private int _freeTourists; |
| | | 29 | | private int _touristsThisDay; |
| | | 30 | | private int _touristsLastDay; |
| | | 31 | | private double _nextTouristCounter; // incremented by spawnrate every tick, whenever it reaches 1, a new free tourist |
| | | 32 | | private double _nextInterval; |
| | | 33 | | private float _avgAnimalSeen; |
| | | 34 | | private int _speed; |
| | | 35 | | private int _remainingTicksOfThisDay; |
| | | 36 | | private int _totalDaysPassed; |
| | | 37 | | private int _winningDaysPassed; |
| | | 38 | | private int _winningDaysWinCon; |
| | | 39 | | private int _touristsWinCon; |
| | | 40 | | private int _animalWinCon; |
| | | 41 | | private int _moneyWinCon; |
| | 1 | 42 | | private (int, int) _tableSize = (128, 72); |
| | | 43 | | private int _jeepsMax; |
| | | 44 | | private int _jeepsOut; |
| | | 45 | | private int _ticksSinceJeep; |
| | | 46 | | private Board _board; |
| | | 47 | | private ITimerAdapter _timer; |
| | | 48 | | private IRandomGenerator _rng; |
| | | 49 | | private List<Herbivore> _herbivores; |
| | | 50 | | private List<Predator> _predators; |
| | | 51 | | private List<Ranger> _rangers; |
| | | 52 | | private List<Plant> _plants; |
| | | 53 | | private List<Jeep> _jeeps; |
| | | 54 | | private List<Jeep> _jeepsToSend; |
| | | 55 | | private bool _disposedValue; |
| | | 56 | | private Dictionary<Type, Vector2I> _herdPoints; |
| | | 57 | | |
| | | 58 | | #endregion |
| | | 59 | | |
| | | 60 | | #region Events |
| | | 61 | | |
| | | 62 | | /// <summary>Occurs when the game is finished</summary> |
| | | 63 | | public event EventHandler<GameOverEventArgs>? GameOver; |
| | | 64 | | /// <summary>Occurs when a jeep is sent from anywhere</summary> |
| | | 65 | | public event EventHandler<JeepEventArgs>? JeepStart; |
| | | 66 | | /// <summary>Occurs when a jeep reaches its goal</summary> |
| | | 67 | | public event EventHandler<EventArgs>? JeepFinish; |
| | | 68 | | /// <summary>Occurs when an animal is placed or born</summary> |
| | | 69 | | public event EventHandler<AnimalEventArgs>? AnimalAdded; |
| | | 70 | | /// <summary>Occurs when a ranger is placed</summary> |
| | | 71 | | public event EventHandler<RangerEventArgs>? RangerAdded; |
| | | 72 | | /// <summary>Occurs when a plant is consumed or regrown</summary> |
| | | 73 | | public event EventHandler<PlantEventArgs>? PlantStateChanged; |
| | | 74 | | /// <summary>Occurs when property value that is important for player display is updated</summary> |
| | | 75 | | public event PropertyChangedEventHandler? PropertyChanged; |
| | | 76 | | |
| | | 77 | | #endregion |
| | | 78 | | |
| | | 79 | | #region Properties |
| | | 80 | | |
| | | 81 | | public int EntryFee { |
| | 1 | 82 | | get => _entryFee; |
| | 1 | 83 | | set { |
| | 1 | 84 | | if (_entryFee != value && value > 0) { |
| | 1 | 85 | | _entryFee = value; |
| | 1 | 86 | | OnPropertyChanged(); |
| | 1 | 87 | | } |
| | 1 | 88 | | } |
| | | 89 | | } |
| | | 90 | | public int TouristsLastDay { |
| | 1 | 91 | | get => _touristsLastDay; |
| | 1 | 92 | | set { |
| | 1 | 93 | | if (_touristsLastDay != value) { |
| | 1 | 94 | | _touristsLastDay = value; |
| | 1 | 95 | | OnPropertyChanged(); |
| | 1 | 96 | | } |
| | 1 | 97 | | } |
| | | 98 | | } |
| | | 99 | | public int TouristsThisDay { |
| | 1 | 100 | | get => _touristsThisDay; |
| | 1 | 101 | | set { |
| | 1 | 102 | | if (_touristsThisDay != value) { |
| | 1 | 103 | | _touristsThisDay = value; |
| | 1 | 104 | | OnPropertyChanged(); |
| | 1 | 105 | | } |
| | 1 | 106 | | } |
| | | 107 | | } |
| | 1 | 108 | | public int FreeTourists => _freeTourists; |
| | 1 | 109 | | public float TouristSpawnRate => BalanceTouristSpawn * _avgAnimalSeen / EntryFee + _rng.Rand(0.0001f, 0.1f); |
| | 1 | 110 | | public int HerbivoreCount => _herbivores.Count; |
| | 1 | 111 | | public int PredatorCount => _predators.Count; |
| | 1 | 112 | | public int RangerCount => _rangers.Count; |
| | | 113 | | public int JeepsMax { |
| | 1 | 114 | | get => _jeepsMax; |
| | 1 | 115 | | set { |
| | 1 | 116 | | if (_jeepsMax != value) { |
| | 1 | 117 | | _jeepsMax = value; |
| | 1 | 118 | | OnPropertyChanged(); |
| | 1 | 119 | | } |
| | 1 | 120 | | } |
| | | 121 | | } |
| | | 122 | | public int JeepsOut { |
| | 1 | 123 | | get => _jeepsOut; |
| | 1 | 124 | | set { |
| | 1 | 125 | | if (_jeepsOut != value) { |
| | 1 | 126 | | _jeepsOut = value; |
| | 1 | 127 | | OnPropertyChanged(); |
| | 1 | 128 | | } |
| | 1 | 129 | | } |
| | | 130 | | } |
| | | 131 | | public int Money { |
| | 1 | 132 | | get => _money; |
| | 1 | 133 | | set { |
| | 1 | 134 | | if (_money != value) { |
| | 1 | 135 | | _money = value; |
| | 1 | 136 | | OnPropertyChanged(); |
| | 1 | 137 | | } |
| | 1 | 138 | | } |
| | | 139 | | } |
| | 1 | 140 | | public int JeepsAvailable => _jeepsMax - _jeepsOut; |
| | 1 | 141 | | public int RemainingTicksOfThisDay => _remainingTicksOfThisDay; |
| | | 142 | | public int TotalDaysPassed { |
| | 1 | 143 | | get => _totalDaysPassed; |
| | 1 | 144 | | set { |
| | 1 | 145 | | if (_totalDaysPassed != value) { |
| | 1 | 146 | | _totalDaysPassed = value; |
| | 1 | 147 | | OnPropertyChanged(); |
| | 1 | 148 | | } |
| | 1 | 149 | | } |
| | | 150 | | } |
| | | 151 | | public int WinningDaysPassed { |
| | 1 | 152 | | get => _winningDaysPassed; |
| | 1 | 153 | | set { |
| | 1 | 154 | | if (_winningDaysPassed != value) { |
| | 1 | 155 | | _winningDaysPassed = value; |
| | 1 | 156 | | OnPropertyChanged(); |
| | 1 | 157 | | } |
| | 1 | 158 | | } |
| | | 159 | | } |
| | 1 | 160 | | public int WinningDaysWinCon => _winningDaysWinCon; |
| | 1 | 161 | | public int TouristsWinCon => _touristsWinCon; |
| | 1 | 162 | | public int AnimalWinCon => _animalWinCon; |
| | 1 | 163 | | public int MoneyWinCon => _moneyWinCon; |
| | 1 | 164 | | public (int, int) TableSize { get { return _tableSize; } } |
| | | 165 | | public int Speed { |
| | 1 | 166 | | get => _speed; |
| | 1 | 167 | | set { |
| | 1 | 168 | | if (value == 0) { |
| | 1 | 169 | | _speed = value; |
| | 1 | 170 | | _timer.Stop(); |
| | 1 | 171 | | } else if (value == 1 || value == 2 || value == 5) { |
| | 1 | 172 | | _speed = value; |
| | 1 | 173 | | if (!_timer.Enabled) { |
| | 1 | 174 | | _nextInterval = 0; |
| | 1 | 175 | | _timer.Interval = 1000d / value; |
| | 1 | 176 | | _timer.Start(); |
| | 1 | 177 | | } else { |
| | 1 | 178 | | _nextInterval = 1000d / value; |
| | 1 | 179 | | } |
| | 1 | 180 | | } |
| | 1 | 181 | | } |
| | | 182 | | } |
| | 1 | 183 | | public bool IsPaused => !_timer.Enabled; |
| | 1 | 184 | | public Difficulty Difficulty => _difficulty; |
| | 1 | 185 | | public Func<Purchasable, Vector2I, bool> IsPlacable => Placable; |
| | | 186 | | |
| | | 187 | | #endregion |
| | | 188 | | |
| | | 189 | | #region Constructors |
| | | 190 | | |
| | 1 | 191 | | public GameModel() : this(new TimerAdapter(), new RandomGenerator()) { } |
| | | 192 | | |
| | 1 | 193 | | public GameModel(ITimerAdapter timer, IRandomGenerator rng) { |
| | 1 | 194 | | _timer = timer; |
| | 1 | 195 | | _timer.Interval = 1000.0; |
| | 1 | 196 | | _timer.Elapsed += Tick; |
| | 1 | 197 | | _rng = rng; |
| | | 198 | | |
| | 1 | 199 | | _herbivores = []; |
| | 1 | 200 | | _predators = []; |
| | 1 | 201 | | _rangers = []; |
| | 1 | 202 | | _plants = []; |
| | 1 | 203 | | _jeeps = []; |
| | 1 | 204 | | _jeepsToSend = []; |
| | 1 | 205 | | _herdPoints = []; |
| | 1 | 206 | | _board = new Board(16, 9, _rng); |
| | 1 | 207 | | } |
| | | 208 | | |
| | | 209 | | #endregion |
| | | 210 | | |
| | | 211 | | #region Public methods |
| | | 212 | | |
| | | 213 | | /// <summary> |
| | | 214 | | /// Starts new game with easy difficulty. |
| | | 215 | | /// </summary> |
| | 1 | 216 | | public void NewGame() { |
| | 1 | 217 | | NewGame(Difficulty.Easy); |
| | 1 | 218 | | } |
| | | 219 | | |
| | | 220 | | /// <summary> |
| | | 221 | | /// Starts new game with given difficulty. |
| | | 222 | | /// </summary> |
| | | 223 | | /// <param name="diff">Difficulty of new game.</param> |
| | 1 | 224 | | public void NewGame(Difficulty diff, Board? board = null) { |
| | 1 | 225 | | _herbivores = []; |
| | 1 | 226 | | _predators = []; |
| | 1 | 227 | | _rangers = []; |
| | 1 | 228 | | _plants = []; |
| | 1 | 229 | | _jeeps = []; |
| | 1 | 230 | | _jeepsToSend = []; |
| | | 231 | | |
| | 1 | 232 | | _ticksSinceJeep = 0; |
| | 1 | 233 | | EntryFee = 50; |
| | 1 | 234 | | _avgAnimalSeen = 1; |
| | 1 | 235 | | _freeTourists = 0; |
| | 1 | 236 | | TouristsLastDay = 0; |
| | 1 | 237 | | TouristsThisDay = 0; |
| | 1 | 238 | | JeepsMax = 0; |
| | 1 | 239 | | JeepsOut = 0; |
| | 1 | 240 | | _difficulty = diff; |
| | 1 | 241 | | _calledGameOver = false; |
| | 1 | 242 | | _remainingTicksOfThisDay = TicksADay; |
| | 1 | 243 | | TotalDaysPassed = 0; |
| | 1 | 244 | | WinningDaysPassed = 0; |
| | 1 | 245 | | _winningDaysWinCon = _difficulty.Value.Item1; |
| | 1 | 246 | | _touristsWinCon = _difficulty.Value.Item2; |
| | 1 | 247 | | _animalWinCon = _difficulty.Value.Item3; |
| | 1 | 248 | | _moneyWinCon = _difficulty.Value.Item4; |
| | 1 | 249 | | Money = (2 * Road.Price * _tableSize.Item1 + Sheep.Price * 20 + Jeep.Price * 5) / 100 * 100; |
| | 1 | 250 | | Speed = 1; |
| | 1 | 251 | | _board = board ?? new Board(_tableSize.Item1, _tableSize.Item2, _rng); |
| | 1 | 252 | | _herdPoints = new Dictionary<Type, Vector2I> { |
| | 1 | 253 | | { typeof(Wolf), new (0, 0) }, |
| | 1 | 254 | | { typeof(Fox), new (0, 0) }, |
| | 1 | 255 | | { typeof(Sheep), new (0, 0) }, |
| | 1 | 256 | | { typeof(Cow), new (0, 0) } |
| | 1 | 257 | | }; |
| | 1 | 258 | | AnimalInit(); |
| | 1 | 259 | | PlantInit(); |
| | 1 | 260 | | } |
| | | 261 | | |
| | 1 | 262 | | public void Pause() { |
| | 1 | 263 | | _timer.Stop(); |
| | 1 | 264 | | } |
| | | 265 | | |
| | 1 | 266 | | public void Resume() { |
| | 1 | 267 | | _timer.Start(); |
| | 1 | 268 | | } |
| | | 269 | | |
| | | 270 | | /// <summary> |
| | | 271 | | /// Purchases an object and places it at the given position, if funds are suffictient. |
| | | 272 | | /// </summary> |
| | | 273 | | /// <param name="purchaseId">Id of the item to purchase</param> |
| | | 274 | | /// <param name="position">Position of the item to purchase</param> |
| | | 275 | | /// <returns>Null if funds are insufficient, the purchased object otherwise</returns> |
| | 1 | 276 | | public Purchasable? Purchase(int purchaseId, Vector2I position) { |
| | 1 | 277 | | if (position.X < 0 || position.Y < 0 || position.X >= _tableSize.Item1 || position.Y >= _tableSize.Item2) return nul |
| | 1 | 278 | | Purchasable? item = PurchasableFactory.Purchase(purchaseId, Money, position * 16); |
| | 1 | 279 | | if (item != null) { |
| | 1 | 280 | | switch (item) { |
| | | 281 | | case Tile tile: |
| | 1 | 282 | | if (!ChangeTile(tile, position.X, position.Y)) return null; |
| | 1 | 283 | | break; |
| | | 284 | | case Animal animal: |
| | 1 | 285 | | AddAnimal(animal); |
| | 1 | 286 | | break; |
| | | 287 | | case Plant plant: |
| | 1 | 288 | | if (!AddPlant(plant, position.X, position.Y)) return null; |
| | 1 | 289 | | break; |
| | | 290 | | case Ranger ranger: |
| | 1 | 291 | | AddRanger(ranger); |
| | 1 | 292 | | break; |
| | | 293 | | case Jeep: |
| | 1 | 294 | | JeepsMax++; |
| | 1 | 295 | | break; |
| | | 296 | | } |
| | 1 | 297 | | Money -= item.Price; |
| | 1 | 298 | | } |
| | 1 | 299 | | return item; |
| | 1 | 300 | | } |
| | | 301 | | |
| | 1 | 302 | | public Tile GetTile(int x, int y) { |
| | 1 | 303 | | return _board[x, y]; |
| | 1 | 304 | | } |
| | | 305 | | |
| | | 306 | | /// <summary> |
| | | 307 | | /// Adds an animal to the list, subscribes to its events. |
| | | 308 | | /// </summary> |
| | | 309 | | /// <param name="animal">The animal being added</param> |
| | 1 | 310 | | public void AddAnimal(Animal animal) { |
| | 1 | 311 | | animal.OffspringSpawned += AddAnimal; |
| | 1 | 312 | | animal.LifeEnded += () => RemoveAnimal(animal); |
| | 1 | 313 | | animal.TileSpeed = new Func<Vector2, float>(p => { |
| | 1 | 314 | | return _board[(int)Math.Round(p.X / 16), (int)Math.Round(p.Y / 16)].Speed; |
| | 1 | 315 | | }); |
| | 1 | 316 | | switch (animal) { |
| | | 317 | | case Herbivore herbivore: |
| | 1 | 318 | | _herbivores.Add(herbivore); |
| | 1 | 319 | | OnPropertyChanged(nameof(HerbivoreCount)); |
| | 1 | 320 | | break; |
| | | 321 | | case Predator predator: |
| | 1 | 322 | | _predators.Add(predator); |
| | 1 | 323 | | OnPropertyChanged(nameof(PredatorCount)); |
| | 1 | 324 | | break; |
| | | 325 | | } |
| | 1 | 326 | | OnAnimalAdded(animal); |
| | 1 | 327 | | } |
| | | 328 | | |
| | 1 | 329 | | public void AddRanger(Ranger ranger) { |
| | 1 | 330 | | _rangers.Add(ranger); |
| | 1 | 331 | | ranger.TileSpeed = new Func<Vector2, float>(p => { |
| | 0 | 332 | | return _board[(int)Math.Round(p.X / 16), (int)Math.Round(p.Y / 16)].Speed; |
| | 0 | 333 | | }); |
| | 1 | 334 | | OnRangerAdded(ranger); |
| | 1 | 335 | | } |
| | | 336 | | |
| | 1 | 337 | | public void RemoveAnimal(Animal animal) { |
| | 1 | 338 | | if (animal is Herbivore herbivore) { |
| | 1 | 339 | | _herbivores.Remove(herbivore); |
| | 1 | 340 | | OnPropertyChanged(nameof(HerbivoreCount)); |
| | 1 | 341 | | } else { |
| | 1 | 342 | | _predators.Remove((Predator)animal); |
| | 1 | 343 | | OnPropertyChanged(nameof(PredatorCount)); |
| | 1 | 344 | | } |
| | 1 | 345 | | } |
| | | 346 | | |
| | | 347 | | /// <summary> |
| | | 348 | | /// Sends a jeep from the start of the road to the end. |
| | | 349 | | /// </summary> |
| | | 350 | | /// <param name="tourists">Number of tourists boarding the jeep</param> |
| | 1 | 351 | | public void SendJeep(int tourists) { |
| | 1 | 352 | | List<Vector2>? path = _board.FindJeepPath(new Vector2I(0, _tableSize.Item2 / 2), new Vector2I(_tableSize.Item1 - 1, |
| | | 353 | | |
| | 1 | 354 | | if (path != null) { |
| | 1 | 355 | | Jeep jeep = new Jeep(path, new Vector2(0, _tableSize.Item2 / 2) * 16, tourists); |
| | 1 | 356 | | _jeeps.Add(jeep); |
| | 1 | 357 | | jeep.Finished += OnJeepFinished; |
| | 1 | 358 | | JeepsOut++; |
| | 1 | 359 | | Money += tourists * EntryFee; |
| | 1 | 360 | | TouristsThisDay += tourists; |
| | 1 | 361 | | OnJeepStart(jeep); |
| | 1 | 362 | | _ticksSinceJeep = 0; |
| | 1 | 363 | | _freeTourists -= tourists; |
| | 1 | 364 | | } |
| | 1 | 365 | | } |
| | | 366 | | |
| | | 367 | | /// <summary> |
| | | 368 | | /// Sends a jeep from the end of the road to the start after 1-5 seconds. |
| | | 369 | | /// </summary> |
| | 1 | 370 | | public async Task SendJeepBack() { |
| | 1 | 371 | | IRandomGenerator r = new RandomGenerator(); |
| | 1 | 372 | | List<Vector2>? path = _board.FindJeepPath(new Vector2I(_tableSize.Item1 - 1, _tableSize.Item2 / 2), new Vector2I(0, |
| | | 373 | | |
| | 1 | 374 | | if (path != null) { |
| | 1 | 375 | | Jeep jeep = new Jeep(path, new Vector2(_tableSize.Item1 - 1, _tableSize.Item2 / 2) * 16, 0); |
| | | 376 | | |
| | 1 | 377 | | await Task.Delay(1000 * (int)r.Rand(1, 5)); |
| | 1 | 378 | | if (_jeepsMax != 0) { |
| | 1 | 379 | | _jeepsToSend.Add(jeep); |
| | 1 | 380 | | } |
| | 1 | 381 | | } |
| | 1 | 382 | | } |
| | | 383 | | |
| | | 384 | | #endregion |
| | | 385 | | |
| | | 386 | | #region Private methods |
| | | 387 | | |
| | 1 | 388 | | private bool ChangeTile(Tile tile, int x, int y) { |
| | 1 | 389 | | if (!Placable((Purchasable)tile, new Vector2I(x, y))) { |
| | 1 | 390 | | return false; |
| | | 391 | | } |
| | | 392 | | |
| | 1 | 393 | | _board.ChangeTile(x, y, tile); |
| | 1 | 394 | | return true; |
| | 1 | 395 | | } |
| | | 396 | | |
| | | 397 | | /// <summary> |
| | | 398 | | /// Places a plant to the grass tile on the given position, subscribes to its events if needed. |
| | | 399 | | /// </summary> |
| | | 400 | | /// <param name="plant">Plant to place</param> |
| | | 401 | | /// <param name="x">X coordinate of the plant</param> |
| | | 402 | | /// <param name="y">Y coordinate of the plant</param> |
| | | 403 | | /// <returns>True if the plant was placed</returns> |
| | 1 | 404 | | private bool AddPlant(Plant plant, int x, int y) { |
| | 1 | 405 | | if (_board[x, y] is not Grass) return false; |
| | 1 | 406 | | if (_board[x, y].Plant != null) return false; |
| | 1 | 407 | | if (plant is RegeneratingPlant regeneratingPlant) { |
| | 1 | 408 | | regeneratingPlant.StateChanged += () => OnPlantStateChanged(regeneratingPlant, new Vector2I(x, y)); |
| | 1 | 409 | | } |
| | 1 | 410 | | _board[x, y].Plant = plant; |
| | 1 | 411 | | _plants.Add(plant); |
| | 1 | 412 | | return true; |
| | 1 | 413 | | } |
| | | 414 | | |
| | | 415 | | /// <summary> |
| | | 416 | | /// Determines if the given object is allowed to be placed at the given position. |
| | | 417 | | /// </summary> |
| | | 418 | | /// <param name="item">Item to be checked</param> |
| | | 419 | | /// <param name="position">Position of the check</param> |
| | | 420 | | /// <returns></returns> |
| | 1 | 421 | | private bool Placable(Purchasable item, Vector2I position) { |
| | 1 | 422 | | if (position.X < 0 || position.Y < 0 || position.X >= _tableSize.Item1 || position.Y >= _tableSize.Item2) return fal |
| | 1 | 423 | | if (item is Tile tile) { |
| | 1 | 424 | | if (_board[position.X, position.Y] is not Grass) return false; |
| | 1 | 425 | | if (_board[position.X, position.Y].Plant != null) return false; |
| | 1 | 426 | | if (tile is Road && !((position.X > 0 && _board[Math.Max(position.X - 1, 0), position.Y] is Road or Gateway) || |
| | 1 | 427 | | (position.X < _tableSize.Item1 - 1 && _board[Math.Min(position.X + 1, _tableSize.Item1 - 1), position.Y] is Road |
| | 1 | 428 | | (position.Y < _tableSize.Item2 - 1 && _board[position.X, Math.Min(position.Y + 1, _tableSize.Item2 - 1)] is Road |
| | 1 | 429 | | (position.Y > 0 && _board[position.X, Math.Max(position.Y - 1, 0)] is Road or Gateway))) return false; |
| | 1 | 430 | | } |
| | | 431 | | |
| | 1 | 432 | | return true; |
| | 1 | 433 | | } |
| | | 434 | | |
| | 1 | 435 | | private bool IsWinning() { |
| | 1 | 436 | | return Money >= MoneyWinCon && TouristsLastDay >= TouristsWinCon && _herbivores.Count >= AnimalWinCon && _predators. |
| | 1 | 437 | | } |
| | | 438 | | |
| | | 439 | | /// <summary> |
| | | 440 | | /// Function that gets called at the end of every day. Ends game if lose conditions are met. |
| | | 441 | | /// </summary> |
| | 1 | 442 | | private void HandleLoseGame() { |
| | 1 | 443 | | if (Money <= 0 || (_herbivores.Count == 0 && _predators.Count == 0)) { |
| | 1 | 444 | | if (!_calledGameOver) { |
| | 1 | 445 | | _calledGameOver = true; |
| | 1 | 446 | | OnGameOver(false); |
| | 1 | 447 | | } |
| | 1 | 448 | | } |
| | 1 | 449 | | } |
| | | 450 | | |
| | | 451 | | /// <summary> |
| | | 452 | | /// Function that gets called at the end of every day. |
| | | 453 | | /// </summary> |
| | | 454 | | /// If the win conditions are met, it increments the WinningDaysPassed counter. |
| | | 455 | | /// Checks if the WinningDaysPassed counter reaches the WinningDayWinCon, then the game ends. |
| | | 456 | | /// If the win conditions are not met, it resets the WinningDaysPassed counter. |
| | 1 | 457 | | private void HandleWinGame() { |
| | 1 | 458 | | if (IsWinning()) { |
| | 1 | 459 | | ++WinningDaysPassed; |
| | 1 | 460 | | if (WinningDaysPassed == WinningDaysWinCon) { |
| | 1 | 461 | | OnGameOver(true); |
| | 1 | 462 | | } |
| | 1 | 463 | | } else { |
| | 1 | 464 | | WinningDaysPassed = 0; |
| | 1 | 465 | | } |
| | 1 | 466 | | } |
| | | 467 | | |
| | | 468 | | /// <summary> |
| | | 469 | | /// Gets called every tick, increments day counters if day passes. |
| | | 470 | | /// Calls HandleWinGame every new day. |
| | | 471 | | /// </summary> |
| | 1 | 472 | | private void ProgressDay() { |
| | 1 | 473 | | --_remainingTicksOfThisDay; |
| | 1 | 474 | | if (_remainingTicksOfThisDay == 0) { |
| | 1 | 475 | | _remainingTicksOfThisDay = TicksADay; |
| | 1 | 476 | | ++TotalDaysPassed; |
| | 1 | 477 | | TouristsLastDay = TouristsThisDay; |
| | 1 | 478 | | TouristsThisDay = 0; |
| | 1 | 479 | | _freeTourists = 0; |
| | 1 | 480 | | PayRangers(); |
| | 1 | 481 | | HandleWinGame(); |
| | 1 | 482 | | HandleLoseGame(); |
| | 1 | 483 | | SetHerdPoints(); |
| | 1 | 484 | | } |
| | 1 | 485 | | } |
| | | 486 | | |
| | | 487 | | /// <summary> |
| | | 488 | | /// Gets called every tick. |
| | | 489 | | /// Adjusts timer's interval in a way that current tick can finish with old interval. |
| | | 490 | | /// </summary> |
| | 1 | 491 | | private void AdjustTimerSpeed() { |
| | 1 | 492 | | if (_nextInterval != 0) { |
| | 0 | 493 | | _timer.Interval = _nextInterval; |
| | 0 | 494 | | _nextInterval = 0; |
| | 0 | 495 | | } |
| | 1 | 496 | | } |
| | | 497 | | |
| | | 498 | | /// <summary> |
| | | 499 | | /// Gets called every tick, decreases regenerating plants' cooldown by one. |
| | | 500 | | /// </summary> |
| | 1 | 501 | | private void RegeneratePlants() { |
| | 1 | 502 | | _plants.ForEach(plant => { |
| | 1 | 503 | | if (plant is RegeneratingPlant regenPlant) { |
| | 1 | 504 | | if (!regenPlant.IsRipe) { |
| | 1 | 505 | | regenPlant.DecreaseCurrentCooldown(); |
| | 1 | 506 | | } |
| | 1 | 507 | | } |
| | 1 | 508 | | }); |
| | 1 | 509 | | } |
| | | 510 | | |
| | | 511 | | /// <summary> |
| | | 512 | | /// Gets called every tick, spawns free tourists according to their spawn rate. |
| | | 513 | | /// </summary> |
| | 1 | 514 | | private void SpawnTourists() { |
| | 1 | 515 | | _nextTouristCounter += TouristSpawnRate; |
| | 1 | 516 | | while (_nextTouristCounter >= 1) { |
| | 1 | 517 | | _nextTouristCounter--; |
| | 1 | 518 | | _freeTourists++; |
| | 1 | 519 | | } |
| | 1 | 520 | | } |
| | | 521 | | |
| | | 522 | | /// <summary> |
| | | 523 | | /// Places animals on the board in their herd's territory. |
| | | 524 | | /// </summary> |
| | 1 | 525 | | private void AnimalInit() { |
| | 1 | 526 | | SetHerdPoints(); |
| | 1 | 527 | | int ratio = _rng.RandI(3, 7); |
| | 1 | 528 | | for (int i = 0; i < ratio; i++) { |
| | 1 | 529 | | Sheep sheep = new(16 * (_herdPoints[typeof(Sheep)] + new Vector2(_rng.Rand(-1, 1), _rng.Rand(-1, 1)))); |
| | 1 | 530 | | AddAnimal(sheep); |
| | 1 | 531 | | Wolf wolf = new(16 * (_herdPoints[typeof(Wolf)] + new Vector2(_rng.Rand(-1, 1), _rng.Rand(-1, 1)))); |
| | 1 | 532 | | AddAnimal(wolf); |
| | 1 | 533 | | } |
| | 1 | 534 | | for (int i = ratio; i < 10; i++) { |
| | 1 | 535 | | Cow cow = new(16 * (_herdPoints[typeof(Cow)] + new Vector2(_rng.Rand(-1, 1), _rng.Rand(-1, 1)))); |
| | 1 | 536 | | AddAnimal(cow); |
| | 1 | 537 | | Fox fox = new(16 * (_herdPoints[typeof(Fox)] + new Vector2(_rng.Rand(-1, 1), _rng.Rand(-1, 1)))); |
| | 1 | 538 | | AddAnimal(fox); |
| | 1 | 539 | | } |
| | 1 | 540 | | } |
| | | 541 | | |
| | | 542 | | /// <summary> |
| | | 543 | | /// Places plants on the board. |
| | | 544 | | /// </summary> |
| | 1 | 545 | | private void PlantInit() { |
| | 1 | 546 | | List<Vector2I> path = _board.FindPath(new Vector2I(0, _board.GetLength(1) / 2), new Vector2I(_board.GetLength(0) - 1 |
| | 1 | 547 | | for (int i = 0; i < _board.GetLength(0); i++) { |
| | 1 | 548 | | for (int j = 0; j < _board.GetLength(1); j++) { |
| | 1 | 549 | | if (_board[i, j] is Grass && _rng.Rand(0, 1) < 0.01f && !path.Contains(new Vector2I(i, j))) { |
| | 1 | 550 | | var purchase = PurchasableFactory.Purchase(_rng.RandI(0, 2) + 4, int.MaxValue, new(0, 0)); |
| | 1 | 551 | | if (purchase is Plant plant) { |
| | 1 | 552 | | AddPlant(plant, i, j); |
| | 1 | 553 | | } |
| | 1 | 554 | | } |
| | 1 | 555 | | } |
| | 1 | 556 | | } |
| | 1 | 557 | | } |
| | | 558 | | |
| | | 559 | | /// <summary> |
| | | 560 | | /// Gets called every day, sets a new point for each herd to go to. |
| | | 561 | | /// </summary> |
| | 1 | 562 | | private void SetHerdPoints() { |
| | 1 | 563 | | foreach (var species in _herdPoints.Keys) { |
| | 1 | 564 | | _herdPoints[species] = new Vector2I(_rng.RandI(1, _board.GetLength(0) - 2), _rng.RandI(1, _board.GetLength(1) - 2) |
| | 1 | 565 | | } |
| | 1 | 566 | | } |
| | | 567 | | |
| | | 568 | | /// <summary> |
| | | 569 | | /// Pay each ranger their wage at the end of each day. |
| | | 570 | | /// </summary> |
| | 1 | 571 | | private void PayRangers() { |
| | 1 | 572 | | _rangers.ForEach(ranger => { |
| | 1 | 573 | | _money -= Ranger.Wage; |
| | 1 | 574 | | }); |
| | 1 | 575 | | Money = _money; // only update view once, instead of updating view for each ranger |
| | 1 | 576 | | } |
| | | 577 | | |
| | | 578 | | /// <summary> |
| | | 579 | | /// Gets called every tick. |
| | | 580 | | /// Handles each animal according to their state. |
| | | 581 | | /// </summary> |
| | 1 | 582 | | private void HandleAnimals() { |
| | 1 | 583 | | List<Herbivore> herbivoresCopy = [.. _herbivores]; |
| | 1 | 584 | | List<Predator> predatorsCopy = [.. _predators]; |
| | | 585 | | |
| | 1 | 586 | | herbivoresCopy.ForEach(herbivore => { |
| | 1 | 587 | | herbivore.State.Tick(); |
| | 1 | 588 | | if (herbivore.State is IdleAnimalState) { |
| | 1 | 589 | | HandleIdleAnimal(herbivore, _herbivores); |
| | 1 | 590 | | } else if (herbivore.State is WalkingAnimalState) { |
| | 1 | 591 | | HandleWalkingAnimal(herbivore); |
| | 1 | 592 | | } else if (herbivore.State is ConsumingAnimalState) { |
| | 1 | 593 | | HandleConsumingAnimal(herbivore); |
| | 1 | 594 | | } |
| | 1 | 595 | | }); |
| | | 596 | | |
| | 1 | 597 | | predatorsCopy.ForEach(predator => { |
| | 1 | 598 | | predator.State.Tick(); |
| | 1 | 599 | | if (predator.State is IdleAnimalState) { |
| | 1 | 600 | | HandleIdleAnimal(predator, _predators); |
| | 1 | 601 | | } else if (predator.State is WalkingAnimalState) { |
| | 1 | 602 | | HandleWalkingAnimal(predator); |
| | 1 | 603 | | } else if (predator.State is ConsumingAnimalState) { |
| | 1 | 604 | | HandleConsumingAnimal(predator); |
| | 1 | 605 | | } |
| | 1 | 606 | | }); |
| | 1 | 607 | | } |
| | | 608 | | |
| | | 609 | | /// <summary> |
| | | 610 | | /// Contains the logic an idle animal runs. |
| | | 611 | | /// </summary> |
| | | 612 | | /// There are four main things that can happen: |
| | | 613 | | /// 1. The animal is hungry and goes to look for food. |
| | | 614 | | /// 2. The animal decides to take a walk to its herd. |
| | | 615 | | /// 3. A male animal wants to mate. |
| | | 616 | | /// - For this a female animal from the same species that is not hungry nor thirsty is required to be nearby. |
| | | 617 | | /// 4. Otherwise the animal remains in idle state. |
| | | 618 | | /// <typeparam name="T">Class extending the Animal abstract class</typeparam> |
| | | 619 | | /// <param name="animal">The idle animal</param> |
| | | 620 | | /// <param name="animals">Collection of animals similar to the idle animal (e.g. herbivores or predators)</param> |
| | 1 | 621 | | private void HandleIdleAnimal<T>(Animal animal, List<T> animals) where T : Animal { |
| | 1 | 622 | | if (animal.IsHungry() || animal.IsThirsty()) { // if animal needs to eat/drink |
| | 1 | 623 | | FindConsumables(animal); |
| | 1 | 624 | | } else { |
| | 1 | 625 | | if (_rng.Rand(0, 1) < 0.5) { // if animal decides to walk |
| | 1 | 626 | | WalkToHerd(animal); |
| | 1 | 627 | | } else if (animal.Sex && animal.IsAdult() && _rng.Rand(0, 1) < 0.7) { // if adult male animal decides to mate |
| | 1 | 628 | | T? partner = FindMatePartner(animal, animals); |
| | 1 | 629 | | if (partner != null) { |
| | 1 | 630 | | animal.State = new MatingAnimalState(animal); |
| | 1 | 631 | | partner.State = new MatingAnimalState(partner); |
| | 1 | 632 | | } |
| | 1 | 633 | | } |
| | 1 | 634 | | } |
| | 1 | 635 | | } |
| | | 636 | | |
| | | 637 | | /// <summary> |
| | | 638 | | /// Sets the given animal's state to walking state, with a destination around its herd's center |
| | | 639 | | /// </summary> |
| | | 640 | | /// <param name="animal"></param> |
| | 1 | 641 | | private void WalkToHerd(Animal animal) { |
| | 1 | 642 | | Vector2I target = _herdPoints[animal.GetType()] + new Vector2I(_rng.RandI(-1, 1), _rng.RandI(-1, 1)); |
| | 1 | 643 | | List<Vector2> path = _board.FindPathR(animal.TilePosition, target, true); |
| | 1 | 644 | | if (path.Count > 0) { |
| | 1 | 645 | | animal.State = new WalkingAnimalState(animal, path); |
| | 1 | 646 | | } |
| | 1 | 647 | | } |
| | | 648 | | |
| | | 649 | | /// <summary> |
| | | 650 | | /// Pathfind to the closest water/food source from memory. |
| | | 651 | | /// </summary> |
| | | 652 | | /// <param name="animal">The animal to send on its way</param> |
| | 1 | 653 | | private void FindConsumables(Animal animal) { |
| | 1 | 654 | | if (animal.IsThirsty()) { |
| | 1 | 655 | | try { |
| | 1 | 656 | | Vector2I target = animal.ClosestWater(); |
| | 1 | 657 | | List<Vector2> path = _board.FindPathR(animal.TilePosition, target, true); |
| | 1 | 658 | | if (path.Count > 0) { |
| | 1 | 659 | | animal.State = new WalkingAnimalState(animal, path); |
| | 1 | 660 | | } |
| | 1 | 661 | | return; |
| | 1 | 662 | | } catch (InvalidOperationException) { |
| | | 663 | | // there is no water stored |
| | | 664 | | // IGNORED, because maybe it can go to a feeding place |
| | 1 | 665 | | } |
| | 1 | 666 | | } |
| | | 667 | | |
| | 1 | 668 | | if (animal.IsHungry()) { |
| | 1 | 669 | | try { |
| | 1 | 670 | | Vector2I target = animal.ClosestFood(); |
| | 1 | 671 | | List<Vector2> path = _board.FindPathR(animal.TilePosition, target, true); |
| | 1 | 672 | | if (path.Count > 0) { |
| | 1 | 673 | | animal.State = new WalkingAnimalState(animal, path); |
| | 1 | 674 | | } |
| | 1 | 675 | | return; |
| | 1 | 676 | | } catch (InvalidOperationException) { |
| | | 677 | | // no food (and water) stored, walk to herd maybe food(/water) will be discovered on the way |
| | 1 | 678 | | } |
| | 1 | 679 | | } |
| | | 680 | | |
| | 1 | 681 | | WalkToHerd(animal); |
| | 1 | 682 | | } |
| | | 683 | | |
| | | 684 | | /// <summary> |
| | | 685 | | /// Finds a not hungry nor thirsty adult female from the given species nearby the given location. |
| | | 686 | | /// </summary> |
| | | 687 | | /// <typeparam name="T">Class that extends the abstract class Animal</typeparam> |
| | | 688 | | /// <param name="animal">The male animal</param> |
| | | 689 | | /// <param name="animals">Collection of animals similar to the male animal (e.g. herbivores or predators)</param> |
| | | 690 | | /// <returns>Reference to the female animal, or null if non found</returns> |
| | 1 | 691 | | private T? FindMatePartner<T>(Animal animal, List<T> animals) where T : Animal { |
| | 1 | 692 | | return animals |
| | 1 | 693 | | .Where(partner => |
| | 1 | 694 | | !partner.Sex && // is female |
| | 1 | 695 | | partner.GetType() == animal.GetType() && // is of the same species |
| | 1 | 696 | | partner.IsAdult() && // is adult |
| | 1 | 697 | | partner.State is IdleAnimalState && // is idle |
| | 1 | 698 | | !(partner.IsHungry() || partner.IsThirsty()) && // is not needy |
| | 1 | 699 | | ArePointsNear(animal.TilePosition, partner.TilePosition, 4) // is close enough |
| | 1 | 700 | | ) |
| | 1 | 701 | | .MinBy(partner => partner.Position.DistanceTo(animal.Position)); |
| | 1 | 702 | | } |
| | | 703 | | |
| | | 704 | | /// <summary> |
| | | 705 | | /// Contains the logic a walking animal runs. |
| | | 706 | | /// </summary> |
| | | 707 | | /// If the animal has reached its destination: |
| | | 708 | | /// - enter consuming state if animal is needy and resource is available in current position |
| | | 709 | | /// - walk to another resource if animal is needy but resource isn't available in current position |
| | | 710 | | /// - enter idle state otherwise |
| | | 711 | | /// <param name="animal">The walking animal</param> |
| | 1 | 712 | | private void HandleWalkingAnimal(Animal animal) { |
| | 1 | 713 | | SaveResources(animal); |
| | | 714 | | |
| | 1 | 715 | | if (!((WalkingAnimalState)animal.State).HasReachedDestination()) { |
| | 1 | 716 | | return; |
| | | 717 | | } |
| | | 718 | | |
| | 1 | 719 | | if (!(animal.IsHungry() || animal.IsThirsty())) { |
| | 1 | 720 | | animal.State = new IdleAnimalState(animal); |
| | 1 | 721 | | return; |
| | | 722 | | } |
| | | 723 | | |
| | 1 | 724 | | if (animal.IsHungry()) { // animal is hungry |
| | 1 | 725 | | if (animal is Herbivore herbivore) { |
| | 1 | 726 | | HandleHungryHerbivore(herbivore); |
| | 1 | 727 | | } else { |
| | 1 | 728 | | HandleHungryPredator((Predator)animal); |
| | 1 | 729 | | } |
| | | 730 | | |
| | | 731 | | // if animal hasn't started consuming yet, there is no food on this tile |
| | 1 | 732 | | if (animal.State is WalkingAnimalState) { |
| | 1 | 733 | | animal.RemoveFoodFromMemory(animal.TilePosition); |
| | 1 | 734 | | } else { |
| | 1 | 735 | | return; |
| | | 736 | | } |
| | 1 | 737 | | } |
| | | 738 | | |
| | 1 | 739 | | if (animal.IsThirsty()) { // animal is thirsty |
| | 1 | 740 | | HandleThirstyAnimal(animal); |
| | 1 | 741 | | } |
| | | 742 | | |
| | | 743 | | // if animal hasn't started consuming yet, there is no water (and no food) on this tile |
| | | 744 | | // but since water tile cannot be removed (as in the player has no means to do that) |
| | | 745 | | // we just need to find new resource |
| | 1 | 746 | | if (animal.State is WalkingAnimalState) { |
| | 1 | 747 | | FindConsumables(animal); |
| | 1 | 748 | | } |
| | 1 | 749 | | } |
| | | 750 | | |
| | 1 | 751 | | private void HandleHungryHerbivore(Herbivore herbivore) { |
| | | 752 | | // hungry animal is a herbivore |
| | | 753 | | // look for plant on this tile |
| | 1 | 754 | | Plant? plant = _board[herbivore.TilePosition.X, herbivore.TilePosition.Y].Plant; |
| | 1 | 755 | | if (plant != null && !(plant is RegeneratingPlant regenPlant && !regenPlant.IsRipe)) { |
| | 1 | 756 | | herbivore.State = new ConsumingAnimalState(herbivore, false, plant, null); |
| | 1 | 757 | | } |
| | 1 | 758 | | } |
| | | 759 | | |
| | 1 | 760 | | private void HandleHungryPredator(Predator predator) { |
| | | 761 | | // hungry animal is a predator |
| | | 762 | | // look for herbivore prey on this tile |
| | 1 | 763 | | Herbivore? prey = _herbivores.FirstOrDefault(herb => herb.TilePosition == predator.TilePosition); |
| | 1 | 764 | | if (prey != null) { |
| | 1 | 765 | | predator.State = new ConsumingAnimalState(predator, false, null, prey); |
| | 1 | 766 | | } |
| | 1 | 767 | | } |
| | | 768 | | |
| | 1 | 769 | | private void HandleThirstyAnimal(Animal animal) { |
| | | 770 | | // look for water on this tile |
| | 1 | 771 | | if (_board[animal.TilePosition.X, animal.TilePosition.Y] is Water) { |
| | 1 | 772 | | animal.State = new ConsumingAnimalState(animal, true, null, null); |
| | 1 | 773 | | } |
| | 1 | 774 | | } |
| | | 775 | | |
| | 1 | 776 | | private bool ArePointsNear(Vector2I a, Vector2I b, int range) { |
| | 1 | 777 | | return Math.Abs(a.X - b.X) <= range && Math.Abs(a.Y - b.Y) <= range; |
| | 1 | 778 | | } |
| | | 779 | | |
| | | 780 | | /// <summary> |
| | | 781 | | /// Determines whether a plant should be saved into memory by an animal or not. |
| | | 782 | | /// Returns true if the plant is a non-regenerating or a ripe regenerating plant; otherwise, false. |
| | | 783 | | /// </summary> |
| | 1 | 784 | | private bool ShouldSavePlant(Plant plant) { |
| | 1 | 785 | | return plant is Weed || (plant is RegeneratingPlant regenPlant && regenPlant.IsRipe); |
| | 1 | 786 | | } |
| | | 787 | | |
| | | 788 | | /// <summary> |
| | | 789 | | /// Saves nearby water and plant locations to a herbivore's memory. |
| | | 790 | | /// </summary> |
| | | 791 | | /// <param name="herbivore">Herbivore looking around</param> |
| | | 792 | | /// <param name="range">The view range of the tile the herbivore is standing on</param> |
| | 1 | 793 | | private void SaveHerbivoreResources(Herbivore herbivore, int range) { |
| | 1 | 794 | | for (int i = 0; i < _board.GetLength(0); i++) { |
| | 1 | 795 | | for (int j = 0; j < _board.GetLength(1); j++) { |
| | 1 | 796 | | if (!ArePointsNear(new Vector2I(i, j), herbivore.TilePosition, range)) { |
| | 1 | 797 | | continue; |
| | | 798 | | } |
| | | 799 | | |
| | 1 | 800 | | if (_board[i, j] is Water) { |
| | 1 | 801 | | herbivore.SaveWater(new Vector2I(i, j)); |
| | 1 | 802 | | } |
| | | 803 | | |
| | 1 | 804 | | if (_board[i, j].Plant is Plant plant && ShouldSavePlant(plant)) { |
| | 1 | 805 | | herbivore.SaveFood(new Vector2I(i, j)); |
| | 1 | 806 | | } |
| | 1 | 807 | | } |
| | 1 | 808 | | } |
| | 1 | 809 | | } |
| | | 810 | | |
| | | 811 | | /// <summary> |
| | | 812 | | /// Saves nearby water and herbivore locations to a predator's memory. |
| | | 813 | | /// </summary> |
| | | 814 | | /// <param name="predator">Predator looking around</param> |
| | | 815 | | /// <param name="range">The view range of the tile the predator is standing on</param> |
| | 1 | 816 | | private void SavePredatorResources(Predator predator, int range) { |
| | 1 | 817 | | for (int i = 0; i < _board.GetLength(0); i++) { |
| | 1 | 818 | | for (int j = 0; j < _board.GetLength(1); j++) { |
| | 1 | 819 | | if (!ArePointsNear(new Vector2I(i, j), predator.TilePosition, range)) { |
| | 1 | 820 | | continue; |
| | | 821 | | } |
| | | 822 | | |
| | 1 | 823 | | if (_board[i, j] is Water) { |
| | 1 | 824 | | predator.SaveWater(new Vector2I(i, j)); |
| | 1 | 825 | | } |
| | 1 | 826 | | } |
| | 1 | 827 | | } |
| | | 828 | | |
| | 1 | 829 | | _herbivores.ForEach(herbivore => { |
| | 1 | 830 | | if (ArePointsNear(herbivore.TilePosition, predator.TilePosition, range)) { |
| | 1 | 831 | | predator.SaveFood(herbivore.TilePosition); |
| | 1 | 832 | | } |
| | 1 | 833 | | }); |
| | 1 | 834 | | } |
| | | 835 | | |
| | | 836 | | /// <summary> |
| | | 837 | | /// Saves eating/drinking places in the provided animal's memory. |
| | | 838 | | /// </summary> |
| | | 839 | | /// For herbivores it scans the board for water and plant locations, saves the top-left corner of the tiles. |
| | | 840 | | /// For predators it scans the board for water locations, saves the top-left corner of the tiles, scans for nearby her |
| | | 841 | | /// <param name="animal">Animal that is looking around</param> |
| | 1 | 842 | | private void SaveResources(Animal animal) { |
| | 1 | 843 | | int range = _board[animal.TilePosition.X, animal.TilePosition.Y].ViewRange; |
| | 1 | 844 | | if (animal is Herbivore herbivore) { |
| | 1 | 845 | | SaveHerbivoreResources(herbivore, range); |
| | 1 | 846 | | } else { |
| | 1 | 847 | | SavePredatorResources((Predator)animal, range); |
| | 1 | 848 | | } |
| | 1 | 849 | | } |
| | | 850 | | |
| | | 851 | | /// <summary> |
| | | 852 | | /// Contains logic a consuming animal runs. |
| | | 853 | | /// </summary> |
| | | 854 | | /// Cleans up the non-regenerating plant from the board after animal has consumed it (if needed). |
| | | 855 | | /// If the animal isn't hungry nor thirsty anymore, it enters a sleeping state for a random time. |
| | | 856 | | /// Else it pathfinds to another nearby resource. |
| | | 857 | | /// <param name="animal">The consuming animal</param> |
| | 1 | 858 | | private void HandleConsumingAnimal(Animal animal) { |
| | | 859 | | // remove non-regenerating plant after consuming it |
| | 1 | 860 | | if (animal is Herbivore && ((ConsumingAnimalState)animal.State).Plant is Weed) { |
| | 1 | 861 | | _board[animal.TilePosition.X, animal.TilePosition.Y].Plant = null; |
| | 1 | 862 | | OnPlantStateChanged(null, animal.TilePosition); |
| | 1 | 863 | | } |
| | | 864 | | |
| | 1 | 865 | | if (!animal.IsHungry() && !animal.IsThirsty()) { |
| | 1 | 866 | | animal.State = new SleepingAnimalState(animal, _rng.RandI(0, 20)); |
| | 1 | 867 | | } else { |
| | 1 | 868 | | FindConsumables(animal); |
| | 1 | 869 | | } |
| | 1 | 870 | | } |
| | | 871 | | |
| | | 872 | | /// <summary> |
| | | 873 | | /// Gets called every tick. |
| | | 874 | | /// Sends jeeps out and back, when conditions are met. |
| | | 875 | | /// Moves jeeps already on the board. |
| | | 876 | | /// </summary> |
| | 1 | 877 | | private void HandleJeeps() { |
| | 1 | 878 | | if (_freeTourists >= 4 && JeepsAvailable > 0) { |
| | 1 | 879 | | SendJeep(4); |
| | 1 | 880 | | } else if (_ticksSinceJeep >= 60 && _freeTourists >= 1 && JeepsAvailable > 0) { |
| | 0 | 881 | | SendJeep(_freeTourists); |
| | 1 | 882 | | } else { |
| | 1 | 883 | | _ticksSinceJeep++; |
| | 1 | 884 | | } |
| | | 885 | | |
| | 1 | 886 | | if (_jeeps.Count != 0) { |
| | 1 | 887 | | List<Herbivore> herbivoresCopy = new(_herbivores); |
| | 1 | 888 | | List<Predator> predatorsCopy = new(_predators); |
| | 1 | 889 | | foreach (Jeep jeep in _jeeps) { |
| | 1 | 890 | | foreach (Animal animal in herbivoresCopy) { |
| | 1 | 891 | | if (jeep.Position.DistanceTo(animal.Position) < JeepRange * 16) { |
| | 1 | 892 | | jeep.AnimalSeen(animal); |
| | 1 | 893 | | } |
| | 1 | 894 | | } |
| | 1 | 895 | | foreach (Animal animal in predatorsCopy) { |
| | 1 | 896 | | if (jeep.Position.DistanceTo(animal.Position) < JeepRange * 16) { |
| | 0 | 897 | | jeep.AnimalSeen(animal); |
| | 0 | 898 | | } |
| | 1 | 899 | | } |
| | 1 | 900 | | } |
| | 1 | 901 | | } |
| | | 902 | | |
| | 1 | 903 | | for (int i = _jeeps.Count - 1; i >= 0; --i) { |
| | 1 | 904 | | if (_jeeps[i].Move()) { |
| | 1 | 905 | | Vector2 pos = _jeeps[i].Position; |
| | 1 | 906 | | _jeeps.RemoveAt(i); |
| | 1 | 907 | | if (pos == new Vector2(_tableSize.Item1 - 1, _tableSize.Item2 / 2) * 16) { |
| | 1 | 908 | | _ = SendJeepBack(); |
| | 1 | 909 | | } else { |
| | 1 | 910 | | JeepsOut--; |
| | 1 | 911 | | } |
| | 1 | 912 | | OnJeepFinished(); |
| | 1 | 913 | | } |
| | 1 | 914 | | } |
| | | 915 | | |
| | 1 | 916 | | if (_jeepsToSend.Count > 0) { |
| | 1 | 917 | | for (int i = _jeepsToSend.Count - 1; i >= 0; i--) { |
| | 1 | 918 | | _jeeps.Add(_jeepsToSend[i]); |
| | 1 | 919 | | OnJeepStart(_jeepsToSend[i]); |
| | 1 | 920 | | _jeepsToSend.RemoveAt(i); |
| | 1 | 921 | | } |
| | 1 | 922 | | } |
| | 1 | 923 | | } |
| | | 924 | | |
| | | 925 | | /// <summary> |
| | | 926 | | /// Gets called every tick. Moves every ranger closer to their target. |
| | | 927 | | /// </summary> |
| | | 928 | | /// Kills targets that are reached in this tick. |
| | | 929 | | /// Gives the player money for each animal killed. |
| | 1 | 930 | | private void HandleRangers() { |
| | 1 | 931 | | try { |
| | 1 | 932 | | foreach (Ranger ranger in _rangers) { |
| | 1 | 933 | | ranger.State.Tick(); |
| | 1 | 934 | | if (ranger.State is IdleRangerState) { |
| | 1 | 935 | | HandleIdleRanger(ranger); |
| | 1 | 936 | | } else if (ranger.State is WalkingRangerState) { |
| | 1 | 937 | | HandleWalkingRanger(ranger); |
| | 1 | 938 | | } else if (ranger.State is ShootingRangerState) { |
| | 1 | 939 | | _ = HandleShootingRanger(ranger); |
| | 1 | 940 | | } |
| | 1 | 941 | | } |
| | 1 | 942 | | } catch (Exception ex) { |
| | 0 | 943 | | GD.Print(ex.Message + ex.Source); |
| | 0 | 944 | | } |
| | 1 | 945 | | } |
| | | 946 | | |
| | 1 | 947 | | private void HandleIdleRanger(Ranger ranger) { |
| | 1 | 948 | | if (ranger.Target is not null) { |
| | 1 | 949 | | var path = _board.FindPathR(ranger.TilePosition, ranger.Target.TilePosition, true); |
| | 1 | 950 | | if (path.Count > 0) { |
| | 1 | 951 | | ranger.State = new WalkingRangerState(ranger, path); |
| | 1 | 952 | | } |
| | | 953 | | // else kill animal ??? depends on when and why findpathr returns empty path |
| | 1 | 954 | | } |
| | 1 | 955 | | } |
| | | 956 | | |
| | 1 | 957 | | private void HandleWalkingRanger(Ranger ranger) { |
| | 1 | 958 | | if (ranger.Target is null) { |
| | 0 | 959 | | ranger.State = new IdleRangerState(ranger); |
| | 0 | 960 | | return; |
| | | 961 | | } |
| | | 962 | | |
| | | 963 | | // walk to target if target moved |
| | 1 | 964 | | var walkingRangerState = (WalkingRangerState)ranger.State; |
| | 1 | 965 | | if (walkingRangerState.Destination != ranger.Target.TilePosition) { |
| | 0 | 966 | | var path = _board.FindPathR(ranger.TilePosition, ranger.Target.TilePosition, true); |
| | 0 | 967 | | if (path.Count > 0) { |
| | 0 | 968 | | ranger.State = new WalkingRangerState(ranger, path); |
| | 0 | 969 | | } |
| | 0 | 970 | | } |
| | | 971 | | |
| | 1 | 972 | | if (walkingRangerState.HasReachedDestination()) { |
| | 1 | 973 | | ranger.State = new ShootingRangerState(ranger); |
| | 1 | 974 | | return; |
| | | 975 | | } |
| | 1 | 976 | | } |
| | | 977 | | |
| | 1 | 978 | | private async Task HandleShootingRanger(Ranger ranger) { |
| | 1 | 979 | | if (ranger.LastKilled is not null) { |
| | 1 | 980 | | if (ranger.LastKilled.Lifetime > 50) { |
| | 0 | 981 | | Money += BountyForPredators * 2; |
| | 1 | 982 | | } else { |
| | 1 | 983 | | Money += BountyForPredators; |
| | 1 | 984 | | } |
| | 1 | 985 | | } |
| | 1 | 986 | | await Task.Delay(100); |
| | 1 | 987 | | ranger.State = new IdleRangerState(ranger); |
| | 1 | 988 | | } |
| | | 989 | | |
| | | 990 | | #endregion |
| | | 991 | | |
| | | 992 | | #region Event handlers |
| | | 993 | | |
| | | 994 | | /// <summary> |
| | | 995 | | /// Calls every time-related method in entities according to the game speed. |
| | | 996 | | /// </summary> |
| | 1 | 997 | | public void Tick(object? sender = null, TimerElapsedEventArgs? e = null) { |
| | 1 | 998 | | AdjustTimerSpeed(); |
| | | 999 | | |
| | 1 | 1000 | | ProgressDay(); |
| | | 1001 | | |
| | 1 | 1002 | | RegeneratePlants(); |
| | | 1003 | | |
| | 1 | 1004 | | SpawnTourists(); |
| | | 1005 | | |
| | 1 | 1006 | | HandleJeeps(); |
| | | 1007 | | |
| | 1 | 1008 | | HandleAnimals(); |
| | | 1009 | | |
| | 1 | 1010 | | HandleRangers(); |
| | 1 | 1011 | | } |
| | | 1012 | | |
| | 1 | 1013 | | private void OnJeepFinished(object? sender, JeepFinishedEventArgs e) { |
| | 1 | 1014 | | _avgAnimalSeen = (_avgAnimalSeen + e.Passengers * e.Animals) / (e.Passengers + 1); |
| | 1 | 1015 | | if (sender is Jeep jeep) { |
| | 1 | 1016 | | jeep.Finished -= OnJeepFinished; |
| | 1 | 1017 | | } |
| | 1 | 1018 | | } |
| | | 1019 | | |
| | 1 | 1020 | | private void OnGameOver(bool isWon) { |
| | 1 | 1021 | | _timer.Stop(); |
| | 1 | 1022 | | GameOver?.Invoke(this, new GameOverEventArgs(isWon)); |
| | 1 | 1023 | | } |
| | | 1024 | | |
| | 1 | 1025 | | private void OnJeepStart(Jeep jeep) { |
| | 1 | 1026 | | JeepStart?.Invoke(this, new JeepEventArgs(jeep)); |
| | 1 | 1027 | | } |
| | | 1028 | | |
| | 1 | 1029 | | private void OnJeepFinished() { |
| | 1 | 1030 | | JeepFinish?.Invoke(this, EventArgs.Empty); |
| | 1 | 1031 | | } |
| | | 1032 | | |
| | 1 | 1033 | | private void OnAnimalAdded(Animal animal) { |
| | 1 | 1034 | | AnimalAdded?.Invoke(this, new AnimalEventArgs(animal)); |
| | 1 | 1035 | | } |
| | | 1036 | | |
| | 1 | 1037 | | private void OnRangerAdded(Ranger ranger) { |
| | 1 | 1038 | | RangerAdded?.Invoke(this, new RangerEventArgs(ranger)); |
| | 1 | 1039 | | } |
| | | 1040 | | |
| | 1 | 1041 | | private void OnPlantStateChanged(RegeneratingPlant? regeneratingPlant, Vector2I position) { |
| | 1 | 1042 | | PlantStateChanged?.Invoke(this, new PlantEventArgs(regeneratingPlant, position)); |
| | 1 | 1043 | | } |
| | | 1044 | | |
| | 1 | 1045 | | protected void OnPropertyChanged([CallerMemberName] string paramName = "") { |
| | 1 | 1046 | | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(paramName)); |
| | 1 | 1047 | | } |
| | | 1048 | | |
| | | 1049 | | #endregion |
| | | 1050 | | |
| | | 1051 | | #region Dispose |
| | | 1052 | | |
| | 1 | 1053 | | protected virtual void Dispose(bool disposing) { |
| | 1 | 1054 | | if (!_disposedValue) { |
| | 1 | 1055 | | if (disposing) { |
| | 1 | 1056 | | _timer?.Dispose(); |
| | 1 | 1057 | | _rng?.Dispose(); |
| | 1 | 1058 | | } |
| | | 1059 | | |
| | 1 | 1060 | | _board = null!; |
| | 1 | 1061 | | _predators = null!; |
| | 1 | 1062 | | _herbivores = null!; |
| | 1 | 1063 | | _rangers = null!; |
| | 1 | 1064 | | _plants = null!; |
| | 1 | 1065 | | _jeeps = null!; |
| | 1 | 1066 | | _disposedValue = true; |
| | 1 | 1067 | | } |
| | 1 | 1068 | | } |
| | | 1069 | | |
| | 1 | 1070 | | public void Dispose() { |
| | | 1071 | | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method |
| | 1 | 1072 | | Dispose(disposing: true); |
| | 1 | 1073 | | GC.SuppressFinalize(this); |
| | 1 | 1074 | | } |
| | | 1075 | | |
| | | 1076 | | #endregion |
| | | 1077 | | } |