diff --git a/Assets/Resources/Data/Buildings.xml b/Assets/Resources/Data/Buildings.xml index 43c757c..4ad8ac4 100644 --- a/Assets/Resources/Data/Buildings.xml +++ b/Assets/Resources/Data/Buildings.xml @@ -17,7 +17,7 @@ 10 3 4 - 0 + 0 BDFA02 @@ -33,7 +33,7 @@ 10 3 4 - 0 + 0 BDFA03 @@ -49,7 +49,7 @@ 13 2 4 - 0 + 0 BDFA04 @@ -65,7 +65,7 @@ 0 5 5 - 0 + 0 BDFA05 @@ -81,7 +81,7 @@ 0 2 5 - 0 + 0 BDFA06 @@ -97,7 +97,7 @@ 10 4 2 - 1 + 1 BDFA07 @@ -113,7 +113,7 @@ 20 2 2 - 5 + 5 @@ -133,7 +133,7 @@ 4 800 0 - 0 + 0 BDDF02 @@ -150,7 +150,7 @@ 2 0 40 - 0 + 0 BDDF03 @@ -167,7 +167,7 @@ 5 2000 0 - 0 + 0 BDDF04 @@ -184,7 +184,7 @@ 1 0 0 - 3 + 3 BDDF05 @@ -201,7 +201,7 @@ 3 5000 0 - 4 + 4 BDDF06 @@ -218,6 +218,6 @@ 3 0 80 - 6 + 6 diff --git a/Assets/Resources/Data/Events.xml b/Assets/Resources/Data/Events.xml deleted file mode 100644 index 5487329..0000000 --- a/Assets/Resources/Data/Events.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/Assets/Resources/Data/GameEvents.xml b/Assets/Resources/Data/GameEvents.xml new file mode 100644 index 0000000..9b8b4d2 --- /dev/null +++ b/Assets/Resources/Data/GameEvents.xml @@ -0,0 +1,52 @@ + + + + + Luke Visits Yoda + LUKE_VISITS_YODA + false + + + + + + + + + + + + + + Luke Confronts Vader + LUKE_CONFRONTS_VADER + true + + + + LUKE_SKYWALKER + DARTH_VADER + + + + + + + + + + + + + + + + Luke Discovers Heritage + LUKE_DISCOVERS_HERITAGE + false + + + + + + diff --git a/Assets/Resources/Data/Officers.xml b/Assets/Resources/Data/Officers.xml index ec60434..b4d8e51 100644 --- a/Assets/Resources/Data/Officers.xml +++ b/Assets/Resources/Data/Officers.xml @@ -9,6 +9,7 @@ Emperor Palpatine FNEMP1 true + false CORUSCANT @@ -55,6 +56,7 @@ Darth Vader FNEMP1 true + false Diplomacy @@ -102,6 +104,7 @@ FNEMP1 false + true Diplomacy @@ -149,6 +152,7 @@ FNEMP1 false + true Diplomacy @@ -196,6 +200,7 @@ FNEMP1 false + true Diplomacy @@ -243,6 +248,7 @@ FNEMP1 false + true Diplomacy @@ -290,6 +296,7 @@ FNEMP1 false + true Diplomacy @@ -337,6 +344,7 @@ FNEMP1 false + true Diplomacy @@ -384,6 +392,7 @@ FNEMP1 false + true Diplomacy @@ -431,6 +440,7 @@ FNEMP1 false + true Diplomacy @@ -478,6 +488,7 @@ FNEMP1 false + true Diplomacy @@ -525,6 +536,7 @@ FNEMP1 false + true Diplomacy @@ -572,6 +584,7 @@ FNEMP1 false + true Diplomacy @@ -619,6 +632,7 @@ FNEMP1 false + true Diplomacy @@ -666,6 +680,7 @@ FNEMP1 false + true Diplomacy @@ -713,6 +728,7 @@ FNEMP1 false + true Diplomacy @@ -760,6 +776,7 @@ FNEMP1 false + true Diplomacy @@ -807,6 +824,7 @@ FNEMP1 false + true Diplomacy @@ -854,6 +872,7 @@ FNEMP1 false + true Diplomacy @@ -901,6 +920,7 @@ FNEMP1 false + true Diplomacy @@ -948,6 +968,7 @@ FNEMP1 false + true Diplomacy @@ -996,6 +1017,7 @@ Mon Mothma FNALL1 true + false Diplomacy @@ -1040,6 +1062,7 @@ Leia Organa FNALL1 true + false Diplomacy @@ -1084,6 +1107,7 @@ Luke Skywalker FNALL1 true + false Diplomacy @@ -1129,6 +1153,7 @@ Han Solo FNALL1 true + false Diplomacy @@ -1176,6 +1201,7 @@ FNALL1 false + true Diplomacy @@ -1223,6 +1249,7 @@ FNALL1 false + true Diplomacy @@ -1270,6 +1297,7 @@ FNALL1 false + true Diplomacy @@ -1317,6 +1345,7 @@ FNALL1 false + true Diplomacy @@ -1364,6 +1393,7 @@ FNALL1 false + true Diplomacy @@ -1411,6 +1441,7 @@ FNALL1 false + true Diplomacy @@ -1458,6 +1489,7 @@ FNALL1 false + true Diplomacy @@ -1505,6 +1537,7 @@ FNALL1 false + true Diplomacy @@ -1552,6 +1585,7 @@ FNALL1 false + true Diplomacy @@ -1599,6 +1633,7 @@ FNALL1 false + true Diplomacy @@ -1646,6 +1681,7 @@ FNALL1 false + true Diplomacy @@ -1693,6 +1729,7 @@ FNALL1 false + true Diplomacy @@ -1740,6 +1777,7 @@ FNALL1 false + true Diplomacy @@ -1787,6 +1825,7 @@ FNALL1 false + true Diplomacy @@ -1834,6 +1873,7 @@ FNALL1 false + true Diplomacy @@ -1881,6 +1921,7 @@ FNALL1 false + true Diplomacy @@ -1928,6 +1969,7 @@ FNALL1 false + true Diplomacy @@ -1967,4 +2009,52 @@ 40 None + + OFAL022 + YODA + Yoda + + FNALL1 + + false + false + + + Diplomacy + 150 + + + Espionage + 150 + + + Combat + 150 + + + Leadership + 150 + + + 100 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + false + false + false + true + 0 + 100 + 0 + None + \ No newline at end of file diff --git a/Assets/Resources/Data/PlanetSystems.xml b/Assets/Resources/Data/PlanetSystems.xml index f2b77dc..2babc9a 100644 --- a/Assets/Resources/Data/PlanetSystems.xml +++ b/Assets/Resources/Data/PlanetSystems.xml @@ -9,8 +9,6 @@ Small High PSSEW - 11648 - 20 469 642 @@ -92,8 +90,6 @@ Small High PSCOR - 11649 - 21 272 96 @@ -175,8 +171,6 @@ Large High PSFAK - 11655 - 27 385 525 @@ -258,8 +252,6 @@ Small High PLDOL - 11653 - 25 472 253 @@ -341,8 +333,6 @@ Medium High PLFAR - 11656 - 28 587 370 @@ -424,8 +414,6 @@ Large High PLSLU - 11653 - 25 472 253 @@ -510,8 +498,6 @@ Large Low PLATR - 11649 - 21 272 96 @@ -593,8 +579,6 @@ Small Low PLMOD - 11661 - 33 191 148 @@ -676,8 +660,6 @@ Medium Low PLQUE - 11663 - 35 113 277 @@ -759,8 +741,6 @@ Small Medium PLCHU - 11650 - 22 132 417 @@ -843,8 +823,6 @@ Medium Medium PLKAN - 11659 - 31 221 569 @@ -926,8 +904,6 @@ Small Medium PLDUF - 11654 - 26 359 663 @@ -1009,8 +985,6 @@ Small Medium PLABR - 11648 - 20 469 642 @@ -1092,8 +1066,6 @@ Large Low PLXAP - 11667 - 39 575 755 @@ -1113,8 +1085,8 @@ PLXAP03 - NORULAC - Norulac + MISSING_NAME + missing_name 585 790 @@ -1175,8 +1147,6 @@ Small Low PLORU - 11662 - 34 695 684 @@ -1258,8 +1228,6 @@ Small Low PLJOS - 11658 - 30 737 548 @@ -1341,8 +1309,6 @@ Small Low PLSUM - 11666 - 38 717 438 @@ -1424,8 +1390,6 @@ Medium Medium PLGLY - 11667 - 39 575 755 @@ -1507,8 +1471,6 @@ Small Medium PLMAY - 11660 - 32 488 129 @@ -1590,8 +1552,6 @@ Small Medium PLCAL - 11652 - 24 380 129 diff --git a/Assets/Resources/Data/Regiments.xml b/Assets/Resources/Data/Regiments.xml index b9961b5..674ddcc 100644 --- a/Assets/Resources/Data/Regiments.xml +++ b/Assets/Resources/Data/Regiments.xml @@ -15,7 +15,7 @@ 3 2 20 - 0 + 0 REEM002 @@ -29,7 +29,7 @@ 5 5 15 - 0 + 0 REEM003 @@ -43,7 +43,7 @@ 6 5 25 - 0 + 0 REEM004 @@ -57,7 +57,7 @@ 2 2 5 - 1 + 1 REEM005 @@ -71,7 +71,7 @@ 8 6 30 - 2 + 2 @@ -88,7 +88,7 @@ 3 5 15 - 0 + 0 REAL002 @@ -102,7 +102,7 @@ 5 5 15 - 0 + 0 REAL003 @@ -116,7 +116,7 @@ 4 2 35 - 1 + 1 REAL004 @@ -130,7 +130,7 @@ 8 9 20 - 2 + 2 REAL005 @@ -144,7 +144,7 @@ 4 4 20 - 3 + 3 diff --git a/Assets/Resources/Data/Starfighters.xml b/Assets/Resources/Data/Starfighters.xml index cd21441..50a92d4 100644 --- a/Assets/Resources/Data/Starfighters.xml +++ b/Assets/Resources/Data/Starfighters.xml @@ -24,7 +24,7 @@ 12 0 0 - 0 + 0 SFEM02 @@ -47,7 +47,7 @@ 12 12 7 - 1 + 1 SFEM03 @@ -70,7 +70,7 @@ 12 0 0 - 3 + 3 SFEM04 @@ -93,7 +93,7 @@ 12 0 10 - 8 + 8 @@ -119,7 +119,7 @@ 12 12 7 - 0 + 0 SFAL02 @@ -142,7 +142,7 @@ 12 12 7 - 0 + 0 SFAL03 @@ -165,7 +165,7 @@ 12 0 0 - 3 + 3 SFAL04 @@ -188,6 +188,6 @@ 20 18 7 - 5 + 5 diff --git a/Assets/Scenes/GameInitializer.unity b/Assets/Scenes/GameInitializer.unity index 6d03ca7..cae0043 100644 --- a/Assets/Scenes/GameInitializer.unity +++ b/Assets/Scenes/GameInitializer.unity @@ -131,7 +131,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 82622251} - - component: {fileID: 82622250} + - component: {fileID: 82622252} m_Layer: 0 m_Name: GameInitializer m_TagString: Untagged @@ -139,18 +139,6 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &82622250 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 82622249} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: ed248910eb46d4891a7f8e50d18abb29, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!4 &82622251 Transform: m_ObjectHideFlags: 0 @@ -166,6 +154,18 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &82622252 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 82622249} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ed248910eb46d4891a7f8e50d18abb29, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!1 &342021166 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Configs/NewGameConfig.cs b/Assets/Scripts/Configs/NewGameConfig.cs index 8448699..0bf761c 100644 --- a/Assets/Scripts/Configs/NewGameConfig.cs +++ b/Assets/Scripts/Configs/NewGameConfig.cs @@ -9,7 +9,7 @@ [Serializable] public class NewGamePlanetSizeConfig : Config { - public int Snall; + public int Small; public int Medium; public int Large; diff --git a/Assets/Scripts/Events/GameAction.cs b/Assets/Scripts/Events/GameAction.cs index b45ad7e..6df6b45 100644 --- a/Assets/Scripts/Events/GameAction.cs +++ b/Assets/Scripts/Events/GameAction.cs @@ -18,32 +18,43 @@ public abstract class GameAction { [PersistableAttribute(Name = "Type")] - public string ActionType { get; set; } + protected string ActionType { get; set; } - public string Value { get; set; } - public Dictionary Parameters { get; set; } + [PersistableAttribute(Name = "Value")] + protected string ActionValue { get; set; } /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public GameAction() { } /// /// Creates a new GameAction with a specific value (as an XML attribute). /// + /// The type of the action. /// The value of the action. - public GameAction(string value) + public GameAction(string type, string value) { - Value = value; + ActionType = type; + ActionValue = value; } /// - /// Creates a new GameAction with specific parameters. + /// /// - /// The parameters of the action. - public GameAction(Dictionary parameters) + /// + protected string GetActionType() { - Parameters = parameters; + return ActionType; + } + + /// + /// + /// + /// + protected string GetActionValue() + { + return ActionValue; } /// diff --git a/Assets/Scripts/Events/GameActions.cs b/Assets/Scripts/Events/GameActions.cs index c5adc86..412d006 100644 --- a/Assets/Scripts/Events/GameActions.cs +++ b/Assets/Scripts/Events/GameActions.cs @@ -2,84 +2,84 @@ using System.Collections.Generic; using System.Linq; +[PersistableObject(Name = "MoveUnits")] public class MoveUnitsAction : GameAction { + public List UnitInstanceIDs { get; set; } = new List(); + public string TargetInstanceID { get; set; } + /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public MoveUnitsAction() : base() { } - public MoveUnitsAction(List nodes, ISceneNode target) - : base( - new Dictionary - { - { "UnitInstanceIDs", nodes.Select(n => n.InstanceID).ToList() }, - { "TargetInstanceID", target.InstanceID }, - } - ) { } - - public MoveUnitsAction(Dictionary parameters) - : base(parameters) { } - public override void Execute(Game game) { // Get the parameters for the action. - List unitInstanceIds = (List)Parameters["UnitInstanceIDs"]; - string targetInstanceId = (string)Parameters["TargetInstanceID"]; - - IMovable movable = game.GetSceneNodeByInstanceID(unitInstanceIds[0]) as IMovable; - ISceneNode target = game.GetSceneNodeByInstanceID(targetInstanceId); + IMovable movable = game.GetSceneNodeByInstanceID(UnitInstanceIDs[0]) as IMovable; + ISceneNode target = game.GetSceneNodeByInstanceID(TargetInstanceID); movable.MoveTo(target); } } +[PersistableObject(Name = "RandomOutcome")] public class RandomOutcomeAction : GameAction { + public List Actions { get; set; } = new List(); private Random random = new Random(); /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public RandomOutcomeAction() : base() { } - public RandomOutcomeAction(Dictionary parameters) - : base(parameters) { } - public override void Execute(Game game) { - double probability = Convert.ToDouble(Value); - - // Get the parameters for the action. - List actions = (List)Parameters["Actions"]; + double probability = Convert.ToDouble(this.GetActionValue()); // Execute a random action. if (random.NextDouble() < probability) { - actions[random.Next(actions.Count)].Execute(game); + Actions[random.Next(Actions.Count)].Execute(game); } } } +[PersistableObject(Name = "TriggerDuel")] +public class TriggerDuelAction : GameAction +{ + public List AttackerInstanceIDs { get; set; } + public List DefenderInstanceIDs { get; set; } + + /// + /// Default constructor used for deserialization. + /// + public TriggerDuelAction() + : base() { } + + public override void Execute(Game game) + { + // @TODO: Implement + } +} + +[PersistableObject(Name = "TriggerEvent")] public class TriggerEventAction : GameAction { + public string EventInstanceID { get; set; } + /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public TriggerEventAction() : base() { } - public TriggerEventAction(Dictionary parameters) - : base(parameters) { } - public override void Execute(Game game) { - // Get the parameters for the action. - string eventId = (string)Parameters["EventID"]; - - GameEvent gameEvent = game.GetPoolEventByID(eventId); + GameEvent gameEvent = game.GetEventByInstanceID(EventInstanceID); gameEvent.Execute(game); } diff --git a/Assets/Scripts/Events/GameConditional.cs b/Assets/Scripts/Events/GameConditional.cs index 4ef658d..f52e96b 100644 --- a/Assets/Scripts/Events/GameConditional.cs +++ b/Assets/Scripts/Events/GameConditional.cs @@ -13,31 +13,42 @@ [PersistableObject] public abstract class GameConditional : BaseGameEntity { - [PersistableMember] - public string Value { get; set; } - public Dictionary Parameters { get; set; } + [PersistableAttribute(Name = "Value")] + public string ConditionalValue { get; set; } + + [PersistableAttribute(Name = "Type")] + public string ConditionalType { get; set; } /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public GameConditional() { } /// /// Creates a new GameConditional with a specific value (as an XML attribute). /// - /// The value of the condition. - public GameConditional(string value) + /// The value of the condition. + public GameConditional(string conditionalValue) + { + ConditionalValue = conditionalValue; + } + + /// + /// + /// + /// + public string GetConditionalValue() { - Value = value; + return ConditionalValue; } /// - /// Creates a new GameConditional with specific parameters. + /// /// - /// The parameters of the condition. - public GameConditional(Dictionary parameters) + /// + public string GetConditionalType() { - Parameters = parameters; + return ConditionalType; } /// diff --git a/Assets/Scripts/Events/GameConditionals.cs b/Assets/Scripts/Events/GameConditionals.cs index 3847fb0..d4a41d3 100644 --- a/Assets/Scripts/Events/GameConditionals.cs +++ b/Assets/Scripts/Events/GameConditionals.cs @@ -5,90 +5,90 @@ /// /// /// +[PersistableObject(Name = "And")] public class AndConditional : GameConditional { + [PersistableMember(Name = "Conditionals")] + public List Conditionals = new List(); + public AndConditional() : base() { } - public AndConditional(Dictionary parameters) - : base(parameters) { } - public override bool IsMet(Game game) { - List conditionals = (List)Parameters["Conditionals"]; - return conditionals.All(conditional => conditional.IsMet(game)); + return Conditionals.All(conditional => conditional.IsMet(game)); } } /// /// /// +[PersistableObject(Name = "Or")] public class OrConditional : GameConditional { + [PersistableMember(Name = "Conditionals")] + public List Conditionals = new List(); + public OrConditional() : base() { } - public OrConditional(Dictionary parameters) - : base(parameters) { } - public override bool IsMet(Game game) { - List conditionals = (List)Parameters["Conditionals"]; - return conditionals.Any(conditional => conditional.IsMet(game)); + return Conditionals.Any(conditional => conditional.IsMet(game)); } } /// /// /// +[PersistableObject(Name = "Not")] public class NotConditional : GameConditional { + [PersistableMember(Name = "Conditionals")] + public List Conditionals = new List(); + public NotConditional() : base() { } - public NotConditional(Dictionary parameters) - : base(parameters) { } - public override bool IsMet(Game game) { - List conditionals = (List)Parameters["Conditionals"]; - return conditionals.All(conditional => !conditional.IsMet(game)); + return Conditionals.All(conditional => !conditional.IsMet(game)); } } /// /// /// +[PersistableObject(Name = "Xor")] public class XorConditional : GameConditional { + [PersistableMember(Name = "Conditionals")] + public List Conditionals = new List(); + public XorConditional() : base() { } - public XorConditional(Dictionary parameters) - : base(parameters) { } - public override bool IsMet(Game game) { - List conditionals = (List)Parameters["Conditionals"]; - return conditionals.Count(conditional => conditional.IsMet(game)) == 1; + return Conditionals.Count(conditional => conditional.IsMet(game)) == 1; } } /// /// /// +[PersistableObject(Name = "AreOnSamePlanet")] public class AreOnSamePlanetConditional : GameConditional { + [PersistableMember(Name = "UnitInstanceIDs")] + public List UnitInstanceIDs { get; set; } + public AreOnSamePlanetConditional() : base() { } - public AreOnSamePlanetConditional(Dictionary parameters) - : base(parameters) { } - public override bool IsMet(Game game) { - List instanceIDs = (List)Parameters["UnitInstanceIDs"]; - List sceneNodes = game.GetSceneNodesByInstanceIDs(instanceIDs); + List sceneNodes = game.GetSceneNodesByInstanceIDs(UnitInstanceIDs); Planet comparator = null; // Check if all units are on the same planet. @@ -115,20 +115,18 @@ public override bool IsMet(Game game) /// /// /// +[PersistableObject(Name = "AreOnOpposingFactions")] public class AreOnOpposingFactionsConditional : GameConditional { + List UnitInstanceIDs { get; set; } = new List(); + public AreOnOpposingFactionsConditional() : base() { } - public AreOnOpposingFactionsConditional(Dictionary parameters) - : base(parameters) { } - public override bool IsMet(Game game) { - List instanceIDs = (List)Parameters["UnitInstanceIDs"]; - // Get the scene nodes for the units. - List sceneNodes = game.GetSceneNodesByInstanceIDs(instanceIDs); + List sceneNodes = game.GetSceneNodesByInstanceIDs(UnitInstanceIDs); // Check if the units are on opposing factions. return sceneNodes.Count == 2 @@ -139,19 +137,18 @@ public override bool IsMet(Game game) /// /// /// +/// +[PersistableObject(Name = "IsOnMission")] public class IsOnMissionConditional : GameConditional { public IsOnMissionConditional() : base() { } - public IsOnMissionConditional(Dictionary parameters) - : base(parameters) { } - public override bool IsMet(Game game) { - string instanceID = (string)Value; - ISceneNode sceneNode = game.GetSceneNodeByInstanceID(instanceID); - + string instanceId = this.GetConditionalValue(); + GameLogger.Log("VALUE " + GetConditionalValue()); + ISceneNode sceneNode = game.GetSceneNodeByInstanceID(instanceId); // Check if the unit is on a mission. return sceneNode != null && sceneNode.GetParent() is Mission; } @@ -160,18 +157,16 @@ public override bool IsMet(Game game) /// /// /// +[PersistableObject(Name = "IsMovable")] public class IsMovableConditional : GameConditional { public IsMovableConditional() : base() { } - public IsMovableConditional(Dictionary parameters) - : base(parameters) { } - public override bool IsMet(Game game) { - string instanceID = (string)Value; - ISceneNode sceneNode = game.GetSceneNodeByInstanceID(instanceID); + string instanceId = this.GetConditionalValue(); + ISceneNode sceneNode = game.GetSceneNodeByInstanceID(instanceId); // Check if the ISceneNode implements IMovable and is movable. if (sceneNode is IMovable movable) @@ -186,19 +181,19 @@ public override bool IsMet(Game game) /// /// /// +/// +[PersistableObject(Name = "AreOnPlanet")] public class AreOnPlanetConditional : GameConditional { + public List UnitInstanceIDs { get; set; } + public AreOnPlanetConditional() : base() { } - public AreOnPlanetConditional(Dictionary parameters) - : base(parameters) { } - public override bool IsMet(Game game) { // Get the instance IDs of the units to check. - List instanceIDs = (List)Parameters["UnitInstanceIDs"]; - List sceneNodes = game.GetSceneNodesByInstanceIDs(instanceIDs); + List sceneNodes = game.GetSceneNodesByInstanceIDs(UnitInstanceIDs); // Check if all units are on a planet. return sceneNodes.All(node => node.GetParentOfType() != null); @@ -208,6 +203,7 @@ public override bool IsMet(Game game) /// /// /// +[PersistableObject(Name = "TickCount")] public class TickCountConditional : GameConditional { private enum ComparisonType @@ -220,25 +216,25 @@ private enum ComparisonType public TickCountConditional() : base() { } - public TickCountConditional(Dictionary parameters) - : base(parameters) { } - public override bool IsMet(Game game) { - string comparisonValue = (string)Parameters["Comparison"]; - ComparisonType comparison = Enum.TryParse(comparisonValue, out ComparisonType result) + ComparisonType comparison = Enum.TryParse( + this.GetConditionalType(), + out ComparisonType result + ) ? result : ComparisonType.EqualTo; - int targetTickCount = Convert.ToInt32(Parameters["Value"]); - // Check if the current tick count meets the comparison. return comparison switch { - ComparisonType.EqualTo => game.CurrentTick == targetTickCount, - ComparisonType.GreaterThan => game.CurrentTick > targetTickCount, - ComparisonType.LessThan => game.CurrentTick < targetTickCount, + ComparisonType.EqualTo => game.CurrentTick + == Convert.ToInt32(this.GetConditionalValue()), + ComparisonType.GreaterThan => game.CurrentTick + > Convert.ToInt32(this.GetConditionalValue()), + ComparisonType.LessThan => game.CurrentTick + < Convert.ToInt32(this.GetConditionalValue()), _ => throw new InvalidSceneOperationException( - "Invalid comparison type for TickCountConditional." + $"Invalid comparison type \"{comparison}\" for TickCountConditional." ), }; } @@ -247,17 +243,15 @@ public override bool IsMet(Game game) /// /// /// +[PersistableObject(Name = "IsEventComplete")] public class IsEventCompleteConditional : GameConditional { public IsEventCompleteConditional() : base() { } - public IsEventCompleteConditional(Dictionary parameters) - : base(parameters) { } - public override bool IsMet(Game game) { - string eventInstanceId = (string)Value; + string eventInstanceId = this.GetConditionalValue(); // Check if the event is complete. return game.IsEventComplete(eventInstanceId); diff --git a/Assets/Scripts/Events/GameEvent.cs b/Assets/Scripts/Events/GameEvent.cs index 4337af8..dc4887b 100644 --- a/Assets/Scripts/Events/GameEvent.cs +++ b/Assets/Scripts/Events/GameEvent.cs @@ -19,11 +19,11 @@ public class GameEvent : BaseGameEntity { // Event Properties public bool IsRepeatable { get; set; } - public List Conditionals { get; set; } - public List Actions { get; set; } + public List Conditionals { get; set; } = new List(); + public List Actions { get; set; } = new List(); /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public GameEvent() { } diff --git a/Assets/Scripts/Events/ScheduledEvent.cs b/Assets/Scripts/Events/ScheduledEvent.cs deleted file mode 100644 index 75569c7..0000000 --- a/Assets/Scripts/Events/ScheduledEvent.cs +++ /dev/null @@ -1,41 +0,0 @@ -/// -/// Represents a scheduled event within the game. -/// A ScheduledEvent is a wrapper around a GameEvent, responsible for tracking -/// when (at what game tick) the event should be executed. -/// -/// -/// The purpose of the ScheduledEvent class is to separate the timing aspect from the logic of the GameEvent itself. -/// -/// This allows the GameEvent class to be immutable and stateless after creation, while the ScheduledEvent class -/// can be modified to change the execution time of the event, in addition to anything else that might be needed. -/// -public class ScheduledEvent -{ - public GameEvent Event { get; } - public int ScheduledTick { get; set; } - - /// - /// Default constructor used for serialization. - /// - public ScheduledEvent() { } - - /// - /// Initializes a new instance of the ScheduledEvent class with a given event and tick. - /// - /// The GameEvent to be executed. - /// The tick at which the event should execute. - public ScheduledEvent(GameEvent gameEvent, int scheduledTick) - { - Event = gameEvent; - ScheduledTick = scheduledTick; - } - - /// - /// Gets the GameEvent associated with this ScheduledEvent. - /// - /// - public GameEvent GetEvent() - { - return Event; - } -} diff --git a/Assets/Scripts/Game/Building.cs b/Assets/Scripts/Game/Building.cs index 989112d..0e2eb39 100644 --- a/Assets/Scripts/Game/Building.cs +++ b/Assets/Scripts/Game/Building.cs @@ -45,6 +45,7 @@ public class Building : LeafNode, IManufacturable, IMovable public int Bombardment { get; set; } public int WeaponStrength { get; set; } public int ShieldStrength { get; set; } + public int WeaponPower { get; set; } // Manufacturing Info public string ProducerOwnerID { get; set; } diff --git a/Assets/Scripts/Game/CapitalShip.cs b/Assets/Scripts/Game/CapitalShip.cs index a145d8d..08365a2 100644 --- a/Assets/Scripts/Game/CapitalShip.cs +++ b/Assets/Scripts/Game/CapitalShip.cs @@ -76,7 +76,7 @@ public class CapitalShip : ContainerNode, IManufacturable, IMovable public string InitialParentInstanceID { get; set; } /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public CapitalShip() { } @@ -98,6 +98,24 @@ public int GetCurrentStarfighterCount() return Starfighters.Count; } + /// + /// + /// + /// + public int GetRegimentCapacity() + { + return RegimentCapacity; + } + + /// + /// + /// + /// + public int GetCurrentRegimentCount() + { + return Regiments.Count; + } + /// /// Adds a starfighter to the capital ship. /// diff --git a/Assets/Scripts/Game/Faction.cs b/Assets/Scripts/Game/Faction.cs index c6ae805..2c1c8ba 100644 --- a/Assets/Scripts/Game/Faction.cs +++ b/Assets/Scripts/Game/Faction.cs @@ -8,17 +8,7 @@ /// public class Faction : BaseGameEntity { - public Dictionary> Messages = new Dictionary< - MessageType, - List - >() - { - { MessageType.Conflict, new List() }, - { MessageType.Mission, new List() }, - { MessageType.PopularSupport, new List() }, - { MessageType.Resource, new List() }, - }; - + public List UnrecruitedOfficers { get; set; } = new List(); public Dictionary< ManufacturingType, SortedDictionary> @@ -49,8 +39,19 @@ public Dictionary< { typeof(Starfighter), new List() }, }; + public Dictionary> Messages = new Dictionary< + MessageType, + List + >() + { + { MessageType.Conflict, new List() }, + { MessageType.Mission, new List() }, + { MessageType.PopularSupport, new List() }, + { MessageType.Resource, new List() }, + }; + /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public Faction() { } @@ -172,6 +173,15 @@ public List GetResearchedTechnologies(ManufacturingType manufacturin .ToList(); } + /// + /// + /// + /// + public Dictionary GetResearchLevels() + { + return ManufacturingResearchLevels; + } + /// /// Returns the research level for a specific manufacturing type. /// diff --git a/Assets/Scripts/Game/Fleet.cs b/Assets/Scripts/Game/Fleet.cs index 891c717..cced508 100644 --- a/Assets/Scripts/Game/Fleet.cs +++ b/Assets/Scripts/Game/Fleet.cs @@ -28,6 +28,10 @@ public int GetStarfighterCapacity() return CapitalShips.Sum(capitalShip => capitalShip.GetStarfighterCapacity()); } + /// + /// + /// + /// public int GetCurrentStarfighterCount() { return CapitalShips.Sum(capitalShip => capitalShip.GetCurrentStarfighterCount()); @@ -42,6 +46,33 @@ public int GetExcessStarfighterCapacity() return GetStarfighterCapacity() - GetCurrentStarfighterCount(); } + /// + /// + /// + /// + public int GetRegimentCapacity() + { + return CapitalShips.Sum(capitalShip => capitalShip.GetRegimentCapacity()); + } + + /// + /// + /// + /// + public int GetCurrentRegimentCount() + { + return CapitalShips.Sum(capitalShip => capitalShip.GetCurrentRegimentCount()); + } + + /// + /// + /// + /// + public int GetExcessRegimentCapacity() + { + return GetRegimentCapacity() - GetCurrentRegimentCount(); + } + /// /// Constructor that initializes the fleet with an owner. /// diff --git a/Assets/Scripts/Game/Game.cs b/Assets/Scripts/Game/Game.cs index 24297c4..2b51671 100644 --- a/Assets/Scripts/Game/Game.cs +++ b/Assets/Scripts/Game/Game.cs @@ -15,8 +15,6 @@ public class Game public int CurrentTick = 0; // Game Events - public Dictionary> ScheduledEvents = - new Dictionary>(); public List EventPool = new List(); public HashSet CompletedEventIDs = new HashSet(); @@ -101,9 +99,8 @@ public GalaxyMap GetGalaxyMap() /// /// The node to attach. /// The parent node to attach the node to. - /// Whether to attach the node and its children recursively. /// Thrown when the node is not allowed to be attached. - public void AttachNode(ISceneNode node, ISceneNode parent, bool recursive = true) + public void AttachNode(ISceneNode node, ISceneNode parent) { // If the node already has a parent, throw an exception. if (node.GetParent() != null) @@ -119,11 +116,8 @@ public void AttachNode(ISceneNode node, ISceneNode parent, bool recursive = true // Register the node to the faction's list of owned units. RegisterOwnedUnit(node); - if (recursive) - { - // Register the node and its children. - node.Traverse(AddSceneNodeByInstanceID); - } + // Register the node and its children. + node.Traverse(AddSceneNodeByInstanceID); } /// @@ -164,7 +158,7 @@ public void AddSceneNodeByInstanceID(ISceneNode node) catch (ArgumentException) { throw new GameException( - $"Node with Instance ID \"{node.GetInstanceID()}\" and Display Name \"{node.GetDisplayName()}\" already exists in the game." + $"Cannot add duplicate node \"{node.GetInstanceID()}\" and Display Name \"{node.GetDisplayName()}\" to scene." ); } } @@ -239,7 +233,7 @@ public List GetSceneNodesByOwnerInstanceID(string ownerInstanceId) /// A list of nodes of type T. public List GetSceneNodesByType() { - var result = new List(); + List result = new List(); // Recursive function to traverse nodes. void Traverse(ISceneNode node) @@ -289,62 +283,31 @@ public void DeregsiterOwnedUnit(ISceneNode node) } /// - /// Retrieves all nodes of a specified type T, stopping further traversal if the type is found. + /// /// - /// The ID of the event to retrieve. - /// The game event with the specified ID. - public GameEvent GetPoolEventByID(string eventID) + /// + public List GetEventPool() { - return EventPool.FirstOrDefault(gameEvent => gameEvent.InstanceID == eventID); - } - - /// - /// Retrieves a list of scheduled events for the specified tick. - /// - /// A list of scheduled events for the specified tick. - public List GetScheduledEvents(int tick) - { - if (ScheduledEvents.TryGetValue(tick, out List scheduledEvents)) - { - return scheduledEvents; - } - else - { - return new List(); - } + return EventPool; } /// - /// Adds a game event to the game event dictionary. + /// /// - /// The game event to add. - /// The tick at which the game event occurs. - public void ScheduleGameEvent(GameEvent gameEvent, int tick) + /// + public void RemoveEvent(GameEvent gameEvent) { - if (ScheduledEvents.ContainsKey(tick)) - { - ScheduledEvents[tick].Add(new ScheduledEvent(gameEvent, tick)); - } - else - { - ScheduledEvents[tick] = new List - { - new ScheduledEvent(gameEvent, tick), - }; - } + EventPool.Remove(gameEvent); } /// - /// Removes a scheduled event from the game event dictionary. + /// /// - /// The scheduled event to remove. - /// The tick at which the scheduled event occurs. - public void RemoveScheduledEvent(ScheduledEvent scheduledEvent, int tick) + /// + /// + public GameEvent GetEventByInstanceID(string instanceId) { - if (ScheduledEvents.ContainsKey(tick)) - { - ScheduledEvents[tick].Remove(scheduledEvent); - } + return EventPool.Find(gameEvent => gameEvent.InstanceID == instanceId); } /// diff --git a/Assets/Scripts/Game/GameSummary.cs b/Assets/Scripts/Game/GameSummary.cs index e370185..993f67f 100644 --- a/Assets/Scripts/Game/GameSummary.cs +++ b/Assets/Scripts/Game/GameSummary.cs @@ -45,7 +45,7 @@ public sealed class GameSummary public string PlayerFactionID; /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public GameSummary() { } } diff --git a/Assets/Scripts/Missions/IMissionParticipant.cs b/Assets/Scripts/Game/IMissionParticipant.cs similarity index 100% rename from Assets/Scripts/Missions/IMissionParticipant.cs rename to Assets/Scripts/Game/IMissionParticipant.cs diff --git a/Assets/Scripts/Game/Message.cs b/Assets/Scripts/Game/Message.cs index 54ce249..76ccf8e 100644 --- a/Assets/Scripts/Game/Message.cs +++ b/Assets/Scripts/Game/Message.cs @@ -16,7 +16,7 @@ public class Message : BaseGameEntity public bool Read = false; /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public Message() { } diff --git a/Assets/Scripts/Game/Officer.cs b/Assets/Scripts/Game/Officer.cs index 5173926..6a62b29 100644 --- a/Assets/Scripts/Game/Officer.cs +++ b/Assets/Scripts/Game/Officer.cs @@ -25,6 +25,7 @@ public class Officer : LeafNode, IMissionParticipant, IMovable // Officer Info public bool IsMain { get; set; } + public bool IsRecruitable { get; set; } public bool IsCaptured { get; set; } public bool CanBetray { get; set; } public bool IsTraitor { get; set; } @@ -88,7 +89,7 @@ public class Officer : LeafNode, IMissionParticipant, IMovable public bool CanImproveMissionSkill => true; /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public Officer() { } diff --git a/Assets/Scripts/Game/Planet.cs b/Assets/Scripts/Game/Planet.cs index 3a0e46f..62f07f8 100644 --- a/Assets/Scripts/Game/Planet.cs +++ b/Assets/Scripts/Game/Planet.cs @@ -52,7 +52,7 @@ public class Planet : ContainerNode new Dictionary>(); /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public Planet() { } @@ -113,30 +113,32 @@ public void SetPopularSupport(string factionInstanceId, int support) else { int overage = totalSupport + supportDifference - MAX_POPULAR_SUPPORT; + ShiftFactionSupport(factionInstanceId, overage); + PopularSupport[factionInstanceId] = support; + } + } - foreach ( - KeyValuePair kvp in PopularSupport - .Where(kvp => kvp.Key != factionInstanceId) - .ToList() - ) - { - int reduction = Math.Min(overage, kvp.Value); - PopularSupport[kvp.Key] -= reduction; - overage -= reduction; - - if (overage == 0) - break; - } - - if (overage > 0) - { - throw new GameStateException( - $"Cannot set popular support for faction {factionInstanceId} to {support}. " - + $"Total support would exceed {MAX_POPULAR_SUPPORT} even after reducing other factions' support." - ); - } + /// + /// Shifts the support of other factions to accommodate the increase in support for the given faction. + /// + /// The faction ID to exclude from reduction. + /// The amount of support to reduce from other factions. + private void ShiftFactionSupport(string excludedFactionId, int overage) + { + // Sort the factions by support in descending order. + foreach ( + KeyValuePair kvp in PopularSupport + .Where(kvp => kvp.Key != excludedFactionId) + .ToList() + ) + { + // Reduce the support for the faction by the minimum of the overage and the faction's current support. + int reduction = Math.Min(overage, kvp.Value); + PopularSupport[kvp.Key] -= reduction; + overage -= reduction; - PopularSupport[factionInstanceId] = support; + if (overage == 0) + break; } } @@ -173,18 +175,7 @@ public int GetBuildTime(IManufacturable manufacturable, int quantity) { int totalMaterialCost = manufacturable.GetConstructionCost() * quantity; ManufacturingType requiredManufacturingType = manufacturable.GetManufacturingType(); - double combinedRate = 0; - - foreach (List buildingList in Buildings.Values) - { - foreach (Building building in buildingList) - { - if (building.GetProductionType() == requiredManufacturingType) - { - combinedRate += 1.0 / building.GetProcessRate(); - } - } - } + double combinedRate = GetCombinedProductionRate(requiredManufacturingType); if (combinedRate == 0) return 0; @@ -193,6 +184,19 @@ public int GetBuildTime(IManufacturable manufacturable, int quantity) return (int)Math.Ceiling(combinedTime); } + /// + /// Calculates the combined production rate for a specific manufacturing type. + /// + /// The manufacturing type to calculate the rate for. + /// The combined production rate. + private double GetCombinedProductionRate(ManufacturingType manufacturingType) + { + return Buildings + .Values.SelectMany(buildingList => buildingList) + .Where(building => building.GetProductionType() == manufacturingType) + .Sum(building => 1.0 / building.GetProcessRate()); + } + /// /// Calculates the total production progress per tick for a given manufacturing type on a planet. /// @@ -200,7 +204,7 @@ public int GetBuildTime(IManufacturable manufacturable, int quantity) /// The calculated progress. public int GetProductionRate(ManufacturingType type) { - double combinedRate = GetBuildings(type).Sum(building => 1.0 / building.GetProcessRate()); + double combinedRate = GetCombinedProductionRate(type); return (int)Math.Ceiling(combinedRate); } @@ -209,10 +213,29 @@ public int GetProductionRate(ManufacturingType type) /// /// The unit to be added to the manufacturing queue. public void AddToManufacturingQueue(IManufacturable manufacturable) + { + ValidateManufacturable(manufacturable); + ManufacturingType type = manufacturable.GetManufacturingType(); + + if (!ManufacturingQueue.ContainsKey(type)) + { + ManufacturingQueue.Add(type, new List()); + } + + manufacturable.SetPosition(GetPosition()); + ManufacturingQueue[type].Add(manufacturable); + } + + /// + /// Validates if a manufacturable can be added to the manufacturing queue. + /// + /// The manufacturable to validate. + /// + private void ValidateManufacturable(IManufacturable manufacturable) { if (manufacturable is ISceneNode sceneNode && sceneNode.GetParent() == null) { - throw new GameStateException( + throw new InvalidSceneOperationException( $"Unit {sceneNode.GetDisplayName()} must have a parent to be added to the manufacturing queue." ); } @@ -221,16 +244,6 @@ public void AddToManufacturingQueue(IManufacturable manufacturable) { throw new SceneAccessException(manufacturable, this); } - - ManufacturingType type = manufacturable.GetManufacturingType(); - - if (!ManufacturingQueue.ContainsKey(type)) - { - ManufacturingQueue.Add(type, new List()); - } - - manufacturable.SetPosition(GetPosition()); - ManufacturingQueue[type].Add(manufacturable); } /// @@ -321,6 +334,17 @@ public List GetBuildings(ManufacturingType productionType) /// The building to add. /// Thrown when the planet is not colonized or at capacity. private void AddBuilding(Building building) + { + ValidateBuilding(building); + BuildingSlot slot = building.GetBuildingSlot(); + Buildings[slot].Add(building); + } + + /// + /// Validates if a building can be added to the planet. + /// + /// The building to validate. + private void ValidateBuilding(Building building) { if (!IsColonized) { @@ -335,18 +359,15 @@ private void AddBuilding(Building building) } BuildingSlot slot = building.GetBuildingSlot(); - if ( - slot == BuildingSlot.Ground && Buildings[slot].Count == GroundSlots - || slot == BuildingSlot.Orbit && Buildings[slot].Count == OrbitSlots + (slot == BuildingSlot.Ground && Buildings[slot].Count == GroundSlots) + || (slot == BuildingSlot.Orbit && Buildings[slot].Count == OrbitSlots) ) { throw new GameStateException( $"Cannot add {building.GetDisplayName()} to {this.GetDisplayName()}. Planet is at capacity." ); } - - Buildings[slot].Add(building); } /// @@ -487,6 +508,11 @@ public override void RemoveChild(ISceneNode child) case Regiment regiment: RemoveRegiment(regiment); break; + default: + throw new InvalidSceneOperationException( + $"Cannot remove {child.GetDisplayName()} from {this.GetDisplayName()}. " + + $"Only fleets, officers, buildings, missions, and regiments are allowed." + ); } } diff --git a/Assets/Scripts/Game/PlanetSystem.cs b/Assets/Scripts/Game/PlanetSystem.cs index 8003cb2..842f7bb 100644 --- a/Assets/Scripts/Game/PlanetSystem.cs +++ b/Assets/Scripts/Game/PlanetSystem.cs @@ -41,7 +41,7 @@ public class PlanetSystem : ContainerNode public List Planets { get; set; } = new List(); /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public PlanetSystem() { } diff --git a/Assets/Scripts/Game/Regiment.cs b/Assets/Scripts/Game/Regiment.cs index f1c4ff7..42c318f 100644 --- a/Assets/Scripts/Game/Regiment.cs +++ b/Assets/Scripts/Game/Regiment.cs @@ -31,7 +31,7 @@ public class Regiment : LeafNode, IManufacturable, IMovable public int PositionY { get; set; } /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public Regiment() { } diff --git a/Assets/Scripts/Game/SpecialForces.cs b/Assets/Scripts/Game/SpecialForces.cs index fa41204..604fbe9 100644 --- a/Assets/Scripts/Game/SpecialForces.cs +++ b/Assets/Scripts/Game/SpecialForces.cs @@ -36,7 +36,7 @@ public class SpecialForces : LeafNode, IMissionParticipant, IManufacturable, IMo public bool CanImproveMissionSkill => true; /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public SpecialForces() { } diff --git a/Assets/Scripts/Game/Starfighter.cs b/Assets/Scripts/Game/Starfighter.cs index d9ce677..60a56a0 100644 --- a/Assets/Scripts/Game/Starfighter.cs +++ b/Assets/Scripts/Game/Starfighter.cs @@ -46,7 +46,7 @@ public class Starfighter : LeafNode, IManufacturable, IMovable public int PositionY { get; set; } /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public Starfighter() { } diff --git a/Assets/Scripts/Game/Technology.cs b/Assets/Scripts/Game/Technology.cs index d6d058e..36924cf 100644 --- a/Assets/Scripts/Game/Technology.cs +++ b/Assets/Scripts/Game/Technology.cs @@ -11,7 +11,7 @@ public class Technology public IManufacturable Manufacturable { get; set; } /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public Technology() { } @@ -28,6 +28,7 @@ public Technology(IManufacturable manufacturable) /// Returns the referenced manufacturable. /// /// + /// public IManufacturable GetReference() { return Manufacturable; @@ -37,6 +38,7 @@ public IManufacturable GetReference() /// Returns a deep copy of the referenced manufacturable. /// /// The deep copy of the referenced manufacturable. + /// public IManufacturable GetReferenceCopy() { IManufacturable clonedManufacturable = Manufacturable.GetDeepCopy(); @@ -48,18 +50,19 @@ public IManufacturable GetReferenceCopy() } /// - /// + /// Returns the manufacturing type of the referenced manufacturable. /// - /// + /// The manufacturing type of the referenced manufacturable. + /// public ManufacturingType GetManufacturingType() { return Manufacturable.GetManufacturingType(); } /// - /// + /// Returns the required research level of the referenced manufacturable. /// - /// + /// The required research level of the referenced manufacturable. public int GetRequiredResearchLevel() { return Manufacturable.GetRequiredResearchLevel(); diff --git a/Assets/Scripts/Generation/CapitalShipGenerator.cs b/Assets/Scripts/Generation/CapitalShipGenerator.cs index a2bf809..25f6a2f 100644 --- a/Assets/Scripts/Generation/CapitalShipGenerator.cs +++ b/Assets/Scripts/Generation/CapitalShipGenerator.cs @@ -21,30 +21,28 @@ public CapitalShipGenerator(GameSummary summary, IResourceManager resourceManage /// /// Retrieves the configuration for all capital ships based on the current galaxy size. /// - /// An array of IConfig objects representing the capital ships for the current galaxy size. + /// An array of objects representing the capital ships for the current galaxy size. private IConfig[] GetCapitalShipConfigs() { GameSize galaxySize = GetGameSummary().GalaxySize; IConfig config = GetConfig(); // Generate a range of galaxy sizes and retrieve configurations for each - return Enumerable - .Range((int)GameSize.Small, (int)galaxySize) - .SelectMany(size => - config.GetValue( - $"CapitalShips.InitialCapitalShips.GalaxySize.{(GameSize)size}" - ) + return config + .GetValue( + $"CapitalShips.InitialCapitalShips.GalaxySize.{(galaxySize).ToString()}" ) .ToArray(); } /// - /// Maps the provided capital ships to their corresponding configurations. + /// Maps the provided capital ships to the config files, capital ship selection and placement + /// are set. The result is a list of capital ships which should then be deployed to the scene graph. /// /// The list of available capital ships. /// The configurations to map to the ships. /// An array of mapped CapitalShip objects with updated configurations. - private CapitalShip[] MapCapitalShipsToConfigs( + private CapitalShip[] GetCapitalShipsToDeploy( CapitalShip[] capitalShips, IConfig[] capitalShipConfigs ) @@ -52,7 +50,7 @@ IConfig[] capitalShipConfigs return capitalShipConfigs .Select(config => { - // Find matching ship and update its properties + // Find matching ship and update its properties. CapitalShip ship = capitalShips.First(s => s.TypeID == config.GetValue("TypeID") ); @@ -184,7 +182,7 @@ public override CapitalShip[] DecorateUnits(CapitalShip[] capitalShips) public override CapitalShip[] DeployUnits(CapitalShip[] units, PlanetSystem[] destinations) { IConfig[] capitalShipConfigs = GetCapitalShipConfigs(); - CapitalShip[] mappedCapitalShips = MapCapitalShipsToConfigs(units, capitalShipConfigs); - return AssignUnits(mappedCapitalShips, destinations); + CapitalShip[] shipsToDeploy = GetCapitalShipsToDeploy(units, capitalShipConfigs); + return AssignUnits(shipsToDeploy, destinations); } } diff --git a/Assets/Scripts/Generation/FactionGenerator.cs b/Assets/Scripts/Generation/FactionGenerator.cs index 6aca01a..e4e705b 100644 --- a/Assets/Scripts/Generation/FactionGenerator.cs +++ b/Assets/Scripts/Generation/FactionGenerator.cs @@ -25,6 +25,13 @@ private void SetFactionHQs(Faction[] factions, PlanetSystem[] planetSystems) Dictionary hqs = factions.ToDictionary(f => f.HQInstanceID); HashSet filledHQs = new HashSet(); + if (planetSystems.Length == 0) + { + throw new GameException( + "Cannot assign faction headquarters. No planet systems available." + ); + } + foreach (PlanetSystem planetSystem in planetSystems) { foreach (Planet planet in planetSystem.Planets) @@ -57,7 +64,7 @@ private void SetStartingPlanets(Faction[] factions, PlanetSystem[] planetSystems string galaxySize = GetGameSummary().GalaxySize.ToString(); int numStartingPlanets = startConfig.GetValue(galaxySize); - // Select and shuffle starting planets + // Select and shuffle starting planets. Queue startingPlanets = new Queue( planetSystems .SelectMany(ps => ps.Planets) @@ -66,7 +73,7 @@ private void SetStartingPlanets(Faction[] factions, PlanetSystem[] planetSystems .Take(numStartingPlanets * factions.Length) ); - // Assign planets to factions + // Assign planets to factions. foreach (Faction faction in factions) { for (int i = 0; i < numStartingPlanets; i++) diff --git a/Assets/Scripts/Generation/GameBuilder.cs b/Assets/Scripts/Generation/GameBuilder.cs index 0ecf608..6f04b3e 100644 --- a/Assets/Scripts/Generation/GameBuilder.cs +++ b/Assets/Scripts/Generation/GameBuilder.cs @@ -1,41 +1,181 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.IO; using System.Linq; -using ICollectionExtensions; -using IEnumerableExtensions; /// -/// Represents a class responsible for building the game by generating the galaxy map and decorating it with units. +/// Builds the game by generating the galaxy map and populating it with units and factions. /// public sealed class GameBuilder { - private PlanetSystemGenerator psGenerator; - private FactionGenerator factionGenerator; - private OfficerGenerator officerGenerator; - private BuildingGenerator buildingGenerator; - private CapitalShipGenerator csGenerator; - private StarfighterGenerator starfighterGenerator; - private GameSummary summary; + private readonly GameSummary summary; + private readonly PlanetSystemGenerator planetSystemGenerator; + private readonly FactionGenerator factionGenerator; + private readonly OfficerGenerator officerGenerator; + private readonly BuildingGenerator buildingGenerator; + private readonly CapitalShipGenerator capitalShipGenerator; + private readonly StarfighterGenerator starfighterGenerator; + private readonly RegimentGenerator regimentGenerator; + private readonly GameEventGenerator gameEventGenerator; /// /// Initializes a new instance of the GameBuilder class. /// - /// The summary of the game. + /// The summary of the game to be built. public GameBuilder(GameSummary summary) { + this.summary = summary; IResourceManager resourceManager = ResourceManager.Instance; - // Initialize our unit generators. - psGenerator = new PlanetSystemGenerator(summary, resourceManager); + planetSystemGenerator = new PlanetSystemGenerator(summary, resourceManager); factionGenerator = new FactionGenerator(summary, resourceManager); officerGenerator = new OfficerGenerator(summary, resourceManager); buildingGenerator = new BuildingGenerator(summary, resourceManager); - csGenerator = new CapitalShipGenerator(summary, resourceManager); + capitalShipGenerator = new CapitalShipGenerator(summary, resourceManager); starfighterGenerator = new StarfighterGenerator(summary, resourceManager); + regimentGenerator = new RegimentGenerator(summary, resourceManager); + gameEventGenerator = new GameEventGenerator(summary, resourceManager); + } - this.summary = summary; + /// + /// Builds the game by generating all necessary components. + /// + /// The fully constructed game. + public Game BuildGame() + { + PlanetSystem[] galaxyMap = GenerateGalaxyMap(); + Faction[] factions = GenerateFactions(galaxyMap); + + Building[] buildings = GenerateBuildings(galaxyMap); + CapitalShip[] capitalShips = GenerateCapitalShips(galaxyMap); + Starfighter[] starfighters = GenerateStarfighters(galaxyMap); + Regiment[] regiments = GenerateRegiments(galaxyMap); + + IUnitGenerationResults officerResults = GenerateOfficers(galaxyMap); + Officer[] unrecruitedOfficers = GetUnrecruitedOfficers(officerResults); + + GameEvent[] gameEvents = GenerateGameEvents(galaxyMap); + + SetupFactionTechnologies(factions, buildings, capitalShips, starfighters, regiments); + + return CreateGame(galaxyMap, factions, gameEvents, unrecruitedOfficers); + } + + /// + /// Generates the galaxy map. + /// + /// An array of PlanetSystem objects representing the galaxy map. + private PlanetSystem[] GenerateGalaxyMap() + { + return planetSystemGenerator.GenerateUnits().SelectedUnits; + } + + /// + /// Generates factions for the game. + /// + /// The galaxy map to use for generation. + /// An array of Faction objects. + private Faction[] GenerateFactions(PlanetSystem[] galaxyMap) + { + return factionGenerator.GenerateUnits(galaxyMap).UnitPool; + } + + /// + /// Generates buildings for the game. + /// + /// The galaxy map to use for generation. + /// An array of Building objects. + private Building[] GenerateBuildings(PlanetSystem[] galaxyMap) + { + return buildingGenerator.GenerateUnits(galaxyMap).UnitPool; + } + + /// + /// Generates capital ships for the game. + /// + /// The galaxy map to use for generation. + /// An array of CapitalShip objects. + private CapitalShip[] GenerateCapitalShips(PlanetSystem[] galaxyMap) + { + return capitalShipGenerator.GenerateUnits(galaxyMap).UnitPool; + } + + /// + /// Generates starfighters for the game. + /// + /// The galaxy map to use for generation. + /// An array of Starfighter objects. + private Starfighter[] GenerateStarfighters(PlanetSystem[] galaxyMap) + { + return starfighterGenerator.GenerateUnits(galaxyMap).UnitPool; + } + + /// + /// Generates regiments for the game. + /// + /// The galaxy map to use for generation. + /// An array of Regiment objects. + private Regiment[] GenerateRegiments(PlanetSystem[] galaxyMap) + { + return regimentGenerator.GenerateUnits(galaxyMap).UnitPool; + } + + /// + /// Generates officers for the game. + /// + /// The galaxy map to use for generation. + /// The results of officer generation, including both selected and unselected officers. + private IUnitGenerationResults GenerateOfficers(PlanetSystem[] galaxyMap) + { + return officerGenerator.GenerateUnits(galaxyMap); + } + + /// + /// Retrieves the list of unrecruited officers. + /// + /// The results of officer generation. + /// An array of Officer objects representing unrecruited officers. + private Officer[] GetUnrecruitedOfficers(IUnitGenerationResults officerResults) + { + return officerResults.UnitPool.Except(officerResults.SelectedUnits).ToArray(); + } + + /// + /// Generates game events for the game. + /// + /// The galaxy map to use for generation. + /// An array of GameEvent objects. + private GameEvent[] GenerateGameEvents(PlanetSystem[] galaxyMap) + { + return gameEventGenerator.GenerateUnits(galaxyMap).UnitPool; + } + + /// + /// Sets up the technology trees for all factions. + /// + /// The array of factions. + /// The array of buildings. + /// The array of capital ships. + /// The array of starfighters. + /// The array of regiments. + private void SetupFactionTechnologies( + Faction[] factions, + Building[] buildings, + CapitalShip[] capitalShips, + Starfighter[] starfighters, + Regiment[] regiments + ) + { + Dictionary factionMap = factions.ToDictionary(faction => + faction.InstanceID + ); + IManufacturable[] combinedTechnologies = buildings + .Cast() + .Concat(capitalShips) + .Concat(starfighters) + .Concat(regiments) + .ToArray(); + + SetTechnologyLevels(factionMap, combinedTechnologies); } /// @@ -68,52 +208,29 @@ IManufacturable[] combinedTechnologies } /// - /// Builds the game by generating the galaxy map and decorating it with units. + /// Creates and returns the final Game object. /// - /// The built game. - public Game BuildGame() + /// The generated galaxy map. + /// The generated factions. + /// The generated game events. + /// The list of unrecruited officers. + /// A fully initialized Game object. + private Game CreateGame( + PlanetSystem[] galaxyMap, + Faction[] factions, + GameEvent[] gameEvents, + Officer[] unrecruitedOfficers + ) { - // Generate our galaxy map with stat decorated planets. - IUnitGenerationResults psResults = psGenerator.GenerateUnits(); - PlanetSystem[] galaxyMap = psResults.SelectedUnits; - - // Decorate the galaxy map with units. - Faction[] factions = factionGenerator.GenerateUnits(galaxyMap).UnitPool; - Building[] buildings = buildingGenerator.GenerateUnits(galaxyMap).UnitPool; - CapitalShip[] capitalShips = csGenerator.GenerateUnits(galaxyMap).UnitPool; - Starfighter[] starfighters = starfighterGenerator.GenerateUnits(galaxyMap).UnitPool; - IUnitGenerationResults officerResults = officerGenerator.GenerateUnits(galaxyMap); - - // Retrieve list of unrecruited officers. - Officer[] unrecruitedOfficers = officerResults - .UnitPool.Except(officerResults.SelectedUnits) - .ToArray(); - - // Initialize each faction's technology tree. - Dictionary factionMap = factions.ToDictionary(faction => - faction.InstanceID - ); - IManufacturable[] combinedTechnologies = Array - .Empty() - .Concat(buildings) - .Concat(capitalShips) - .Concat(starfighters) - .ToArray(); - - // Set the technology tree for each faction. - SetTechnologyLevels(factionMap, combinedTechnologies); - - // Set the galaxy map. - GalaxyMap galaxy = new GalaxyMap { PlanetSystems = galaxyMap.ToList() }; + GalaxyMap galaxy = new GalaxyMap { PlanetSystems = galaxyMap.ToList() }; - // Initialize our new game. - Game game = new Game + return new Game { + EventPool = gameEvents.ToList(), Summary = this.summary, - Factions = factions.ToList(), + Factions = factions.ToList(), Galaxy = galaxy, UnrecruitedOfficers = unrecruitedOfficers.ToList(), }; - return game; } } diff --git a/Assets/Scripts/Generation/GameEventGenerator.cs b/Assets/Scripts/Generation/GameEventGenerator.cs new file mode 100644 index 0000000..9f00360 --- /dev/null +++ b/Assets/Scripts/Generation/GameEventGenerator.cs @@ -0,0 +1,44 @@ +/// +/// +/// +public class GameEventGenerator : UnitGenerator +{ + /// + /// Constructs an EventGenerator object. + /// + /// The GameSummary options selected by the player. + /// The resource manager from which to load game data. + public GameEventGenerator(GameSummary summary, IResourceManager resourceManager) + : base(summary, resourceManager) { } + + /// + /// + /// + /// + /// + public override GameEvent[] DecorateUnits(GameEvent[] events) + { + return events; + } + + /// + /// + /// + /// + /// + /// + public override GameEvent[] DeployUnits(GameEvent[] events, PlanetSystem[] destinations) + { + return events; + } + + /// + /// + /// + /// + /// + public override GameEvent[] SelectUnits(GameEvent[] events) + { + return events; + } +} diff --git a/Assets/Scripts/Generation/OfficerGenerator.cs b/Assets/Scripts/Generation/OfficerGenerator.cs index 983e622..3a7925b 100644 --- a/Assets/Scripts/Generation/OfficerGenerator.cs +++ b/Assets/Scripts/Generation/OfficerGenerator.cs @@ -33,6 +33,7 @@ private Officer[] SelectInitialOfficers(Dictionary> office foreach (var (ownerInstanceId, officers) in officersByFaction) { IEnumerable reducedOfficers = officers + .Where(officer => officer.IsMain || officer.IsRecruitable) .TakeWhile((officer, index) => officer.IsMain || index < numAllowedOfficers) .Select(officer => { @@ -117,11 +118,11 @@ public override Officer[] SelectUnits(Officer[] units) if (officer.OwnerInstanceID != null) { - officersByFaction[factionId].Insert(0, officer); // Add to front + officersByFaction[factionId].Insert(0, officer); } else { - officersByFaction[factionId].Add(officer); // Add to end + officersByFaction[factionId].Add(officer); } } } diff --git a/Assets/Scripts/Generation/PlanetSystemGenerator.cs b/Assets/Scripts/Generation/PlanetSystemGenerator.cs index 8b7567b..6687a00 100644 --- a/Assets/Scripts/Generation/PlanetSystemGenerator.cs +++ b/Assets/Scripts/Generation/PlanetSystemGenerator.cs @@ -72,14 +72,16 @@ private void SetColonizationStatus(PlanetSystem parentsystem, Planet planet) /// public override PlanetSystem[] SelectUnits(PlanetSystem[] units) { - GameSize galaxySize = GetGameSummary().GalaxySize; + int galaxySize = (int)GetGameSummary().GalaxySize; List galaxyMap = new List(); - IEnumerable sizeRange = Enumerable.Range((int)GameSize.Small, (int)galaxySize); - + // Select planet systems with visibility greater than or equal to the galaxy size. foreach (PlanetSystem planetSystem in units) { - if (sizeRange.Contains((int)planetSystem.Visibility)) + int visibilityValue = (int)planetSystem.Visibility; + + // Add the planet system to the galaxy map if it is visible. + if (visibilityValue <= galaxySize) { galaxyMap.Add(planetSystem); } diff --git a/Assets/Scripts/Generation/RegimentGenerator.cs b/Assets/Scripts/Generation/RegimentGenerator.cs new file mode 100644 index 0000000..6155b5b --- /dev/null +++ b/Assets/Scripts/Generation/RegimentGenerator.cs @@ -0,0 +1,47 @@ +using System.Linq; + +public class RegimentGenerator : UnitGenerator +{ + /// + /// Constructs an EventGenerator object. + /// + /// The GameSummary options selected by the player. + /// The resource manager from which to load game data. + public RegimentGenerator(GameSummary summary, IResourceManager resourceManager) + : base(summary, resourceManager) { } + + /// + /// + /// + /// + /// + public override Regiment[] SelectUnits(Regiment[] regiments) + { + return regiments + .Where(regiment => + regiment.RequiredResearchLevel <= GetGameSummary().StartingResearchLevel + ) + .ToArray(); + } + + /// + /// + /// + /// + /// + public override Regiment[] DecorateUnits(Regiment[] regiments) + { + return regiments; + } + + /// + /// + /// + /// + /// + /// + public override Regiment[] DeployUnits(Regiment[] regiments, PlanetSystem[] destinations) + { + return regiments; + } +} diff --git a/Assets/Scripts/Generation/StarfighterGenerator.cs b/Assets/Scripts/Generation/StarfighterGenerator.cs index 02eacc4..e54b6c4 100644 --- a/Assets/Scripts/Generation/StarfighterGenerator.cs +++ b/Assets/Scripts/Generation/StarfighterGenerator.cs @@ -26,8 +26,11 @@ public StarfighterGenerator(GameSummary summary, IResourceManager resourceManage /// An array of all starfighters. public override Starfighter[] SelectUnits(Starfighter[] starfighters) { - // No op. - return starfighters; + return starfighters + .Where(starfighter => + starfighter.RequiredResearchLevel <= GetGameSummary().StartingResearchLevel + ) + .ToArray(); } /// diff --git a/Assets/Scripts/Generation/UnitGenerationResults.cs b/Assets/Scripts/Generation/UnitGenerationResults.cs new file mode 100644 index 0000000..d6fcf32 --- /dev/null +++ b/Assets/Scripts/Generation/UnitGenerationResults.cs @@ -0,0 +1,18 @@ +/// +/// +/// +/// +class UnitGenerationResults : IUnitGenerationResults + where TUnit : BaseGameEntity +{ + public TUnit[] UnitPool { get; set; } + public TUnit[] SelectedUnits { get; set; } + public TUnit[] DeployedUnits { get; set; } + + public UnitGenerationResults(TUnit[] unitPool, TUnit[] selectedUnits, TUnit[] deployedUnits) + { + UnitPool = unitPool; + SelectedUnits = selectedUnits; + DeployedUnits = deployedUnits; + } +} diff --git a/Assets/Scripts/Generation/UnitGenerator.cs b/Assets/Scripts/Generation/UnitGenerator.cs index 825a184..1ed4ca5 100644 --- a/Assets/Scripts/Generation/UnitGenerator.cs +++ b/Assets/Scripts/Generation/UnitGenerator.cs @@ -1,24 +1,5 @@ using System; -/// -/// -/// -/// -class UnitGenerationResults : IUnitGenerationResults - where TUnit : BaseGameEntity -{ - public TUnit[] UnitPool { get; set; } - public TUnit[] SelectedUnits { get; set; } - public TUnit[] DeployedUnits { get; set; } - - public UnitGenerationResults(TUnit[] unitPool, TUnit[] selectedUnits, TUnit[] deployedUnits) - { - UnitPool = unitPool; - SelectedUnits = selectedUnits; - DeployedUnits = deployedUnits; - } -} - /// /// /// diff --git a/Assets/Scripts/Managers/AIManager.cs b/Assets/Scripts/Managers/AIManager.cs index 0aa6da1..f170da0 100644 --- a/Assets/Scripts/Managers/AIManager.cs +++ b/Assets/Scripts/Managers/AIManager.cs @@ -2,15 +2,15 @@ using System.Linq; /// -/// @NOTE: This is a dummy class meant for testing. It does NOT represent the -/// actual AI that will be implemented in the game. +/// Manages AI behavior for factions in the game. +/// Note: This is a simplified implementation for testing purposes. /// public class AIManager { - private Game game; - private MissionManager missionManager; - private UnitManager unitManager; - private PlanetManager planetManager; + private readonly Game game; + private readonly MissionManager missionManager; + private readonly UnitManager unitManager; + private readonly PlanetManager planetManager; public AIManager( Game game, @@ -26,170 +26,255 @@ PlanetManager planetManager } /// - /// Updates the AI for all factions. + /// Updates the AI for all AI-controlled factions. /// public void Update() { - // Update the AI for each faction. - foreach (Faction faction in game.Factions) + foreach (Faction faction in game.Factions.Where(f => f.IsAIControlled())) { - if (faction.IsAIControlled()) - { - UpdateFaction(faction); - } + UpdateFaction(faction); } } /// - /// Updates the AI for the specified faction. + /// Updates various aspects of the AI for a specific faction. /// - /// + /// The faction to update. private void UpdateFaction(Faction faction) { UpdateOfficers(faction); - UpdateManufacturing(faction); + UpdateBuildings(faction); UpdateShipyards(faction); + UpdateTrainingFacilities(faction); } /// - /// - /// - /// - /// - private List GetAvailableOfficers(string ownerInstanceId) - { - return game.GetSceneNodesByOwnerInstanceID(ownerInstanceId) - .FindAll(o => o.IsMovable()); - } - - /// - /// + /// Manages officer assignments for missions. /// + /// The faction to update officer assignments for. private void UpdateOfficers(Faction faction) { - List officers = faction.GetAvailableOfficers(faction); + List availableOfficers = faction.GetAvailableOfficers(faction); - foreach (Officer officer in officers) + foreach (Officer officer in availableOfficers) { - if (officer.IsMovable()) + if (!officer.IsMovable()) + continue; + + if (officer.IsMain && game.GetUnrecruitedOfficers(faction.InstanceID).Any()) + { + InitiateRecruitmentMission(officer, faction); + } + else if ( + officer.IsMain + || officer.GetSkillValue(MissionParticipantSkill.Diplomacy) > 60 + ) { - if (officer.IsMain && game.GetUnrecruitedOfficers(faction.InstanceID).Count > 0) - { - Planet nearestFriendlyPlanet = faction.GetNearestPlanetTo(officer); - - GameLogger.Log( - $"Sending {officer.GetDisplayName()} on a recruitment mission to {nearestFriendlyPlanet.GetDisplayName()}." - ); - - missionManager.InitiateMission( - MissionType.Recruitment, - officer, - nearestFriendlyPlanet - ); - } - else if ( - officer.IsMain - || officer.GetSkillValue(MissionParticipantSkill.Diplomacy) > 60 - ) - { - Planet nearestUnownedPlanet = - officer - .GetParentOfType() - .GetChildrenByOwnerInstanceID(null) - .FirstOrDefault() as Planet; - - GameLogger.Log( - $"Sending {officer.GetDisplayName()} on a diplomacy mission to {nearestUnownedPlanet.GetDisplayName()}." - ); - - missionManager.InitiateMission( - MissionType.Diplomacy, - officer, - nearestUnownedPlanet - ); - } + InitiateDiplomacyMission(officer); } } } /// - /// + /// Initiates a recruitment mission for the given officer. /// - private void UpdateManufacturing(Faction faction) + /// The officer to send on the recruitment mission. + /// The faction the officer belongs to. + private void InitiateRecruitmentMission(Officer officer, Faction faction) { - List idleConstructionFacilities = faction.GetIdleFacilities( - ManufacturingType.Building + Planet nearestFriendlyPlanet = faction.GetNearestPlanetTo(officer); + GameLogger.Log( + $"Sending {officer.GetDisplayName()} on a recruitment mission to {nearestFriendlyPlanet.GetDisplayName()}." ); + missionManager.InitiateMission(MissionType.Recruitment, officer, nearestFriendlyPlanet); + } + + /// + /// Initiates a diplomacy mission for the given officer. + /// + /// The officer to send on the diplomacy mission. + private void InitiateDiplomacyMission(Officer officer) + { + Planet nearestUnownedPlanet = + officer + .GetParentOfType() + .GetChildrenByOwnerInstanceID(null) + .FirstOrDefault() as Planet; - if (idleConstructionFacilities.Count == 0) + if (nearestUnownedPlanet != null) { - return; + GameLogger.Log( + $"Sending {officer.GetDisplayName()} on a diplomacy mission to {nearestUnownedPlanet.GetDisplayName()}." + ); + missionManager.InitiateMission(MissionType.Diplomacy, officer, nearestUnownedPlanet); } + } - List buildingOptions = faction.GetResearchedTechnologies( + /// + /// Manages construction of buildings. + /// + /// The faction to update building construction for. + private void UpdateBuildings(Faction faction) + { + List idleConstructionFacilities = faction.GetIdleFacilities( ManufacturingType.Building ); + if (!idleConstructionFacilities.Any()) + return; - Technology constructionYardTech = buildingOptions - .FindAll(technology => - (technology.GetReference() as Building).GetBuildingType() - == BuildingType.ConstructionFacility - ) - .LastOrDefault(); + Technology constructionYardTech = GetHighestTierTechnology( + faction, + ManufacturingType.Building, + BuildingType.ConstructionFacility + ); foreach (Planet planet in idleConstructionFacilities) { - int facilityCount = planet.GetBuildingTypeCount(BuildingType.ConstructionFacility); - if ( - constructionYardTech.GetReference() is Building building - && planet.GetAvailableSlots(building.GetBuildingSlot()) > 0 - && facilityCount < 5 - ) + if (ShouldBuildConstructionFacility(planet, constructionYardTech)) { planetManager.AddToManufacturingQueue(planet, planet, constructionYardTech, 1); } } } + /// + /// Determines if a construction facility should be built on the given planet. + /// + /// The planet to check. + /// The construction yard technology to use. + /// True if a construction facility should be built, false otherwise. + private bool ShouldBuildConstructionFacility(Planet planet, Technology constructionYardTech) + { + if (constructionYardTech?.GetReference() is not Building building) + return false; + + return planet.GetAvailableSlots(building.GetBuildingSlot()) > 0 + && planet.GetBuildingTypeCount(BuildingType.ConstructionFacility) < 5; + } + + /// + /// Manages production of ships. + /// + /// The faction to update ship production for. private void UpdateShipyards(Faction faction) { List idleShipyards = faction.GetIdleFacilities(ManufacturingType.Ship); + if (!idleShipyards.Any()) + return; + + Technology starfighterTech = GetHighestTierTechnology( + faction, + ManufacturingType.Ship, + typeof(Starfighter) + ); + + foreach (Planet planet in idleShipyards) + { + AssignStarfightersToFleets(faction, planet, starfighterTech); + } + } - if (idleShipyards.Count == 0) + /// + /// Assigns starfighters to fleets with available capacity. + /// + /// The faction to assign starfighters for. + /// The planet with the idle shipyard. + /// The starfighter technology to use. + private void AssignStarfightersToFleets( + Faction faction, + Planet planet, + Technology starfighterTech + ) + { + List fleets = faction + .GetOwnedUnitsByType() + .OrderBy(fleet => fleet.GetExcessStarfighterCapacity()) + .Where(fleet => fleet.GetExcessStarfighterCapacity() > 0) + .ToList(); + + foreach (Fleet fleet in fleets) { - return; + planetManager.AddToManufacturingQueue(planet, fleet, starfighterTech, 1); } + } - List shipyardOptions = faction.GetResearchedTechnologies( - ManufacturingType.Ship - ); + /// + /// Manages training of troops. + /// + /// The faction to update troop training for. + private void UpdateTrainingFacilities(Faction faction) + { + List idleTrainingFacilities = faction.GetIdleFacilities(ManufacturingType.Troop); + if (!idleTrainingFacilities.Any()) + return; - Technology starfighterTech = shipyardOptions - .FindAll(technology => (technology.GetReference().GetType() == typeof(Starfighter))) - .LastOrDefault(); + Technology regimentTech = GetHighestTierTechnology( + faction, + ManufacturingType.Troop, + typeof(Regiment) + ); - foreach (Planet planet in idleShipyards) + foreach (Planet planet in idleTrainingFacilities) { - int shipyardCount = planet.GetBuildingTypeCount(BuildingType.Shipyard); + AssignRegimentsToFleets(faction, planet, regimentTech); + } + } - List fleets = faction.GetOwnedUnitsByType(); - List sortedFleets = fleets - .OrderBy(fleet => fleet.GetExcessStarfighterCapacity()) - .ToList(); + /// + /// Assigns regiments to fleets with available capacity. + /// + /// The faction to assign regiments for. + /// The planet with the idle training facility. + /// The regiment technology to use. + private void AssignRegimentsToFleets(Faction faction, Planet planet, Technology regimentTech) + { + List fleets = faction + .GetOwnedUnitsByType() + .OrderBy(fleet => fleet.GetExcessRegimentCapacity()) + .Where(fleet => fleet.GetExcessRegimentCapacity() > 0) + .ToList(); - GameLogger.Log($"Found {fleets.Count} fleets for {faction.GetDisplayName()}."); - foreach (Fleet fleet in sortedFleets) - { - GameLogger.Log( - $"Adding {fleet.GetExcessStarfighterCapacity()} {starfighterTech.GetReference().GetDisplayName()} to the manufacturing queue of {planet.GetDisplayName()}." - ); - planetManager.AddToManufacturingQueue( - planet, - fleet, - starfighterTech, - fleet.GetExcessStarfighterCapacity() - ); - } + foreach (Fleet fleet in fleets) + { + planetManager.AddToManufacturingQueue(planet, fleet, regimentTech, 1); } } + + /// + /// Retrieves the highest-tier technology for the given manufacturing type and reference type. + /// + /// The faction to get the technology for. + /// The manufacturing type to filter by. + /// The type of the technology reference to filter by. + /// The highest-tier technology matching the criteria, or null if not found. + private Technology GetHighestTierTechnology( + Faction faction, + ManufacturingType manufacturingType, + System.Type referenceType + ) + { + return faction + .GetResearchedTechnologies(manufacturingType) + .Where(tech => tech.GetReference().GetType() == referenceType) + .LastOrDefault(); + } + + /// + /// Retrieves the highest-tier technology for the given manufacturing type and building type. + /// + /// The faction to get the technology for. + /// The manufacturing type to filter by. + /// The building type to filter by. + /// The highest-tier technology matching the criteria, or null if not found. + private Technology GetHighestTierTechnology( + Faction faction, + ManufacturingType manufacturingType, + BuildingType buildingType + ) + { + return faction + .GetResearchedTechnologies(manufacturingType) + .Where(tech => (tech.GetReference() as Building)?.GetBuildingType() == buildingType) + .LastOrDefault(); + } } diff --git a/Assets/Scripts/Managers/GameEventManager.cs b/Assets/Scripts/Managers/GameEventManager.cs index d7a1784..6f44510 100644 --- a/Assets/Scripts/Managers/GameEventManager.cs +++ b/Assets/Scripts/Managers/GameEventManager.cs @@ -14,65 +14,41 @@ public GameEventManager(Game game) this.game = game; } - /// - /// Schedules the specified game event to occur at the specified tick. - /// - /// - /// - public void ScheduleEvent(GameEvent gameEvent, int tick) + private void ProcessEvent(GameEvent gameEvent) { - game.ScheduleGameEvent(gameEvent, tick); - } - - /// - /// - /// - /// - /// - public void ScheduleEvent(List actions, int tick) - { - List conditionals = new List(); - GameEvent gameEvent = new GameEvent(conditionals, actions); - ScheduleEvent(gameEvent, tick); + if (gameEvent.AreConditionsMet(game)) + { + GameLogger.Log($"Executing game event: {gameEvent.GetDisplayName()}"); + gameEvent.Execute(game); + game.AddCompletedEvent(gameEvent); + } } /// /// /// - /// - /// - /// - public void ScheduleEvent( - List conditionals, - List actions, - int tick - ) + /// + public void ProcessEvents(List gameEvents) { - GameEvent gameEvent = new GameEvent(conditionals, actions); - ScheduleEvent(gameEvent, tick); - } + List eventsToRemove = new List(); - /// - /// Processes the game events for the specified tick. - /// - /// The current tick. - public void ProcessEvents(int currentTick) - { - List scheduledEvents = game.GetScheduledEvents(currentTick); - - // Check if there are any events scheduled for this tick. - if (scheduledEvents.Any()) + foreach (GameEvent gameEvent in gameEvents) { - // Execute each event. - foreach (ScheduledEvent scheduledEvent in scheduledEvents) - { - GameEvent gameEvent = scheduledEvent.GetEvent(); + ProcessEvent(gameEvent); - gameEvent.Execute(game); - - // Add the event to the list of completed events. - game.AddCompletedEvent(gameEvent); + if (!gameEvent.IsRepeatable) + { + if (!gameEvent.IsRepeatable) + { + eventsToRemove.Add(gameEvent); + } } } + + // Remove events that are no longer needed. + foreach (GameEvent eventToRemove in eventsToRemove) + { + game.RemoveEvent(eventToRemove); + } } } diff --git a/Assets/Scripts/Managers/GameManager.cs b/Assets/Scripts/Managers/GameManager.cs index 299aaae..9581555 100644 --- a/Assets/Scripts/Managers/GameManager.cs +++ b/Assets/Scripts/Managers/GameManager.cs @@ -118,7 +118,7 @@ public void Update() /// /// /// - private void ProcessNode(ISceneNode node) + private void UpdateNode(ISceneNode node) { // Update the movement of movable units. if (node is IMovable moveable) @@ -149,12 +149,13 @@ private void ProcessTick() GameLogger.Log("Tick: " + game.CurrentTick); - game.Galaxy.Traverse(ProcessNode); - - // Update game states. - aiManager.Update(); + // Update the state of each scene node in the game. + game.GetGalaxyMap().Traverse(UpdateNode); // Process any events scheduled for this tick. - eventManager.ProcessEvents(game.CurrentTick); + eventManager.ProcessEvents(game.GetEventPool()); + + // Update the NPC AI factions in the game. + aiManager.Update(); } } diff --git a/Assets/Scripts/Managers/MissionManager.cs b/Assets/Scripts/Managers/MissionManager.cs index 77d541a..0b87709 100644 --- a/Assets/Scripts/Managers/MissionManager.cs +++ b/Assets/Scripts/Managers/MissionManager.cs @@ -3,172 +3,96 @@ using System.Linq; /// -/// -/// -public enum MissionType -{ - Diplomacy, - Recruitment, -} - -/// -/// Manager for handling missions in the game. -/// This includes scheduling missions and rescheduling mission events. +/// Manager for handling the lifecycle of missions in the game. +/// Responsible for updating missions and managing their progression. +/// Creation and initiation of missions are delegated to the . /// public class MissionManager { - private Game game; - private readonly Random random = new Random(); + private readonly Game game; + private readonly MissionFactory missionFactory; /// - /// + /// Initializes a new instance of the class. /// - /// + /// The game instance being managed. public MissionManager(Game game) { this.game = game; + // Initialize the MissionFactory for mission creation and initiation. + this.missionFactory = new MissionFactory(game); } /// - /// + /// Initiates a mission with a single participant and target. /// - /// - /// - /// - /// - /// - /// - /// - private Mission CreateMission( - MissionType missionType, - string ownerInstanceId, - List mainParticipants, - List decoyParticipants, - ISceneNode target - ) - { - return missionType switch - { - MissionType.Diplomacy => new DiplomacyMission( - ownerInstanceId, - target.InstanceID, - mainParticipants, - decoyParticipants - ), - MissionType.Recruitment => new RecruitmentMission( - ownerInstanceId, - target.InstanceID, - mainParticipants, - decoyParticipants - ), - _ => throw new ArgumentException($"Unhandled mission type: {missionType}"), - }; - } - - /// - /// - /// - /// - /// - /// + /// The type of mission to initiate. + /// The main participant of the mission. + /// The target of the mission. public void InitiateMission( - MissionType type, + MissionType missionType, IMissionParticipant participant, ISceneNode target ) { + // Wrap the participant in a list to use the overload for multiple participants. List mainParticipants = new List { participant }; List decoyParticipants = new List(); string ownerInstanceId = participant.OwnerInstanceID; - InitiateMission(type, ownerInstanceId, mainParticipants, decoyParticipants, target); - } - - /// - /// Initiates a mission with the specified parameters. - /// The mission is scheduled to occur at the next possible tick. - /// - /// The type of mission to initiate. - /// The Instance ID of the owner of the mission. - /// The main participants of the mission. - /// The decoy participants of the mission. - /// The target of the mission. This can be a planet or a unit. - public void InitiateMission( - MissionType missionType, - string ownerInstanceId, - List mainParticipants, - List decoyParticipants, - ISceneNode target - ) - { - if (mainParticipants.Count == 0) - { - throw new ArgumentException("Main participants list cannot be empty."); - } - - // Get the nearest planet related to the target and the participants' current planet. - Planet closestPlanet = target is Planet ? (Planet)target : target.GetParentOfType(); - IMissionParticipant firstParticipant = mainParticipants.FirstOrDefault(); - Planet currentPlanet = firstParticipant.GetParentOfType(); - - // Instantiate the mission based on the mission type. - Mission mission = CreateMission( + missionFactory.CreateAndInitiateMission( missionType, ownerInstanceId, mainParticipants, decoyParticipants, target ); - - // Attach the mission to scene graph. - game.AttachNode(mission, closestPlanet, false); - - // Initiate the mission with the given arguments. - // This will set the movement status of all participants to InTransit. - mission.Initiate(); } /// - /// + /// Updates the state of an ongoing mission. + /// This involves incrementing mission progress, evaluating success, + /// and handling mission completion or continuation. /// - /// + /// The mission to update. public void UpdateMission(Mission mission) { - List missions = game.GetSceneNodesByType(); - GalaxyMap galaxyMap = game.GetGalaxyMap(); - - // Increment the mission progress. + // Increment the mission's progress. mission.IncrementProgress(); + // Check if the mission is complete. if (mission.IsComplete()) { - // Evaluate the mission success. + // Evaluate the mission's success or failure. mission.Execute(game); - // Check if the mission can continue. - // If so, reset the mission progress and re-initiate the mission. + // Check if the mission can continue (e.g., repeatable missions). if (mission.CanContinue(game)) { + // Reset progress and re-initiate the mission. mission.Initiate(); } - // Otherwise, move the participants to the closest planet and remove the mission. else { - // Move the units to the closest planet. + // Handle mission completion and return participants to the nearest planet. + + // Get all participants (both main and decoy) that can be moved. List combinedParticipants = mission .GetAllParticipants() .Cast() .ToList(); + // Find the nearest planet to the mission's location for participants to return to. Faction faction = game.GetFactionByOwnerInstanceID(mission.OwnerInstanceID); - Planet planet = faction.GetNearestPlanetTo(mission); + Planet nearestPlanet = faction.GetNearestPlanetTo(mission); + // Move each participant to the nearest planet. foreach (IMovable movable in combinedParticipants) { - movable.MoveTo(planet); + movable.MoveTo(nearestPlanet); } - // Permanently remove the mission from the game. + // Remove the mission from the game permanently. game.DetachNode(mission); } } diff --git a/Assets/Scripts/Managers/PlanetManager.cs b/Assets/Scripts/Managers/PlanetManager.cs index 33d6238..37a1d11 100644 --- a/Assets/Scripts/Managers/PlanetManager.cs +++ b/Assets/Scripts/Managers/PlanetManager.cs @@ -48,7 +48,7 @@ int quantity IManufacturable clonedNode = technology.GetReferenceCopy(); clonedNode.SetOwnerInstanceID(planet.GetOwnerInstanceID()); GameLogger.Log( - $"Adding {clonedNode.GetDisplayName()} to manufacturing queue on {planet.GetDisplayName()}" + $"{planet.GetDisplayName()} adding {quantity} {clonedNode.GetDisplayName()}(s) to manufacturing queue on {planet.GetDisplayName()}" ); game.AttachNode(clonedNode, target); planet.AddToManufacturingQueue(clonedNode); @@ -135,7 +135,7 @@ ref int index $"Manufacturable completed: {manufacturable.GetDisplayName()} {manufacturable.GetParent().GetDisplayName()}" ); productionQueue.RemoveAt(index); - index--; // Decrement index to account for removed item. + index--; // Decrement index to account for the removed item. manufacturable.SetManufacturingStatus(ManufacturingStatus.Complete); diff --git a/Assets/Scripts/Missions/AbductionMission.cs b/Assets/Scripts/Missions/AbductionMission.cs index 6ed4b6e..5649c31 100644 --- a/Assets/Scripts/Missions/AbductionMission.cs +++ b/Assets/Scripts/Missions/AbductionMission.cs @@ -6,7 +6,7 @@ public class AbductionMission : Mission public Officer TargetOfficer { get; set; } /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public AbductionMission() : base() diff --git a/Assets/Scripts/Missions/DiplomacyMission.cs b/Assets/Scripts/Missions/DiplomacyMission.cs index 62fcd4f..99c2a36 100644 --- a/Assets/Scripts/Missions/DiplomacyMission.cs +++ b/Assets/Scripts/Missions/DiplomacyMission.cs @@ -4,7 +4,7 @@ public class DiplomacyMission : Mission { /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public DiplomacyMission() : base() diff --git a/Assets/Scripts/Missions/Mission.cs b/Assets/Scripts/Missions/Mission.cs index 2d4ca7c..8b813cb 100644 --- a/Assets/Scripts/Missions/Mission.cs +++ b/Assets/Scripts/Missions/Mission.cs @@ -17,6 +17,7 @@ public abstract class Mission : ContainerNode [PersistableIgnore] public List DecoyParticipants { get; set; } public MissionParticipantSkill ParticipantSkill { get; set; } + public bool HasInitiated = false; // Success Probability Variables [PersistableIgnore] @@ -59,7 +60,7 @@ public abstract class Mission : ContainerNode protected static readonly Random random = new Random(); /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// protected Mission() { } @@ -131,6 +132,8 @@ public void Initiate() // Then, set the max progress to a random value between the min and max ticks. CurrentProgress = 0; MaxProgress = random.Next(MinTicks, MaxTicks); + + HasInitiated = true; } /// @@ -419,7 +422,13 @@ public void Execute(Game game) /// public override IEnumerable GetChildren() { - return MainParticipants.Cast().Concat(DecoyParticipants.Cast()); + if (HasInitiated) + { + return MainParticipants.Cast().Concat(DecoyParticipants.Cast()); + } + + // Return an empty list. + return new List(); } /// diff --git a/Assets/Scripts/Missions/MissionFactory.cs b/Assets/Scripts/Missions/MissionFactory.cs new file mode 100644 index 0000000..043cd3c --- /dev/null +++ b/Assets/Scripts/Missions/MissionFactory.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; + +/// +/// +/// +public enum MissionType +{ + Diplomacy, + Recruitment, +} + +/// +/// Factory class responsible for creating and initializing missions. +/// +public class MissionFactory +{ + private readonly Game game; + + /// + /// Initializes a new instance of the class. + /// + /// The game instance for which the factory creates missions. + public MissionFactory(Game game) + { + this.game = game; + } + + /// + /// Creates a mission based on the specified mission type and parameters. + /// + /// The type of mission to create. + /// The Instance ID of the owner of the mission. + /// The main participants of the mission. + /// The decoy participants of the mission. + /// The target of the mission. + /// A new instance of the requested mission type. + public Mission CreateMission( + MissionType missionType, + string ownerInstanceId, + List mainParticipants, + List decoyParticipants, + ISceneNode target + ) + { + return missionType switch + { + MissionType.Diplomacy => new DiplomacyMission( + ownerInstanceId, + target.InstanceID, + mainParticipants, + decoyParticipants + ), + MissionType.Recruitment => new RecruitmentMission( + ownerInstanceId, + target.InstanceID, + mainParticipants, + decoyParticipants + ), + _ => throw new ArgumentException($"Unhandled mission type: {missionType}"), + }; + } + + /// + /// Creates and initiates a mission, attaching it to the game scene graph. + /// + /// The type of mission to create. + /// The Instance ID of the mission owner. + /// The main participants of the mission. + /// The decoy participants of the mission. + /// The target of the mission. + public Mission CreateAndInitiateMission( + MissionType missionType, + string ownerInstanceId, + List mainParticipants, + List decoyParticipants, + ISceneNode target + ) + { + if (mainParticipants.Count == 0) + { + throw new ArgumentException("Main participants list cannot be empty."); + } + + // Create the mission. + Mission mission = CreateMission( + missionType, + ownerInstanceId, + mainParticipants, + decoyParticipants, + target + ); + + // Determine the closest planet to the target for attaching the mission. + Planet closestPlanet = target is Planet ? (Planet)target : target.GetParentOfType(); + + // Attach the mission to the scene graph. + game.AttachNode(mission, closestPlanet); + + // Initiate the mission (e.g., set participants to InTransit). + mission.Initiate(); + + return mission; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public Mission CreateAndInitiateMission( + string missionTypeString, + string ownerInstanceId, + List mainParticipants, + List decoyParticipants, + ISceneNode target + ) + { + if (Enum.TryParse(missionTypeString, true, out MissionType missionType)) + { + return CreateAndInitiateMission( + missionType, + ownerInstanceId, + mainParticipants, + decoyParticipants, + target + ); + } + else + { + throw new ArgumentException($"Invalid mission type: {missionTypeString} ."); + } + } +} diff --git a/Assets/Scripts/Missions/RecruitmentMission.cs b/Assets/Scripts/Missions/RecruitmentMission.cs index 1affbe4..e7e338b 100644 --- a/Assets/Scripts/Missions/RecruitmentMission.cs +++ b/Assets/Scripts/Missions/RecruitmentMission.cs @@ -5,7 +5,7 @@ public class RecruitmentMission : Mission { /// - /// Default constructor used for serialization. + /// Default constructor used for deserialization. /// public RecruitmentMission() : base() @@ -56,25 +56,37 @@ protected override void OnSuccess(Game game) { Planet planet = GetParent() as Planet; - List unrecruitedOfficers = game.GetUnrecruitedOfficers(OwnerInstanceID); - Officer recruitedOfficer = unrecruitedOfficers.RandomElement(); - recruitedOfficer.OwnerInstanceID = OwnerInstanceID; + if (game.GetUnrecruitedOfficers(OwnerInstanceID).Count > 0) + { + List unrecruitedOfficers = game.GetUnrecruitedOfficers(OwnerInstanceID); + Officer recruitedOfficer = unrecruitedOfficers.RandomElement(); + recruitedOfficer.OwnerInstanceID = OwnerInstanceID; - game.RemoveUnrecruitedOfficer(recruitedOfficer); + game.RemoveUnrecruitedOfficer(recruitedOfficer); - // Attach the recruited officer to the planet. - game.AttachNode(recruitedOfficer, planet); + // Attach the recruited officer to the planet. + game.AttachNode(recruitedOfficer, planet); - GameLogger.Log( - "Recruited officer " - + recruitedOfficer.GetDisplayName() - + " to " - + planet.GetDisplayName() - + " by " - + MainParticipants[0].GetDisplayName() - ); + GameLogger.Log( + "Recruited officer " + + recruitedOfficer.GetDisplayName() + + " to " + + planet.GetDisplayName() + + " by " + + MainParticipants[0].GetDisplayName() + ); + } + else + { + // @TODO: No one left to recruit so abort the mission. + } } + /// + /// + /// + /// + /// public override bool CanContinue(Game game) { return game.GetUnrecruitedOfficers(OwnerInstanceID).Count > 0; diff --git a/Assets/Scripts/Util/Serialization/PersistableAttributeAttribute.cs b/Assets/Scripts/Util/Attributes/PersistableAttributeAttribute.cs similarity index 100% rename from Assets/Scripts/Util/Serialization/PersistableAttributeAttribute.cs rename to Assets/Scripts/Util/Attributes/PersistableAttributeAttribute.cs diff --git a/Assets/Scripts/Util/Serialization/PersistableIgnoreAttribute.cs b/Assets/Scripts/Util/Attributes/PersistableIgnoreAttribute.cs similarity index 100% rename from Assets/Scripts/Util/Serialization/PersistableIgnoreAttribute.cs rename to Assets/Scripts/Util/Attributes/PersistableIgnoreAttribute.cs diff --git a/Assets/Scripts/Util/Serialization/PersistableIncludeAttribute.cs b/Assets/Scripts/Util/Attributes/PersistableIncludeAttribute.cs similarity index 87% rename from Assets/Scripts/Util/Serialization/PersistableIncludeAttribute.cs rename to Assets/Scripts/Util/Attributes/PersistableIncludeAttribute.cs index 577fbd0..ad08113 100644 --- a/Assets/Scripts/Util/Serialization/PersistableIncludeAttribute.cs +++ b/Assets/Scripts/Util/Attributes/PersistableIncludeAttribute.cs @@ -20,6 +20,11 @@ public class PersistableIncludeAttribute : Attribute /// public Type PersistableType { get; } + /// + /// Initializes a new instance of the class. + /// + public PersistableIncludeAttribute() { } + /// /// Initializes a new instance of the class. /// diff --git a/Assets/Scripts/Util/Serialization/PersistableMemberAttribute.cs b/Assets/Scripts/Util/Attributes/PersistableMemberAttribute.cs similarity index 100% rename from Assets/Scripts/Util/Serialization/PersistableMemberAttribute.cs rename to Assets/Scripts/Util/Attributes/PersistableMemberAttribute.cs diff --git a/Assets/Scripts/Util/Serialization/PersistableObjectAttribute.cs b/Assets/Scripts/Util/Attributes/PersistableObjectAttribute.cs similarity index 100% rename from Assets/Scripts/Util/Serialization/PersistableObjectAttribute.cs rename to Assets/Scripts/Util/Attributes/PersistableObjectAttribute.cs diff --git a/Assets/Scripts/Util/Common/GameLogger.cs b/Assets/Scripts/Util/Common/GameLogger.cs index 1ec5b79..560aecd 100644 --- a/Assets/Scripts/Util/Common/GameLogger.cs +++ b/Assets/Scripts/Util/Common/GameLogger.cs @@ -138,7 +138,8 @@ private static void InitializeLogFile() { if (!File.Exists(logFilePath)) { - File.Create(logFilePath).Dispose(); // Create and immediately close the file. + // Create and immediately close the file. + File.Create(logFilePath).Dispose(); } WriteToFile($"Log initialized at {DateTime.Now}"); } diff --git a/Assets/Scripts/Util/Common/TypeHelper.cs b/Assets/Scripts/Util/Common/TypeHelper.cs index a29d299..0d49402 100644 --- a/Assets/Scripts/Util/Common/TypeHelper.cs +++ b/Assets/Scripts/Util/Common/TypeHelper.cs @@ -140,6 +140,8 @@ public static bool HasAttribute(Type type, Type attributeType) /// The converted primitive value. public static object ConvertToPrimitive(string content, Type targetType) { + if (targetType.IsEnum) + return Enum.Parse(targetType, content); if (targetType == typeof(string)) return content; if (targetType == typeof(int)) diff --git a/Assets/Scripts/Util/Serialization/GameSerializer.cs b/Assets/Scripts/Util/Serialization/GameSerializer.cs index 64094b2..a5dcbc6 100644 --- a/Assets/Scripts/Util/Serialization/GameSerializer.cs +++ b/Assets/Scripts/Util/Serialization/GameSerializer.cs @@ -255,7 +255,6 @@ private static void WritePersistable(object obj, XmlWriter writer, string key = objType, ReflectionHelper.OperationType.Write ); - writer.WriteStartElement(key ?? objType.Name); foreach (MemberInfo attribute in attributes) @@ -541,22 +540,23 @@ private static object ReadPersistable( IDictionary attributes = ReflectionHelper.GetPersistableAttributeMap( actualType, - ReflectionHelper.OperationType.Write + ReflectionHelper.OperationType.Read ); + IEnumerable members = ReflectionHelper.GetPersistableMembers( actualType, ReflectionHelper.OperationType.Read ); - if (reader.IsEmptyElement) + if (reader.HasAttributes) { - reader.Read(); - return obj; + ReadAttributes(reader, attributes, obj); } - if (reader.HasAttributes) + if (reader.IsEmptyElement) { - ReadAttributes(reader, attributes, obj); + reader.Read(); + return obj; } reader.ReadStartElement(); @@ -565,7 +565,9 @@ private static object ReadPersistable( { if (reader.NodeType == XmlNodeType.Element) { + string elementName = reader.Name; MemberInfo member = FindMemberForElement(members, reader.Name); + if (member != null) { object value = ReadMember(member, reader); @@ -573,7 +575,9 @@ private static object ReadPersistable( } else { - reader.Skip(); + throw new InvalidOperationException( + $"Unknown element '{elementName}' encountered while deserializing {objType.Name}." + ); } } else @@ -638,10 +642,12 @@ object obj reader.MoveToAttribute(i); if (attributes.TryGetValue(reader.Name, out MemberInfo attribute)) { - object value = ReadValue(ReflectionHelper.GetMemberType(attribute), reader); + Type attributeType = ReflectionHelper.GetMemberType(attribute); + object value = TypeHelper.ConvertToPrimitive(reader.Value, attributeType); ReflectionHelper.SetMemberValue(attribute, obj, value); } } + reader.MoveToElement(); } /// @@ -659,6 +665,7 @@ string elementName { PersistableIncludeAttribute[] typeAttributes = (PersistableIncludeAttribute[]) Attribute.GetCustomAttributes(member, typeof(PersistableIncludeAttribute)); + foreach (PersistableIncludeAttribute typeAttr in typeAttributes) { if (typeAttr.PersistableType.Name == elementName) @@ -748,7 +755,6 @@ public static IEnumerable GetPersistableMembers( OperationType operationType ) { - // Determine if a member is persistable based on the operation type bool IsPersistable(MemberInfo member) { bool isPublicField = member is FieldInfo field && field.IsPublic; @@ -775,20 +781,17 @@ bool IsPersistable(MemberInfo member) && !hasAlternativePersistableAttribute; } - // Retrieve persistable fields. IEnumerable fields = classType .GetFields(CommonBindingFlags) .Where(IsPersistable) .Cast(); - // Retrieve persistable properties. IEnumerable properties = classType .GetProperties(CommonBindingFlags) .Where(property => property.CanRead && property.CanWrite) .Where(IsPersistable) .Cast(); - // Combine and return the result return fields.Concat(properties); } @@ -803,7 +806,6 @@ public static IEnumerable GetPersistableAttributes( OperationType operationType ) { - // Determine if a member is a persistable attribute based on the operation type bool IsPersistableAttribute(MemberInfo member) { bool hasPersistableAttribute = member @@ -821,20 +823,17 @@ bool IsPersistableAttribute(MemberInfo member) return hasPersistableAttribute; } - // Retrieve persistable fields. IEnumerable fields = classType .GetFields(CommonBindingFlags) .Where(IsPersistableAttribute) .Cast(); - // Retrieve persistable properties. IEnumerable properties = classType .GetProperties(CommonBindingFlags) .Where(property => property.CanRead && property.CanWrite) .Where(IsPersistableAttribute) .Cast(); - // Combine and return the result return fields.Concat(properties); } @@ -871,18 +870,27 @@ public static IDictionary GetPersistableAttributeMap( OperationType operationType ) { - return GetPersistableAttributes(classType, operationType) - .ToDictionary( - member => - ( - (PersistableAttributeAttribute) - Attribute.GetCustomAttribute( - member, - typeof(PersistableAttributeAttribute) - ) - )?.Name ?? member.Name, - member => member - ); + Dictionary attributeMap = new Dictionary(); + Type currentType = classType; + + while (currentType != null) + { + foreach (MemberInfo member in GetPersistableAttributes(currentType, operationType)) + { + PersistableAttributeAttribute attr = (PersistableAttributeAttribute) + Attribute.GetCustomAttribute(member, typeof(PersistableAttributeAttribute)); + string key = attr?.Name ?? member.Name; + + if (!attributeMap.ContainsKey(key)) + { + attributeMap[key] = member; + } + } + + currentType = currentType.BaseType; + } + + return attributeMap; } /// @@ -933,7 +941,9 @@ public static object GetMemberValue(MemberInfo member, object obj) public static void SetMemberValue(MemberInfo member, object obj, object value) { if (value == null) + { return; + } switch (member) { diff --git a/Assets/Tests/EditMode/Game/GameTests.cs b/Assets/Tests/EditMode/Game/GameTests.cs index 10914a3..8c5e3ee 100644 --- a/Assets/Tests/EditMode/Game/GameTests.cs +++ b/Assets/Tests/EditMode/Game/GameTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -5,206 +6,434 @@ [TestFixture] public class GameTests { - private List factions = new List - { - new Faction { InstanceID = "FNALL1" }, - new Faction { InstanceID = "FNEMP1" }, - }; + private Game game; + private GameSummary summary; + private Faction faction1; + private Faction faction2; + private GalaxyMap galaxyMap; + private PlanetSystem planetSystem; + private Planet planet; + private Fleet fleet; - [Test] - public void InitializeGame_WithSummary_SetsPropertiesCorrectly() + [SetUp] + public void SetUp() { - GameSummary summary = new GameSummary + // Initialize game summary. + summary = new GameSummary { - GalaxySize = GameSize.Large, - Difficulty = GameDifficulty.Easy, - VictoryCondition = GameVictoryCondition.Headquarters, - ResourceAvailability = GameResourceAvailability.Abundant, - PlayerFactionID = "FNALL1", + GalaxySize = GameSize.Medium, + Difficulty = GameDifficulty.Medium, + VictoryCondition = GameVictoryCondition.Conquest, + ResourceAvailability = GameResourceAvailability.Normal, + PlayerFactionID = "FACTION1", }; - Game game = new Game(summary) - { - Summary = summary, - Factions = factions, - Galaxy = new GalaxyMap(), - }; + // Create factions. + faction1 = new Faction { InstanceID = "FACTION1", DisplayName = "Alliance" }; + faction2 = new Faction { InstanceID = "FACTION2", DisplayName = "Empire" }; - Assert.IsNotNull(game.Summary, "Game summary should not be null."); - Assert.AreEqual(GameSize.Large, game.Summary.GalaxySize, "GalaxySize should match."); - Assert.AreEqual(GameDifficulty.Easy, game.Summary.Difficulty, "Difficulty should match."); - Assert.AreEqual( - GameVictoryCondition.Headquarters, - game.Summary.VictoryCondition, - "VictoryCondition should match." - ); - Assert.AreEqual( - GameResourceAvailability.Abundant, - game.Summary.ResourceAvailability, - "ResourceAvailability should match." + // Create game objects. + galaxyMap = new GalaxyMap(); + planetSystem = new PlanetSystem { InstanceID = "SYSTEM1" }; + planet = new Planet { InstanceID = "PLANET1", OwnerInstanceID = "FACTION1" }; + fleet = new Fleet { InstanceID = "FLEET1", OwnerInstanceID = "FACTION1" }; + + // Initialize the game. + game = new Game(summary); + game.Factions.Add(faction1); + game.Factions.Add(faction2); + } + + [Test] + public void Constructor_WithSummary_InitializesCorrectly() + { + // Verify game initialization. + Assert.AreEqual(summary, game.Summary, "Game summary should match the provided summary"); + Assert.IsNotNull(game.Galaxy, "Galaxy should be initialized"); + Assert.AreEqual(0, game.CurrentTick, "Current tick should be initialized to 0"); + Assert.IsEmpty(game.EventPool, "Event pool should be empty initially"); + Assert.IsEmpty(game.CompletedEventIDs, "Completed event IDs should be empty initially"); + } + + [Test] + public void GetFactions_ReturnsCorrectFactions() + { + // Get factions and verify the count and contents. + List factions = game.GetFactions(); + Assert.AreEqual(2, factions.Count, "Should return two factions"); + Assert.Contains(faction1, factions, "Should contain faction1"); + Assert.Contains(faction2, factions, "Should contain faction2"); + } + + [Test] + public void GetFactionByOwnerInstanceID_ReturnsCorrectFaction() + { + // Get faction by ID and verify. + Faction retrievedFaction = game.GetFactionByOwnerInstanceID("FACTION1"); + Assert.AreEqual(faction1, retrievedFaction, "Should return the correct faction"); + } + + [Test] + public void GetFactionByOwnerInstanceID_ThrowsException_WhenFactionNotFound() + { + // Attempt to get non-existent faction. + Assert.Throws( + () => game.GetFactionByOwnerInstanceID("NONEXISTENT"), + "Should throw exception for non-existent faction" ); - Assert.AreEqual("FNALL1", game.Summary.PlayerFactionID, "PlayerFactionID should match."); - Assert.IsNotNull(game.Galaxy, "Galaxy should not be null."); } [Test] - public void AttachNode_ToParent_AddsNodeCorrectly() + public void GetGalaxyMap_ReturnsCorrectGalaxyMap() { - PlanetSystem planetSystem = new PlanetSystem(); - GalaxyMap galaxy = new GalaxyMap - { - PlanetSystems = new List { planetSystem }, - }; - Game game = new Game { Factions = factions, Galaxy = galaxy }; - Planet planet = new Planet { OwnerInstanceID = "FNALL1" }; + // Get galaxy map and verify. + GalaxyMap retrievedGalaxyMap = game.GetGalaxyMap(); + Assert.AreEqual(game.Galaxy, retrievedGalaxyMap, "Should return the correct galaxy map"); + } + [Test] + public void AttachNode_AddsNodeCorrectly() + { + // Attach node and verify. game.AttachNode(planet, planetSystem); + Assert.AreEqual( + planetSystem, + planet.GetParent(), + "Planet should have planetSystem as parent" + ); Assert.Contains( planet, planetSystem.GetChildren().ToList(), - "Planet should be attached to the PlanetSystem." + "PlanetSystem should contain planet as child" + ); + Assert.IsTrue( + game.NodesByInstanceID.ContainsKey(planet.InstanceID), + "Game should contain planet in NodesByInstanceID" + ); + Assert.IsTrue( + game.GetFactionByOwnerInstanceID(planet.OwnerInstanceID) + .GetAllOwnedUnits() + .Contains(planet), + "Faction should contain planet in owned units" ); } [Test] - public void DetachNode_FromParent_RemovesNodeCorrectly() + public void AttachNode_ThrowsException_WhenNodeAlreadyHasParent() { - PlanetSystem planetSystem = new PlanetSystem(); - GalaxyMap galaxy = new GalaxyMap - { - PlanetSystems = new List { planetSystem }, - }; - Game game = new Game { Factions = factions, Galaxy = galaxy }; - Planet planet = new Planet { OwnerInstanceID = "FNALL1" }; + // Attach node to a parent. + game.AttachNode(planet, planetSystem); + + // Attempt to attach the same node to another parent. + Assert.Throws( + () => game.AttachNode(planet, new PlanetSystem()), + "Should throw exception when attaching a node that already has a parent" + ); + } + [Test] + public void DetachNode_RemovesNodeCorrectly() + { + // Attach and then detach node. game.AttachNode(planet, planetSystem); game.DetachNode(planet); + // Verify detachment is successful and node is removed from all relevant structures. + Assert.IsNull(planet.GetParent(), "Planet should have no parent after detachment"); Assert.IsFalse( planetSystem.GetChildren().Contains(planet), - "Planet should be detached from the PlanetSystem." + "PlanetSystem should not contain planet as child" + ); + Assert.IsFalse( + game.NodesByInstanceID.ContainsKey(planet.InstanceID), + "Game should not contain planet in NodesByInstanceID" + ); + Assert.IsFalse( + game.GetFactionByOwnerInstanceID(planet.OwnerInstanceID) + .GetAllOwnedUnits() + .Contains(planet), + "Faction should not contain planet in owned units" + ); + } + + [Test] + public void DetachNode_ThrowsException_WhenNodeHasNoParent() + { + // Attempt to detach a node with no parent. + Assert.Throws( + () => game.DetachNode(planet), + "Should throw exception when detaching a node with no parent" ); - Assert.IsNull(planet.GetParent(), "Planet should not have a parent after being detached."); } [Test] - public void AttachNode_WithExistingParent_ThrowsException() + public void AddSceneNodeByInstanceID_AddsNodeCorrectly() { - Game game = new Game { Factions = factions }; - Planet planet1 = new Planet { OwnerInstanceID = "FNALL1" }; - Planet planet2 = new Planet { OwnerInstanceID = "FNALL1" }; - Fleet fleet = new Fleet { OwnerInstanceID = "FNALL1" }; + // Add node and verify that it is added to the game. + game.AddSceneNodeByInstanceID(planet); + Assert.IsTrue( + game.NodesByInstanceID.ContainsKey(planet.InstanceID), + "Game should contain planet in NodesByInstanceID" + ); + } - game.AttachNode(fleet, planet1); + [Test] + public void AddSceneNodeByInstanceID_ThrowsException_WhenDuplicateNodeAdded() + { + // Add node to the game. + game.AddSceneNodeByInstanceID(planet); - Assert.Throws( - () => game.AttachNode(fleet, planet2), - "Exception should be thrown when attaching a node with a parent." + // Attempt to add the same node again. + Assert.Throws( + () => game.AddSceneNodeByInstanceID(planet), + "Should throw exception when adding a duplicate node" ); } [Test] - public void DetachNode_WithoutParent_ThrowsException() + public void RemoveSceneNodeByInstanceID_RemovesNodeCorrectly() { - Game game = new Game(); - Fleet fleet = new Fleet { OwnerInstanceID = "FNALL1" }; + // Add and then remove node from the game. + game.AddSceneNodeByInstanceID(planet); + game.RemoveSceneNodeByInstanceID(planet); - Assert.Throws( - () => game.DetachNode(fleet), - "Exception should be thrown when detaching a node without a parent." + // Verify removal from all relevant structures. + Assert.IsFalse( + game.NodesByInstanceID.ContainsKey(planet.InstanceID), + "Game should not contain planet in NodesByInstanceID after removal" ); } [Test] - public void DetachNodeByInstanceID_RegistersAndDeregistersCorrectly() + public void GetSceneNodeByInstanceID_ReturnsCorrectNode() { - Planet planet = new Planet { OwnerInstanceID = "FNALL1" }; - PlanetSystem planetSystem = new PlanetSystem { Planets = new List { planet } }; - GalaxyMap galaxy = new GalaxyMap(); + // Add node and retrieve it. + game.AddSceneNodeByInstanceID(planet); + Planet retrievedNode = game.GetSceneNodeByInstanceID(planet.InstanceID); - Game game = new Game - { - Factions = new List { new Faction { InstanceID = "FNALL1" } }, - Galaxy = galaxy, - }; + // Verify retrieval of the correct node. + Assert.AreEqual(planet, retrievedNode, "Should return the correct node"); + } + + [Test] + public void GetSceneNodeByInstanceID_ReturnsNull_WhenNodeNotFound() + { + // Attempt to retrieve non-existent node. + Planet retrievedNode = game.GetSceneNodeByInstanceID("NONEXISTENT"); + Assert.IsNull(retrievedNode, "Should return null for non-existent node"); + } - game.AttachNode(planetSystem, game.Galaxy); + [Test] + public void GetSceneNodesByInstanceIDs_ReturnsCorrectNodes() + { + // Add nodes to the game. + game.AddSceneNodeByInstanceID(planet); + game.AddSceneNodeByInstanceID(fleet); - Assert.AreEqual( - game.GetSceneNodeByInstanceID(planetSystem.InstanceID), - planetSystem + // Retrieve nodes by IDs. + List retrievedNodes = game.GetSceneNodesByInstanceIDs( + new List { planet.InstanceID, fleet.InstanceID } ); - Assert.AreEqual(game.GetSceneNodeByInstanceID(planet.InstanceID), planet); - game.DetachNode(planetSystem); + // Verify retrieval of planets and fleets. + Assert.AreEqual(2, retrievedNodes.Count, "Should return two nodes"); + Assert.Contains(planet, retrievedNodes, "Should contain planet"); + Assert.Contains(fleet, retrievedNodes, "Should contain fleet"); + } - Assert.IsNull(game.GetSceneNodeByInstanceID(planetSystem.InstanceID)); - Assert.IsNull(game.GetSceneNodeByInstanceID(planet.InstanceID)); + [Test] + public void GetSceneNodesByOwnerInstanceID_ReturnsCorrectNodes() + { + // Add nodes to the game. + game.AddSceneNodeByInstanceID(planet); + game.AddSceneNodeByInstanceID(fleet); + + // Retrieve nodes by owner ID. + List retrievedNodes = game.GetSceneNodesByOwnerInstanceID( + "FACTION1" + ); + + // Verify retrieval of planets and fleets. + Assert.AreEqual(2, retrievedNodes.Count, "Should return two nodes"); + Assert.Contains(planet, retrievedNodes, "Should contain planet"); + Assert.Contains(fleet, retrievedNodes, "Should contain fleet"); } [Test] - public void SerializeAndDeserialize_GameObject_ReturnsEquivalentObject() + public void GetSceneNodesByType_ReturnsCorrectNodes() { - // Arrange: Create a game object with some data - Game game = new Game - { - Summary = new GameSummary - { - GalaxySize = GameSize.Medium, - Difficulty = GameDifficulty.Medium, - VictoryCondition = GameVictoryCondition.Headquarters, - ResourceAvailability = GameResourceAvailability.Limited, - PlayerFactionID = "FNALL1", - }, - Factions = new List - { - new Faction { InstanceID = "FNALL1", DisplayName = "Alliance" }, - new Faction { InstanceID = "FNEMP1", DisplayName = "Empire" }, - }, - UnrecruitedOfficers = new List - { - new Officer { InstanceID = "OFC001", DisplayName = "Luke Skywalker" }, - new Officer { InstanceID = "OFC002", DisplayName = "Darth Vader" }, - }, - }; + // Set up galaxy structure. + game.Galaxy = galaxyMap; + game.AttachNode(planetSystem, galaxyMap); + game.AttachNode(planet, planetSystem); + game.AttachNode(fleet, planet); - // Act: Serialize and deserialize the object - string serializedGame = SerializationHelper.Serialize(game); - Game deserializedGame = SerializationHelper.Deserialize(serializedGame); + // Retrieve planets and verify the count and contents. + List retrievedPlanets = game.GetSceneNodesByType(); + Assert.AreEqual(1, retrievedPlanets.Count, "Should return one planet"); + Assert.Contains(planet, retrievedPlanets, "Should contain the specific planet"); + + // Retrieve fleets and verify the count and contents. + List retrievedFleets = game.GetSceneNodesByType(); + Assert.AreEqual(1, retrievedFleets.Count, "Should return one fleet"); + Assert.Contains(fleet, retrievedFleets, "Should contain the specific fleet"); + } + + [Test] + public void RegisterOwnedUnit_AddsUnitToFaction() + { + // Register unit. + game.RegisterOwnedUnit(planet); + + // Verify registration is successful. + Assert.IsTrue( + game.GetFactionByOwnerInstanceID(planet.OwnerInstanceID) + .GetAllOwnedUnits() + .Contains(planet), + "Faction should contain planet in owned units after registration" + ); + } + + [Test] + public void DeregsiterOwnedUnit_RemovesUnitFromFaction() + { + // Register and then deregister unit. + game.RegisterOwnedUnit(planet); + game.DeregsiterOwnedUnit(planet); + + // Verify deregistration was successful. + Assert.IsFalse( + game.GetFactionByOwnerInstanceID(planet.OwnerInstanceID) + .GetAllOwnedUnits() + .Contains(planet), + "Faction should not contain planet in owned units after deregistration" + ); + } + + [Test] + public void GetEventPool_ReturnsCorrectEventPool() + { + // Add events to pool. + GameEvent event1 = new GameEvent { InstanceID = "EVENT1" }; + GameEvent event2 = new GameEvent { InstanceID = "EVENT2" }; + game.EventPool.Add(event1); + game.EventPool.Add(event2); + + // Retrieve event pool and verify the count and contents. + List eventPool = game.GetEventPool(); + Assert.AreEqual(2, eventPool.Count, "Should return two events"); + Assert.Contains(event1, eventPool, "Should contain event1"); + Assert.Contains(event2, eventPool, "Should contain event2"); + } + + [Test] + public void RemoveEvent_RemovesEventCorrectly() + { + // Add event and then remove it. + GameEvent event1 = new GameEvent { InstanceID = "EVENT1" }; + game.EventPool.Add(event1); + game.RemoveEvent(event1); + + // Verify removal from the event pool. + Assert.IsFalse( + game.EventPool.Contains(event1), + "Event pool should not contain the removed event" + ); + } + + [Test] + public void GetEventByInstanceID_ReturnsCorrectEvent() + { + // Add event and retrieve it. + GameEvent event1 = new GameEvent { InstanceID = "EVENT1" }; + game.EventPool.Add(event1); + + // Retrieve event and verify. + GameEvent retrievedEvent = game.GetEventByInstanceID("EVENT1"); + Assert.AreEqual(event1, retrievedEvent, "Should return the correct event"); + } + + [Test] + public void AddCompletedEvent_AddsEventToCompletedList() + { + // Add completed event to the completed list. + GameEvent event1 = new GameEvent { InstanceID = "EVENT1" }; + game.AddCompletedEvent(event1); + + // Verify addition to the completed list. + Assert.IsTrue( + game.CompletedEventIDs.Contains(event1.InstanceID), + "Completed event IDs should contain the added event's ID" + ); + } + + [Test] + public void IsEventComplete_ReturnsCorrectStatus() + { + // Add completed event to the completed list. + GameEvent event1 = new GameEvent { InstanceID = "EVENT1" }; + game.AddCompletedEvent(event1); + + // Check completion status. + Assert.IsTrue(game.IsEventComplete("EVENT1"), "EVENT1 should be marked as complete"); + Assert.IsFalse(game.IsEventComplete("EVENT2"), "EVENT2 should not be marked as complete"); + } + + [Test] + public void Galaxy_Setter_InitializesGalaxyCorrectly() + { + // Set up galaxy structure. + game.Galaxy = galaxyMap; + game.AttachNode(planetSystem, galaxyMap); + game.AttachNode(planet, planetSystem); + game.AttachNode(fleet, planetSystem); + + // Verify galaxy initialization. + Assert.IsTrue( + game.NodesByInstanceID.ContainsKey(galaxyMap.InstanceID), + "Game should contain galaxyMap in NodesByInstanceID" + ); + Assert.IsTrue( + game.NodesByInstanceID.ContainsKey(planetSystem.InstanceID), + "Game should contain planetSystem in NodesByInstanceID" + ); + Assert.IsTrue( + game.NodesByInstanceID.ContainsKey(planet.InstanceID), + "Game should contain planet in NodesByInstanceID" + ); + Assert.IsTrue( + game.NodesByInstanceID.ContainsKey(fleet.InstanceID), + "Game should contain fleet in NodesByInstanceID" + ); - // Assert: Verify the deserialized object matches the original - Assert.IsNotNull(deserializedGame); - Assert.AreEqual(game.Summary.GalaxySize, deserializedGame.Summary.GalaxySize); - Assert.AreEqual(game.Summary.Difficulty, deserializedGame.Summary.Difficulty); - Assert.AreEqual(game.Summary.VictoryCondition, deserializedGame.Summary.VictoryCondition); Assert.AreEqual( - game.Summary.ResourceAvailability, - deserializedGame.Summary.ResourceAvailability + galaxyMap, + planetSystem.GetParent(), + "PlanetSystem should have galaxyMap as parent" + ); + Assert.AreEqual( + planetSystem, + planet.GetParent(), + "Planet should have planetSystem as parent" + ); + Assert.AreEqual( + planetSystem, + fleet.GetParent(), + "Fleet should have planetSystem as parent" ); - Assert.AreEqual(game.Summary.PlayerFactionID, deserializedGame.Summary.PlayerFactionID); - Assert.AreEqual(game.Factions.Count, deserializedGame.Factions.Count); - for (int i = 0; i < game.Factions.Count; i++) - { - Assert.AreEqual(game.Factions[i].InstanceID, deserializedGame.Factions[i].InstanceID); - Assert.AreEqual( - game.Factions[i].GetDisplayName(), - deserializedGame.Factions[i].GetDisplayName() - ); - } - - Assert.AreEqual(game.UnrecruitedOfficers.Count, deserializedGame.UnrecruitedOfficers.Count); - for (int i = 0; i < game.UnrecruitedOfficers.Count; i++) - { - Assert.AreEqual( - game.UnrecruitedOfficers[i].InstanceID, - deserializedGame.UnrecruitedOfficers[i].InstanceID - ); - Assert.AreEqual( - game.UnrecruitedOfficers[i].GetDisplayName(), - deserializedGame.UnrecruitedOfficers[i].GetDisplayName() - ); - } + Assert.IsTrue( + game.GetFactionByOwnerInstanceID(planet.OwnerInstanceID) + .GetAllOwnedUnits() + .Contains(planet), + "Faction should contain planet in owned units" + ); + Assert.IsTrue( + game.GetFactionByOwnerInstanceID(fleet.OwnerInstanceID) + .GetAllOwnedUnits() + .Contains(fleet), + "Faction should contain fleet in owned units" + ); } } diff --git a/Assets/Tests/EditMode/Game/PlanetTests.cs b/Assets/Tests/EditMode/Game/PlanetTests.cs index 14df637..97ce93a 100644 --- a/Assets/Tests/EditMode/Game/PlanetTests.cs +++ b/Assets/Tests/EditMode/Game/PlanetTests.cs @@ -215,7 +215,7 @@ public void AddToManufacturingQueue_UnitWithoutParent_ThrowsException() { IManufacturable unit = new Starfighter(); - Assert.Throws( + Assert.Throws( () => planet.AddToManufacturingQueue(unit), "Adding a manufacturable unit without a parent should throw a GameStateException." ); diff --git a/Assets/Tests/EditMode/Generation/GameBuilderTests.cs b/Assets/Tests/EditMode/Generation/GameBuilderTests.cs index b5c6f2b..1e7d060 100644 --- a/Assets/Tests/EditMode/Generation/GameBuilderTests.cs +++ b/Assets/Tests/EditMode/Generation/GameBuilderTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,57 +8,172 @@ [TestFixture] public class GameBuilderTests { - private Game game; + private static readonly Lazy LazyGameTestCases = new Lazy( + () => + new[] + { + CreateGame(GameSize.Small, GameDifficulty.Medium, GameVictoryCondition.Conquest), + CreateGame(GameSize.Medium, GameDifficulty.Medium, GameVictoryCondition.Conquest), + CreateGame(GameSize.Large, GameDifficulty.Medium, GameVictoryCondition.Conquest), + } + ); - [OneTimeSetUp] - public void OneTimeSetUp() + private static Game[] GameTestCases => LazyGameTestCases.Value; + + private static Game CreateGame( + GameSize size, + GameDifficulty difficulty, + GameVictoryCondition victoryCondition + ) { - // Create a new GameSummary object with specific configurations + // Create a new GameSummary object with specific configurations. GameSummary summary = new GameSummary { - GalaxySize = GameSize.Large, - Difficulty = GameDifficulty.Easy, - VictoryCondition = GameVictoryCondition.Headquarters, - ResourceAvailability = GameResourceAvailability.Abundant, + GalaxySize = size, + Difficulty = difficulty, + VictoryCondition = victoryCondition, + ResourceAvailability = GameResourceAvailability.Normal, PlayerFactionID = "FNALL1", }; - // Create a new GameBuilder instance with the summary + // Create a new GameBuilder instance with the summary. GameBuilder builder = new GameBuilder(summary); - // Build the game using the GameBuilder - game = builder.BuildGame(); + // Build the game using the GameBuilder. + return builder.BuildGame(); + } + + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_SetsConsistentOwners(Game game) + { + // Traverse the galaxy map to find planets. + game.Galaxy.Traverse(node => + { + // Skip nodes without an owner. + if (node.GetOwnerInstanceID() == null) + { + return; + } + + List children = node.GetChildren().ToList(); + + // Ensure each child has the same owner as its parent. + foreach (ISceneNode child in children) + { + Assert.AreEqual( + node.GetOwnerInstanceID(), + child.GetOwnerInstanceID(), + $"Child \"{child.GetDisplayName()}\" should have the same owner as its parent, \"{node.GetDisplayName()}\"." + ); + } + }); + } + + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_SetsChildParentRelationships(Game game) + { + game.Galaxy.Traverse(node => + { + List children = node.GetChildren().ToList(); + + foreach (ISceneNode child in children) + { + // Ensure the child has the parent as its parent. + Assert.AreEqual( + node, + child.GetParent(), + "Child should have the parent as its parent." + ); + } + }); } - [Test] - public void BuildGame_CreatesGameSummary() + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_SetsGameSummary(Game game) { Assert.IsNotNull(game, "Game should not be null."); + Assert.IsNotNull(game.Summary, "Game summary should not be null."); - // Assert that the game's summary properties match the provided configurations - Assert.AreEqual(GameSize.Large, game.Summary.GalaxySize, "GalaxySize should match."); - Assert.AreEqual(GameDifficulty.Easy, game.Summary.Difficulty, "Difficulty should match."); - Assert.AreEqual( - GameVictoryCondition.Headquarters, - game.Summary.VictoryCondition, - "VictoryCondition should match." + // Check that the game's summary properties are within expected ranges. + Assert.IsTrue( + Enum.IsDefined(typeof(GameSize), game.Summary.GalaxySize), + "GalaxySize should be a valid enum value." + ); + Assert.IsTrue( + Enum.IsDefined(typeof(GameDifficulty), game.Summary.Difficulty), + "Difficulty should be a valid enum value." + ); + Assert.IsTrue( + Enum.IsDefined(typeof(GameVictoryCondition), game.Summary.VictoryCondition), + "VictoryCondition should be a valid enum value." ); - Assert.AreEqual( - GameResourceAvailability.Abundant, - game.Summary.ResourceAvailability, - "ResourceAvailability should match." + Assert.IsTrue( + Enum.IsDefined(typeof(GameResourceAvailability), game.Summary.ResourceAvailability), + "ResourceAvailability should be a valid enum value." ); - Assert.AreEqual("FNALL1", game.Summary.PlayerFactionID, "PlayerFactionID should match."); + + // Check that PlayerFactionID is not null or empty. + Assert.IsFalse( + string.IsNullOrEmpty(game.Summary.PlayerFactionID), + "PlayerFactionID should not be null or empty." + ); + } + + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_SetsFactions(Game game) + { + Assert.IsNotNull(game.Factions, "Factions should not be null."); + + // Ensure the game has at least two factions. + Assert.GreaterOrEqual(game.Factions.Count, 2, "Game should have at least two factions."); } - [Test] - public void BuildGame_SetsHQs() + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_SetsFactionTechnologies(Game game) + { + foreach (Faction faction in game.Factions) + { + // Ensure the faction has technology levels. + Assert.IsNotEmpty(faction.TechnologyLevels, "Faction should have technology levels."); + + // Ensure the faction has at least one technology level for each manufacturing type. + foreach ( + KeyValuePair< + ManufacturingType, + SortedDictionary> + > manufacturingTypeTechLevels in faction.TechnologyLevels + ) + { + ManufacturingType manufacturingType = manufacturingTypeTechLevels.Key; + SortedDictionary> techLevels = + manufacturingTypeTechLevels.Value; + + Assert.IsNotEmpty( + techLevels, + $"Faction should have technology levels for {manufacturingType}." + ); + + foreach (KeyValuePair> levelTechnologies in techLevels) + { + int level = levelTechnologies.Key; + List technologies = levelTechnologies.Value; + + Assert.IsNotEmpty( + technologies, + $"Faction should have at least one technology for {manufacturingType} at level {level}." + ); + } + } + } + } + + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_SetsHQs(Game game) { // Assert that the game's factions and galaxy map are not null. Assert.IsNotNull(game.Factions, "Factions should not be null."); Assert.IsNotNull(game.Galaxy, "GalaxyMap should not be null."); - // Iterate through each faction in the game. foreach (Faction faction in game.Factions) { // Check if the faction has a headquarters on any planet in the galaxy map. @@ -68,12 +184,12 @@ public void BuildGame_SetsHQs() ); // Assert that the faction has a headquarters - Assert.IsTrue(hasHQ, $"Faction {faction.InstanceID} should have a headquarters."); + Assert.IsTrue(hasHQ, $"Faction {faction.GetDisplayName()} should have a headquarters."); } } - [Test] - public void BuildGame_AssignsFactionsPlanets() + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_AssignsFactionsPlanets(Game game) { Dictionary> factionPlanets = new Dictionary>(); @@ -102,59 +218,44 @@ public void BuildGame_AssignsFactionsPlanets() } } - [Test] - public void BuildGame_SetsCorrectOwners() + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_DeploysOfficers(Game game) { - // Traverse the galaxy map to find planets. + List officers = new List(); + + // Traverse the galaxy map to find officers. game.Galaxy.Traverse(node => { - // Skip nodes without an owner. - if (node.GetOwnerInstanceID() == null) - { - return; - } - - List children = node.GetChildren().ToList(); - - // Ensure each child has the same owner as its parent. - foreach (ISceneNode child in children) + if (node is Officer officer) { - Assert.AreEqual( - node.GetOwnerInstanceID(), - child.GetOwnerInstanceID(), - "Child should have the same owner as its parent." - ); + officers.Add(officer); } }); + + // Ensure the game has at least two officers. + Assert.Greater(officers.Count, 2, "Game should have at least two officers."); } - [Test] - public void BuildGame_DeploysOfficers() + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_InitializesOfficers(Game game) { - List officers = new List(); - // Traverse the galaxy map to find officers. game.Galaxy.Traverse(node => { if (node is Officer officer) { - officers.Add(officer); - // Ensure at least one skill is non-zero. bool hasNonZeroSkill = officer.Skills.Values.Any(skillValue => skillValue > 0); Assert.IsTrue( hasNonZeroSkill, - $"Officer {officer.InstanceID} should have at least one non-zero skill." + $"Officer {officer.GetDisplayName()} should have at least one non-zero skill." ); } }); - - // Ensure the game has at least two officers. - Assert.Greater(officers.Count, 2, "Game should have at least two officers."); } - [Test] - public void BuildGame_DeploysFleets() + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_DeploysFleets(Game game) { Dictionary fleetsPerFaction = new Dictionary(); @@ -175,18 +276,18 @@ public void BuildGame_DeploysFleets() } }); - foreach (var factionID in game.Factions) + foreach (var faction in game.Factions) { // Ensure the faction has at least one fleet. Assert.IsTrue( - fleetsPerFaction.ContainsKey(factionID.InstanceID), - $"Faction {factionID.InstanceID} should have at least one fleet." + fleetsPerFaction.ContainsKey(faction.GetInstanceID()), + $"Faction {faction.GetDisplayName()} should have at least one fleet." ); } } - [Test] - public void BuildGame_DeploysMaxOneFleet() + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_DeploysMaxOneFleet(Game game) { // Traverse the galaxy map to find planets. game.Galaxy.Traverse(node => @@ -197,14 +298,14 @@ public void BuildGame_DeploysMaxOneFleet() Assert.LessOrEqual( planet.GetFleets().Count(), 1, - $"Planet {planet.InstanceID} should have at most one fleet." + $"Planet {planet.GetDisplayName()} should have at most one fleet." ); } }); } - [Test] - public void BuildGame_DeploysCapitalShips() + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_DeploysCapitalShips(Game game) { // Traverse the galaxy map to find fleets. game.Galaxy.Traverse(node => @@ -212,9 +313,7 @@ public void BuildGame_DeploysCapitalShips() if (node is Fleet fleet) { bool hasCapitalShips = fleet.GetChildren().Count() > 0; - GameLogger.Log( - $"Fleet {fleet.InstanceID} has {fleet.GetChildren().Count()} capital ships. Parent {fleet.GetParent().GetDisplayName()}" - ); + // Ensure the fleet has at least one capital ship. Assert.IsTrue( hasCapitalShips, @@ -223,4 +322,15 @@ public void BuildGame_DeploysCapitalShips() } }); } + + [Test, TestCaseSource(nameof(GameTestCases))] + public void BuildGame_SetsGameEvents(Game game) + { + // Ensure the game has at least one event in the event pool. + Assert.GreaterOrEqual( + game.GetEventPool().Count(), + 1, + "Game should have at most one event in the event pool." + ); + } } diff --git a/Assets/Tests/EditMode/Managers/SaveGameManagerTests.cs b/Assets/Tests/EditMode/Managers/SaveGameManagerTests.cs index 1c74089..cbd065b 100644 --- a/Assets/Tests/EditMode/Managers/SaveGameManagerTests.cs +++ b/Assets/Tests/EditMode/Managers/SaveGameManagerTests.cs @@ -130,7 +130,6 @@ public void TestBasicSceneGraphLoaded() // Create planets. Planet planet = new Planet { DisplayName = "Planet", OwnerInstanceID = "FNALL1" }; - // planetSystem.Planets.Add(planet); game.AttachNode(planet, planetSystem); // Create fleets. diff --git a/Assets/Tests/EditMode/Util/Serialization/GameSerializerTests.cs b/Assets/Tests/EditMode/Util/Serialization/GameSerializerTests.cs index 3329eb9..dacad8b 100644 --- a/Assets/Tests/EditMode/Util/Serialization/GameSerializerTests.cs +++ b/Assets/Tests/EditMode/Util/Serialization/GameSerializerTests.cs @@ -159,6 +159,9 @@ public class ItemWithCustomName [PersistableMember(Name = "CustomNamedProperty")] public string Property { get; set; } + [PersistableAttribute(Name = "CustomNamedAttribute")] + public string Attribute { get; set; } + public ItemWithCustomName() { } } @@ -1281,12 +1284,16 @@ public void Deserialize_EnumArray_RetainsValues() public void Serialize_ItemWithCustomName_UsesCustomName() { GameSerializer serializer = new GameSerializer(typeof(ItemWithCustomName)); - ItemWithCustomName item = new ItemWithCustomName { Property = "TestValue" }; + ItemWithCustomName item = new ItemWithCustomName + { + Property = "TestValue", + Attribute = "TestAttr", + }; string serializedXml = SerializeToString(serializer, item); string expectedXml = @" - + TestValue "; @@ -1303,7 +1310,7 @@ public void Deserialize_ItemWithCustomName_UsesCustomName() GameSerializer serializer = new GameSerializer(typeof(ItemWithCustomName)); string xmlInput = @" - + TestValue "; @@ -1317,6 +1324,11 @@ public void Deserialize_ItemWithCustomName_UsesCustomName() deserialized.Property, "Deserialized value should match the input for the custom-named property." ); + Assert.AreEqual( + "TestAttr", + deserialized.Attribute, + "Deserialized value should match the input for the custom-named attribute." + ); } [Test]