diff --git a/C7/Lua/rules/civ3.lua b/C7/Lua/rules/civ3.lua index d590cfa24..aac1852aa 100644 --- a/C7/Lua/rules/civ3.lua +++ b/C7/Lua/rules/civ3.lua @@ -7,4 +7,5 @@ return { --]] terraforms = require "civ3.terraforms", terrain_improvements = require "civ3.terrain_improvements", + inflows = require "civ3.inflows" } diff --git a/C7/Lua/rules/civ3/inflows.lua b/C7/Lua/rules/civ3/inflows.lua new file mode 100644 index 000000000..6c9fa8b8b --- /dev/null +++ b/C7/Lua/rules/civ3/inflows.lua @@ -0,0 +1,124 @@ +local function rules() + return GAME_DATA().rules +end + +local inflows = {} + +-- to match an item in a list based on a predicate +local function any(list, predicate) + for _, v in ipairs(list) do + if predicate(v) then + return true + end + end + return false +end + +local function doubles_wealth_production(tech) + return tech.DoublesWealthProduction == true +end + +-- context is [ Player player, City city ] +-- this is the actual (minimal) implementation we would do for Wealth, for conquests +local function extra_commerce_calculation(context) + local player = context.player + local city = context.city + + local useful_shields = city.CurrentProductionYield().useful + local known_techs = player.GetKnownTechs() + local double_effect = any(known_techs, doubles_wealth_production) + local ratio = double_effect and (rules().ShieldCostPerGold / 2) or rules().ShieldCostPerGold + + return math.max(1, useful_shields / ratio) +end + +-- Any and all of the table values below should return an integer +-- that will be added to the respective base value. +-- +-- for example commerce will be added to the overall commerce income, culture to the current culture per turn. +-- +-- maintenance, unitsupport and corruption are the ones that will be subtracted by their current base value +-- so if you want MORE corruption/maintenance/unit support, the number should be negative + +inflows.result = { + wealth = { + commerce = function(context) + return extra_commerce_calculation(context) + end, + }, + -- add new inflow(s) as the example below. + -- The respective json entry with the yieldCalculation having the path to this new item + -- should be added in the json save file, under inflows + -- not all fields like commerce, culture, science etc are mandatory, as long as they reflect the ones in the json + + --placeholder = { + -- commerce = function(context) + -- -- replace with a hadcoded value, a method call, etc + -- return 0 + -- end, + -- culture = function(context) + -- -- replace with a hadcoded value, a method call, etc + -- return 0 + -- end, + -- science = function(context) + -- -- replace with a hadcoded value, a method call, etc + -- return 0 + -- end, + -- happiness = function(context) + -- -- replace with a hadcoded value, a method call, etc + -- return 0 + -- end, + -- maintenance = function(context) + -- -- replace with a hadcoded value, a method call, etc + -- return 0 + -- end, + -- unitsupport = function(context) + -- -- replace with a hadcoded value, a method call, etc + -- return 0 + -- end, + -- corruption = function(context) + -- -- replace with a hadcoded value, a method call, etc + -- return 0 + -- end, + --}, + + + -- json example of an entry + + --{ + -- "name": "Placeholder", <- this is the display name in the production box + -- "iconRowIndex": 29, + -- "localYield": [ + -- { + -- "yieldType": "commerce", + -- "yieldCalculation": "inflows.result.placeholder.commerce" + -- }, + -- { + -- "yieldType": "culture", + -- "yieldCalculation": "inflows.result.placeholder.culture" + -- }, + -- { + -- "yieldType": "science", + -- "yieldCalculation": "inflows.result.placeholder.science" + -- }, + -- { + -- "yieldType": "happiness", + -- "yieldCalculation": "inflows.result.placeholder.happiness" + -- }, + -- { + -- "yieldType": "maintenance", + -- "yieldCalculation": "inflows.result.placeholder.maintenance" + -- }, + -- { + -- "yieldType": "unitsupport", + -- "yieldCalculation": "inflows.result.placeholder.unitsupport" + -- }, + -- { + -- "yieldType": "corruption", + -- "yieldCalculation": "inflows.result.placeholder.corruption" + -- } + -- ] + --} +} + +return inflows diff --git a/C7/Lua/texture_configs/civ3/building_icons.lua b/C7/Lua/texture_configs/civ3/building_icons.lua index 66e2d34ca..05bb9cd10 100644 --- a/C7/Lua/texture_configs/civ3/building_icons.lua +++ b/C7/Lua/texture_configs/civ3/building_icons.lua @@ -17,11 +17,11 @@ local building_icons = { } function building_icons.small:map_object_to_sprite(building) - if (building:GetType().Name ~= "Building") then - error "Expected a Building object" + if (building:GetType().Name ~= "Building" and building.GetType().Name ~= "Inflow") then + error("Expected a Building or a Inflow object, got " .. type(building:GetType().Name)) end - local y = 1 + 33 * (1 + building.iconRowIndex) + local y = 33 + (SMALL_ICON_HEIGHT + 1) * building.iconRowIndex return { path = self.extra_data.path, @@ -30,8 +30,8 @@ function building_icons.small:map_object_to_sprite(building) end function building_icons.large:map_object_to_sprite(building) - if (building:GetType().Name ~= "Building") then - error "Expected a Building object" + if (building:GetType().Name ~= "Building" and building.GetType().Name ~= "Inflow") then + error("Expected a Building or a Inflow object, got " .. type(building:GetType().Name)) end local y = 33 + (LARGE_ICON_HEIGHT + 1) * building.iconRowIndex diff --git a/C7/Text/c7-static-map-save-standalone.json b/C7/Text/c7-static-map-save-standalone.json index dcc720264..784f5e96e 100644 --- a/C7/Text/c7-static-map-save-standalone.json +++ b/C7/Text/c7-static-map-save-standalone.json @@ -71317,6 +71317,18 @@ ] } ], + "inflows": [ + { + "name": "Wealth", + "iconRowIndex": 29, + "localYield": [ + { + "yieldType": "commerce", + "yieldCalculation": "inflows.result.wealth.commerce" + } + ] + } + ], "players": [ { "id": "player-1", @@ -73497,9 +73509,10 @@ "scoutUnitType": "Scout", "maxRankOfWorkableTiles": 2, "maxRankOfBarbarianCampTiles": 2, - "defaultDealDuration": 20, + "defaultDealDuration": 20, "treasuryInterestRate": 0.05, - "maxInterest": 50 + "maxInterest": 50, + "shieldCostPerGold": 4 }, "techs": [ { @@ -73989,6 +74002,9 @@ "y": 276, "prerequisites": [ "tech-32" + ], + "flags": [ + "doublesWealthProduction" ] }, { diff --git a/C7/Text/c7-static-map-save.json b/C7/Text/c7-static-map-save.json index 5fd4d54f3..b7404a7b4 100644 --- a/C7/Text/c7-static-map-save.json +++ b/C7/Text/c7-static-map-save.json @@ -73890,6 +73890,18 @@ ] } ], + "inflows": [ + { + "name": "Wealth", + "iconRowIndex": 29, + "localYield": [ + { + "yieldType": "commerce", + "yieldCalculation": "inflows.result.wealth.commerce" + } + ] + } + ], "players": [ { "id": "player-1", @@ -76070,9 +76082,10 @@ "scoutUnitType": "Scout", "maxRankOfWorkableTiles": 2, "maxRankOfBarbarianCampTiles": 2, - "defaultDealDuration": 20, + "defaultDealDuration": 20, "treasuryInterestRate": 0.05, - "maxInterest": 50 + "maxInterest": 50, + "shieldCostPerGold": 4 }, "techs": [ { @@ -76562,6 +76575,9 @@ "y": 276, "prerequisites": [ "tech-32" + ], + "flags": [ + "doublesWealthProduction" ] }, { diff --git a/C7/UIElements/CityScreen/CityScreen.cs b/C7/UIElements/CityScreen/CityScreen.cs index 731eb6df9..813253acf 100644 --- a/C7/UIElements/CityScreen/CityScreen.cs +++ b/C7/UIElements/CityScreen/CityScreen.cs @@ -593,6 +593,8 @@ private void RenderProductionDetails(GameData gameData, City city) { child.QueueFree(); } + int marginTop = 35; + if (city.itemBeingProduced is UnitPrototype up) { AnimationManager animationManager = mapView.game.animationController.civ3AnimData.forUnit(up, MapUnit.AnimatedAction.DEFAULT).animationManager; ShaderMaterial material = TextureLoader.GetShaderMaterialForUnit(city.owner.colorIndex); @@ -601,7 +603,7 @@ private void RenderProductionDetails(GameData gameData, City city) { // Add the base sprite. Sprite2D baseImageSprite = new(); baseImageSprite.Texture = baseImage; - baseImageSprite.Position = new Vector2(productionButton.TextureNormal.GetWidth() / 2, 35); + baseImageSprite.Position = new Vector2(productionButton.TextureNormal.GetWidth() / 2.0f, marginTop); productionButton.AddChild(baseImageSprite); // Add the tint sprite, hooking up the shader. @@ -613,7 +615,12 @@ private void RenderProductionDetails(GameData gameData, City city) { } else if (city.itemBeingProduced is Building b) { Sprite2D icon = new(); icon.Texture = TextureLoader.Load("building_icons.large", b, useCache: true); - icon.Position = new Vector2(productionButton.TextureNormal.GetWidth() / 2, 35); + icon.Position = new Vector2(productionButton.TextureNormal.GetWidth() / 2.0f, marginTop); + productionButton.AddChild(icon); + } else if (city.itemBeingProduced is Inflow inflow) { + Sprite2D icon = new(); + icon.Texture = TextureLoader.Load("building_icons.large", inflow, useCache: true); + icon.Position = new Vector2(productionButton.TextureNormal.GetWidth() / 2.0f, marginTop); productionButton.AddChild(icon); } @@ -626,6 +633,7 @@ private void RenderProductionDetails(GameData gameData, City city) { EngineStorage.ReadGameData((GameData gameData) => { city.SetItemBeingProduced(p); RenderProductionDetails(gameData, city); + RenderCulture(city); }); }); } @@ -664,11 +672,13 @@ private void RenderShieldBox(int shieldCost, int shieldsInBox) { child.QueueFree(); } + int itemsPerColumn = (int)Math.Ceiling((float)shieldCost / shieldsInBoxContainer.Columns); + if (itemsPerColumn == 0) return; + int width = (int)shieldsInBoxContainer.GetParent().Size.X; int height = (int)shieldsInBoxContainer.GetParent().Size.Y; shieldsInBoxContainer.Columns = (int)Math.Ceiling(Math.Sqrt(shieldCost)); - int itemsPerColumn = (int)Math.Ceiling((float)shieldCost / shieldsInBoxContainer.Columns); int iconSize = Math.Min(height / itemsPerColumn, width / shieldsInBoxContainer.Columns); for (int i = 0; i < Math.Min(shieldCost, shieldsInBox); ++i) { diff --git a/C7/UIElements/CityScreen/ProductionMenu.cs b/C7/UIElements/CityScreen/ProductionMenu.cs index b910ea13f..1aacdbd32 100644 --- a/C7/UIElements/CityScreen/ProductionMenu.cs +++ b/C7/UIElements/CityScreen/ProductionMenu.cs @@ -19,7 +19,7 @@ public override void _Ready() { // Load the font we'll use. FontFile font = ResourceLoader.Load("res://Fonts/NotoSans-Regular.ttf", null, ResourceLoader.CacheMode.Ignore); - font.FixedSize = 12; + font.FixedSize = 10; fontTheme.DefaultFont = font; } @@ -36,6 +36,9 @@ public void AddItems(GameData gameData, City city, Action choosePro AddChild(tree); tree.Columns = 2; tree.Size = new Vector2(203, 360); + tree.SetColumnExpand(0, true); + tree.SetColumnExpand(1, false); + tree.SetColumnCustomMinimumWidth(1, 50); TradingTree.ConfigureTreeTheme(tree, fontTheme); TreeItem root = TradingTree.CreateTreeRoot(tree); @@ -46,11 +49,15 @@ public void AddItems(GameData gameData, City city, Action choosePro TreeItem child = tree.CreateItem(root); string text = $"{option.name}"; if (option is UnitPrototype proto) { - text += $" {proto.attack}.{proto.defense}.{proto.movement}"; + string attackDesc = (proto.bombard > 0) ? $"{proto.attack}({proto.bombard})" : proto.attack.ToString(); + text += $" {attackDesc}.{proto.defense}.{proto.movement}"; } child.SetText(0, text); child.SetText(1, $"{buildTime} turns"); child.SetIcon(0, RightClickChooseProductionMenu.GetProducibleIcon(option)); + child.SetCustomMinimumHeight(40); + child.SetAutowrapMode(0, TextServer.AutowrapMode.WordSmart); + tree.SetColumnTitleAlignment(1, HorizontalAlignment.Right); itemMapping[child] = option; } diff --git a/C7/UIElements/RightClickMenu.cs b/C7/UIElements/RightClickMenu.cs index e0308e677..e92440d71 100644 --- a/C7/UIElements/RightClickMenu.cs +++ b/C7/UIElements/RightClickMenu.cs @@ -290,6 +290,8 @@ public static ImageTexture GetProducibleIcon(IProducible producible) { return TextureLoader.Load("unit_icons", proto, useCache: true); } else if (producible is Building b) { return TextureLoader.Load("building_icons.small", b, useCache: true); + } else if (producible is Inflow inflow) { + return TextureLoader.Load("building_icons.small", inflow, useCache: true); } else { return null; } diff --git a/C7Engine/AI/ChooseProducible.cs b/C7Engine/AI/ChooseProducible.cs index 9e8d1d82b..f5c27759d 100644 --- a/C7Engine/AI/ChooseProducible.cs +++ b/C7Engine/AI/ChooseProducible.cs @@ -44,11 +44,21 @@ private static float ScoreProducible(ProducibleStats stats, City city, Player pl return ScoreUnit(stats, city, player, unit); } else if (option is Building building) { return ScoreBuilding(stats, city, player, building); + } else if (option is Inflow inflow) { + return ScoreInflow(stats, city, player, inflow); } else { throw new Exception($"Unexpected producible: {option}"); } } + private static float ScoreInflow(ProducibleStats stats, City city, Player player, Inflow inflow) { + // TODO: score this properly, right now we give a very low score to avoid auto picking this + // I am not sure how civ III does this, although I guess we should consider stuff like + // no more buildings to build, reached unit cap, very bad economy with no other options, + // the WealthNever and WealthOften flags from RACE, etc + return -1000f; + } + private static float ScoreUnit(ProducibleStats stats, City city, Player player, UnitPrototype unit) { bool isSettler = unit.actions.Contains(UnitAction.BuildCity); bool isWorker = unit.isWorker; diff --git a/C7Engine/C7GameData/City.cs b/C7Engine/C7GameData/City.cs index e0b57755a..dd1c482a7 100644 --- a/C7Engine/C7GameData/City.cs +++ b/C7Engine/C7GameData/City.cs @@ -3,6 +3,7 @@ using System.Linq; using Serilog; using C7Engine; +using MoonSharp.Interpreter; namespace C7GameData { public class CityBuilding { @@ -18,6 +19,7 @@ public struct CommerceBreakdown { public int taxes; public int beakers; public int happiness; + public int wealth; } public struct CorruptableValue { @@ -141,7 +143,8 @@ public IEnumerable ListProductionOptions(GameData gameData) { HashSet accessibleResources = GetAccessibleResources(gameData); IEnumerable producibles = gameData.unitPrototypes.Cast() - .Concat(gameData.Buildings.Cast()); + .Concat(gameData.Buildings.Cast() + .Concat(gameData.Inflows.Cast())); return producibles.Where(p => p.CanProduce(this, accessibleResources)); } @@ -309,6 +312,10 @@ public void HandleCityProduction(GameData gameData) { } } } + } else if (producedItem is Inflow inflow) { + // we don't want to "complete" the inflow production + // unless the player has a reason to change it, this should be ongoing + return; } SetItemBeingProduced(ChooseProducible.Choose(this, owner)); @@ -505,7 +512,7 @@ public CorruptableValue CurrentProductionYield() { return result; } - public CommerceBreakdown CurrentCommerceYield(bool respectCivilDisorder = true) { + public CommerceBreakdown CurrentCommerceYieldRaw(bool respectCivilDisorder = true) { int uncorruptedCommerce = location.commerceYield(this).yield; foreach (CityResident r in residents) { uncorruptedCommerce += r.tileWorked.commerceYield(this).yield; @@ -543,7 +550,38 @@ public CommerceBreakdown CurrentCommerceYield(bool respectCivilDisorder = true) return result; } - public int MaintenanceCosts() { + [MoonSharpHidden] + public CommerceBreakdown CurrentCommerceYield(bool respectCivilDisorder = true) { + CommerceBreakdown result = CurrentCommerceYieldRaw(respectCivilDisorder); + + // commerce lua infow + if (this.itemBeingProduced is Inflow inflowCommerce && inflowCommerce.TryGetInflowYieldFunc(InflowYield.commerce, out var commerceYieldFunc)) { + int extraCommerce = commerceYieldFunc.Invoke(new ScriptContext(this.owner, this)); + result.wealth += extraCommerce; + } + + // science lua infow + if (this.itemBeingProduced is Inflow inflowScience && inflowScience.TryGetInflowYieldFunc(InflowYield.science, out var scienceYieldFunc)) { + int extraBeakers = scienceYieldFunc.Invoke(new ScriptContext(this.owner, this)); + result.beakers += extraBeakers; + } + + // happiness lua infow + if (this.itemBeingProduced is Inflow inflowHappiness && inflowHappiness.TryGetInflowYieldFunc(InflowYield.happiness, out var happinessYieldFunc)) { + int extraHappiness = happinessYieldFunc.Invoke(new ScriptContext(this.owner, this)); + result.happiness += extraHappiness; + } + + // corruption lua infow + if (this.itemBeingProduced is Inflow inflowCorruption && inflowCorruption.TryGetInflowYieldFunc(InflowYield.corruption, out var corruptionYieldFunc)) { + int lessCorruption = corruptionYieldFunc.Invoke(new ScriptContext(this.owner, this)); + result.corrupted -= lessCorruption; + } + + return result; + } + + public int MaintenanceCostsRaw() { int result = 0; foreach (CityBuilding cb in constructed_buildings) { result += cb.building.maintenanceCost; @@ -551,6 +589,18 @@ public int MaintenanceCosts() { return result; } + [MoonSharpHidden] + public int MaintenanceCosts() { + int result = 0; + result += MaintenanceCostsRaw(); + // maintenance lua infow + if (this.itemBeingProduced is Inflow inflowMaintenance && inflowMaintenance.TryGetInflowYieldFunc(InflowYield.maintenance, out var maintenanceYieldFunc)) { + int lessMaintenance = maintenanceYieldFunc.Invoke(new ScriptContext(this.owner, this)); + result -= lessMaintenance; + } + return result; + } + public int FoodGrowthPerTurn() { return CurrentFoodYield() - FoodConsumedPerTurn(); } @@ -594,7 +644,7 @@ public int GetCulture() { return perPlayerCulture[owner]; } - public int GetCulturePerTurn() { + public int GetCulturePerTurnRaw() { int result = 0; foreach (CityBuilding cb in GetBuildings()) { result += cb.building.culturePerTurn; @@ -602,6 +652,18 @@ public int GetCulturePerTurn() { return result; } + [MoonSharpHidden] + public int GetCulturePerTurn() { + int result = GetCulturePerTurnRaw(); + // culture lua infow + if (this.itemBeingProduced is Inflow inflow && inflow.TryGetInflowYieldFunc(InflowYield.culture, out var cultureYieldFunc)) { + int extraCulture = cultureYieldFunc.Invoke(new ScriptContext(this.owner, this)); + result += extraCulture; + } + + return result; + } + public int GetBorderExpansionLevel() { // Give ourselves a minimum of 1 culture to avoid taking the log of 0 int culture = Math.Max(1, GetCulture()); @@ -755,8 +817,10 @@ public bool UpdateCultureAndCheckForExpansion() { int start = GetBorderExpansionLevel(); foreach (CityBuilding cb in GetBuildings()) { cb.totalCulture += cb.building.culturePerTurn; - perPlayerCulture[owner] += cb.building.culturePerTurn; } + + perPlayerCulture[owner] += GetCulturePerTurn(); + return start != GetBorderExpansionLevel(); } diff --git a/C7Engine/C7GameData/GameData.cs b/C7Engine/C7GameData/GameData.cs index a1aa392b2..7e527fbce 100644 --- a/C7Engine/C7GameData/GameData.cs +++ b/C7Engine/C7GameData/GameData.cs @@ -21,6 +21,7 @@ public class GameData { public List mapUnits { get; set; } = new List(); public List unitPrototypes = new(); public List Buildings = new(); + public List Inflows = new(); // The names of all great wonders that have been built. public HashSet GreatWondersBuilt = new(); diff --git a/C7Engine/C7GameData/IProducible.cs b/C7Engine/C7GameData/IProducible.cs index 279cf2b2a..048560b16 100644 --- a/C7Engine/C7GameData/IProducible.cs +++ b/C7Engine/C7GameData/IProducible.cs @@ -3,7 +3,7 @@ namespace C7GameData { /** * Represents something that can be produced by a city. - * Known examples are Buildings and UnitPrototypes. + * Known examples are Buildings and UnitPrototypes and Inflows. */ public interface IProducible { string name { get; set; } diff --git a/C7Engine/C7GameData/ImportCiv3.cs b/C7Engine/C7GameData/ImportCiv3.cs index e146571a0..79e5624c6 100644 --- a/C7Engine/C7GameData/ImportCiv3.cs +++ b/C7Engine/C7GameData/ImportCiv3.cs @@ -966,8 +966,13 @@ List ImportCityBuildingsFromBiq(int cityIndex, ID player) { PRTO[] unitPrototypes = biq.Prto ?? defaultBiq.Prto; BiqData theBiq = biq.Bldg is null ? defaultBiq : biq; + // 29 is the wealth code + // In .sav files wealth is ConstructingType 1, but we want to translate it differently + if (city is { ConstructingType: 1, Constructing: 29 }) + city.ConstructingType = 0; + return city.ConstructingType switch { - 0 => ("Worker", ProducibleType.UNIT), // TODO: Wealth production is not implemented yet + 0 => (theBiq.Bldg[city.Constructing].Name, ProducibleType.INFLOW), 1 => (theBiq.Bldg[city.Constructing].Name, ProducibleType.BUILDING), 2 => (unitPrototypes[city.Constructing].Name, ProducibleType.UNIT), _ => throw new NotImplementedException() @@ -1210,6 +1215,14 @@ private void ImportBuildings() { foreach (BLDG bldg in Bldg) { if (bldg.Name == "Wealth") { + SaveInflow inflow = new () { + name = bldg.Name, + iconRowIndex = pediaIcons.buildingToRowNumberMapping[bldg.CivilopediaEntry], + localYield = [ + new SaveLocalYield(InflowYield.commerce, "inflows.result.wealth.commerce"), + ], + }; + save.Inflows.Add(inflow); continue; // We don't consider Wealth as a building } @@ -1509,6 +1522,7 @@ private void ImportTechs() { return new[] { (t.BonusTechToFirstCivThatResearches, SaveTech.Flag.BonusTechToFirstCivThatResearches), (t.EnablesBridges, SaveTech.Flag.EnablesBridges), + (t.DoublesWealthProduction, SaveTech.Flag.DoublesWealthProduction), } .Where(t => t.Item1) .Select(t => t.Item2); @@ -1699,6 +1713,7 @@ private void ImportRules() { save.Rules.MaxRankOfWorkableTiles = 2; save.Rules.MaxRankOfBarbarianCampTiles = 2; save.Rules.DefaultDealDuration = 20; + save.Rules.ShieldCostPerGold = rule.ShieldsCostPerGold; } private static void SetWorldWrap(SavData civ3Save, SaveGame save) { diff --git a/C7Engine/C7GameData/Inflow.cs b/C7Engine/C7GameData/Inflow.cs new file mode 100644 index 000000000..464aa62fb --- /dev/null +++ b/C7Engine/C7GameData/Inflow.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using C7Engine; +using C7GameData.Save; + +namespace C7GameData; + +// TODO: ideas for other city related stuff that are not yet implemented; +// pollution + -, luxury happy face + -, some tile modifier? , +// more/less units in the city -> happier/sadder population + +// We should keep these names in all lower case form +// to avoid any case mismatch between this code, the json and lua. +// It's not pretty but at least it will be consistent across all these. +public enum InflowYield { + commerce, + culture, + science, + happiness, + maintenance, + unitsupport, + corruption, +} +public class Inflow : IProducible { + public string name { get; set; } + public int populationCost { get; set; } + public Tech requiredTech { get; set; } + public HashSet requiredResources { get; set; } + + public int iconRowIndex; + public List localYield { get; set; } + // TODO: Implement a globalYield where for example, 10 cities must be producing this in order for something to happen + + public int ShieldCost(HashSet civTraits, float costFactor) { + // TODO: add the option to consume shields + return 0; + } + + public bool CanProduce(City city, HashSet accessibleResources) { + // TODO: add the option to unlock after researching a tech, or have a certain building, or having a certain resource + // as well as rendering it obsolete by a building/tech/resource. Basically make this as configurable as it can be + return true; + } + + public Inflow(SaveInflow saveInflow, LuaRulesEngine luaRulesEngine) { + this.name = saveInflow.name; + this.iconRowIndex = saveInflow.iconRowIndex; + this.localYield = saveInflow.localYield.ConvertAll(y => new LocalYield(y.yieldType, luaRulesEngine, y.yieldCalculation)); + } + + public Func GetInflowYieldFunc(InflowYield yieldType) { + return this.localYield.FirstOrDefault(y => y.yieldType == yieldType).yieldCalculation; + } + + public bool TryGetInflowYieldFunc(InflowYield yieldType, out Func yieldFunc) { + Func yieldCalculation = GetInflowYieldFunc(yieldType); + if (yieldCalculation != null) { + yieldFunc = yieldCalculation; + return true; + } + + yieldFunc = null; + return false; + } +} + +public struct LocalYield { + public InflowYield yieldType { get; set; } + public readonly Func yieldCalculation; + + public LocalYield() { } + public LocalYield(InflowYield type, LuaRulesEngine rulesEngine, string yieldCalculation = null) { + this.yieldType = type; + if (yieldCalculation != null) { + this.yieldCalculation = rulesEngine.ImportFunc>(yieldCalculation); + } + } +} +public struct SaveLocalYield { + public InflowYield yieldType { get; set; } + public string yieldCalculation; + + public SaveLocalYield() { } + public SaveLocalYield(InflowYield type, string yieldCalculation) { + this.yieldType = type; + this.yieldCalculation = yieldCalculation; + } +} + +public struct ScriptContext(Player player, City city) { + public Player player = player; + public City city = city; +} diff --git a/C7Engine/C7GameData/Player.cs b/C7Engine/C7GameData/Player.cs index 5ded2de79..7b54ddf3f 100644 --- a/C7Engine/C7GameData/Player.cs +++ b/C7Engine/C7GameData/Player.cs @@ -2,27 +2,28 @@ using System.Collections.Generic; using System.Linq; using C7Engine.AI.StrategicAI; -using C7GameData.Save; using C7Engine; +using MoonSharp.Interpreter; using Serilog; using static C7GameData.EraUtils; namespace C7GameData { public struct PlayerCommerceBreakdown { - public int corrupted; // Amount of commerce lost directly to corruption - public int taxes; // Amount of treasury income from REGULAR citizens working tiles - public int taxmenTaxes; // Amount of treasury income from tax collector specialists - public int beakers; // Amount of commerce going to science - public int happiness; // Amount of commerce going to entertainment - public int fromOtherCivs; // Income from other Civ GPT deals - public int toOtherCivs; // Expenses paid to other Civ GPT deals - public int interest; // Interest income from Wall Street-flag small wonder - public int maintenance; // Expenses due to aggregate building maintenance - public int unitSupport; // Expenses due to unit support costs + public int corrupted; // Amount of commerce lost directly to corruption + public int taxes; // Amount of treasury income from REGULAR citizens working tiles + public int taxmenTaxes; // Amount of treasury income from tax collector specialists + public int beakers; // Amount of commerce going to science + public int happiness; // Amount of commerce going to entertainment + public int fromOtherCivs; // Income from other Civ GPT deals + public int toOtherCivs; // Expenses paid to other Civ GPT deals + public int interest; // Interest income from Wall Street-flag small wonder + public int maintenance; // Expenses due to aggregate building maintenance + public int unitSupport; // Expenses due to unit support costs + public int wealthProduction; // Amount of extra commerce from "building" an Inflow that produces commerce public int Inflows() { - return corrupted + taxes + taxmenTaxes + beakers + happiness + fromOtherCivs + interest; + return corrupted + taxes + taxmenTaxes + beakers + happiness + fromOtherCivs + interest + wealthProduction; } public int Outflows() { @@ -34,7 +35,7 @@ public int Netflows() { } public int CityInflows() { - return corrupted + taxes + beakers + happiness; + return corrupted + taxes + beakers + happiness + wealthProduction; } } public class Player { @@ -353,6 +354,10 @@ public override string ToString() { return ""; } + public List GetKnownTechs() { + return EngineStorage.gameData?.techs.Where(x => this.knownTechs.Contains(x.id)).ToList(); + } + public PlayerCommerceBreakdown AggregateFlows() { var result = new PlayerCommerceBreakdown { @@ -365,7 +370,8 @@ public PlayerCommerceBreakdown AggregateFlows() { toOtherCivs = 0, interest = 0, maintenance = 0, - unitSupport = 0 + unitSupport = 0, + wealthProduction = 0 }; // If player has no cities, apply no expenses or income. @@ -382,6 +388,7 @@ public PlayerCommerceBreakdown AggregateFlows() { result.beakers += cityCommerce.beakers; result.happiness += cityCommerce.happiness; result.maintenance += city.MaintenanceCosts(); + result.wealthProduction += cityCommerce.wealth; interestBuildings += city.constructed_buildings.Count(cb => cb.building.treasuryEarnsInterest); @@ -834,7 +841,7 @@ private bool CanAdvanceToNextEra(GameData gameData) { return true; } - public (int, int, int) TotalUnitsAllowedUnitsAndSupportCost() { + public (int, int, int) TotalUnitsAllowedUnitsAndSupportCostRaw() { int freeUnits = 0; Difficulty difficulty = EngineStorage.gameData.gameDifficulty; @@ -865,6 +872,23 @@ private bool CanAdvanceToNextEra(GameData gameData) { return (totalUnits, allowedUnits, unitSupportCost); } + [MoonSharpHidden] + public (int, int, int) TotalUnitsAllowedUnitsAndSupportCost() { + (int totalUnits, int allowedUnits, int unitSupportCost) result = TotalUnitsAllowedUnitsAndSupportCostRaw(); + + foreach (City city in cities) { + // unitSupport lua infow + if (city.itemBeingProduced is Inflow inflowUnitSupport && inflowUnitSupport.TryGetInflowYieldFunc(InflowYield.unitsupport, out var unitSupportYieldFunc)) { + int unitSupportLess = unitSupportYieldFunc.Invoke(new ScriptContext(this, city)); + result.unitSupportCost -= unitSupportLess; + } + } + + result.unitSupportCost = Math.Max(0, result.unitSupportCost); + + return (result.totalUnits, result.allowedUnits, result.unitSupportCost); + } + // See https://forums.civfanatics.com/threads/military-advisor-relative-strength-assessment-definition.62980/post-1211499 and // https://forums.civfanatics.com/threads/study-of-inner-workings-of-military-advisor.83599/ public float CalculateMilitaryStrength() { diff --git a/C7Engine/C7GameData/Rules.cs b/C7Engine/C7GameData/Rules.cs index d02742209..305a70634 100644 --- a/C7Engine/C7GameData/Rules.cs +++ b/C7Engine/C7GameData/Rules.cs @@ -20,5 +20,6 @@ public class Rules { public int DefaultDealDuration; public float TreasuryInterestRate = .05f; public int MaxInterest = 50; + public int ShieldCostPerGold; } } diff --git a/C7Engine/C7GameData/Save/SaveCity.cs b/C7Engine/C7GameData/Save/SaveCity.cs index db584d99b..485366f66 100644 --- a/C7Engine/C7GameData/Save/SaveCity.cs +++ b/C7Engine/C7GameData/Save/SaveCity.cs @@ -35,7 +35,7 @@ public CityBuilding ToCityBuilding(List buildings, List player } - public enum ProducibleType { WEALTH, BUILDING, UNIT }; + public enum ProducibleType { INFLOW, BUILDING, UNIT }; public class SaveCity : IHasID { public ID id { get; set; } @@ -63,6 +63,7 @@ public SaveCity(City city) { name = city.name; producible = city.itemBeingProduced.name; producibleType = city.itemBeingProduced switch { + Inflow => ProducibleType.INFLOW, UnitPrototype => ProducibleType.UNIT, Building => ProducibleType.BUILDING, }; @@ -89,13 +90,15 @@ public City ToCity(GameMap gameMap, List unitPrototypes, List civilizations, List buildings, - List citizenTypes) { + List citizenTypes, + List inflows) { City city = new City{ id = id, location = gameMap.tileAt(location.X, location.Y), owner = players.Find(p => p.id == owner), name = name, itemBeingProduced = producibleType switch { + ProducibleType.INFLOW => inflows.Find(inflow => inflow.name == producible), ProducibleType.UNIT => unitPrototypes.Find(proto => proto.name == producible), ProducibleType.BUILDING => buildings.Find(building => building.name == producible), }, diff --git a/C7Engine/C7GameData/Save/SaveGame.cs b/C7Engine/C7GameData/Save/SaveGame.cs index 222f9a9ef..767755c4f 100644 --- a/C7Engine/C7GameData/Save/SaveGame.cs +++ b/C7Engine/C7GameData/Save/SaveGame.cs @@ -7,6 +7,7 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using C7Engine; +using Serilog; namespace C7GameData.Save { @@ -57,6 +58,7 @@ public static SaveGame FromGameData(GameData data) { TerrainTypes = data.terrainTypes, Resources = data.Resources, Buildings = data.Buildings.ConvertAll(building => building.ToSaveBuilding()), + Inflows = data.Inflows.ConvertAll(inflow => new SaveInflow(inflow)), GreatWondersBuilt = data.GreatWondersBuilt, BarbarianInfo = data.barbarianInfo, Units = data.mapUnits.ConvertAll(unit => new SaveUnit(unit, data.map)), @@ -111,6 +113,7 @@ public GameData ToGameData(string luaRulesDir) { ConvertMapAndPlayers(data); ConvertBuildings(data); ConvertUnits(data); + ConvertInflow(data); ConvertCities(data); ConvertBarbarianInfo(data); ConvertStrengthBonuses(data); @@ -233,6 +236,10 @@ private void ConvertTechnologies(GameData data) { } } + private void ConvertInflow(GameData data) { + data.Inflows = Inflows.ConvertAll(saveInflow => new Inflow(saveInflow, data.luaRulesEngine)); + } + private void ConvertBuildings(GameData data) { data.Buildings = Buildings.ConvertAll(building => new Building(building, data)); @@ -315,7 +322,7 @@ private void ConvertUnits(GameData data) { private void ConvertCities(GameData data) { // cities require game map for location and players for city owner data.cities = Cities.ConvertAll(city => - city.ToCity(data.map, data.players, data.unitPrototypes, Civilizations, data.Buildings, CitizenTypes) + city.ToCity(data.map, data.players, data.unitPrototypes, Civilizations, data.Buildings, CitizenTypes, data.Inflows) ); // add references to map tiles after units and cities are defined @@ -384,6 +391,7 @@ private void ConvertHealRates(GameData data) { public List Units = new List(); public List UnitPrototypes = []; public List Buildings = []; + public List Inflows = []; public HashSet GreatWondersBuilt = new(); public List Players = new List(); public List Cities = new List(); diff --git a/C7Engine/C7GameData/Save/SaveInflow.cs b/C7Engine/C7GameData/Save/SaveInflow.cs new file mode 100644 index 000000000..056dab347 --- /dev/null +++ b/C7Engine/C7GameData/Save/SaveInflow.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace C7GameData.Save; + +public class SaveInflow { + public string name { get; set; } + public int iconRowIndex = 0; + public List localYield { get; set; } + + public SaveInflow() { } + + public SaveInflow(Inflow inflow) { + this.name = inflow.name; + this.iconRowIndex = inflow.iconRowIndex; + this.localYield = inflow.localYield.ConvertAll(y => new SaveLocalYield(y.yieldType, GetYieldCalc(y.yieldType))); + } + + public string GetYieldCalc(InflowYield yieldType) { + return $"inflows.result.{this.name}.{yieldType}".ToLower(); + } +} diff --git a/C7Engine/C7GameData/Save/SaveTech.cs b/C7Engine/C7GameData/Save/SaveTech.cs index f070482f3..88d616869 100644 --- a/C7Engine/C7GameData/Save/SaveTech.cs +++ b/C7Engine/C7GameData/Save/SaveTech.cs @@ -8,7 +8,8 @@ namespace C7GameData.Save { public class SaveTech { public enum Flag { BonusTechToFirstCivThatResearches, - EnablesBridges + EnablesBridges, + DoublesWealthProduction, } public ID id; @@ -42,8 +43,9 @@ public C7GameData.Tech ToTechWithoutPrereqs() { CivilopediaEntry = this.CivilopediaEntry, Cost = this.Cost, RequiredForEraAdvancement = this.RequiredForEraAdvancement, - BonusTechToFirstCivThatResearches = this.flags.Contains(SaveTech.Flag.BonusTechToFirstCivThatResearches), - EnablesBridges = this.flags.Contains(SaveTech.Flag.EnablesBridges), + BonusTechToFirstCivThatResearches = this.flags.Contains(Flag.BonusTechToFirstCivThatResearches), + EnablesBridges = this.flags.Contains(Flag.EnablesBridges), + DoublesWealthProduction = this.flags.Contains(Flag.DoublesWealthProduction), EraCivilopediaName = this.EraCivilopediaName, SmallIconPath = this.SmallIconPath, X = this.X, diff --git a/C7Engine/C7GameData/Tech.cs b/C7Engine/C7GameData/Tech.cs index 26df5a190..00b50de51 100644 --- a/C7Engine/C7GameData/Tech.cs +++ b/C7Engine/C7GameData/Tech.cs @@ -11,6 +11,7 @@ public class Tech { public bool RequiredForEraAdvancement; public bool BonusTechToFirstCivThatResearches; public bool EnablesBridges; + public bool DoublesWealthProduction; // The civilopedia name of the era this tech is part of // (like ERA_Ancient_Times). This is what art lookups are based on. diff --git a/QueryCiv3/SavSections/City.cs b/QueryCiv3/SavSections/City.cs index f7e3f1db6..dfef22354 100644 --- a/QueryCiv3/SavSections/City.cs +++ b/QueryCiv3/SavSections/City.cs @@ -31,7 +31,7 @@ public unsafe struct CITY { public int ShieldsCollected; public int Pollution; public int Constructing; // Index into BLDG or UNIT - public int ConstructingType; // 0: wealth, 1: building, 2: unit + public int ConstructingType; // 0: wealth (is this ever in the files or is it assumed?), 1: building, 2: unit public int YearBuilt; private int UnknownBuffer2;