diff --git a/FNAF_Clone.sln.DotSettings.user b/FNAF_Clone.sln.DotSettings.user
index f8595f6..f51aa32 100644
--- a/FNAF_Clone.sln.DotSettings.user
+++ b/FNAF_Clone.sln.DotSettings.user
@@ -1,11 +1,15 @@
True
+ ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
\ No newline at end of file
diff --git a/FNAF_Clone/ClientEnemy.cs b/FNAF_Clone/ClientEnemy.cs
new file mode 100644
index 0000000..7a2b982
--- /dev/null
+++ b/FNAF_Clone/ClientEnemy.cs
@@ -0,0 +1,21 @@
+using FNAF_Clone.GUI;
+using FNAF_Clone.Map;
+using GlobalClassLib;
+using MonoGameLibrary.Graphics;
+
+namespace FNAF_Clone;
+
+public class ClientEnemy : GlobalEnemy {
+ public ClientEnemy(int typeId, string name, int id, UIElement sprite, int difficulty, MapTileProjection location) : base(difficulty, id) {
+ Name = name;
+ TypeId = typeId;
+ Sprite = sprite;
+ Location = location;
+ }
+
+ // public TextureRegion Sprite { get; set; }
+
+ public UIElement Sprite { get; set; }
+ public override string Name{ get; }
+ public override int TypeId{ get; }
+}
\ No newline at end of file
diff --git a/FNAF_Clone/ClientEnemyManager.cs b/FNAF_Clone/ClientEnemyManager.cs
new file mode 100644
index 0000000..bf7ebbd
--- /dev/null
+++ b/FNAF_Clone/ClientEnemyManager.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using FNAF_Clone.GUI;
+using FNAF_Clone.Map;
+using GlobalClassLib;
+using Microsoft.Xna.Framework;
+
+namespace FNAF_Clone;
+
+public class ClientEnemyManager {
+ private static Dictionary enemies = new();
+ private static Point cameraCorner = new Point(64, 64);
+
+ public static void AddEnemy(ClientEnemy enemy) {
+ enemies.Add(enemy.Id, enemy);
+ UIManager.AddEnemySprite(enemy.Id, enemy.Sprite);
+ }
+
+ public static void AddEnemyByTemplate(EnemyType type, int id, int difficulty, MapTileProjection location) {
+ switch (type){
+ case EnemyType.LURK:
+ AddEnemy(new ClientEnemy((int)type, "Lurk", id, new UIElement(UIManager.enemyAtlas[0], cameraCorner), difficulty, location));
+ break;
+ case EnemyType.NEKO:
+ AddEnemy(new ClientEnemy((int)type, "Neko", id, new UIElement(UIManager.enemyAtlas[1], cameraCorner), difficulty, location));
+ break;
+ case EnemyType.SPOT:
+ UIElement element =
+ new UIElement([UIManager.enemyAtlas[2], UIManager.enemyAtlas[3], UIManager.enemyAtlas[4], UIManager.enemyAtlas[5]], cameraCorner);
+ element.SetTexture(2);
+ AddEnemy(new ClientEnemy((int)type, "Spot", id, element, difficulty, location));
+ break;
+ }
+ }
+
+ public static void Move(int id, MapTileProjection tile) {
+ enemies[id].Location = tile;
+ }
+
+ public static ClientEnemy Get(int id) => enemies[id];
+
+ public static ClientEnemy[] GetByLocation(MapTileProjection tile) {
+ List output = new();
+ foreach (var e in enemies.Values){
+ if (e.Location == tile){
+ output.Add(e);
+ }
+ }
+
+ return output.ToArray();
+ }
+}
\ No newline at end of file
diff --git a/FNAF_Clone/CommandManager.cs b/FNAF_Clone/CommandManager.cs
index 5f2999f..dd48958 100644
--- a/FNAF_Clone/CommandManager.cs
+++ b/FNAF_Clone/CommandManager.cs
@@ -43,6 +43,8 @@ public class CommandManager {
SendToggleRemoteDoor(direction);
return;
}
+
+ if (direction == Direction.SOUTH) return;
Client.Player.state.doorStates[(int)direction] = !Client.Player.state.doorStates[(int)direction];
UIManager.ChangeDoorState(direction, Client.Player.state.doorStates[(int)direction]);
Client.SendCommands([PlayerCommand.SET_DOOR_OFFICE(direction, Client.Player.state.doorStates[(int)direction])]);
diff --git a/FNAF_Clone/Content/Content.mgcb b/FNAF_Clone/Content/Content.mgcb
index 261666d..362ea42 100644
--- a/FNAF_Clone/Content/Content.mgcb
+++ b/FNAF_Clone/Content/Content.mgcb
@@ -13,12 +13,27 @@
#---------------------------------- Content ---------------------------------#
+#begin images/enemies-definition.xml
+/copy:images/enemies-definition.xml
+
#begin images/monitor-definition.xml
/copy:images/monitor-definition.xml
#begin images/office-definition.xml
/copy:images/office-definition.xml
+#begin images/SpriteSheet_enemies.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/SpriteSheet_enemies.png
+
#begin images/SpriteSheet_monitor.png
/importer:TextureImporter
/processor:TextureProcessor
diff --git a/FNAF_Clone/Content/images/SpriteSheet_enemies.png b/FNAF_Clone/Content/images/SpriteSheet_enemies.png
new file mode 100644
index 0000000..174c692
Binary files /dev/null and b/FNAF_Clone/Content/images/SpriteSheet_enemies.png differ
diff --git a/FNAF_Clone/Content/images/enemies-definition.xml b/FNAF_Clone/Content/images/enemies-definition.xml
new file mode 100755
index 0000000..76e73d4
--- /dev/null
+++ b/FNAF_Clone/Content/images/enemies-definition.xml
@@ -0,0 +1,15 @@
+
+
+ images/SpriteSheet_enemies
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FNAF_Clone/EventProcessor.cs b/FNAF_Clone/EventProcessor.cs
index 6b3b0bb..a6fa8e6 100644
--- a/FNAF_Clone/EventProcessor.cs
+++ b/FNAF_Clone/EventProcessor.cs
@@ -64,9 +64,30 @@ public class EventProcessor {
UIManager.ChangeRemoteDoorState((e.Args[1], e.Args[2]), e.Args[3] == 1);
break;
- case -1: // movement
- throw new NotImplementedException();
-
+ case 6: // spawn
+ Console.WriteLine($"E: Spawned enemy {e.Args[0]} at {e.Args[3]}");
+ ClientEnemyManager.AddEnemyByTemplate((EnemyType)e.Args[0], e.Args[1], e.Args[2], ClientMapManager.Get(e.Args[3]));
+ UIManager.UpdateCameras([e.Args[3]]);
+ break;
+
+ case 7: // movement
+ Console.WriteLine($"E: Enemy {e.Args[0]} moved to {e.Args[1]}");
+ int oldPos = ClientEnemyManager.Get(e.Args[0]).Location!.Id;
+ ClientEnemyManager.Move(e.Args[0], ClientMapManager.Get(e.Args[1]));
+ UIManager.UpdateCameras([oldPos, e.Args[1]]);
+ break;
+
+ case 9:
+ Console.WriteLine($"E: Enemy {e.Args[0]} reset to {e.Args[1]}");
+ int preResetPos = ClientEnemyManager.Get(e.Args[0]).Location!.Id;
+ ClientEnemyManager.Move(e.Args[0], ClientMapManager.Get(e.Args[1]));
+ UIManager.UpdateCameras([preResetPos, e.Args[1]]);
+ break;
+
+ case 10:
+ Console.WriteLine($"E: Spot:{e.Args[0]} turned {(e.Args[1] == 1 ? "on" : " off")}");
+ ClientEnemyManager.Get(e.Args[0]).Sprite.SetTexture(e.Args[1] == 1 ? 0 : 2);
+ break;
}
}
}
diff --git a/FNAF_Clone/GUI/Screen.cs b/FNAF_Clone/GUI/Screen.cs
index 1b29afe..10e0460 100644
--- a/FNAF_Clone/GUI/Screen.cs
+++ b/FNAF_Clone/GUI/Screen.cs
@@ -52,6 +52,7 @@ public class Screen {
}
public UIElement this[string id] => elements[id];
+ public UIElement TryGetElement(string id) => elements.TryGetValue(id, out var val) ? val : null;
public void AddElement(string id, UIElement element) {
elements.Add(id, element);
@@ -80,8 +81,6 @@ public class Screen {
}
public void Update() {
-
-
foreach (var keyValuePair in elements){
keyValuePair.Value.Update();
}
diff --git a/FNAF_Clone/GUI/UIElement.cs b/FNAF_Clone/GUI/UIElement.cs
index 7166e29..9b7f0f9 100644
--- a/FNAF_Clone/GUI/UIElement.cs
+++ b/FNAF_Clone/GUI/UIElement.cs
@@ -69,9 +69,7 @@ public class UIElement {
currentTextureId = textureId;
}
- public void Update() {
-
- }
+ public virtual void Update() { }
public bool IsWithinBounds(Point pos) {
return pos.X >= Math.Min(screenSpaceBounds.Item1.X, screenSpaceBounds.Item2.X) && pos.X <= Math.Max(screenSpaceBounds.Item1.X, screenSpaceBounds.Item2.X) &&
diff --git a/FNAF_Clone/GUI/UIManager.cs b/FNAF_Clone/GUI/UIManager.cs
index 4afd137..2c44baf 100644
--- a/FNAF_Clone/GUI/UIManager.cs
+++ b/FNAF_Clone/GUI/UIManager.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
using FNAF_Clone.Map;
using GlobalClassLib;
using Microsoft.Xna.Framework;
@@ -21,15 +23,19 @@ public class UIManager {
private static TextureAtlas testAtlas;
private static TextureAtlas officeAtlas;
private static TextureAtlas monitorAtlas;
+ public static TextureAtlas enemyAtlas;
+
public static int GlobalPixelMultiplier{ get; private set; }
private Dictionary<(int, int), UIElement> doorElements = new();
+ private static Dictionary enemyElements = new();
public static void InitUI() {
GlobalPixelMultiplier = Core.graphicsDevice.Viewport.Height / 360;
testAtlas = TextureAtlas.FromFile(Core.content, "images/testBlocks-definition.xml");
officeAtlas = TextureAtlas.FromFile(Core.content, "images/office-definition.xml");
monitorAtlas = TextureAtlas.FromFile(Core.content, "images/monitor-definition.xml");
+ enemyAtlas = TextureAtlas.FromFile(Core.content, "images/enemies-definition.xml");
Screen.AddScreens([officeScreen, monitorScreen]);
Screen.SetScreen(ScreenTypes.OFFICE);
@@ -67,6 +73,7 @@ public class UIManager {
monitorScreen.AddElement("eye-player", new UIElement(monitorAtlas[24], monitorScreen["room"+Client.Player.state.camera].Bounds.Item1));
monitorScreen.AddElement("eye-opponent", new UIElement([monitorAtlas[23], monitorAtlas[22]], monitorScreen["room"+Client.Opponent.state.camera].Bounds.Item1));
+ UpdateCameras([Client.Player.state.camera]);
}
public static void SpawnDoors(TileConnectorProjection[] doors) {
@@ -91,6 +98,13 @@ public class UIManager {
monitorScreen.AddElement("p2-office-door-left", new UIElement([monitorAtlas[17], monitorAtlas[18]], new Point(400, 144)));
}
+
+ public static void AddEnemySprite(int id, UIElement sprite) {
+ monitorScreen.AddElement($"enemy{id}", sprite);
+ enemyElements.Add(id, sprite);
+ sprite.Visible = false;
+ }
+
public static void ChangeDoorState(Direction dir, bool state) {
int stateInt = state ? 1 : 0;
@@ -141,12 +155,25 @@ public class UIManager {
public static void ChangeCamera(int id) {
monitorScreen["eye-player"].SetPosition(monitorScreen["room"+id].Bounds.Item1);
+ UpdateCameras([id]);
}
+
+ public static void UpdateCameras(int[] camIds) {
+ if (camIds.Contains(Client.Player.state.camera)){
+ enemyElements.Values.Where(e => e.Visible).ToList().ForEach(e => e.Visible = false);
+ ClientEnemy[] enemies = ClientEnemyManager.GetByLocation(ClientMapManager.Get(Client.Player.state.camera));;
+ foreach (var enemy in enemies){
+ enemyElements.TryGetValue(enemy.Id, out var element);
+ if (element == null) continue;
+ element.Visible = true;
+ }
+ }
+ }
+
public static void ChangeCameraOpponent(int id) {
monitorScreen["eye-opponent"].SetPosition(monitorScreen["room"+id].Bounds.Item1);
}
-
// private static Point GetRoomUIPos((int x, int y) pos) {
// return new Point(336 + (32 * pos.x), 144 + (32 * pos.y));
// }
diff --git a/FNAF_Server/CommandProcessor.cs b/FNAF_Server/CommandProcessor.cs
index 0565a67..e6d32d9 100644
--- a/FNAF_Server/CommandProcessor.cs
+++ b/FNAF_Server/CommandProcessor.cs
@@ -11,6 +11,7 @@ public class CommandProcessor {
switch (playerCommand.ID){
case 0:
Console.WriteLine($"C: Player {pid} switched to camera {playerCommand.Args[0]}");
+ currentPlayer.state.camera = playerCommand.Args[0];
Server.SendUpdateToAll([GameEvent.SWITCH_CAM(pid, playerCommand.Args[0])]);
break;
case 1:
@@ -20,8 +21,10 @@ public class CommandProcessor {
Server.SendUpdateToAll([GameEvent.TOGGLE_MONITOR(pid, monitorState)]);
break;
case 2:
- bool doorState = playerCommand.Args[1] == 1;
+ bool doorState = playerCommand.Args[1] == 1; // TODO: block office doors
currentPlayer.state.doorStates[playerCommand.Args[0]] = doorState;
+ MapManager.Get(currentPlayer.state.officeTileId).GetConnector(currentPlayer.state.neighbouringTiles[playerCommand.Args[0]]).Blocked = doorState;
+
Console.WriteLine($"C: Player {pid} {(doorState ? "closed" : "opened")} door {playerCommand.Args[0]}");
Server.SendUpdateToAll([GameEvent.TOGGLE_DOOR_OFFICE(pid,playerCommand.Args[0] ,doorState)]);
break;
diff --git a/FNAF_Server/Enemies/Enemy.cs b/FNAF_Server/Enemies/Enemy.cs
new file mode 100644
index 0000000..1620c47
--- /dev/null
+++ b/FNAF_Server/Enemies/Enemy.cs
@@ -0,0 +1,26 @@
+using FNAF_Server.Map;
+using GlobalClassLib;
+
+namespace FNAF_Server.Enemies;
+
+public abstract class Enemy : GlobalEnemy {
+ protected Enemy(int difficulty) : base(difficulty) {
+ }
+
+ public abstract bool BlocksTile { get; set; }
+ public bool Spawned { get; set; }
+
+ public virtual void SpawnSilent(MapTile location) {
+ Console.WriteLine($"!!! Silent spawn not implemented for enemy {TypeId} ({Name}), reverting to regular spawn");
+ Spawn(location);
+ }
+
+ public override void Spawn(MapTile location) {
+ base.Spawn(location);
+ Spawned = true;
+ // EnemyManager.AddEnemy(this);
+ }
+
+ public abstract void Reset();
+ public abstract void Attack(ServerPlayer player);
+}
\ No newline at end of file
diff --git a/FNAF_Server/Enemies/EnemyManager.cs b/FNAF_Server/Enemies/EnemyManager.cs
new file mode 100644
index 0000000..0eba22c
--- /dev/null
+++ b/FNAF_Server/Enemies/EnemyManager.cs
@@ -0,0 +1,29 @@
+using FNAF_Server.Map;
+
+namespace FNAF_Server.Enemies;
+
+public class EnemyManager {
+ private static Dictionary enemies = new();
+
+ public static void Update() {
+ foreach (var pair in enemies){
+ if (pair.Value.Spawned) pair.Value.Update();
+ }
+ }
+
+ public static Enemy AddEnemy(Enemy enemy) {
+ enemies.Add(enemy.Id, enemy);
+ return enemy;
+ }
+
+ public static Enemy[] GetByLocation(MapTile tile) {
+ List output = new();
+ foreach (var e in enemies.Values){
+ if (e.Location == tile){
+ output.Add(e);
+ }
+ }
+
+ return output.ToArray();
+ }
+}
\ No newline at end of file
diff --git a/FNAF_Server/Enemies/LurkEnemy.cs b/FNAF_Server/Enemies/LurkEnemy.cs
new file mode 100644
index 0000000..569f807
--- /dev/null
+++ b/FNAF_Server/Enemies/LurkEnemy.cs
@@ -0,0 +1,118 @@
+using System.Diagnostics;
+using System.Reflection;
+using FNAF_Server.Map;
+using GlobalClassLib;
+using PacketLib;
+
+namespace FNAF_Server.Enemies;
+
+public class LurkEnemy : Enemy {
+ public override string Name{ get; } = "Lurk";
+ public override int TypeId{ get; } = (int)EnemyType.LURK;
+
+ public override bool BlocksTile{ get; set; } = true;
+
+ private LurkPathfinder pathfinder;
+
+ // private int movementRollInterval = 5000;
+ // private static readonly double CHANCE_DENOMINATOR = 2 + Math.Pow(1.5f, 10); // d10 Lurk will have a 100% chance to move
+ // private double movementChance => (2 + Math.Pow(1.5f, Difficulty)) / CHANCE_DENOMINATOR; // chance scales exponentially using a constant multiplier
+
+ private MovementOpportunity movementOpportunity;
+
+ public LurkEnemy(int difficulty) : base(difficulty) {
+ pathfinder = new LurkPathfinder(this, 1);
+ }
+ public override void Spawn(MapTile location) {
+ base.Spawn(location);
+ // stopwatch.Start();
+ movementOpportunity = new MovementOpportunity(5000, ((2 + Math.Pow(1.5f, Difficulty)) * Math.Sign(Difficulty)) / (2 + Math.Pow(1.5f, 10)));
+ movementOpportunity.Start();
+ pathfinder.takenPath.Add(Location);
+ Server.SendUpdateToAll([GameEvent.ENEMY_SPAWN(TypeId, Id, Difficulty, Location.Id)]);
+ }
+
+ public override void Reset() {
+ pathfinder.takenPath.Clear();
+ pathfinder.takenPath.Add(Location);
+
+ MapTile[] resetLocations = new[]{MapManager.Get(7), MapManager.Get(12), MapManager.Get(17)}.Where(t => EnemyManager.GetByLocation(t).Length == 0).ToArray();
+ Location = resetLocations[new Random().Next(resetLocations.Length)];
+ Server.SendUpdateToAll([GameEvent.ENEMY_RESET(Id, Location.Id)]);
+ }
+
+ public override void Attack(ServerPlayer player) {
+ throw new NotImplementedException();
+ }
+
+
+ public override void Update() {
+ base.Update();
+
+ if (movementOpportunity.CheckAndRoll()){
+ Pathfinder.Decision decision = pathfinder.DecideNext(MapManager.Get(10));
+ switch (decision.type){
+ case Pathfinder.Decision.MoveType:
+ Location = decision.nextTile!;
+ Server.SendUpdateToAll([GameEvent.ENEMY_MOVEMENT(Id, Location.Id)]);
+ Console.WriteLine($"Enemy {TypeId} ({Name}) moving to {Location.PositionAsString})");
+ break;
+ case Pathfinder.Decision.AttackType:
+ throw new NotImplementedException();
+ case Pathfinder.Decision.ResetType:
+ Reset();
+ break;
+ case Pathfinder.Decision.WaitType:
+ break;
+ }
+ }
+ }
+
+ private class LurkPathfinder : RoamingPathfinder {
+ // public override List FindPath(MapTile start, MapTile end) {
+ // throw new NotImplementedException();
+ // }
+
+ private Random random = new();
+
+ private int tolerance;
+ public List takenPath = new();
+
+ public LurkPathfinder(Enemy enemy, int tolerance) : base(enemy) {
+ this.tolerance = tolerance;
+ }
+
+ public override Decision DecideNext(MapTile goal) {
+ Dictionary distances = Crawl(goal);
+
+ List closerTiles = GetNeighbours(Enemy.Location,
+ c => !c.Blocked || c.Type == ConnectorType.DOOR_OFFICE,
+ t => distances.ContainsKey(t) && distances[t] <= distances[Enemy.Location] + tolerance);
+ if (closerTiles.Contains(goal)){
+ if (Enemy.Location.GetConnector(goal)!.Blocked){
+ return Decision.Reset();
+ }
+
+ return Decision.Attack(Server.P1); // TODO: un-hardcode this
+ }
+
+ if (closerTiles.Count != 0 && closerTiles.All(t => takenPath.Contains(t))){
+ takenPath.Clear();
+ return DecideNext(goal);
+ }
+ closerTiles.RemoveAll(t => takenPath.Contains(t));
+
+ closerTiles.ForEach(t => distances[t] += Enemy.Location.GetConnector(t)!.Value);
+ double roll = random.NextDouble() * closerTiles.Sum(t => 1.0 / distances[t]);
+ foreach (var tile in closerTiles){
+ if (roll <= 1.0 / distances[tile]){
+ takenPath.Add(tile);
+ return Decision.Move(tile);
+ }
+ roll -= 1.0 / distances[tile];
+ }
+
+ return Decision.Wait();
+ }
+ }
+}
\ No newline at end of file
diff --git a/FNAF_Server/Enemies/MovementOpportunity.cs b/FNAF_Server/Enemies/MovementOpportunity.cs
new file mode 100644
index 0000000..6e1a1f2
--- /dev/null
+++ b/FNAF_Server/Enemies/MovementOpportunity.cs
@@ -0,0 +1,60 @@
+using System.Diagnostics;
+
+namespace FNAF_Server.Enemies;
+
+public class MovementOpportunity {
+ public int Interval{ get; set; }
+ // public double ChanceDenominator;
+ public double MovementChance{ get; set; }
+
+ public bool Running => stopwatch.IsRunning;
+
+ public bool ConstantChance{ get; private set; }
+
+ private Stopwatch stopwatch = new();
+ private int stopwatchOffset = 0;
+
+ private Random random = new();
+
+ public MovementOpportunity(int intervalMs, double movementChance) {
+ Interval = intervalMs;
+ MovementChance = movementChance;
+ GuaranteeSuccess(false);
+ ConstantChance = true; // unused
+ }
+
+ public MovementOpportunity(int intervalMs) {
+ Interval = intervalMs;
+ GuaranteeSuccess(true);
+ MovementChance = 1;
+ }
+
+ public void Start() {
+ stopwatch.Start();
+ }
+ public void Stop() {
+ stopwatch.Stop();
+ }
+
+ public bool Check() {
+ if (stopwatch.ElapsedMilliseconds + stopwatchOffset >= Interval){
+ stopwatchOffset = (int)(stopwatch.ElapsedMilliseconds - Interval);
+ stopwatch.Restart();
+ return true;
+ }
+ return false;
+ }
+
+ public bool Roll() => roll();
+ private Func roll = () => true;
+
+ public bool CheckAndRoll() {
+ if (Check()) return Roll();
+ return false;
+ }
+
+ public void GuaranteeSuccess(bool value) {
+ roll = value ? () => true : () => random.NextDouble() <= MovementChance;
+ }
+
+}
\ No newline at end of file
diff --git a/FNAF_Server/Enemies/NekoEnemy.cs b/FNAF_Server/Enemies/NekoEnemy.cs
new file mode 100644
index 0000000..34a90ac
--- /dev/null
+++ b/FNAF_Server/Enemies/NekoEnemy.cs
@@ -0,0 +1,106 @@
+using FNAF_Server.Map;
+using GlobalClassLib;
+using PacketLib;
+
+namespace FNAF_Server.Enemies;
+
+public class NekoEnemy : Enemy {
+ private MovementOpportunity movementOpportunity;
+ private NekoPathfinder pathfinder;
+
+ public NekoEnemy(int difficulty) : base(difficulty) {
+ pathfinder = new NekoPathfinder(this, 1);
+ }
+
+ public override string Name{ get; } = "Neko";
+ public override int TypeId{ get; } = 1;
+ public override bool BlocksTile{ get; set; } = true;
+
+
+ public override void Spawn(MapTile location) {
+ base.Spawn(location);
+ // stopwatch.Start();
+ movementOpportunity = new MovementOpportunity(5000, ((2 + Math.Pow(1.5f, Difficulty)) * Math.Sign(Difficulty)) / (2 + Math.Pow(1.5f, 10)));
+ movementOpportunity.Start();
+ pathfinder.takenPath.Add(Location);
+ Server.SendUpdateToAll([GameEvent.ENEMY_SPAWN(TypeId, Id, Difficulty, Location.Id)]);
+ }
+
+ public override void Reset() {
+ pathfinder.takenPath.Clear();
+ MapTile[] resetLocations = new[]{MapManager.Get(7), MapManager.Get(12), MapManager.Get(17)}.Where(t => EnemyManager.GetByLocation(t).Length == 0).ToArray();
+ Location = resetLocations[new Random().Next(resetLocations.Length)];
+ Server.SendUpdateToAll([GameEvent.ENEMY_RESET(Id, Location.Id)]);
+ }
+
+ public override void Attack(ServerPlayer player) {
+ throw new NotImplementedException();
+ }
+
+
+ public override void Update() {
+ base.Update();
+
+ if (movementOpportunity.CheckAndRoll()){
+ Pathfinder.Decision decision = pathfinder.DecideNext(MapManager.Get(10));
+ switch (decision.type){
+ case Pathfinder.Decision.MoveType:
+ Location = decision.nextTile!;
+ Server.SendUpdateToAll([GameEvent.ENEMY_MOVEMENT(Id, Location.Id)]);
+ Console.WriteLine($"Enemy {TypeId} ({Name}) moving to {Location.PositionAsString})");
+ break;
+ case Pathfinder.Decision.AttackType:
+ throw new NotImplementedException();
+ case Pathfinder.Decision.ResetType:
+ Reset();
+ break;
+ case Pathfinder.Decision.WaitType:
+ break;
+ }
+ }
+ }
+
+ private class NekoPathfinder : RoamingPathfinder {
+ private Random random = new();
+
+ private int tolerance;
+ public List takenPath = new();
+
+ public NekoPathfinder(Enemy enemy, int tolerance) : base(enemy) {
+ this.tolerance = tolerance;
+ }
+
+ public override Decision DecideNext(MapTile goal) {
+ Dictionary distances = Crawl(goal);
+
+ List closerTiles = GetNeighbours(Enemy.Location,
+ c => !c.Blocked || c.Type == ConnectorType.DOOR_OFFICE,
+ t => distances.ContainsKey(t) && distances[t] <= distances[Enemy.Location] + tolerance);
+ if (closerTiles.Contains(goal)){
+ if (Enemy.Location.GetConnector(goal)!.Blocked){
+ return Decision.Reset();
+ }
+
+ return Decision.Attack(Server.P1); // TODO: un-hardcode this
+ }
+
+ if (closerTiles.Count != 0 && closerTiles.All(t => takenPath.Contains(t))){
+ takenPath.Clear();
+ return DecideNext(goal);
+ }
+ closerTiles.RemoveAll(t => takenPath.Contains(t));
+
+ closerTiles.ForEach(t => distances[t] += Enemy.Location.GetConnector(t)!.Value);
+ double roll = random.NextDouble() * closerTiles.Sum(t => 1.0 / distances[t]);
+ foreach (var tile in closerTiles){
+ if (roll <= 1.0 / distances[tile]){
+ takenPath.Add(tile);
+ return Decision.Move(tile);
+ }
+ roll -= 1.0 / distances[tile];
+ }
+
+ return Decision.Wait();
+ }
+ }
+}
\ No newline at end of file
diff --git a/FNAF_Server/Enemies/Pathfinder.cs b/FNAF_Server/Enemies/Pathfinder.cs
new file mode 100644
index 0000000..6c658c8
--- /dev/null
+++ b/FNAF_Server/Enemies/Pathfinder.cs
@@ -0,0 +1,36 @@
+using FNAF_Server.Map;
+
+namespace FNAF_Server.Enemies;
+
+public abstract class Pathfinder {
+ public Enemy Enemy;
+ public Pathfinder(Enemy enemy) {
+ Enemy = enemy;
+ }
+ public abstract Decision DecideNext(MapTile goal);
+ // protected abstract Dictionary Crawl(MapTile tile); // fill Distances with all reachable tiles
+
+ protected virtual List GetNeighbours(MapTile tile, Predicate connectorFilter, Predicate tileFilter) {
+ return tile.GetAllConnectors().Where(c => connectorFilter(c)).Select(c => c.OtherTile(tile)).Where(t => tileFilter(t)).ToList();
+ }
+
+ public class Decision {
+ public int type;
+
+ public MapTile? nextTile;
+ public ServerPlayer? attackTarget;
+
+ private Decision() { }
+
+ public static Decision Move(MapTile tile) => new() { type = MoveType, nextTile = tile };
+ public static Decision Attack(ServerPlayer player) => new(){ type = AttackType, attackTarget = player };
+ public static Decision Reset() => new(){ type = ResetType };
+ public static Decision Wait() => new(){ type = WaitType };
+
+ public const int MoveType = 0;
+ public const int AttackType = 1;
+ public const int ResetType = 2;
+ public const int WaitType = 3;
+
+ }
+}
\ No newline at end of file
diff --git a/FNAF_Server/Enemies/RoamingPathfinder.cs b/FNAF_Server/Enemies/RoamingPathfinder.cs
new file mode 100644
index 0000000..5260d58
--- /dev/null
+++ b/FNAF_Server/Enemies/RoamingPathfinder.cs
@@ -0,0 +1,29 @@
+using FNAF_Server.Map;
+using GlobalClassLib;
+
+namespace FNAF_Server.Enemies;
+
+public abstract class RoamingPathfinder : Pathfinder{
+ protected RoamingPathfinder(Enemy enemy) : base(enemy) {
+ }
+
+ protected Dictionary Crawl(MapTile tile) {
+ Dictionary distances = new(){ [tile] = 0 };
+ CrawlStep(tile, distances);
+ return distances;
+ }
+
+ private void CrawlStep(MapTile tile, Dictionary distances) {
+ List neighbours = GetNeighbours(tile,
+ c => (!c.Blocked || c.Type == ConnectorType.DOOR_OFFICE) && tile != Enemy.Location,
+ t =>
+ (!distances.ContainsKey(t) || distances[t] > distances[tile]) &&
+ (!Enemy.BlocksTile || EnemyManager.GetByLocation(t).All(e => !e.BlocksTile || e == Enemy)));
+
+ neighbours.ForEach(t => distances[t] = distances[tile] + tile.GetConnector(t)!.Value);
+
+ if (neighbours.Count != 0){
+ neighbours.ForEach(t => CrawlStep(t, distances));
+ }
+ }
+}
\ No newline at end of file
diff --git a/FNAF_Server/Enemies/SpotEnemy.cs b/FNAF_Server/Enemies/SpotEnemy.cs
new file mode 100644
index 0000000..a95e691
--- /dev/null
+++ b/FNAF_Server/Enemies/SpotEnemy.cs
@@ -0,0 +1,95 @@
+using FNAF_Server.Map;
+using GlobalClassLib;
+using PacketLib;
+
+namespace FNAF_Server.Enemies;
+
+public class SpotEnemy : Enemy {
+ public SpotEnemy(int difficulty) : base(difficulty) {
+ path = [MapManager.Get(10), MapManager.Get(11), MapManager.Get(12), MapManager.Get(13), MapManager.Get(14)];
+ pathId = 2;
+ movementOpportunity = new(6000, (2 * Math.Sign(difficulty) + difficulty) / 12.0);
+ }
+
+ public override string Name{ get; } = "Spot";
+ public override int TypeId{ get; } = (int)EnemyType.SPOT;
+ public override bool BlocksTile{ get; set; } = false;
+
+ public bool Active{ get; set; } = false;
+
+ private MovementOpportunity movementOpportunity;
+ private MapTile[] path;
+ private int pathId;
+
+ private int p1WatchCounter = 0;
+ private int p2WatchCounter = 0;
+
+ public override void Reset() {
+ pathId = 2;
+ Location = path[pathId];
+ Server.SendUpdateToAll([GameEvent.ENEMY_RESET(Id, Location.Id)]);
+ }
+
+ public override void Attack(ServerPlayer player) {
+ throw new NotImplementedException();
+ }
+
+ public override void Update() {
+ if (GameLogic.NSecondUpdate){
+ if(!movementOpportunity.Running)
+ movementOpportunity.Start();
+
+ if (Active){
+ if (Server.P1.state.monitorUp && Server.P1.state.camera == Location.Id) p1WatchCounter++;
+ if (Server.P2.state.monitorUp && Server.P2.state.camera == Location.Id) p2WatchCounter++;
+
+ Console.WriteLine($"P1: {p1WatchCounter} | P2: {p2WatchCounter}");
+ }
+ }
+
+ if (movementOpportunity.CheckAndRoll()){
+ if(!Active) {
+ Active = true;
+ movementOpportunity.Interval = 12_000;
+ movementOpportunity.GuaranteeSuccess(true);
+ Server.SendUpdateToAll([GameEvent.SPOT_SET_ACTIVE(Id, true)]);
+ }
+ else{
+ movementOpportunity.Interval = 6000;
+ movementOpportunity.GuaranteeSuccess(false);
+ movementOpportunity.Stop();
+
+ if (p1WatchCounter > p2WatchCounter){
+ pathId++;
+ if (Location.GetConnector(path[pathId])!.Blocked){
+ Reset();
+ }
+ else if (pathId == path.Length - 1){
+ Attack(Server.P1);
+ }
+ }
+ else if (p2WatchCounter > p1WatchCounter){
+ pathId--;
+ if (Location.GetConnector(path[pathId])!.Blocked){
+ Reset();
+ }
+ else if (pathId == 0){
+ Attack(Server.P2);
+ }
+ }
+
+ Location = path[pathId];
+ Active = false;
+ p1WatchCounter = 0;
+ p2WatchCounter = 0;
+ Server.SendUpdateToAll([GameEvent.ENEMY_MOVEMENT(Id, Location.Id), GameEvent.SPOT_SET_ACTIVE(Id, false)]);
+ }
+ }
+ }
+
+ public override void Spawn(MapTile location) {
+ base.Spawn(location);
+ Server.SendUpdateToAll([GameEvent.ENEMY_SPAWN(TypeId, Id, Difficulty, Location.Id)]);
+
+ }
+}
\ No newline at end of file
diff --git a/FNAF_Server/GameLogic.cs b/FNAF_Server/GameLogic.cs
index 550303f..84cbcf1 100644
--- a/FNAF_Server/GameLogic.cs
+++ b/FNAF_Server/GameLogic.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics;
+using FNAF_Server.Enemies;
using FNAF_Server.Map;
using GlobalClassLib;
using PacketLib;
@@ -7,10 +9,19 @@ namespace FNAF_Server;
public class GameLogic {
public const int START_CAMERA = 12;
+
+ private static MovementOpportunity secondCycleTimer = new(1000);
+ public static bool NSecondUpdate{ get; private set; } = false;
+ public static MapTile P1Office;
+ public static MapTile P2Office;
+ public static MapTile[] Offices =>[P1Office, P2Office];
+
public static void Init() {
// Create map
MapManager.InitMap();
+ P1Office = MapManager.Get(10);
+ P2Office = MapManager.Get(14);
//Send map to client
TileConnector[] connectors = MapManager.GetAllConnectors();
@@ -20,13 +31,21 @@ public class GameLogic {
connectorsConverted[i * 3 + 1] = connectors[i].Tiles.tile2.Id;
connectorsConverted[i * 3 + 2] = (int)connectors[i].Type;
}
- Server.SendPacket(new MapInitPacket{Connectors = connectorsConverted, UpsideDown = true}, Server.P1.peer);
- Server.SendPacket(new MapInitPacket{Connectors = connectorsConverted, UpsideDown = false}, Server.P2.peer);
+ Server.SendPacket(new MapInitPacket{Connectors = connectorsConverted, UpsideDown = false}, Server.P1.peer);
+ Server.SendPacket(new MapInitPacket{Connectors = connectorsConverted, UpsideDown = true}, Server.P2.peer);
+ // EnemyManager.AddEnemy(new LurkEnemy(0)).Spawn(MapManager.Get(12));
+ EnemyManager.AddEnemy(new NekoEnemy(10)).Spawn(MapManager.Get(2));
+ EnemyManager.AddEnemy(new SpotEnemy(0)).Spawn(MapManager.Get(12));
+
+ secondCycleTimer.Start();
}
public static void Update() {
+ if(secondCycleTimer.Check()) NSecondUpdate = true;
+ EnemyManager.Update();
+
+ NSecondUpdate = false;
}
-
}
\ No newline at end of file
diff --git a/FNAF_Server/Server.cs b/FNAF_Server/Server.cs
index c9e991d..4b31125 100644
--- a/FNAF_Server/Server.cs
+++ b/FNAF_Server/Server.cs
@@ -107,18 +107,26 @@ public class Server {
},
username = packet.username
});
-
- SendPacket(new JoinAcceptPacket { state = newPlayer.state }, peer);
-
+
+
if (Players.Count == 1){
+ newPlayer.state.officeTileId = 10;
+ newPlayer.state.neighbouringTiles = [5, 11, 15];
+ SendPacket(new JoinAcceptPacket { state = newPlayer.state }, peer);
P1 = newPlayer;
}
else{
+ newPlayer.state.officeTileId = 14;
+ newPlayer.state.neighbouringTiles = [19, 13, 9];
+
+ SendPacket(new JoinAcceptPacket { state = newPlayer.state }, peer);
P2 = newPlayer;
+
SendPacket(new OpponentInitPacket{state = newPlayer.state}, P1.peer);
SendPacket(new OpponentInitPacket{state = P1.state}, P2.peer);
- GameLogic.Init(); // TODO: move this to the condition above to wait for the other player
+ GameLogic.Init();
}
+
}
public static void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) {
@@ -133,29 +141,12 @@ public class Server {
public static void OnCommandReceived(PlayerCommandPacket packet, NetPeer peer) {
CommandProcessor.Evaluate(packet.commands, peer.Id);
-
- // PlayerCommand[] commands = packet.commands;
-
- // foreach (var playerCommand in commands){
- // switch (playerCommand.ID){
- // case 0:
- // Console.WriteLine($"C: Player {peer.Id} switched to camera {playerCommand.Args[0]}");
- // SendUpdateToAll([GameEvent.SWITCH_CAM(peer.Id, playerCommand.Args[0])]);
- // break;
- // case 1:
- // bool newState = !Players[(uint)peer.Id].state.monitorUp;
- // Players[(uint)peer.Id].state.monitorUp = newState;
- // Console.WriteLine($"C: Player {peer.Id} toggled camera {(newState ? "on" : "off")}");
- // SendUpdateToAll([GameEvent.TOGGLE_MONITOR(peer.Id, newState)]);
- // break;
- // }
- // }
}
public static void Update() {
server.PollEvents();
// Console.WriteLine("update");
- GameLogic.Update(); // TODO: add a parameter for player input
+ GameLogic.Update();
}
public static void Run(CancellationToken cancellationToken = default)
diff --git a/GlobalClassLib/EnemyType.cs b/GlobalClassLib/EnemyType.cs
new file mode 100644
index 0000000..d873f4a
--- /dev/null
+++ b/GlobalClassLib/EnemyType.cs
@@ -0,0 +1,11 @@
+using System.Diagnostics.Contracts;
+
+namespace GlobalClassLib;
+
+public enum EnemyType {
+ LURK,
+ NEKO,
+ SPOT,
+ DASH,
+ MARE
+}
\ No newline at end of file
diff --git a/GlobalClassLib/GlobalEnemy.cs b/GlobalClassLib/GlobalEnemy.cs
new file mode 100644
index 0000000..f722a1d
--- /dev/null
+++ b/GlobalClassLib/GlobalEnemy.cs
@@ -0,0 +1,35 @@
+namespace GlobalClassLib;
+
+public abstract class GlobalEnemy where TTile : GlobalMapTile where TCon : GlobalTileConnector {
+ public int Difficulty { get; set; }
+ public TTile? Location { get; set; }
+ public bool Visible { get; set; }
+ public abstract string Name{ get; }
+
+ public abstract int TypeId{ get; }
+ public int Id { get; }
+
+ private static int nextId = 0;
+
+
+ // public static Dictionary> PresentEnemies = new();
+
+ public GlobalEnemy(int difficulty) {
+ Difficulty = difficulty;
+ Id = nextId++;
+ }
+
+ public GlobalEnemy(int difficulty, int id) {
+ Difficulty = difficulty;
+ Id = id;
+ }
+ public virtual void Spawn(TTile location) {
+ // PresentEnemies.Add(location, this);
+ Location = location;
+ Visible = true;
+ Console.WriteLine($"Enemy {Id} ({Name}) spawned at {location.Id} {location.GridPosition}");
+ }
+
+ public virtual void Update() { }
+
+}
\ No newline at end of file
diff --git a/GlobalClassLib/GlobalMapTile.cs b/GlobalClassLib/GlobalMapTile.cs
index dcc44fa..719be66 100644
--- a/GlobalClassLib/GlobalMapTile.cs
+++ b/GlobalClassLib/GlobalMapTile.cs
@@ -1,7 +1,7 @@
namespace GlobalClassLib;
public abstract class GlobalMapTile where TCon : GlobalTileConnector where TTile : GlobalMapTile { // TTile should be the inheriting class
- public int Id { get; private set; }
+ public int Id { get; }
public bool Lit { get; set; } = false;
public (int x, int y) GridPosition { get; private set; }
@@ -28,17 +28,22 @@ public abstract class GlobalMapTile where TCon : GlobalTileConnecto
public void AddConnectors(TCon[] connectors) =>
Array.ForEach(connectors, AddConnector);
- public override string ToString() => $"{PositionAsString} -> {string.Join(", ", connectors.Select(c => c.Tiles.Item2.PositionAsString))}";
+ public override string ToString() => $"[{Id}] -> {string.Join(", ", connectors.Select(c => $"[{c.OtherTile((TTile)this).Id}]"))}";
+ public override int GetHashCode() => Id.GetHashCode();
public string PositionAsString => $"[{Id}]"; // for debug purposes
public TCon? GetConnector(int id) {
foreach (var con in connectors){
- if (con.Tiles.Item2.Id == id) return con;
+ if (con.OtherTile((TTile)this).Id == id) return con;
+ }
+ return null;
+ }
+ public TCon? GetConnector(TTile tile) {
+ foreach (var con in connectors){
+ if (con.OtherTile((TTile)this) == tile) return con;
}
-
return null;
}
public TCon[] GetAllConnectors() => connectors.ToArray();
-
}
\ No newline at end of file
diff --git a/PacketLib/GameEvent.cs b/PacketLib/GameEvent.cs
index 66dbc6c..46a8eb5 100644
--- a/PacketLib/GameEvent.cs
+++ b/PacketLib/GameEvent.cs
@@ -6,16 +6,18 @@ namespace PacketLib;
#nullable disable
public struct GameEvent : INetSerializable{
public static GameEvent PLAYER_JOIN(int pid) => new(){ID = 0, Args = [pid] }; // TODO: username sync
- public static GameEvent PLAYER_LEAVE(int pid) => new(){ID = 1, Args = [pid] };
+ public static GameEvent PLAYER_LEAVE(int pid) => new(){ID = 1, Args = [pid] }; // TODO: id constants
public static GameEvent SWITCH_CAM(int pid, int id) => new(){ID = 2, Args = [pid, id] };
public static GameEvent TOGGLE_MONITOR(int pid, bool state) => new(){ID = 3, Args = [pid, state ? 1 : 0]};
public static GameEvent TOGGLE_DOOR_OFFICE(int pid, int doorId, bool state) => new(){ID = 4, Args = [pid, doorId, state ? 1 : 0]};
public static GameEvent TOGGLE_DOOR_REMOTE(int pid, (int, int) doorId, bool state) => new(){ID = 5, Args = [pid, doorId.Item1, doorId.Item2, state ? 1 : 0]};
+
+ public static GameEvent ENEMY_SPAWN(int enemyTypeId, int enemyId, int difficulty, int camId) => new(){ ID = 6, Args = [enemyTypeId, enemyId, difficulty, camId] };
+ public static GameEvent ENEMY_MOVEMENT(int enemyId, int camId) => new(){ID = 7, Args = [enemyId, camId]};
+ public static GameEvent ENEMY_ATTACK(int enemyId, int pid) => new(){ ID = 8, Args =[enemyId, pid] };
+ public static GameEvent ENEMY_RESET(int enemyId, int camId) => new(){ID = 9, Args = [enemyId, camId]};
-
-
-
- public static GameEvent ENEMY_MOVEMENT(int enemyId, int camId) => new(){ID = -1, Args = [enemyId, camId]};
+ public static GameEvent SPOT_SET_ACTIVE(int enemyId, bool state) => new(){ ID = 10, Args = [enemyId, state ? 1 : 0] };
public int ID{ get; set; }
public bool Hideable => ID < 0;
diff --git a/PacketLib/PlayerState.cs b/PacketLib/PlayerState.cs
index be66414..8e4abcc 100644
--- a/PacketLib/PlayerState.cs
+++ b/PacketLib/PlayerState.cs
@@ -3,19 +3,22 @@ using LiteNetLib.Utils;
namespace PacketLib;
-public struct PlayerState : INetSerializable {
+public struct PlayerState : INetSerializable { // TODO: separate mutable and immutable data
public int pid;
public int camera;
public bool monitorUp;
+ public int officeTileId;
public bool[] doorStates;
-
+ public int[] neighbouringTiles; // the indexes should correspond in both arrays
public void Serialize(NetDataWriter writer) {
writer.Put(pid);
writer.Put(camera);
writer.Put(monitorUp);
writer.PutArray(doorStates);
+ writer.Put(officeTileId);
+ writer.PutArray(neighbouringTiles);
}
public void Deserialize(NetDataReader reader) {
@@ -23,6 +26,8 @@ public struct PlayerState : INetSerializable {
camera = reader.GetInt();
monitorUp = reader.GetBool();
doorStates = reader.GetBoolArray();
+ officeTileId = reader.GetInt();
+ neighbouringTiles = reader.GetIntArray();
}
}