diff --git a/C7/Lua/game_modes/base-ruleset.json b/C7/Lua/game_modes/base-ruleset.json index 656a5ac1f..34d4ac623 100644 --- a/C7/Lua/game_modes/base-ruleset.json +++ b/C7/Lua/game_modes/base-ruleset.json @@ -774,6 +774,39 @@ "bombard": 0, "movement": 1, "iconIndex": 0, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -798,6 +831,39 @@ "bombard": 0, "movement": 1, "iconIndex": 1, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -832,6 +898,15 @@ "bombard": 0, "movement": 2, "iconIndex": 2, + "producibleBy": [ + "Russia", + "America", + "Zululand", + "Mongols", + "Arabia", + "Hittites", + "Portugal" + ], "upgradeTo": "Explorer", "unproducible": false, "categories": [ @@ -857,6 +932,38 @@ "bombard": 0, "movement": 2, "iconIndex": 3, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -881,6 +988,39 @@ "bombard": 0, "movement": 1, "iconIndex": 4, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -908,6 +1048,39 @@ "bombard": 0, "movement": 1, "iconIndex": 5, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -935,6 +1108,39 @@ "bombard": 0, "movement": 1, "iconIndex": 6, + "producibleBy": [ + "A Barbarian Chiefdom", + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Swordsman", "unproducible": false, "categories": [ @@ -960,6 +1166,37 @@ "bombard": 1, "movement": 1, "iconIndex": 7, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca" + ], "upgradeTo": "Longbowman", "unproducible": false, "categories": [ @@ -985,6 +1222,35 @@ "bombard": 0, "movement": 1, "iconIndex": 8, + "producibleBy": [ + "Rome", + "Egypt", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Korea", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Pikeman", "unproducible": false, "categories": [ @@ -1010,6 +1276,36 @@ "bombard": 0, "movement": 1, "iconIndex": 9, + "producibleBy": [ + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Medieval Infantry", "unproducible": false, "categories": [ @@ -1038,6 +1334,37 @@ "bombard": 0, "movement": 2, "iconIndex": 10, + "producibleBy": [ + "Rome", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Horseman", "unproducible": false, "categories": [ @@ -1066,6 +1393,39 @@ "bombard": 0, "movement": 2, "iconIndex": 11, + "producibleBy": [ + "A Barbarian Chiefdom", + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Knight", "unproducible": false, "categories": [ @@ -1094,6 +1454,36 @@ "bombard": 0, "movement": 1, "iconIndex": 12, + "producibleBy": [ + "Rome", + "Egypt", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Korea", + "Sumeria", + "Hittites", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Musketman", "unproducible": false, "categories": [ @@ -1122,6 +1512,38 @@ "bombard": 2, "movement": 1, "iconIndex": 13, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Guerilla", "unproducible": false, "categories": [ @@ -1147,6 +1569,38 @@ "bombard": 0, "movement": 1, "iconIndex": 14, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Rifleman", "unproducible": false, "categories": [ @@ -1175,6 +1629,34 @@ "bombard": 0, "movement": 2, "iconIndex": 15, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "America", + "France", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Cavalry", "unproducible": false, "categories": [ @@ -1204,6 +1686,39 @@ "bombard": 0, "movement": 1, "iconIndex": 16, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Infantry", "unproducible": false, "categories": [ @@ -1229,6 +1744,37 @@ "bombard": 0, "movement": 3, "iconIndex": 17, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -1257,6 +1803,39 @@ "bombard": 0, "movement": 1, "iconIndex": 18, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Mech Infantry", "unproducible": false, "categories": [ @@ -1285,6 +1864,38 @@ "bombard": 0, "movement": 2, "iconIndex": 19, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Modern Armor", "unproducible": false, "categories": [ @@ -1314,6 +1925,39 @@ "bombard": 0, "movement": 2, "iconIndex": 20, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -1342,6 +1986,39 @@ "bombard": 0, "movement": 3, "iconIndex": 21, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -1370,6 +2047,39 @@ "bombard": 4, "movement": 1, "iconIndex": 22, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Trebuchet", "unproducible": false, "categories": [ @@ -1396,6 +2106,38 @@ "bombard": 8, "movement": 1, "iconIndex": 23, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Artillery", "unproducible": false, "categories": [ @@ -1426,6 +2168,39 @@ "bombard": 12, "movement": 1, "iconIndex": 24, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Radar Artillery", "unproducible": false, "categories": [ @@ -1452,6 +2227,39 @@ "bombard": 16, "movement": 2, "iconIndex": 25, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -1480,6 +2288,39 @@ "bombard": 16, "movement": 1, "iconIndex": 26, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -1507,6 +2348,39 @@ "bombard": 0, "movement": 1, "iconIndex": 27, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -1536,6 +2410,39 @@ "bombard": 0, "movement": 1, "iconIndex": 28, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -1564,6 +2471,38 @@ "bombard": 0, "movement": 3, "iconIndex": 29, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Inca", + "Maya" + ], "upgradeTo": "Caravel", "unproducible": false, "categories": [ @@ -1589,6 +2528,38 @@ "bombard": 0, "movement": 4, "iconIndex": 30, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Galleon", "unproducible": false, "categories": [ @@ -1614,6 +2585,38 @@ "bombard": 3, "movement": 5, "iconIndex": 31, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Sea" @@ -1643,6 +2646,39 @@ "bombard": 0, "movement": 4, "iconIndex": 32, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Transport", "unproducible": false, "categories": [ @@ -1668,6 +2704,39 @@ "bombard": 6, "movement": 3, "iconIndex": 33, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Destroyer", "unproducible": false, "categories": [ @@ -1698,6 +2767,39 @@ "bombard": 0, "movement": 6, "iconIndex": 34, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Sea" @@ -1725,6 +2827,39 @@ "bombard": 0, "movement": 7, "iconIndex": 35, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Sea" @@ -1752,6 +2887,39 @@ "bombard": 0, "movement": 4, "iconIndex": 36, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Sea" @@ -1779,6 +2947,39 @@ "bombard": 6, "movement": 8, "iconIndex": 37, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Sea" @@ -1807,6 +3008,39 @@ "bombard": 8, "movement": 5, "iconIndex": 38, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Sea" @@ -1835,6 +3069,39 @@ "bombard": 6, "movement": 7, "iconIndex": 39, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Sea" @@ -1864,6 +3131,39 @@ "bombard": 0, "movement": 5, "iconIndex": 40, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Sea" @@ -1891,6 +3191,39 @@ "bombard": 3, "movement": 1, "iconIndex": 41, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Jet Fighter", "unproducible": false, "categories": [ @@ -1917,6 +3250,39 @@ "bombard": 12, "movement": 1, "iconIndex": 42, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Air" @@ -1942,6 +3308,39 @@ "bombard": 0, "movement": 1, "iconIndex": 43, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Air" @@ -1968,6 +3367,38 @@ "bombard": 3, "movement": 1, "iconIndex": 44, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Air" @@ -1994,6 +3425,39 @@ "bombard": 6, "movement": 1, "iconIndex": 45, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Air" @@ -2020,6 +3484,39 @@ "bombard": 18, "movement": 1, "iconIndex": 46, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Air" @@ -2045,6 +3542,39 @@ "bombard": 0, "movement": 3, "iconIndex": 47, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": true, "categories": [ "Land" @@ -2068,6 +3598,39 @@ "bombard": 0, "movement": 1, "iconIndex": 48, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": true, "categories": [ "Land" @@ -2092,10 +3655,10 @@ "bombard": 0, "movement": 2, "iconIndex": 49, + "producibleBy": [ + "Aztecs" + ], "upgradeTo": "Swordsman", - "unique": { - "civilization": "Aztecs" - }, "unproducible": false, "categories": [ "Land" @@ -2120,11 +3683,10 @@ "bombard": 1, "movement": 1, "iconIndex": 50, + "producibleBy": [ + "Babylon" + ], "upgradeTo": "Longbowman", - "unique": { - "replace": "Archer", - "civilization": "Babylon" - }, "unproducible": false, "categories": [ "Land" @@ -2149,11 +3711,10 @@ "bombard": 0, "movement": 1, "iconIndex": 51, + "producibleBy": [ + "Greece" + ], "upgradeTo": "Musketman", - "unique": { - "replace": "Spearman", - "civilization": "Greece" - }, "unproducible": false, "categories": [ "Land" @@ -2178,11 +3739,10 @@ "bombard": 0, "movement": 2, "iconIndex": 52, + "producibleBy": [ + "Zululand" + ], "upgradeTo": "Musketman", - "unique": { - "replace": "Spearman", - "civilization": "Zululand" - }, "unproducible": false, "categories": [ "Land" @@ -2207,11 +3767,10 @@ "bombard": 0, "movement": 1, "iconIndex": 53, + "producibleBy": [ + "Rome" + ], "upgradeTo": "Medieval Infantry", - "unique": { - "replace": "Swordsman", - "civilization": "Rome" - }, "unproducible": false, "categories": [ "Land" @@ -2239,11 +3798,10 @@ "bombard": 0, "movement": 1, "iconIndex": 54, + "producibleBy": [ + "Persia" + ], "upgradeTo": "Medieval Infantry", - "unique": { - "replace": "Swordsman", - "civilization": "Persia" - }, "unproducible": false, "categories": [ "Land" @@ -2271,11 +3829,10 @@ "bombard": 0, "movement": 2, "iconIndex": 55, + "producibleBy": [ + "Egypt" + ], "upgradeTo": "Knight", - "unique": { - "replace": "Chariot", - "civilization": "Egypt" - }, "unproducible": false, "categories": [ "Land" @@ -2303,11 +3860,10 @@ "bombard": 0, "movement": 3, "iconIndex": 56, + "producibleBy": [ + "China" + ], "upgradeTo": "Cavalry", - "unique": { - "replace": "Knight", - "civilization": "China" - }, "unproducible": false, "categories": [ "Land" @@ -2336,11 +3892,10 @@ "bombard": 0, "movement": 2, "iconIndex": 57, + "producibleBy": [ + "Iroquois" + ], "upgradeTo": "Knight", - "unique": { - "replace": "Horseman", - "civilization": "Iroquois" - }, "unproducible": false, "categories": [ "Land" @@ -2368,11 +3923,10 @@ "bombard": 2, "movement": 1, "iconIndex": 58, + "producibleBy": [ + "France" + ], "upgradeTo": "Rifleman", - "unique": { - "replace": "Musketman", - "civilization": "France" - }, "unproducible": false, "categories": [ "Land" @@ -2400,11 +3954,10 @@ "bombard": 0, "movement": 2, "iconIndex": 59, + "producibleBy": [ + "Japan" + ], "upgradeTo": "Cavalry", - "unique": { - "replace": "Knight", - "civilization": "Japan" - }, "unproducible": false, "categories": [ "Land" @@ -2432,11 +3985,10 @@ "bombard": 0, "movement": 2, "iconIndex": 60, + "producibleBy": [ + "India" + ], "upgradeTo": "Cavalry", - "unique": { - "replace": "Knight", - "civilization": "India" - }, "unproducible": false, "categories": [ "Land" @@ -2461,10 +4013,9 @@ "bombard": 0, "movement": 3, "iconIndex": 61, - "unique": { - "replace": "Cavalry", - "civilization": "Russia" - }, + "producibleBy": [ + "Russia" + ], "unproducible": false, "categories": [ "Land" @@ -2493,11 +4044,10 @@ "bombard": 0, "movement": 3, "iconIndex": 62, + "producibleBy": [ + "Germany" + ], "upgradeTo": "Modern Armor", - "unique": { - "replace": "Tank", - "civilization": "Germany" - }, "unproducible": false, "categories": [ "Land" @@ -2526,10 +4076,9 @@ "bombard": 4, "movement": 5, "iconIndex": 63, - "unique": { - "replace": "Frigate", - "civilization": "England" - }, + "producibleBy": [ + "England" + ], "unproducible": false, "categories": [ "Sea" @@ -2559,10 +4108,9 @@ "bombard": 6, "movement": 1, "iconIndex": 64, - "unique": { - "replace": "Jet Fighter", - "civilization": "America" - }, + "producibleBy": [ + "America" + ], "unproducible": false, "categories": [ "Air" @@ -2589,6 +4137,39 @@ "bombard": 3, "movement": 5, "iconIndex": 65, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Sea" @@ -2617,11 +4198,10 @@ "bombard": 0, "movement": 2, "iconIndex": 76, + "producibleBy": [ + "Mongols" + ], "upgradeTo": "Cavalry", - "unique": { - "replace": "Knight", - "civilization": "Mongols" - }, "unproducible": false, "categories": [ "Land" @@ -2649,10 +4229,9 @@ "bombard": 0, "movement": 2, "iconIndex": 75, - "unique": { - "replace": "Explorer", - "civilization": "Spain" - }, + "producibleBy": [ + "Spain" + ], "unproducible": false, "categories": [ "Land" @@ -2680,11 +4259,10 @@ "bombard": 0, "movement": 1, "iconIndex": 74, + "producibleBy": [ + "Scandinavia" + ], "upgradeTo": "Guerilla", - "unique": { - "replace": "Longbowman", - "civilization": "Scandinavia" - }, "unproducible": false, "categories": [ "Land" @@ -2709,10 +4287,9 @@ "bombard": 0, "movement": 3, "iconIndex": 77, - "unique": { - "replace": "Cavalry", - "civilization": "Ottomans" - }, + "producibleBy": [ + "Ottomans" + ], "unproducible": false, "categories": [ "Land" @@ -2741,11 +4318,10 @@ "bombard": 0, "movement": 2, "iconIndex": 78, + "producibleBy": [ + "Celts" + ], "upgradeTo": "Medieval Infantry", - "unique": { - "replace": "Swordsman", - "civilization": "Celts" - }, "unproducible": false, "categories": [ "Land" @@ -2773,11 +4349,10 @@ "bombard": 0, "movement": 3, "iconIndex": 82, + "producibleBy": [ + "Arabia" + ], "upgradeTo": "Cavalry", - "unique": { - "replace": "Knight", - "civilization": "Arabia" - }, "unproducible": false, "categories": [ "Land" @@ -2806,11 +4381,10 @@ "bombard": 0, "movement": 1, "iconIndex": 80, + "producibleBy": [ + "Carthage" + ], "upgradeTo": "Pikeman", - "unique": { - "replace": "Spearman", - "civilization": "Carthage" - }, "unproducible": false, "categories": [ "Land" @@ -2835,10 +4409,10 @@ "bombard": 8, "movement": 1, "iconIndex": 79, + "producibleBy": [ + "Korea" + ], "upgradeTo": "Artillery", - "unique": { - "civilization": "Korea" - }, "unproducible": false, "categories": [ "Land" @@ -2867,6 +4441,38 @@ "bombard": 0, "movement": 1, "iconIndex": 84, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Guerilla", "unproducible": false, "categories": [ @@ -2895,6 +4501,39 @@ "bombard": 3, "movement": 1, "iconIndex": 85, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "TOW Infantry", "unproducible": false, "categories": [ @@ -2919,6 +4558,7 @@ "bombard": 0, "movement": 1, "iconIndex": 121, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -2939,6 +4579,7 @@ "bombard": 0, "movement": 2, "iconIndex": 97, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -2961,6 +4602,7 @@ "bombard": 0, "movement": 2, "iconIndex": 100, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -2983,6 +4625,7 @@ "bombard": 0, "movement": 2, "iconIndex": 103, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3005,6 +4648,7 @@ "bombard": 0, "movement": 2, "iconIndex": 107, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3027,6 +4671,7 @@ "bombard": 0, "movement": 2, "iconIndex": 108, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3049,6 +4694,7 @@ "bombard": 0, "movement": 2, "iconIndex": 116, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3071,6 +4717,7 @@ "bombard": 0, "movement": 2, "iconIndex": 115, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3093,6 +4740,7 @@ "bombard": 0, "movement": 2, "iconIndex": 110, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3115,6 +4763,7 @@ "bombard": 0, "movement": 2, "iconIndex": 120, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3137,6 +4786,7 @@ "bombard": 0, "movement": 2, "iconIndex": 99, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3159,6 +4809,7 @@ "bombard": 0, "movement": 2, "iconIndex": 104, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3181,6 +4832,7 @@ "bombard": 0, "movement": 2, "iconIndex": 105, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3203,6 +4855,7 @@ "bombard": 0, "movement": 2, "iconIndex": 117, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3225,6 +4878,7 @@ "bombard": 0, "movement": 2, "iconIndex": 98, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3247,6 +4901,7 @@ "bombard": 0, "movement": 2, "iconIndex": 101, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3269,6 +4924,7 @@ "bombard": 0, "movement": 2, "iconIndex": 114, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3291,6 +4947,7 @@ "bombard": 0, "movement": 2, "iconIndex": 113, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3313,6 +4970,7 @@ "bombard": 0, "movement": 2, "iconIndex": 109, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3335,6 +4993,7 @@ "bombard": 0, "movement": 2, "iconIndex": 119, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3357,6 +5016,7 @@ "bombard": 0, "movement": 2, "iconIndex": 102, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3379,6 +5039,7 @@ "bombard": 0, "movement": 2, "iconIndex": 111, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3401,6 +5062,7 @@ "bombard": 0, "movement": 2, "iconIndex": 106, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3423,6 +5085,7 @@ "bombard": 0, "movement": 2, "iconIndex": 112, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3445,6 +5108,7 @@ "bombard": 0, "movement": 2, "iconIndex": 118, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3467,11 +5131,10 @@ "bombard": 0, "movement": 1, "iconIndex": 175, + "producibleBy": [ + "Sumeria" + ], "upgradeTo": "Pikeman", - "unique": { - "replace": "Warrior", - "civilization": "Sumeria" - }, "unproducible": false, "categories": [ "Land" @@ -3496,11 +5159,10 @@ "bombard": 0, "movement": 2, "iconIndex": 186, + "producibleBy": [ + "Hittites" + ], "upgradeTo": "Knight", - "unique": { - "replace": "Chariot", - "civilization": "Hittites" - }, "unproducible": false, "categories": [ "Land" @@ -3527,6 +5189,7 @@ "bombard": 0, "movement": 2, "iconIndex": 169, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3549,6 +5212,7 @@ "bombard": 0, "movement": 2, "iconIndex": 174, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3571,6 +5235,7 @@ "bombard": 0, "movement": 2, "iconIndex": 173, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3594,11 +5259,10 @@ "bombard": 0, "movement": 4, "iconIndex": 198, + "producibleBy": [ + "Portugal" + ], "upgradeTo": "Galleon", - "unique": { - "replace": "Caravel", - "civilization": "Portugal" - }, "unproducible": false, "categories": [ "Sea" @@ -3623,11 +5287,10 @@ "bombard": 0, "movement": 1, "iconIndex": 181, + "producibleBy": [ + "Netherlands" + ], "upgradeTo": "Rifleman", - "unique": { - "replace": "Pikeman", - "civilization": "Netherlands" - }, "unproducible": false, "categories": [ "Land" @@ -3654,6 +5317,7 @@ "bombard": 0, "movement": 2, "iconIndex": 172, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3677,6 +5341,39 @@ "bombard": 6, "movement": 1, "iconIndex": 190, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Cannon", "unproducible": false, "categories": [ @@ -3702,6 +5399,7 @@ "bombard": 0, "movement": 2, "iconIndex": 170, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3724,6 +5422,7 @@ "bombard": 0, "movement": 2, "iconIndex": 171, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3746,6 +5445,7 @@ "bombard": 0, "movement": 2, "iconIndex": 168, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3768,11 +5468,10 @@ "bombard": 0, "movement": 2, "iconIndex": 176, + "producibleBy": [ + "Inca" + ], "upgradeTo": "Explorer", - "unique": { - "replace": "Scout", - "civilization": "Inca" - }, "unproducible": false, "categories": [ "Land" @@ -3797,11 +5496,10 @@ "bombard": 0, "movement": 1, "iconIndex": 177, + "producibleBy": [ + "Maya" + ], "upgradeTo": "Longbowman", - "unique": { - "replace": "Archer", - "civilization": "Maya" - }, "unproducible": false, "categories": [ "Land" @@ -3826,11 +5524,10 @@ "bombard": 2, "movement": 3, "iconIndex": 196, + "producibleBy": [ + "Byzantines" + ], "upgradeTo": "Caravel", - "unique": { - "replace": "Galley", - "civilization": "Byzantines" - }, "unproducible": false, "categories": [ "Sea" @@ -3856,6 +5553,39 @@ "bombard": 7, "movement": 6, "iconIndex": 200, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "AEGIS Cruiser", "unproducible": false, "categories": [ @@ -3884,6 +5614,7 @@ "bombard": 0, "movement": 1, "iconIndex": 180, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3910,6 +5641,7 @@ "bombard": 0, "movement": 2, "iconIndex": 187, + "producibleBy": [], "unproducible": true, "categories": [ "Land" @@ -3935,6 +5667,39 @@ "movement": 2, "iconIndex": 195, "upgradeTo": "Galley", + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Sea" @@ -3959,6 +5724,39 @@ "bombard": 0, "movement": 1, "iconIndex": 185, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Modern Paratrooper", "unproducible": false, "categories": [ @@ -3988,6 +5786,39 @@ "bombard": 6, "movement": 1, "iconIndex": 203, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -4012,6 +5843,39 @@ "bombard": 0, "movement": 1, "iconIndex": 205, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "upgradeTo": "Mobile SAM", "unproducible": false, "categories": [ @@ -4037,6 +5901,39 @@ "bombard": 0, "movement": 2, "iconIndex": 206, + "producibleBy": [ + "Rome", + "Egypt", + "Greece", + "Babylon", + "Germany", + "Russia", + "China", + "America", + "Japan", + "France", + "India", + "Persia", + "Aztecs", + "Zululand", + "Iroquois", + "England", + "Mongols", + "Spain", + "Scandinavia", + "Ottomans", + "Celts", + "Arabia", + "Carthage", + "Korea", + "Sumeria", + "Hittites", + "Netherlands", + "Portugal", + "Byzantines", + "Inca", + "Maya" + ], "unproducible": false, "categories": [ "Land" @@ -7189,7 +9086,8 @@ "treasuryInterestRate": 0.05, "maxInterest": 50, "shieldCostPerGold": 4, - "shieldRateForDisbanding": 0.25 + "shieldRateForDisbanding": 0.25, + "allowLesserUnitProduction": false }, "techs": [ { diff --git a/C7/UIElements/Advisors/TechBox.cs b/C7/UIElements/Advisors/TechBox.cs index 271660e4f..5e77dd36f 100644 --- a/C7/UIElements/Advisors/TechBox.cs +++ b/C7/UIElements/Advisors/TechBox.cs @@ -1,9 +1,6 @@ - using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Text.RegularExpressions; using C7Engine; using C7GameData; using Godot; @@ -250,23 +247,10 @@ private int CalculateTechBoxSizeCost(GameData gameData) { List units = new(); // List of units that require this tech to be built, taking into account that some are unique List unitsRequiringTech = gameData.unitPrototypes.Where(u => u.requiredTech == tech).ToList(); - HashSet replacements = new(); - - // Filter out all the units that are getting replaced - // e.x. Numidian Mercenary replaces Spearman - foreach (UnitPrototype u in unitsRequiringTech) { - if (u.unique != null && EngineStorage.gameData.GetFirstHumanPlayer().civilization == u.unique.civilization) { - if (u.unique.replace != null) - replacements.Add(u.unique.replace.name); - } - } // Then add the correct units to the list foreach (UnitPrototype u in unitsRequiringTech) { - if (u.unique == null && !replacements.Contains(u.name)) { - units.Add(u); - } - if (u.unique != null && EngineStorage.gameData.GetFirstHumanPlayer().civilization == u.unique.civilization) { + if (u.producibleBy.Contains(EngineStorage.gameData.GetFirstHumanPlayer().civilization)) { units.Add(u); } } diff --git a/C7Engine/C7GameData/Civilization.cs b/C7Engine/C7GameData/Civilization.cs index d9ff6898d..39f1fd518 100644 --- a/C7Engine/C7GameData/Civilization.cs +++ b/C7Engine/C7GameData/Civilization.cs @@ -74,30 +74,28 @@ public class SettlerTileAdjustments { public SettlerTileAdjustments Adjustments = new(); - [JsonIgnore] - public UnitPrototype uniqueUnit; - - private UnitPrototype GetDirectUpgrade(UnitPrototype unit) { - // Check if a regular upgrade is replaced by a unique unit - if (uniqueUnit != null - && uniqueUnit.unique.replace != null - && uniqueUnit.unique.replace == unit.upgradeTo) { - return uniqueUnit; - } - - return unit.upgradeTo; - } - + // This method is primarily here to satisfy the weird upgrade chains from the .biq and .sav files public List GetUpgradeChain(UnitPrototype unit) { List result = []; var current = unit; while (true) { - var upgrade = GetDirectUpgrade(current); + var upgrade = current.upgradeTo; if (upgrade == null) break; - result.Add(upgrade); + var upgradeIsAvailable = upgrade.producibleBy.Contains(this) && !result.Contains(upgrade); + + if (upgradeIsAvailable) { + result.Add(upgrade); + } current = upgrade; + + } + + for (int i = result.Count - 1; i >= 0; i--) { + if (!result[i].producibleBy.Contains(this)) { + result.Remove(result[i]); + } } return result; @@ -108,13 +106,7 @@ public bool IsUnitAvailable(UnitPrototype unit) { return false; } - // Check if unit is replaced by a unique unit - if (uniqueUnit != null && uniqueUnit.unique.replace == unit) { - return false; - } - - // Check if unit is a unique unit from another civilization - if (unit.unique != null && unit.unique.civilization != this) { + if (!unit.producibleBy.Contains(this)) { return false; } diff --git a/C7Engine/C7GameData/GameData.cs b/C7Engine/C7GameData/GameData.cs index 97c73687c..4ae8b778b 100644 --- a/C7Engine/C7GameData/GameData.cs +++ b/C7Engine/C7GameData/GameData.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Serilog; using C7Engine.Lua; using C7Engine.Pathing; using System.Threading.Tasks; using C7Engine; +[assembly: InternalsVisibleTo("EngineTests")] namespace C7GameData { public class GameData { private static ILogger log = Log.ForContext(); diff --git a/C7Engine/C7GameData/ImportCiv3.cs b/C7Engine/C7GameData/ImportCiv3.cs index dd37bd067..4a1fde8e0 100644 --- a/C7Engine/C7GameData/ImportCiv3.cs +++ b/C7Engine/C7GameData/ImportCiv3.cs @@ -64,7 +64,6 @@ private void ImportSharedBiqData() { ImportCiv3Resources(); ImportTerraforms(); ImportUnitPrototypes(); - ImportUniqueUnitReplacements(); ImportUnitUpgrades(); ImportBuildings(); ImportCiv3TerrainTypes(); @@ -1048,16 +1047,6 @@ private static IEnumerable GetUnitTerraforms(PRTO prto) { if (prto.BuildFortress) yield return TerraformKey.BuildFortress; } - private SaveUnitPrototype.Unique ImportUniqueUnitData(PRTO prto) { - int civIndex = prto.AvailableTo.GetUniqueCivIndex(); - - if (civIndex == -1) { - return null; - } - - return new() { civilization = save.Civilizations[civIndex].name }; - } - private static bool IsUnproducible(PRTO prto) { int[] availableTo = prto.AvailableTo.GetAvailableCivIndexes().ToArray(); @@ -1065,6 +1054,17 @@ private static bool IsUnproducible(PRTO prto) { return availableTo.Length == 0 || prto.ShieldCost < 1 || prto.Army; } + private HashSet ImportUnitAvailability(PRTO prto) { + HashSet availableToCivs = []; + int[] availableTo = prto.AvailableTo.GetAvailableCivIndexes().ToArray(); + for (int i = 0; i < biq.Race.Length; ++i) { + if (availableTo.Contains(i)) + availableToCivs.Add(biq.Race[i].Name); + } + + return availableToCivs; + } + private void ImportUnitPrototypes() { PRTO[] Prto = biq.Prto ?? defaultBiq.Prto; foreach (PRTO prto in Prto) { @@ -1089,7 +1089,6 @@ private void ImportUnitPrototypes() { prototype.actions.UnionWith(GetUnitActions(prto)); prototype.terraformActions.UnionWith(GetUnitTerraforms(prto).Select(tfKey => terraformIdByCiv3Key[tfKey])); - prototype.unique = ImportUniqueUnitData(prto); prototype.unproducible = IsUnproducible(prto); if (prto.Required != -1) { @@ -1104,6 +1103,8 @@ private void ImportUnitPrototypes() { prototype.requiredResources.Add(save.Resources[prto.RequiredResource2].Key); } + prototype.producibleBy = ImportUnitAvailability(prto); + //Temporary check until #330 is finished if (!save.UnitPrototypes.Where(p => p.name == prototype.name).Any()) { save.UnitPrototypes.Add(prototype); @@ -1111,79 +1112,16 @@ private void ImportUnitPrototypes() { } } - // This method assigns standard units that are replaced by unique units. - // - // A unique unit replaces a standard unit if both share the same tech requirement - // and the standard unit is unproducible by the civilization to which the unique unit belongs. - // - // For example, this method updates the Mounted Warrior prototype to indicate that it replaces the Horseman. - private void ImportUniqueUnitReplacements() { - var unitPrototypeDict = save.UnitPrototypes.ToDictionary(b => b.name); - - // Group unique units by civilization. - // In the base ruleset a civilization only has one unique unit, - // but this may vary in scenarios. - var uniqueUnitPrototypesByCiv = save.UnitPrototypes - .Where(u => u.unique != null) - .ToLookup(u => u.unique.civilization); - - PRTO[] Prto = biq.Prto ?? defaultBiq.Prto; - - foreach (PRTO standardUnitPrto in Prto) { - string standardUnitName = standardUnitPrto.Name; - SaveUnitPrototype standardUnit = unitPrototypeDict[standardUnitName]; - - // Skip units that are either unique or unproducible (cannot be built normally) - if (standardUnit.unique != null) { - continue; - } - - if (standardUnit.unproducible) { - continue; - } - - // For each civilization that cannot build the standard unit - foreach (int civIndex in standardUnitPrto.AvailableTo.GetUnavailableCivIndexes()) { - if (civIndex >= save.Civilizations.Count) { - break; - } - - var uniqueUnits = uniqueUnitPrototypesByCiv[save.Civilizations[civIndex].name]; - - foreach (SaveUnitPrototype uniqueUnit in uniqueUnits) { - // If the unique unit has the same tech requirement as the standard unit, - // mark the unique unit as a replacement for the standard unit - if (uniqueUnit.requiredTech == standardUnit.requiredTech) { - uniqueUnit.unique.replace = standardUnitName; - } - } - } - } - } - - // This method loads unit upgrades from CIV3 data. In CIV3, unique units are part of the upgrade chain. - // - // For example, the upgrade path for Horseman looks like this: - // Horseman->Mounted Warrior->Three-Man Chariot->Knight->Keshik->Ansar Warrior->Rider->Samurai->War Elephant->Cavalry. - // see also: https://forums.civfanatics.com/threads/how-to-upgrade-regular-units-to-uus.108396/ - // - // When loading this data, the method ignores the unique units in the upgrade chain. - // Instead, each unit of the chain will be assigned an upgrade that represents the closest non-unique unit - // that also requires a tech advancement over the base unit. - // - // For example, this method will mark that Horseman upgrades to Knight and that Keshik upgrades to Cavalry. private void ImportUnitUpgrades() { Dictionary upgradeDict = BuildUpgradeDict(); foreach (SaveUnitPrototype proto in save.UnitPrototypes) { - proto.upgradeTo = GetUnitUpgrade(proto, upgradeDict); + proto.upgradeTo = upgradeDict[proto]?.name; } } // This method builds a Dictionary of unit upgrades based on the CIV3 data. // The dictionary represents the raw upgrade relationships as defined in the game files. - // The dictionary serves as an intermediate data structure for the ImportUnitUpgrades process, - // before filtering out unique units. private Dictionary BuildUpgradeDict() { PRTO[] Prto = biq.Prto ?? defaultBiq.Prto; var unitPrototypeDict = save.UnitPrototypes.ToDictionary(b => b.name); @@ -1203,29 +1141,6 @@ private Dictionary BuildUpgradeDict() { return upgradeDict; } - // This method returns the name of the first valid unit upgrade in the upgrade chain. - // A valid upgrade must require a different technology than the base unit and must not be a unique unit. - // If no valid upgrade is found, it returns null. - private static string GetUnitUpgrade(SaveUnitPrototype proto, Dictionary upgradeDict) { - SaveUnitPrototype currentProto = proto; - - while (true) { - // Check if there's an upgrade available - var upgrade = upgradeDict[currentProto]; - if (upgrade == null) { - return null; - } - - // If this upgrade represents a technology advancement over the base unit and is not a unique unit, return it - if (upgrade.requiredTech != proto.requiredTech && upgrade.unique == null) { - return upgrade.name; - } - - // Otherwise, continue checking the upgrade chain - currentProto = upgrade; - } - } - private void ImportBuildings() { BLDG[] Bldg = biq.Bldg ?? defaultBiq.Bldg; @@ -1731,6 +1646,7 @@ private void ImportRules() { save.Rules.DefaultDealDuration = 20; save.Rules.ShieldCostPerGold = rule.ShieldsCostPerGold; save.Rules.ShieldRateForDisbanding = 0.25f; + save.Rules.AllowLesserUnitProduction = false; } private static void SetWorldWrap(SavData civ3Save, SaveGame save) { diff --git a/C7Engine/C7GameData/Rules.cs b/C7Engine/C7GameData/Rules.cs index 3fd8c9b2c..2f1fb8dab 100644 --- a/C7Engine/C7GameData/Rules.cs +++ b/C7Engine/C7GameData/Rules.cs @@ -22,5 +22,6 @@ public class Rules { public int MaxInterest = 50; public int ShieldCostPerGold; public float ShieldRateForDisbanding; // per cent + public bool AllowLesserUnitProduction; // for example, allow building a Spearman/Pikeman when we can build a Musketman (simultaneously) } } diff --git a/C7Engine/C7GameData/Save/SaveGame.cs b/C7Engine/C7GameData/Save/SaveGame.cs index 0d65ea5f2..9efb00762 100644 --- a/C7Engine/C7GameData/Save/SaveGame.cs +++ b/C7Engine/C7GameData/Save/SaveGame.cs @@ -7,7 +7,6 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using C7Engine; -using Serilog; namespace C7GameData.Save { @@ -292,19 +291,7 @@ private void ConvertUnits(GameData data) { proto.requiredTech = techDict[saveProto.requiredTech]; } - if (saveProto.unique != null) { - Civilization civ = civDict[saveProto.unique.civilization]; - - proto.unique = new() { - civilization = civ - }; - - if (saveProto.unique.replace != null) { - proto.unique.replace = unitPrototypeDict[saveProto.unique.replace]; - } - - civ.uniqueUnit = proto; - } + proto.producibleBy = saveProto.producibleBy.Select(c => civDict[c]).ToHashSet(); proto.requiredResources = saveProto.requiredResources.Select(a => resDict[a]).ToHashSet(); } diff --git a/C7Engine/C7GameData/Save/SaveUnitPrototype.cs b/C7Engine/C7GameData/Save/SaveUnitPrototype.cs index 030c41851..fc2b18299 100644 --- a/C7Engine/C7GameData/Save/SaveUnitPrototype.cs +++ b/C7Engine/C7GameData/Save/SaveUnitPrototype.cs @@ -3,11 +3,6 @@ namespace C7GameData.Save { public class SaveUnitPrototype { - public class Unique { - public string replace; - public string civilization; - }; - public string name { get; set; } public string artName { get; set; } public int shieldCost { get; set; } @@ -19,8 +14,9 @@ public class Unique { public int movement { get; set; } public int iconIndex { get; set; } + public HashSet producibleBy = []; + public string upgradeTo; - public Unique unique; public bool unproducible; public HashSet categories = new HashSet(); @@ -47,19 +43,13 @@ public SaveUnitPrototype(UnitPrototype proto) { if (proto.upgradeTo != null) upgradeTo = proto.upgradeTo.name; - if (proto.unique != null) { - unique = new() { - civilization = proto.unique.civilization.name, - replace = proto.unique.replace?.name - }; - } - categories = new HashSet(proto.categories); actions = proto.actions; attributes = new HashSet(proto.attributes); requiredResources = proto.requiredResources.Select(r => r.Key).ToHashSet(); terraformActions = proto.terraformActions.Select(r => r.Id).ToHashSet(); + producibleBy = proto.producibleBy.Select(r => r.name).ToHashSet(); } } } diff --git a/C7Engine/C7GameData/UnitPrototype.cs b/C7Engine/C7GameData/UnitPrototype.cs index 189453e5e..f4390dd82 100644 --- a/C7Engine/C7GameData/UnitPrototype.cs +++ b/C7Engine/C7GameData/UnitPrototype.cs @@ -4,7 +4,7 @@ namespace C7GameData { using System; using System.Linq; using C7Engine; - using C7GameData.Save; + using Save; public enum UnitAction { BuildCity, @@ -23,11 +23,6 @@ public enum UnitAction { * For example, a Spearman might have 1 attack, 2 defense, and 1 movement. **/ public class UnitPrototype : IProducible { - public class Unique { - public Civilization civilization; - public UnitPrototype replace; - } - public string name { get; set; } // The name to use when searching for animations for this unit. public string artName { get; set; } @@ -39,8 +34,9 @@ public class Unique { public int bombard { get; set; } public int movement { get; set; } public int iconIndex { get; set; } + public HashSet producibleBy { get; set; } = []; + public UnitPrototype upgradeTo; - public Unique unique; public bool unproducible; public HashSet categories = new HashSet(); @@ -52,6 +48,7 @@ public class Unique { public HashSet requiredResources { get; set; } = []; public HashSet terraformActions = []; + public bool isWorker => terraformActions.Count > 0; public UnitPrototype() { } @@ -68,6 +65,18 @@ public UnitPrototype(SaveUnitPrototype proto, IEnumerable terraforms) terraformActions = proto.terraformActions.Select(id => terraforms.First(t => t.Id == id)).ToHashSet(); } + public int ShieldCost(HashSet civTraits, float costFactor) { + return (int)(shieldCost * costFactor); + } + + public bool IsLandUnit() { + return categories.Contains("Land"); + } + + public bool IsSeaUnit() { + return categories.Contains("Sea"); + } + public override string ToString() { return $"{name} ({attack}/{defense}/{movement})"; } @@ -93,45 +102,80 @@ public MapUnit GetInstance(GameData gameData) { return instance; } - public bool CanProduce(City city, HashSet accessibleResources) { - return MeetsProductionRequirements(city, accessibleResources) && !IsUnitObsolete(city, accessibleResources); - } - - public int ShieldCost(HashSet civTraits, float costFactor) { - return (int)(shieldCost * costFactor); - } + private UnitPrototype GetUnitUpgrade(Civilization civ) { + while (true) { + if (upgradeTo == null) return null; + if (!upgradeTo.producibleBy.Contains(civ)) { + upgradeTo.GetUnitUpgrade(civ); + } - public bool IsLandUnit() { - return categories.Contains("Land"); + return upgradeTo; + } } - public bool IsSeaUnit() { - return categories.Contains("Sea"); + public bool CanProduce(City city, HashSet accessibleResources) { + var unitUpgradeChain = city.owner.civilization.GetUpgradeChain(this); + return city.owner.civilization.IsUnitAvailable(this) + && this.MeetsProductionRequirements(city, accessibleResources) + && !this.IsUnitObsolete(city, accessibleResources); } // TODO: Consider golden ages when determining whether a unit is obsolete. // If a golden age has not yet been triggered and a unit can trigger one, // it shouldn't be marked as obsolete, even if its upgrade is available. private bool IsUnitObsolete(City city, HashSet accessibleResources) { - List upgradeChain = city.owner.civilization.GetUpgradeChain(this); + if (EngineStorage.gameData.rules.AllowLesserUnitProduction) return false; + + if (this.GetProducibleUpgrade(city, accessibleResources) == null) return false; - return upgradeChain.Any(upgrade => upgrade.MeetsProductionRequirements(city, accessibleResources)); + return true; } - private bool MeetsProductionRequirements(City city, HashSet accessibleResources) { - if (!city.owner.civilization.IsUnitAvailable(this)) { - return false; - } + // For example, if we want to check if the Trebuchet is obsolete for the Koreans, + // what we can do, because we don't want to hardcode anywhere that + // the Hwacha is the "replacement" to the Cannon (which is Trebuchet's upgrade) + // we can check if the upgrade (Artillery) of the upgrade (Cannon) of our unit (Trebuchet) + // has other units that upgrade to it and are available to the Koreans. + // This is how we get that, since we can built a Hwacha, + // which upgrades to Artillery, that the Trebuchet is obsolete. + private List GetUnitsThatUpgradeToThisUpgrade() { + List allUnits = EngineStorage.gameData.unitPrototypes.Where( + p => p.upgradeTo != null && p.upgradeTo == this.upgradeTo?.upgradeTo + ).ToList(); + return allUnits; + } + + // This should be the method we use to get the "true" upgrade of a given unit + public UnitPrototype GetProducibleUpgrade(City city, HashSet accessibleResources) { + UnitPrototype upgrade = this.GetUnitUpgrade(city.owner.civilization); + if (upgrade == null) return null; + + var unitUpgradeChain = city.owner.civilization.GetUpgradeChain(this); + + var potentialUnits = this.GetUnitsThatUpgradeToThisUpgrade(); + var units = unitUpgradeChain.Concat(potentialUnits.Where(uu => !unitUpgradeChain.Contains(uu))); + + // picking the last item, as when trying to get the upgrade for a Warrior, + // and Medieval Infantry is available, we don't want to return the Swordsman + var unitUpgrade = units.LastOrDefault(uu => + uu.MeetsProductionRequirements(city, accessibleResources) + && uu.producibleBy.Contains(city.owner.civilization) + ); + + return unitUpgrade; + } + + private bool MeetsProductionRequirements(City city, HashSet accessibleResources) { if (!city.owner.HasRequiredTechnology(this)) { return false; } - if (categories.Contains("Sea") && !city.location.NeighborsWater()) { + if (this.IsSeaUnit() && !city.location.NeighborsWater()) { return false; } - if (!requiredResources.All(accessibleResources.Contains)) { + if (!this.requiredResources.All(accessibleResources.Contains)) { return false; } diff --git a/EngineTests/GameData/MultiTurnDealsTest.cs b/EngineTests/GameData/MultiTurnDealsTest.cs index b55021037..5f9803f49 100644 --- a/EngineTests/GameData/MultiTurnDealsTest.cs +++ b/EngineTests/GameData/MultiTurnDealsTest.cs @@ -1,51 +1,15 @@ using System; using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; using C7GameData; using C7GameData.Save; +using EngineTests.Utils; using QueryCiv3; using Xunit; namespace EngineTests.GameData; -public class MultiTurnDealTest { - private static readonly string C7GameDataTestsFolderName = "EngineTests"; +public class MultiTurnDealTest : RemoteSaveLoader { private const string SAVES_FOLDER = "saves/multi-turn-deals"; - private static string getDataPath(string file) => Path.Combine(testDirectory, "data", file); - private static string defaultBicPath => Path.Combine(Civ3Location.GetCiv3Path(), "Conquests", "conquests.biq"); - private static string defaultPediaIconsPath => Path.Combine(Civ3Location.GetCiv3Path(), "Conquests", "Text", "PediaIcons.txt"); - - private static string testDirectory { - get { - string[] parts = AppDomain.CurrentDomain.BaseDirectory.Split(Path.DirectorySeparatorChar); - int pos = parts.Reverse().ToList().FindIndex(s => s == C7GameDataTestsFolderName); - string up = string.Concat("..", Path.DirectorySeparatorChar); - string relativePath = string.Concat(Enumerable.Repeat(up, pos - 1)); - return Path.GetFullPath(relativePath); - } - } - - private static async Task<(SaveGame game, Exception ex, string savePath)> LoadGameAndData(string saveName, string savesFolder, string uri, string biqPath = "default", string pediaPath = "default") { - string savesPath = getDataPath(savesFolder); - Directory.CreateDirectory(savesPath); - - string savePath = Path.Combine(savesPath, saveName); - using HttpClient client = new(); - byte[] fileData = await client.GetByteArrayAsync($"{uri}"); - await File.WriteAllBytesAsync(savePath, fileData); - - FileInfo saveFile = new DirectoryInfo(savesPath).GetFiles().First(f => f.Name == saveName); - - SaveGame game = null; - Exception ex = Record.Exception(() => { - game = ImportCiv3.ImportSav(saveFile.FullName, biqPath == "default" ? defaultBicPath : biqPath, (relativeModePath) => - { return pediaPath == "default" ? defaultPediaIconsPath : pediaPath; }); - }); - return (game, ex, savePath); - } - [Fact] public async void TestMultiTurnDeal_Save_A() { // When running the tests via github actions, civ3 isn't installed so we diff --git a/EngineTests/GameData/UnitPrototypeTest.cs b/EngineTests/GameData/UnitPrototypeTest.cs new file mode 100644 index 000000000..1907fa217 --- /dev/null +++ b/EngineTests/GameData/UnitPrototypeTest.cs @@ -0,0 +1,554 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using C7Engine; +using C7GameData; +using C7GameData.Save; +using EngineTests.Utils; +using QueryCiv3; +using Xunit; + +namespace EngineTests.GameData; + +public class UnitPrototypeConquestsTest : RemoteSaveLoader { + private const string SAVES_FOLDER = "saves/unit-availability"; + [Fact] + public async void UnitAvailability_SAV() { + // This tests a Conquests game with Conquests rules from a .SAV file + + #region setup + // When running the tests via github actions, civ3 isn't installed so we + // can't load the default bic. + // + // See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables + // for a full list of env vars. + string is_on_github = System.Environment.GetEnvironmentVariable("CI"); + if (is_on_github != null) { + return; + } + + string saveName = "Conquests 16 Players.SAV"; + string uri = "https://www.dropbox.com/scl/fi/gmxbx1mtrammzfc6vly1g/Conquests-16-Players.SAV?rlkey=2z1es5aetqva4ymv59qduq1at&st=d0udmb3w&dl=1"; + + (SaveGame game, Exception ex, string savePath) = await LoadGameAndData(saveName, SAVES_FOLDER, uri); + + Assert.Null(ex); + Assert.NotNull(game); + Assert.True(File.Exists(savePath)); + + C7GameData.GameData gd = game.ToGameData(PathUtils.luaRulesDir); + EngineStorage.InitializeGameDataForTests(gd); + + List protos = gd.unitPrototypes; + + // setup civilizations and players + var americans = gd.civilizations.FirstOrDefault(c => c.name == "America"); + var amer = new Player() { civilization = americans }; + var ottomans = gd.civilizations.FirstOrDefault(c => c.name == "Ottomans"); + var otto = new Player() { civilization = ottomans }; + var russians = gd.civilizations.FirstOrDefault(c => c.name == "Russia"); + var rus = new Player() { civilization = russians }; + var koreans = gd.civilizations.FirstOrDefault(c => c.name == "Korea"); + var kor = new Player() { civilization = koreans }; + var hittites = gd.civilizations.FirstOrDefault(c => c.name == "Hittites"); + var hit = new Player() { civilization = hittites }; + + // setup resources + var noResources = new HashSet(){}; + var horses = gd.Resources.FirstOrDefault(r => r.Key == "Horses"); + var iron = gd.Resources.FirstOrDefault(r => r.Key == "Iron"); + var saltpeter = gd.Resources.FirstOrDefault(r => r.Key == "Saltpeter"); + var rubber = gd.Resources.FirstOrDefault(r => r.Key == "Rubber"); + var oil = gd.Resources.FirstOrDefault(r => r.Key == "Oil"); + var aluminum = gd.Resources.FirstOrDefault(r => r.Key == "Aluminum"); + + // setup techs + var wheel = gd.techs.FirstOrDefault(t => t.Name == "The Wheel"); + var horsebackRiding = gd.techs.FirstOrDefault(t => t.Name == "Horseback Riding"); + var mathematics = gd.techs.FirstOrDefault(t => t.Name == "Mathematics"); + var engineering = gd.techs.FirstOrDefault(t => t.Name == "Engineering"); + var chivalry = gd.techs.FirstOrDefault(t => t.Name == "Chivalry"); + var metallurgy = gd.techs.FirstOrDefault(t => t.Name == "Metallurgy"); + var milTradition = gd.techs.FirstOrDefault(t => t.Name == "Military Tradition"); + var flight = gd.techs.FirstOrDefault(t => t.Name == "Flight"); + var replaceableParts = gd.techs.FirstOrDefault(t => t.Name == "Replaceable Parts"); + var rocketry = gd.techs.FirstOrDefault(t => t.Name == "Rocketry"); + + // setup some unit prototypes + var horseman = protos.FirstOrDefault(p => p.name == "Horseman"); + var chariot = protos.FirstOrDefault(p => p.name == "Chariot"); + var threeManChariot = protos.FirstOrDefault(p => p.name == "Three-Man Chariot"); + var knight = protos.FirstOrDefault(p => p.name == "Knight"); + var cavalry = protos.FirstOrDefault(p => p.name == "Cavalry"); + var sipahi = protos.FirstOrDefault(p => p.name == "Sipahi"); + var cossack = protos.FirstOrDefault(p => p.name == "Cossack"); + var catapult = protos.FirstOrDefault(p => p.name == "Catapult"); + var trebuchet = protos.FirstOrDefault(p => p.name == "Trebuchet"); + var cannon = protos.FirstOrDefault(p => p.name == "Cannon"); + var hwacha = protos.FirstOrDefault(p => p.name == "Hwach'a"); + var artillery = protos.FirstOrDefault(p => p.name == "Artillery"); + var fighter = protos.FirstOrDefault(p => p.name == "Fighter"); + var jet = protos.FirstOrDefault(p => p.name == "Jet Fighter"); + var f15 = protos.FirstOrDefault(p => p.name == "F-15"); + + #endregion + + #region Ottomans + Tile ankaraTile = new Tile(ID.None("tile")); + City ankara = new City(ankaraTile, otto, "Ankara", gd.ids.CreateID("Ankara")); + + Assert.True(horseman.producibleBy.Contains(ottomans)); + Assert.True(knight.producibleBy.Contains(ottomans)); + Assert.True(sipahi.producibleBy.Contains(ottomans)); + Assert.False(cavalry.producibleBy.Contains(ottomans)); + + otto.knownTechs.Add(horsebackRiding.id); + + Assert.True(horseman.CanProduce(ankara, new HashSet() { horses, iron })); + Assert.False(knight.CanProduce(ankara, new HashSet() { horses, iron, saltpeter })); + Assert.False(sipahi.CanProduce(ankara, new HashSet() { horses, iron, saltpeter })); + Assert.False(cavalry.CanProduce(ankara, new HashSet() { horses, iron, saltpeter })); + + otto.knownTechs.Add(chivalry.id); + // The Horseman is now obsolete + Assert.False(horseman.CanProduce(ankara, new HashSet() { horses, iron })); + Assert.True(horseman.GetProducibleUpgrade(ankara, new HashSet() { horses, iron }) == knight); + + // even though we have saltpeter, we don't know Military Tradition, so the Knight is not yet obsolete + Assert.True(knight.CanProduce(ankara, new HashSet() { horses, iron, saltpeter })); + Assert.False(cavalry.CanProduce(ankara, new HashSet() { horses, iron, saltpeter })); + + otto.knownTechs.Add(milTradition.id); + // the Knight is now obsolete + Assert.False(knight.CanProduce(ankara, new HashSet() { horses, iron, saltpeter })); + Assert.True(knight.GetProducibleUpgrade(ankara, new HashSet() { horses, iron, saltpeter }) == sipahi); + Assert.False(cavalry.CanProduce(ankara, new HashSet() { horses, iron, saltpeter })); + + Assert.True(sipahi.GetProducibleUpgrade(ankara, new HashSet() { horses, iron, saltpeter }) == null); + #endregion + + #region Hittites + Tile hattusasTile = new Tile(ID.None("tile")); + City hattusas = new City(hattusasTile, hit, "Hattusas", gd.ids.CreateID("Hattusas")); + + Assert.True(horseman.producibleBy.Contains(hittites)); + Assert.False(chariot.producibleBy.Contains(hittites)); + Assert.True(threeManChariot.producibleBy.Contains(hittites)); + + + hit.knownTechs.Add(wheel.id); + + Assert.False(horseman.CanProduce(hattusas, new HashSet() { horses })); + Assert.False(chariot.CanProduce(hattusas, new HashSet() { horses })); + Assert.True(threeManChariot.CanProduce(hattusas, new HashSet() { horses })); + + #endregion + + #region Russians + Tile moscowTile = new Tile(ID.None("tile")); + City moscow = new City(moscowTile, rus, "Moscow", gd.ids.CreateID("Moscow")); + + Assert.True(horseman.producibleBy.Contains(russians)); + Assert.True(knight.producibleBy.Contains(russians)); + Assert.True(cossack.producibleBy.Contains(russians)); + Assert.False(cavalry.producibleBy.Contains(russians)); + + rus.knownTechs.Add(horsebackRiding.id); + + Assert.True(horseman.CanProduce(moscow, new HashSet() { horses, iron })); + Assert.False(knight.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + Assert.False(sipahi.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + Assert.False(cavalry.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + + rus.knownTechs.Add(chivalry.id); + // The Horseman is now obsolete + Assert.False(horseman.CanProduce(moscow, new HashSet() { horses, iron })); + Assert.True(horseman.GetProducibleUpgrade(moscow, new HashSet() { horses, iron }) == knight); + + // even though we have saltpeter, we don't know Military Tradition, so the Knight is not yet obsolete + Assert.True(knight.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + Assert.False(cavalry.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + Assert.False(cossack.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + + rus.knownTechs.Add(milTradition.id); + // the Knight is now obsolete + Assert.False(knight.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + Assert.True(knight.GetProducibleUpgrade(moscow, new HashSet() { horses, iron, saltpeter }) == cossack); + Assert.False(cavalry.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + + Assert.True(cossack.GetProducibleUpgrade(moscow, new HashSet() { horses, iron, saltpeter }) == null); + #endregion + + #region Korean + Tile seoulTile = new Tile(ID.None("tile")); + City seoul = new City(seoulTile, kor, "Seoul", gd.ids.CreateID("Seoul")); + + // In conquests they both the Cannon and the Hwacha are permitted in the rules for the Koreans, + // maybe an oversight in the original rules, but it's there. + // We only care that the Trebuchet can upgrade to a Hwacha and not to a Cannon + Assert.True(catapult.producibleBy.Contains(koreans)); + Assert.True(trebuchet.producibleBy.Contains(koreans)); + Assert.True(cannon.producibleBy.Contains(koreans)); + Assert.True(hwacha.producibleBy.Contains(koreans)); + + Assert.False(catapult.CanProduce(seoul, noResources)); + Assert.False(trebuchet.CanProduce(seoul, noResources)); + Assert.False(cannon.CanProduce(seoul, noResources)); + Assert.False(hwacha.CanProduce(seoul, noResources)); + + kor.knownTechs.Add(mathematics.id); + + Assert.True(catapult.CanProduce(seoul, noResources)); + Assert.False(trebuchet.CanProduce(seoul, noResources)); + Assert.False(cannon.CanProduce(seoul, noResources)); + Assert.False(hwacha.CanProduce(seoul, noResources)); + + kor.knownTechs.Add(engineering.id); + + Assert.False(catapult.CanProduce(seoul, noResources)); + Assert.True(trebuchet.CanProduce(seoul, noResources)); + Assert.False(cannon.CanProduce(seoul, noResources)); + Assert.False(hwacha.CanProduce(seoul, noResources)); + + Assert.True(catapult.GetProducibleUpgrade(seoul, noResources) == trebuchet); + + kor.knownTechs.Add(metallurgy.id); + + Assert.False(catapult.CanProduce(seoul, new HashSet() { saltpeter })); + Assert.False(trebuchet.CanProduce(seoul, new HashSet() { saltpeter })); + Assert.False(cannon.CanProduce(seoul, new HashSet() { saltpeter })); + Assert.True(hwacha.CanProduce(seoul, new HashSet() { saltpeter })); + + Assert.True(trebuchet.GetProducibleUpgrade(seoul, new HashSet() { saltpeter }) == hwacha); + Assert.False(trebuchet.GetProducibleUpgrade(seoul, new HashSet() { saltpeter }) == cannon); + + kor.knownTechs.Add(replaceableParts.id); + Assert.False(hwacha.CanProduce(seoul, new HashSet() { saltpeter })); + Assert.True(artillery.CanProduce(seoul, new HashSet() { })); + Assert.True(hwacha.GetProducibleUpgrade(seoul, new HashSet() { }) == artillery); + + #endregion + + #region America + Tile seattleTile = new Tile(ID.None("tile")); + City seattle = new City(seattleTile, amer, "Seattle", gd.ids.CreateID("Seattle")); + + Assert.True(fighter.producibleBy.Contains(americans)); + Assert.False(jet.producibleBy.Contains(americans)); + Assert.True(f15.producibleBy.Contains(americans)); + + Assert.False(fighter.CanProduce(seattle, new HashSet() { oil, aluminum })); + Assert.False(jet.CanProduce(seattle, new HashSet() { oil, aluminum })); + Assert.False(f15.CanProduce(seattle, new HashSet() { oil, aluminum })); + + amer.knownTechs.Add(flight.id); + Assert.True(fighter.CanProduce(seattle, new HashSet() { oil, aluminum })); + Assert.False(jet.CanProduce(seattle, new HashSet() { oil, aluminum })); + Assert.False(f15.CanProduce(seattle, new HashSet() { oil, aluminum })); + + Assert.True(fighter.GetProducibleUpgrade(seattle, new HashSet() { oil, aluminum }) == null); + + amer.knownTechs.Add(rocketry.id); + Assert.True(fighter.CanProduce(seattle, new HashSet() { oil })); + Assert.False(fighter.CanProduce(seattle, new HashSet() { oil, aluminum })); + Assert.False(jet.CanProduce(seattle, new HashSet() { oil, aluminum })); + Assert.True(f15.CanProduce(seattle, new HashSet() { oil, aluminum })); + + Assert.True(fighter.GetProducibleUpgrade(seattle, new HashSet() { oil, aluminum }) == f15); + #endregion + } + + [Fact] + public async void UnitAvailability_JSON() { + // This tests a Conquests game with Conquests rules from a .json file + #region setup + // When running the tests via github actions, civ3 isn't installed so we + // can't load the default bic. + // + // See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables + // for a full list of env vars. + string is_on_github = System.Environment.GetEnvironmentVariable("CI"); + if (is_on_github != null) { + return; + } + + string saveName = "Conquests 16 Players.json"; + string uri = "https://www.dropbox.com/scl/fi/57e1sycuoml8xqu1xypah/Conquests-16-Players.json?rlkey=45ukhvcnujqapniwidg4w6bic&st=p5pvpzm0&dl=1"; + + (SaveGame game, Exception ex, string savePath) = await LoadGameAndData(saveName, SAVES_FOLDER, uri); + + Assert.Null(ex); + Assert.NotNull(game); + Assert.True(File.Exists(savePath)); + + C7GameData.GameData gd = game.ToGameData(PathUtils.luaRulesDir); + EngineStorage.InitializeGameDataForTests(gd); + + List protos = gd.unitPrototypes; + + // setup civilizations and players + var americans = gd.civilizations.FirstOrDefault(c => c.name == "America"); + var amer = new Player() { civilization = americans }; + var ottomans = gd.civilizations.FirstOrDefault(c => c.name == "Ottomans"); + var otto = new Player() { civilization = ottomans }; + var russians = gd.civilizations.FirstOrDefault(c => c.name == "Russia"); + var rus = new Player() { civilization = russians }; + var koreans = gd.civilizations.FirstOrDefault(c => c.name == "Korea"); + var kor = new Player() { civilization = koreans }; + var hittites = gd.civilizations.FirstOrDefault(c => c.name == "Hittites"); + var hit = new Player() { civilization = hittites }; + + // setup resources + var noResources = new HashSet(){}; + var horses = gd.Resources.FirstOrDefault(r => r.Key == "Horses"); + var iron = gd.Resources.FirstOrDefault(r => r.Key == "Iron"); + var saltpeter = gd.Resources.FirstOrDefault(r => r.Key == "Saltpeter"); + var rubber = gd.Resources.FirstOrDefault(r => r.Key == "Rubber"); + var oil = gd.Resources.FirstOrDefault(r => r.Key == "Oil"); + var aluminum = gd.Resources.FirstOrDefault(r => r.Key == "Aluminum"); + + // setup techs + var wheel = gd.techs.FirstOrDefault(t => t.Name == "The Wheel"); + var horsebackRiding = gd.techs.FirstOrDefault(t => t.Name == "Horseback Riding"); + var mathematics = gd.techs.FirstOrDefault(t => t.Name == "Mathematics"); + var engineering = gd.techs.FirstOrDefault(t => t.Name == "Engineering"); + var chivalry = gd.techs.FirstOrDefault(t => t.Name == "Chivalry"); + var metallurgy = gd.techs.FirstOrDefault(t => t.Name == "Metallurgy"); + var milTradition = gd.techs.FirstOrDefault(t => t.Name == "Military Tradition"); + var flight = gd.techs.FirstOrDefault(t => t.Name == "Flight"); + var replaceableParts = gd.techs.FirstOrDefault(t => t.Name == "Replaceable Parts"); + var rocketry = gd.techs.FirstOrDefault(t => t.Name == "Rocketry"); + + // setup some unit prototypes + var horseman = protos.FirstOrDefault(p => p.name == "Horseman"); + var chariot = protos.FirstOrDefault(p => p.name == "Chariot"); + var threeManChariot = protos.FirstOrDefault(p => p.name == "Three-Man Chariot"); + var knight = protos.FirstOrDefault(p => p.name == "Knight"); + var cavalry = protos.FirstOrDefault(p => p.name == "Cavalry"); + var sipahi = protos.FirstOrDefault(p => p.name == "Sipahi"); + var cossack = protos.FirstOrDefault(p => p.name == "Cossack"); + var catapult = protos.FirstOrDefault(p => p.name == "Catapult"); + var trebuchet = protos.FirstOrDefault(p => p.name == "Trebuchet"); + var cannon = protos.FirstOrDefault(p => p.name == "Cannon"); + var hwacha = protos.FirstOrDefault(p => p.name == "Hwach'a"); + var artillery = protos.FirstOrDefault(p => p.name == "Artillery"); + var fighter = protos.FirstOrDefault(p => p.name == "Fighter"); + var jet = protos.FirstOrDefault(p => p.name == "Jet Fighter"); + var f15 = protos.FirstOrDefault(p => p.name == "F-15"); + + #endregion + + #region Hittites + Tile hattusasTile = new Tile(ID.None("tile")); + City hattusas = new City(hattusasTile, hit, "Hattusas", gd.ids.CreateID("Hattusas")); + + Assert.True(horseman.producibleBy.Contains(hittites)); + Assert.False(chariot.producibleBy.Contains(hittites)); + Assert.True(threeManChariot.producibleBy.Contains(hittites)); + + + hit.knownTechs.Add(wheel.id); + + Assert.False(horseman.CanProduce(hattusas, new HashSet() { horses })); + Assert.False(chariot.CanProduce(hattusas, new HashSet() { horses })); + Assert.True(threeManChariot.CanProduce(hattusas, new HashSet() { horses })); + + #endregion + + #region Russians + Tile moscowTile = new Tile(ID.None("tile")); + City moscow = new City(moscowTile, rus, "Moscow", gd.ids.CreateID("Moscow")); + + Assert.True(horseman.producibleBy.Contains(russians)); + Assert.True(knight.producibleBy.Contains(russians)); + Assert.True(cossack.producibleBy.Contains(russians)); + Assert.False(cavalry.producibleBy.Contains(russians)); + + rus.knownTechs.Add(horsebackRiding.id); + + Assert.True(horseman.CanProduce(moscow, new HashSet() { horses, iron })); + Assert.False(knight.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + Assert.False(sipahi.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + Assert.False(cavalry.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + + rus.knownTechs.Add(chivalry.id); + // The Horseman is now obsolete + Assert.False(horseman.CanProduce(moscow, new HashSet() { horses, iron })); + Assert.True(horseman.GetProducibleUpgrade(moscow, new HashSet() { horses, iron }) == knight); + + // even though we have saltpeter, we don't know Military Tradition, so the Knight is not yet obsolete + Assert.True(knight.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + Assert.False(cavalry.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + Assert.False(cossack.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + + rus.knownTechs.Add(milTradition.id); + // the Knight is now obsolete + Assert.False(knight.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + Assert.True(knight.GetProducibleUpgrade(moscow, new HashSet() { horses, iron, saltpeter }) == cossack); + Assert.False(cavalry.CanProduce(moscow, new HashSet() { horses, iron, saltpeter })); + + Assert.True(cossack.GetProducibleUpgrade(moscow, new HashSet() { horses, iron, saltpeter }) == null); + #endregion + + #region Korean + Tile seoulTile = new Tile(ID.None("tile")); + City seoul = new City(seoulTile, kor, "Seoul", gd.ids.CreateID("Seoul")); + + // In conquests they both the Cannon and the Hwacha are permitted in the rules for the Koreans, + // maybe an oversight in the original rules, but it's there. + // We only care that the Trebuchet can upgrade to a Hwacha and not to a Cannon + Assert.True(catapult.producibleBy.Contains(koreans)); + Assert.True(trebuchet.producibleBy.Contains(koreans)); + Assert.True(cannon.producibleBy.Contains(koreans)); + Assert.True(hwacha.producibleBy.Contains(koreans)); + + Assert.False(catapult.CanProduce(seoul, noResources)); + Assert.False(trebuchet.CanProduce(seoul, noResources)); + Assert.False(cannon.CanProduce(seoul, noResources)); + Assert.False(hwacha.CanProduce(seoul, noResources)); + + kor.knownTechs.Add(mathematics.id); + + Assert.True(catapult.CanProduce(seoul, noResources)); + Assert.False(trebuchet.CanProduce(seoul, noResources)); + Assert.False(cannon.CanProduce(seoul, noResources)); + Assert.False(hwacha.CanProduce(seoul, noResources)); + + kor.knownTechs.Add(engineering.id); + + Assert.False(catapult.CanProduce(seoul, noResources)); + Assert.True(trebuchet.CanProduce(seoul, noResources)); + Assert.False(cannon.CanProduce(seoul, noResources)); + Assert.False(hwacha.CanProduce(seoul, noResources)); + + Assert.True(catapult.GetProducibleUpgrade(seoul, noResources) == trebuchet); + + kor.knownTechs.Add(metallurgy.id); + + Assert.False(catapult.CanProduce(seoul, new HashSet() { saltpeter })); + Assert.False(trebuchet.CanProduce(seoul, new HashSet() { saltpeter })); + Assert.False(cannon.CanProduce(seoul, new HashSet() { saltpeter })); + Assert.True(hwacha.CanProduce(seoul, new HashSet() { saltpeter })); + + Assert.True(trebuchet.GetProducibleUpgrade(seoul, new HashSet() { saltpeter }) == hwacha); + Assert.False(trebuchet.GetProducibleUpgrade(seoul, new HashSet() { saltpeter }) == cannon); + + kor.knownTechs.Add(replaceableParts.id); + Assert.False(hwacha.CanProduce(seoul, new HashSet() { saltpeter })); + Assert.True(artillery.CanProduce(seoul, new HashSet() { })); + Assert.True(hwacha.GetProducibleUpgrade(seoul, new HashSet() { }) == artillery); + + #endregion + } + +} + +public class UnitPrototypeScenarioTest : RemoteSaveLoader { + private const string SAVES_FOLDER = "saves/unit-availability"; + [Fact] + public async void UnitAvailability_SAV() { + // This tests a Conquests scenario with custom rules + + #region setup + // When running the tests via github actions, civ3 isn't installed so we + // can't load the default bic. + // + // See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables + // for a full list of env vars. + string is_on_github = System.Environment.GetEnvironmentVariable("CI"); + if (is_on_github != null) { + return; + } + string scenarioBiqPath = Path.Combine(Civ3Location.GetCiv3Path(), "Conquests", "Conquests", "4 Middle Ages.biq"); + string scenarioPediaPath = Path.Combine(Civ3Location.GetCiv3Path(), "Conquests", "Conquests", "Middle Ages", "Text", "PediaIcons.txt"); + + string saveName = "Middle Ages Scenario Abbasids, 843 AD.SAV"; + string uri = "https://www.dropbox.com/scl/fi/nz7wp7whu326i7em8jle9/Middle-Ages-Scenario-Abbasids-843-AD.SAV?rlkey=oz65m286jbchytd3yuu5ksr6f&st=b3dsheus&dl=1"; + + (SaveGame game, Exception ex, string savePath) = await LoadGameAndData(saveName, SAVES_FOLDER, uri, scenarioBiqPath, scenarioPediaPath); + + Assert.Null(ex); + Assert.NotNull(game); + Assert.True(File.Exists(savePath)); + + C7GameData.GameData gd = game.ToGameData(PathUtils.luaRulesDir); + EngineStorage.InitializeGameDataForTests(gd); + + List protos = gd.unitPrototypes; + + // setup civilizations and players + var byzantines = gd.civilizations.FirstOrDefault(c => c.name == "Byzantines"); + var byz = new Player() { civilization = byzantines }; + var kievanRus = gd.civilizations.FirstOrDefault(c => c.name == "Kievan Rus"); + var rus = new Player() { civilization = kievanRus }; + var bulgars = gd.civilizations.FirstOrDefault(c => c.name == "Bulgars"); + var bul = new Player() { civilization = bulgars }; + + // setup resources + var noResources = new HashSet(){}; + var horses = gd.Resources.FirstOrDefault(r => r.Key == "Horses"); + var iron = gd.Resources.FirstOrDefault(r => r.Key == "Iron"); + + // setup techs + var horsebackRiding = gd.techs.FirstOrDefault(t => t.Name == "Horseback Riding"); + var heavyCavalry = gd.techs.FirstOrDefault(t => t.Name == "Heavy Cavalry"); + var feudalism = gd.techs.FirstOrDefault(t => t.Name == "Feudalism"); + + // setup some unit prototypes + var horseman = protos.FirstOrDefault(p => p.name == "Horseman"); + var cataphract = protos.FirstOrDefault(p => p.name == "Cataphract"); + var knight = protos.FirstOrDefault(p => p.name == "Knight"); + + #endregion + + #region Byzantines + Tile constantinopleTile = new Tile(ID.None("tile")); + City constantinople = new City(constantinopleTile, byz, "Constantinople", gd.ids.CreateID("Constantinople")); + + Assert.True(horseman.producibleBy.Contains(byzantines)); + Assert.True(cataphract.producibleBy.Contains(byzantines)); + Assert.False(knight.producibleBy.Contains(byzantines)); + + byz.knownTechs.Add(horsebackRiding.id); + + Assert.True(horseman.CanProduce(constantinople, new HashSet() { horses, iron })); + Assert.False(knight.CanProduce(constantinople, new HashSet() { horses, iron })); + Assert.False(cataphract.CanProduce(constantinople, new HashSet() { horses, iron })); + + byz.knownTechs.Add(heavyCavalry.id); + Assert.True(horseman.CanProduce(constantinople, new HashSet() { horses })); + Assert.False(horseman.CanProduce(constantinople, new HashSet() { horses, iron })); + Assert.True(cataphract.CanProduce(constantinople, new HashSet() { horses, iron })); + Assert.True(horseman.GetProducibleUpgrade(constantinople, new HashSet() { horses }) == null); + Assert.True(horseman.GetProducibleUpgrade(constantinople, new HashSet() { horses, iron }) == cataphract); + + byz.knownTechs.Add(feudalism.id); + // Cataphract upgrades to Knight, not the other way around + // even if the Knight becomes available later on in the game + Assert.False(knight.CanProduce(constantinople, new HashSet() { horses, iron })); + Assert.True(cataphract.CanProduce(constantinople, new HashSet() { horses, iron })); + #endregion + + #region Bulgars + Tile sofiaTile = new Tile(ID.None("tile")); + City sofia = new City(sofiaTile, bul, "Sofia", gd.ids.CreateID("Sofia")); + + Assert.True(horseman.producibleBy.Contains(bulgars)); + Assert.False(cataphract.producibleBy.Contains(bulgars)); + Assert.False(knight.producibleBy.Contains(bulgars)); + + bul.knownTechs.Add(horsebackRiding.id); + + Assert.True(horseman.CanProduce(sofia, new HashSet() { horses, iron })); + Assert.False(knight.CanProduce(sofia, new HashSet() { horses, iron })); + Assert.False(cataphract.CanProduce(sofia, new HashSet() { horses, iron })); + + bul.knownTechs.Add(heavyCavalry.id); + Assert.True(horseman.GetProducibleUpgrade(sofia, new HashSet() { horses, iron }) == null); + + bul.knownTechs.Add(feudalism.id); + Assert.True(horseman.GetProducibleUpgrade(sofia, new HashSet() { horses, iron }) == null); + #endregion + } +} diff --git a/EngineTests/Utils/RemoteSaveLoader.cs b/EngineTests/Utils/RemoteSaveLoader.cs new file mode 100644 index 000000000..39d45bad5 --- /dev/null +++ b/EngineTests/Utils/RemoteSaveLoader.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using C7Engine; +using C7GameData.Save; +using QueryCiv3; +using Xunit; + +namespace EngineTests.Utils; + +public class RemoteSaveLoader { + protected static readonly string C7GameDataTestsFolderName = "EngineTests"; + private static string getDataPath(string file) => Path.Combine(testDirectory, "data", file); + private static string defaultBicPath => Path.Combine(Civ3Location.GetCiv3Path(), "Conquests", "conquests.biq"); + private static string defaultPediaIconsPath => Path.Combine(Civ3Location.GetCiv3Path(), "Conquests", "Text", "PediaIcons.txt"); + + private static string testDirectory { + get { + string[] parts = AppDomain.CurrentDomain.BaseDirectory.Split(Path.DirectorySeparatorChar); + int pos = parts.Reverse().ToList().FindIndex(s => s == C7GameDataTestsFolderName); + string up = string.Concat("..", Path.DirectorySeparatorChar); + string relativePath = string.Concat(Enumerable.Repeat(up, pos - 1)); + return Path.GetFullPath(relativePath); + } + } + + protected static async Task<(SaveGame game, Exception ex, string savePath)> LoadGameAndData(string saveName, string savesFolder, string uri, string biqPath = "default", string pediaPath = "default") { + string savesPath = getDataPath(savesFolder); + Directory.CreateDirectory(savesPath); + + string savePath = Path.Combine(savesPath, saveName); + using HttpClient client = new(); + byte[] fileData = await client.GetByteArrayAsync($"{uri}"); + if (uri.Contains("dropbox") && uri.EndsWith("&dl=0")) + throw new Exception("Change the dl=0 to dl=1 at the end of the url to able to download the file, " + + "otherwise we will get an error from the .biq not being able to find the correct file headers"); + await File.WriteAllBytesAsync(savePath, fileData); + + FileInfo saveFile = new DirectoryInfo(savesPath).GetFiles().First(f => f.Name == saveName); + + SaveGame game = null; + Exception ex = Record.Exception(() => { + game = SaveManager.LoadSave(saveFile.FullName, biqPath == "default" ? defaultBicPath : biqPath, + (relativeModePath) => { return pediaPath == "default" ? defaultPediaIconsPath : pediaPath; }); + }); + + return (game, ex, savePath); + } +}