From 0fb30b622acd0cbbcfb2988d80b6f2c5a3cada25 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 27 Jan 2026 19:40:30 +0200 Subject: [PATCH 1/4] Inflow implementation --- C7/Lua/rules/civ3.lua | 1 + C7/Lua/rules/civ3/inflows.lua | 81 +++++++++++++++++++ .../texture_configs/civ3/building_icons.lua | 10 +-- C7/Text/c7-static-map-save-standalone.json | 64 ++++++++++++++- C7/Text/c7-static-map-save.json | 64 ++++++++++++++- C7/UIElements/CityScreen/CityScreen.cs | 18 ++++- C7/UIElements/CityScreen/ProductionMenu.cs | 11 ++- C7/UIElements/RightClickMenu.cs | 2 + C7Engine/AI/ChooseProducible.cs | 10 +++ C7Engine/C7GameData/City.cs | 33 +++++++- C7Engine/C7GameData/GameData.cs | 1 + C7Engine/C7GameData/IProducible.cs | 2 +- C7Engine/C7GameData/ImportCiv3.cs | 17 +++- C7Engine/C7GameData/Inflow.cs | 74 +++++++++++++++++ C7Engine/C7GameData/Player.cs | 34 ++++---- C7Engine/C7GameData/Rules.cs | 1 + C7Engine/C7GameData/Save/SaveCity.cs | 7 +- C7Engine/C7GameData/Save/SaveGame.cs | 10 ++- C7Engine/C7GameData/Save/SaveInflow.cs | 21 +++++ C7Engine/C7GameData/Save/SaveTech.cs | 8 +- C7Engine/C7GameData/Tech.cs | 1 + QueryCiv3/SavSections/City.cs | 2 +- 22 files changed, 432 insertions(+), 40 deletions(-) create mode 100644 C7/Lua/rules/civ3/inflows.lua create mode 100644 C7Engine/C7GameData/Inflow.cs create mode 100644 C7Engine/C7GameData/Save/SaveInflow.cs 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..5f6e335fa --- /dev/null +++ b/C7/Lua/rules/civ3/inflows.lua @@ -0,0 +1,81 @@ +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 + +-- context is [ int input, List techs ] +-- this is the actual implementation we would do for Wealth, for conquests +local function extra_commerce_calculation(context) + local useful_shields = context.input + local known_techs = context.techs + local double_effect = any(known_techs, + function(x) + return x.DoublesWealthProduction == true + end + ) + local ratio = rules().ShieldCostPerGold + return math.max(1, useful_shields / (double_effect and (ratio / 2) or ratio)) +end + +-- example +local function extra_culture_calculation(context) + local city_culture = context.input + return math.max(1, city_culture/2) +end + +-- example +local function extra_science_calculation(context) + local beakers = context.input + return math.max(0, beakers/10) +end + +inflows.result = { + wealth = { + commerce = function(context) + return extra_commerce_calculation(context) + end, + culture = function(context) + return extra_culture_calculation(context) + end, + science = function(context) + return extra_science_calculation(context) + end, + }, + -- example + cultivation = { + commerce = function(context) + return extra_commerce_calculation(context) * 2 + end, + culture = function(context) + return extra_culture_calculation(context) * 2 + end, + science = function(context) + return extra_science_calculation(context) * 2 + end, + }, + -- example + expertise = { + commerce = function(context) + return extra_commerce_calculation(context) * 3 + end, + culture = function(context) + return extra_culture_calculation(context) * 3 + end, + science = function(context) + return extra_science_calculation(context) * 3 + end, + }, +} + +return inflows \ No newline at end of file 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..739f59720 100644 --- a/C7/Text/c7-static-map-save-standalone.json +++ b/C7/Text/c7-static-map-save-standalone.json @@ -71317,6 +71317,62 @@ ] } ], + "inflows": [ + { + "name": "Wealth", + "iconRowIndex": 29, + "localYield": [ + { + "yieldType": "commerce", + "yieldCalculation": "inflows.result.wealth.commerce" + }, + { + "yieldType": "culture", + "yieldCalculation": "inflows.result.wealth.culture" + }, + { + "yieldType": "science", + "yieldCalculation": "inflows.result.wealth.science" + } + ] + }, + { + "name": "Cultivation", + "iconRowIndex": 29, + "localYield": [ + { + "yieldType": "commerce", + "yieldCalculation": "inflows.result.cultivation.commerce" + }, + { + "yieldType": "culture", + "yieldCalculation": "inflows.result.cultivation.culture" + }, + { + "yieldType": "science", + "yieldCalculation": "inflows.result.cultivation.science" + } + ] + }, + { + "name": "Expertise", + "iconRowIndex": 29, + "localYield": [ + { + "yieldType": "commerce", + "yieldCalculation": "inflows.result.expertise.commerce" + }, + { + "yieldType": "culture", + "yieldCalculation": "inflows.result.expertise.culture" + }, + { + "yieldType": "science", + "yieldCalculation": "inflows.result.expertise.science" + } + ] + } + ], "players": [ { "id": "player-1", @@ -73497,9 +73553,10 @@ "scoutUnitType": "Scout", "maxRankOfWorkableTiles": 2, "maxRankOfBarbarianCampTiles": 2, - "defaultDealDuration": 20, + "defaultDealDuration": 20, "treasuryInterestRate": 0.05, - "maxInterest": 50 + "maxInterest": 50, + "shieldCostPerGold": 4 }, "techs": [ { @@ -73989,6 +74046,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..6e4159a34 100644 --- a/C7/Text/c7-static-map-save.json +++ b/C7/Text/c7-static-map-save.json @@ -73890,6 +73890,62 @@ ] } ], + "inflows": [ + { + "name": "Wealth", + "iconRowIndex": 29, + "localYield": [ + { + "yieldType": "commerce", + "yieldCalculation": "inflows.result.wealth.commerce" + }, + { + "yieldType": "culture", + "yieldCalculation": "inflows.result.wealth.culture" + }, + { + "yieldType": "science", + "yieldCalculation": "inflows.result.wealth.science" + } + ] + }, + { + "name": "Cultivation", + "iconRowIndex": 29, + "localYield": [ + { + "yieldType": "commerce", + "yieldCalculation": "inflows.result.cultivation.commerce" + }, + { + "yieldType": "culture", + "yieldCalculation": "inflows.result.cultivation.culture" + }, + { + "yieldType": "science", + "yieldCalculation": "inflows.result.cultivation.science" + } + ] + }, + { + "name": "Expertise", + "iconRowIndex": 29, + "localYield": [ + { + "yieldType": "commerce", + "yieldCalculation": "inflows.result.expertise.commerce" + }, + { + "yieldType": "culture", + "yieldCalculation": "inflows.result.expertise.culture" + }, + { + "yieldType": "science", + "yieldCalculation": "inflows.result.expertise.science" + } + ] + } + ], "players": [ { "id": "player-1", @@ -76070,9 +76126,10 @@ "scoutUnitType": "Scout", "maxRankOfWorkableTiles": 2, "maxRankOfBarbarianCampTiles": 2, - "defaultDealDuration": 20, + "defaultDealDuration": 20, "treasuryInterestRate": 0.05, - "maxInterest": 50 + "maxInterest": 50, + "shieldCostPerGold": 4 }, "techs": [ { @@ -76562,6 +76619,9 @@ "y": 276, "prerequisites": [ "tech-32" + ], + "flags": [ + "doublesWealthProduction" ] }, { diff --git a/C7/UIElements/CityScreen/CityScreen.cs b/C7/UIElements/CityScreen/CityScreen.cs index 731eb6df9..8d5f7ab4f 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,7 +633,8 @@ 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..6cfb686f1 100644 --- a/C7Engine/C7GameData/City.cs +++ b/C7Engine/C7GameData/City.cs @@ -18,6 +18,7 @@ public struct CommerceBreakdown { public int taxes; public int beakers; public int happiness; + public int wealth; } public struct CorruptableValue { @@ -141,7 +142,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 +311,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)); @@ -540,6 +546,19 @@ public CommerceBreakdown CurrentCommerceYield(bool respectCivilDisorder = true) result.taxes += cr.citizenType.Taxes; } + // Wealth + if (this.itemBeingProduced is Inflow inflowCommerce && inflowCommerce.GetCommerceYieldFunc() != null) { + int usefulShields = this.CurrentProductionYield().useful; + int extraCommerce = inflowCommerce.GetCommerceYieldFunc().Invoke(new ScriptContext(usefulShields, this.owner.GetKnownTechs())); + result.wealth += extraCommerce; + } + + // Expertise + if (this.itemBeingProduced is Inflow inflowScience && result.beakers > 0 && inflowScience.GetScienceYieldFunc() != null) { + int extraBeakers = inflowScience.GetScienceYieldFunc().Invoke(new ScriptContext(result.beakers, this.owner.GetKnownTechs())); + result.beakers += extraBeakers; + } + return result; } @@ -594,11 +613,17 @@ public int GetCulture() { return perPlayerCulture[owner]; } - public int GetCulturePerTurn() { + public int GetCulturePerTurn(bool bonus = true) { int result = 0; foreach (CityBuilding cb in GetBuildings()) { result += cb.building.culturePerTurn; } + + // this should go last I reckon + if (bonus && itemBeingProduced is Inflow inflow && inflow.GetCultureYieldFunc() != null) { + int extraCulture = inflow.GetCultureYieldFunc().Invoke(new ScriptContext(result, this.owner.GetKnownTechs())); + result += extraCulture; + } return result; } @@ -757,6 +782,10 @@ public bool UpdateCultureAndCheckForExpansion() { cb.totalCulture += cb.building.culturePerTurn; perPlayerCulture[owner] += cb.building.culturePerTurn; } + + + // TODO: add extra from Inflow + 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..0537602fd 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..266867b44 --- /dev/null +++ b/C7Engine/C7GameData/Inflow.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using C7Engine; +using C7GameData.Save; + +namespace C7GameData; + +public enum InflowYield { + Commerce, + Culture, + Science, +} +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; } + + public int ShieldCost(HashSet civTraits, float costFactor) { + return 0; + } + + public bool CanProduce(City city, HashSet accessibleResources) { + 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 GetCommerceYieldFunc() { + return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.Commerce).yieldCalculation; + } + public Func GetCultureYieldFunc() { + return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.Culture).yieldCalculation; + } + public Func GetScienceYieldFunc() { + return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.Science).yieldCalculation; + } +} + +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(int input, List techs) { + public int input = input; + public List techs = techs; +} diff --git a/C7Engine/C7GameData/Player.cs b/C7Engine/C7GameData/Player.cs index 5ded2de79..7b8b2abed 100644 --- a/C7Engine/C7GameData/Player.cs +++ b/C7Engine/C7GameData/Player.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using C7Engine.AI.StrategicAI; -using C7GameData.Save; using C7Engine; using Serilog; using static C7GameData.EraUtils; @@ -10,19 +9,20 @@ 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 +34,7 @@ public int Netflows() { } public int CityInflows() { - return corrupted + taxes + beakers + happiness; + return corrupted + taxes + beakers + happiness + wealthProduction; } } public class Player { @@ -353,6 +353,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 +369,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 +387,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); 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; From ad35cca7f54cf0b6f82b77ebf6658577b7c9d4a2 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 27 Jan 2026 20:07:40 +0200 Subject: [PATCH 2/4] Fix whitespace --- C7/UIElements/CityScreen/CityScreen.cs | 4 ++-- C7Engine/C7GameData/City.cs | 30 +++++++++++++------------- C7Engine/C7GameData/Inflow.cs | 6 +++--- C7Engine/C7GameData/Player.cs | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/C7/UIElements/CityScreen/CityScreen.cs b/C7/UIElements/CityScreen/CityScreen.cs index 8d5f7ab4f..813253acf 100644 --- a/C7/UIElements/CityScreen/CityScreen.cs +++ b/C7/UIElements/CityScreen/CityScreen.cs @@ -633,8 +633,8 @@ private void RenderProductionDetails(GameData gameData, City city) { EngineStorage.ReadGameData((GameData gameData) => { city.SetItemBeingProduced(p); RenderProductionDetails(gameData, city); - RenderCulture(city); - }); + RenderCulture(city); + }); }); } diff --git a/C7Engine/C7GameData/City.cs b/C7Engine/C7GameData/City.cs index 6cfb686f1..63e64e23a 100644 --- a/C7Engine/C7GameData/City.cs +++ b/C7Engine/C7GameData/City.cs @@ -546,19 +546,19 @@ public CommerceBreakdown CurrentCommerceYield(bool respectCivilDisorder = true) result.taxes += cr.citizenType.Taxes; } - // Wealth - if (this.itemBeingProduced is Inflow inflowCommerce && inflowCommerce.GetCommerceYieldFunc() != null) { - int usefulShields = this.CurrentProductionYield().useful; - int extraCommerce = inflowCommerce.GetCommerceYieldFunc().Invoke(new ScriptContext(usefulShields, this.owner.GetKnownTechs())); - result.wealth += extraCommerce; - } - - // Expertise - if (this.itemBeingProduced is Inflow inflowScience && result.beakers > 0 && inflowScience.GetScienceYieldFunc() != null) { - int extraBeakers = inflowScience.GetScienceYieldFunc().Invoke(new ScriptContext(result.beakers, this.owner.GetKnownTechs())); - result.beakers += extraBeakers; - } - + // Wealth + if (this.itemBeingProduced is Inflow inflowCommerce && inflowCommerce.GetCommerceYieldFunc() != null) { + int usefulShields = this.CurrentProductionYield().useful; + int extraCommerce = inflowCommerce.GetCommerceYieldFunc().Invoke(new ScriptContext(usefulShields, this.owner.GetKnownTechs())); + result.wealth += extraCommerce; + } + + // Expertise + if (this.itemBeingProduced is Inflow inflowScience && result.beakers > 0 && inflowScience.GetScienceYieldFunc() != null) { + int extraBeakers = inflowScience.GetScienceYieldFunc().Invoke(new ScriptContext(result.beakers, this.owner.GetKnownTechs())); + result.beakers += extraBeakers; + } + return result; } @@ -619,7 +619,7 @@ public int GetCulturePerTurn(bool bonus = true) { result += cb.building.culturePerTurn; } - // this should go last I reckon + // this should go last I reckon if (bonus && itemBeingProduced is Inflow inflow && inflow.GetCultureYieldFunc() != null) { int extraCulture = inflow.GetCultureYieldFunc().Invoke(new ScriptContext(result, this.owner.GetKnownTechs())); result += extraCulture; @@ -784,7 +784,7 @@ public bool UpdateCultureAndCheckForExpansion() { } - // TODO: add extra from Inflow + // TODO: add extra from Inflow return start != GetBorderExpansionLevel(); } diff --git a/C7Engine/C7GameData/Inflow.cs b/C7Engine/C7GameData/Inflow.cs index 266867b44..e87f6c06e 100644 --- a/C7Engine/C7GameData/Inflow.cs +++ b/C7Engine/C7GameData/Inflow.cs @@ -7,9 +7,9 @@ namespace C7GameData; public enum InflowYield { - Commerce, - Culture, - Science, + Commerce, + Culture, + Science, } public class Inflow : IProducible { public string name { get; set; } diff --git a/C7Engine/C7GameData/Player.cs b/C7Engine/C7GameData/Player.cs index 7b8b2abed..b1c62d6a5 100644 --- a/C7Engine/C7GameData/Player.cs +++ b/C7Engine/C7GameData/Player.cs @@ -387,7 +387,7 @@ public PlayerCommerceBreakdown AggregateFlows() { result.beakers += cityCommerce.beakers; result.happiness += cityCommerce.happiness; result.maintenance += city.MaintenanceCosts(); - result.wealthProduction += cityCommerce.wealth; + result.wealthProduction += cityCommerce.wealth; interestBuildings += city.constructed_buildings.Count(cb => cb.building.treasuryEarnsInterest); From 89e28c7f01c3f36e101a5890db3ebff2d755d123 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 4 Feb 2026 15:54:04 +0200 Subject: [PATCH 3/4] Addressed review comments Added some extra functionality. Some guardrails for Lua not to call directly methods that might call themselves again in an endless loop. --- C7/Lua/rules/civ3/inflows.lua | 167 +++++++++++++-------- C7/Text/c7-static-map-save-standalone.json | 44 ------ C7/Text/c7-static-map-save.json | 44 ------ C7Engine/C7GameData/City.cs | 65 ++++++-- C7Engine/C7GameData/ImportCiv3.cs | 2 +- C7Engine/C7GameData/Inflow.cs | 41 +++-- C7Engine/C7GameData/Player.cs | 20 ++- 7 files changed, 207 insertions(+), 176 deletions(-) diff --git a/C7/Lua/rules/civ3/inflows.lua b/C7/Lua/rules/civ3/inflows.lua index 5f6e335fa..6c9fa8b8b 100644 --- a/C7/Lua/rules/civ3/inflows.lua +++ b/C7/Lua/rules/civ3/inflows.lua @@ -6,76 +6,119 @@ 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 + for _, v in ipairs(list) do + if predicate(v) then + return true end - return false + end + return false end - --- context is [ int input, List techs ] --- this is the actual implementation we would do for Wealth, for conquests -local function extra_commerce_calculation(context) - local useful_shields = context.input - local known_techs = context.techs - local double_effect = any(known_techs, - function(x) - return x.DoublesWealthProduction == true - end - ) - local ratio = rules().ShieldCostPerGold - return math.max(1, useful_shields / (double_effect and (ratio / 2) or ratio)) + +local function doubles_wealth_production(tech) + return tech.DoublesWealthProduction == true end --- example -local function extra_culture_calculation(context) - local city_culture = context.input - return math.max(1, city_culture/2) +-- 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 --- example -local function extra_science_calculation(context) - local beakers = context.input - return math.max(0, beakers/10) -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, - culture = function(context) - return extra_culture_calculation(context) - end, - science = function(context) - return extra_science_calculation(context) - end, - }, - -- example - cultivation = { - commerce = function(context) - return extra_commerce_calculation(context) * 2 - end, - culture = function(context) - return extra_culture_calculation(context) * 2 - end, - science = function(context) - return extra_science_calculation(context) * 2 - end, - }, - -- example - expertise = { - commerce = function(context) - return extra_commerce_calculation(context) * 3 - end, - culture = function(context) - return extra_culture_calculation(context) * 3 - end, - science = function(context) - return extra_science_calculation(context) * 3 - end, + 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 \ No newline at end of file +return inflows diff --git a/C7/Text/c7-static-map-save-standalone.json b/C7/Text/c7-static-map-save-standalone.json index 739f59720..784f5e96e 100644 --- a/C7/Text/c7-static-map-save-standalone.json +++ b/C7/Text/c7-static-map-save-standalone.json @@ -71325,50 +71325,6 @@ { "yieldType": "commerce", "yieldCalculation": "inflows.result.wealth.commerce" - }, - { - "yieldType": "culture", - "yieldCalculation": "inflows.result.wealth.culture" - }, - { - "yieldType": "science", - "yieldCalculation": "inflows.result.wealth.science" - } - ] - }, - { - "name": "Cultivation", - "iconRowIndex": 29, - "localYield": [ - { - "yieldType": "commerce", - "yieldCalculation": "inflows.result.cultivation.commerce" - }, - { - "yieldType": "culture", - "yieldCalculation": "inflows.result.cultivation.culture" - }, - { - "yieldType": "science", - "yieldCalculation": "inflows.result.cultivation.science" - } - ] - }, - { - "name": "Expertise", - "iconRowIndex": 29, - "localYield": [ - { - "yieldType": "commerce", - "yieldCalculation": "inflows.result.expertise.commerce" - }, - { - "yieldType": "culture", - "yieldCalculation": "inflows.result.expertise.culture" - }, - { - "yieldType": "science", - "yieldCalculation": "inflows.result.expertise.science" } ] } diff --git a/C7/Text/c7-static-map-save.json b/C7/Text/c7-static-map-save.json index 6e4159a34..b7404a7b4 100644 --- a/C7/Text/c7-static-map-save.json +++ b/C7/Text/c7-static-map-save.json @@ -73898,50 +73898,6 @@ { "yieldType": "commerce", "yieldCalculation": "inflows.result.wealth.commerce" - }, - { - "yieldType": "culture", - "yieldCalculation": "inflows.result.wealth.culture" - }, - { - "yieldType": "science", - "yieldCalculation": "inflows.result.wealth.science" - } - ] - }, - { - "name": "Cultivation", - "iconRowIndex": 29, - "localYield": [ - { - "yieldType": "commerce", - "yieldCalculation": "inflows.result.cultivation.commerce" - }, - { - "yieldType": "culture", - "yieldCalculation": "inflows.result.cultivation.culture" - }, - { - "yieldType": "science", - "yieldCalculation": "inflows.result.cultivation.science" - } - ] - }, - { - "name": "Expertise", - "iconRowIndex": 29, - "localYield": [ - { - "yieldType": "commerce", - "yieldCalculation": "inflows.result.expertise.commerce" - }, - { - "yieldType": "culture", - "yieldCalculation": "inflows.result.expertise.culture" - }, - { - "yieldType": "science", - "yieldCalculation": "inflows.result.expertise.science" } ] } diff --git a/C7Engine/C7GameData/City.cs b/C7Engine/C7GameData/City.cs index 63e64e23a..5d258c17b 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 { @@ -511,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; @@ -546,23 +547,41 @@ public CommerceBreakdown CurrentCommerceYield(bool respectCivilDisorder = true) result.taxes += cr.citizenType.Taxes; } - // Wealth + return result; + } + + [MoonSharpHidden] + public CommerceBreakdown CurrentCommerceYield(bool respectCivilDisorder = true) { + CommerceBreakdown result = CurrentCommerceYieldRaw(respectCivilDisorder); + + // commerce lua infow if (this.itemBeingProduced is Inflow inflowCommerce && inflowCommerce.GetCommerceYieldFunc() != null) { - int usefulShields = this.CurrentProductionYield().useful; - int extraCommerce = inflowCommerce.GetCommerceYieldFunc().Invoke(new ScriptContext(usefulShields, this.owner.GetKnownTechs())); + int extraCommerce = inflowCommerce.GetCommerceYieldFunc().Invoke(new ScriptContext(this.owner, this)); result.wealth += extraCommerce; } - // Expertise - if (this.itemBeingProduced is Inflow inflowScience && result.beakers > 0 && inflowScience.GetScienceYieldFunc() != null) { - int extraBeakers = inflowScience.GetScienceYieldFunc().Invoke(new ScriptContext(result.beakers, this.owner.GetKnownTechs())); + // science lua infow + if (this.itemBeingProduced is Inflow inflowScience && inflowScience.GetScienceYieldFunc() != null) { + int extraBeakers = inflowScience.GetScienceYieldFunc().Invoke(new ScriptContext(this.owner, this)); result.beakers += extraBeakers; } + // happiness lua infow + if (this.itemBeingProduced is Inflow inflowHappiness && inflowHappiness.GetHappinessYieldFunc() != null) { + int extraHappiness = inflowHappiness.GetHappinessYieldFunc().Invoke(new ScriptContext(this.owner, this)); + result.happiness += extraHappiness; + } + + // corruption lua infow + if (this.itemBeingProduced is Inflow inflowCorruption && inflowCorruption.GetCorruptionYieldFunc() != null) { + int lessCorruption = inflowCorruption.GetCorruptionYieldFunc().Invoke(new ScriptContext(this.owner, this)); + result.corrupted -= lessCorruption; + } + return result; } - public int MaintenanceCosts() { + public int MaintenanceCostsRaw() { int result = 0; foreach (CityBuilding cb in constructed_buildings) { result += cb.building.maintenanceCost; @@ -570,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.GetMaintenanceYieldFunc() != null) { + int lessMaintenance = inflowMaintenance.GetMaintenanceYieldFunc().Invoke(new ScriptContext(this.owner, this)); + result -= lessMaintenance; + } + return result; + } + public int FoodGrowthPerTurn() { return CurrentFoodYield() - FoodConsumedPerTurn(); } @@ -613,17 +644,23 @@ public int GetCulture() { return perPlayerCulture[owner]; } - public int GetCulturePerTurn(bool bonus = true) { + public int GetCulturePerTurnRaw() { int result = 0; foreach (CityBuilding cb in GetBuildings()) { result += cb.building.culturePerTurn; } + return result; + } - // this should go last I reckon - if (bonus && itemBeingProduced is Inflow inflow && inflow.GetCultureYieldFunc() != null) { - int extraCulture = inflow.GetCultureYieldFunc().Invoke(new ScriptContext(result, this.owner.GetKnownTechs())); + [MoonSharpHidden] + public int GetCulturePerTurn() { + int result = GetCulturePerTurnRaw(); + // culture lua infow + if (this.itemBeingProduced is Inflow inflow && inflow.GetCultureYieldFunc() != null) { + int extraCulture = inflow.GetCultureYieldFunc().Invoke(new ScriptContext(this.owner, this)); result += extraCulture; } + return result; } @@ -780,11 +817,9 @@ public bool UpdateCultureAndCheckForExpansion() { int start = GetBorderExpansionLevel(); foreach (CityBuilding cb in GetBuildings()) { cb.totalCulture += cb.building.culturePerTurn; - perPlayerCulture[owner] += cb.building.culturePerTurn; } - - // TODO: add extra from Inflow + perPlayerCulture[owner] += GetCulturePerTurn(); return start != GetBorderExpansionLevel(); } diff --git a/C7Engine/C7GameData/ImportCiv3.cs b/C7Engine/C7GameData/ImportCiv3.cs index 0537602fd..79e5624c6 100644 --- a/C7Engine/C7GameData/ImportCiv3.cs +++ b/C7Engine/C7GameData/ImportCiv3.cs @@ -1219,7 +1219,7 @@ private void ImportBuildings() { name = bldg.Name, iconRowIndex = pediaIcons.buildingToRowNumberMapping[bldg.CivilopediaEntry], localYield = [ - new SaveLocalYield(InflowYield.Commerce, "inflows.result.wealth.commerce"), + new SaveLocalYield(InflowYield.commerce, "inflows.result.wealth.commerce"), ], }; save.Inflows.Add(inflow); diff --git a/C7Engine/C7GameData/Inflow.cs b/C7Engine/C7GameData/Inflow.cs index e87f6c06e..59d4aa3c2 100644 --- a/C7Engine/C7GameData/Inflow.cs +++ b/C7Engine/C7GameData/Inflow.cs @@ -6,10 +6,21 @@ 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, + commerce, + culture, + science, + happiness, + maintenance, + unitsupport, + corruption, } public class Inflow : IProducible { public string name { get; set; } @@ -35,13 +46,25 @@ public Inflow(SaveInflow saveInflow, LuaRulesEngine luaRulesEngine) { } public Func GetCommerceYieldFunc() { - return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.Commerce).yieldCalculation; + return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.commerce).yieldCalculation; } public Func GetCultureYieldFunc() { - return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.Culture).yieldCalculation; + return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.culture).yieldCalculation; } public Func GetScienceYieldFunc() { - return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.Science).yieldCalculation; + return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.science).yieldCalculation; + } + public Func GetHappinessYieldFunc() { + return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.happiness).yieldCalculation; + } + public Func GetMaintenanceYieldFunc() { + return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.maintenance).yieldCalculation; + } + public Func GetUnitSupportYieldFunc() { + return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.unitsupport).yieldCalculation; + } + public Func GetCorruptionYieldFunc() { + return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.corruption).yieldCalculation; } } @@ -68,7 +91,7 @@ public SaveLocalYield(InflowYield type, string yieldCalculation) { } } -public struct ScriptContext(int input, List techs) { - public int input = input; - public List techs = techs; +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 b1c62d6a5..9a90d945e 100644 --- a/C7Engine/C7GameData/Player.cs +++ b/C7Engine/C7GameData/Player.cs @@ -3,6 +3,7 @@ using System.Linq; using C7Engine.AI.StrategicAI; using C7Engine; +using MoonSharp.Interpreter; using Serilog; using static C7GameData.EraUtils; @@ -840,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; @@ -871,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.GetUnitSupportYieldFunc() != null) { + int unitSupportLess = inflowUnitSupport.GetUnitSupportYieldFunc().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() { From 5840a0c7caba6695d02ac974261ba0fc9884c482 Mon Sep 17 00:00:00 2001 From: Stavros Date: Thu, 5 Feb 2026 18:10:40 +0200 Subject: [PATCH 4/4] Added a few TODOs. Tidied up the Inflow API to reduce code repetition of how we access the yield func --- C7Engine/C7GameData/City.cs | 24 ++++++++++++------------ C7Engine/C7GameData/Inflow.cs | 35 ++++++++++++++++------------------- C7Engine/C7GameData/Player.cs | 4 ++-- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/C7Engine/C7GameData/City.cs b/C7Engine/C7GameData/City.cs index 5d258c17b..dd1c482a7 100644 --- a/C7Engine/C7GameData/City.cs +++ b/C7Engine/C7GameData/City.cs @@ -555,26 +555,26 @@ public CommerceBreakdown CurrentCommerceYield(bool respectCivilDisorder = true) CommerceBreakdown result = CurrentCommerceYieldRaw(respectCivilDisorder); // commerce lua infow - if (this.itemBeingProduced is Inflow inflowCommerce && inflowCommerce.GetCommerceYieldFunc() != null) { - int extraCommerce = inflowCommerce.GetCommerceYieldFunc().Invoke(new ScriptContext(this.owner, this)); + 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.GetScienceYieldFunc() != null) { - int extraBeakers = inflowScience.GetScienceYieldFunc().Invoke(new ScriptContext(this.owner, this)); + 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.GetHappinessYieldFunc() != null) { - int extraHappiness = inflowHappiness.GetHappinessYieldFunc().Invoke(new ScriptContext(this.owner, this)); + 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.GetCorruptionYieldFunc() != null) { - int lessCorruption = inflowCorruption.GetCorruptionYieldFunc().Invoke(new ScriptContext(this.owner, this)); + 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; } @@ -594,8 +594,8 @@ public int MaintenanceCosts() { int result = 0; result += MaintenanceCostsRaw(); // maintenance lua infow - if (this.itemBeingProduced is Inflow inflowMaintenance && inflowMaintenance.GetMaintenanceYieldFunc() != null) { - int lessMaintenance = inflowMaintenance.GetMaintenanceYieldFunc().Invoke(new ScriptContext(this.owner, this)); + 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; @@ -656,8 +656,8 @@ public int GetCulturePerTurnRaw() { public int GetCulturePerTurn() { int result = GetCulturePerTurnRaw(); // culture lua infow - if (this.itemBeingProduced is Inflow inflow && inflow.GetCultureYieldFunc() != null) { - int extraCulture = inflow.GetCultureYieldFunc().Invoke(new ScriptContext(this.owner, this)); + if (this.itemBeingProduced is Inflow inflow && inflow.TryGetInflowYieldFunc(InflowYield.culture, out var cultureYieldFunc)) { + int extraCulture = cultureYieldFunc.Invoke(new ScriptContext(this.owner, this)); result += extraCulture; } diff --git a/C7Engine/C7GameData/Inflow.cs b/C7Engine/C7GameData/Inflow.cs index 59d4aa3c2..464aa62fb 100644 --- a/C7Engine/C7GameData/Inflow.cs +++ b/C7Engine/C7GameData/Inflow.cs @@ -30,12 +30,16 @@ public class Inflow : IProducible { 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; } @@ -45,26 +49,19 @@ public Inflow(SaveInflow saveInflow, LuaRulesEngine luaRulesEngine) { this.localYield = saveInflow.localYield.ConvertAll(y => new LocalYield(y.yieldType, luaRulesEngine, y.yieldCalculation)); } - public Func GetCommerceYieldFunc() { - return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.commerce).yieldCalculation; + public Func GetInflowYieldFunc(InflowYield yieldType) { + return this.localYield.FirstOrDefault(y => y.yieldType == yieldType).yieldCalculation; } - public Func GetCultureYieldFunc() { - return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.culture).yieldCalculation; - } - public Func GetScienceYieldFunc() { - return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.science).yieldCalculation; - } - public Func GetHappinessYieldFunc() { - return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.happiness).yieldCalculation; - } - public Func GetMaintenanceYieldFunc() { - return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.maintenance).yieldCalculation; - } - public Func GetUnitSupportYieldFunc() { - return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.unitsupport).yieldCalculation; - } - public Func GetCorruptionYieldFunc() { - return this.localYield.FirstOrDefault(y => y.yieldType == InflowYield.corruption).yieldCalculation; + + public bool TryGetInflowYieldFunc(InflowYield yieldType, out Func yieldFunc) { + Func yieldCalculation = GetInflowYieldFunc(yieldType); + if (yieldCalculation != null) { + yieldFunc = yieldCalculation; + return true; + } + + yieldFunc = null; + return false; } } diff --git a/C7Engine/C7GameData/Player.cs b/C7Engine/C7GameData/Player.cs index 9a90d945e..7b54ddf3f 100644 --- a/C7Engine/C7GameData/Player.cs +++ b/C7Engine/C7GameData/Player.cs @@ -878,8 +878,8 @@ private bool CanAdvanceToNextEra(GameData gameData) { foreach (City city in cities) { // unitSupport lua infow - if (city.itemBeingProduced is Inflow inflowUnitSupport && inflowUnitSupport.GetUnitSupportYieldFunc() != null) { - int unitSupportLess = inflowUnitSupport.GetUnitSupportYieldFunc().Invoke(new ScriptContext(this, city)); + if (city.itemBeingProduced is Inflow inflowUnitSupport && inflowUnitSupport.TryGetInflowYieldFunc(InflowYield.unitsupport, out var unitSupportYieldFunc)) { + int unitSupportLess = unitSupportYieldFunc.Invoke(new ScriptContext(this, city)); result.unitSupportCost -= unitSupportLess; } }