Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,144 changes: 2,021 additions & 123 deletions C7/Lua/game_modes/base-ruleset.json

Large diffs are not rendered by default.

18 changes: 1 addition & 17 deletions C7/UIElements/Advisors/TechBox.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -250,23 +247,10 @@ private int CalculateTechBoxSizeCost(GameData gameData) {
List<UnitPrototype> units = new();
// List of units that require this tech to be built, taking into account that some are unique
List<UnitPrototype> unitsRequiringTech = gameData.unitPrototypes.Where(u => u.requiredTech == tech).ToList();
HashSet<string> 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);
}
}
Expand Down
38 changes: 15 additions & 23 deletions C7Engine/C7GameData/Civilization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<UnitPrototype> GetUpgradeChain(UnitPrototype unit) {
List<UnitPrototype> 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;
Expand All @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions C7Engine/C7GameData/GameData.cs
Original file line number Diff line number Diff line change
@@ -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<GameData>();
Expand Down
114 changes: 15 additions & 99 deletions C7Engine/C7GameData/ImportCiv3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ private void ImportSharedBiqData() {
ImportCiv3Resources();
ImportTerraforms();
ImportUnitPrototypes();
ImportUniqueUnitReplacements();
ImportUnitUpgrades();
ImportBuildings();
ImportCiv3TerrainTypes();
Expand Down Expand Up @@ -1048,23 +1047,24 @@ private static IEnumerable<TerraformKey> 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();

// TODO: Implement proper logic for Army production
return availableTo.Length == 0 || prto.ShieldCost < 1 || prto.Army;
}

private HashSet<string> ImportUnitAvailability(PRTO prto) {
HashSet<string> 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) {
Expand All @@ -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) {
Expand All @@ -1104,86 +1103,25 @@ 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);
}
}
}

// 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<SaveUnitPrototype, SaveUnitPrototype> 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<SaveUnitPrototype, SaveUnitPrototype> BuildUpgradeDict() {
PRTO[] Prto = biq.Prto ?? defaultBiq.Prto;
var unitPrototypeDict = save.UnitPrototypes.ToDictionary(b => b.name);
Expand All @@ -1203,29 +1141,6 @@ private Dictionary<SaveUnitPrototype, SaveUnitPrototype> 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<SaveUnitPrototype, SaveUnitPrototype> 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;

Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions C7Engine/C7GameData/Rules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
15 changes: 1 addition & 14 deletions C7Engine/C7GameData/Save/SaveGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using C7Engine;
using Serilog;

namespace C7GameData.Save {

Expand Down Expand Up @@ -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();
}
Expand Down
16 changes: 3 additions & 13 deletions C7Engine/C7GameData/Save/SaveUnitPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -19,8 +14,9 @@ public class Unique {
public int movement { get; set; }
public int iconIndex { get; set; }

public HashSet<string> producibleBy = [];

public string upgradeTo;
public Unique unique;
public bool unproducible;

public HashSet<string> categories = new HashSet<string>();
Expand All @@ -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<string>(proto.categories);
actions = proto.actions;
attributes = new HashSet<string>(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();
}
}
}
Loading
Loading