From a20cabca8aee723fd7222f0a9a056e9d527c8852 Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Wed, 11 Feb 2026 00:56:21 +0000 Subject: [PATCH 1/5] Don't show popup when non-human players are awarded shields for forest clearing --- C7Engine/C7GameData/Tile.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/C7Engine/C7GameData/Tile.cs b/C7Engine/C7GameData/Tile.cs index 03e9aa707..fb0080f34 100644 --- a/C7Engine/C7GameData/Tile.cs +++ b/C7Engine/C7GameData/Tile.cs @@ -510,7 +510,11 @@ public void MaybeAwardForestClearingShields() { int shieldsAwarded = EngineStorage.gameData.rules.ForestValueInShields; c.shieldsStored += shieldsAwarded; c.shieldsStored = Math.Min(c.shieldsStored, c.owner.ShieldCost(c.itemBeingProduced)); - new MsgShowTemporaryPopup($"{shieldsAwarded} shields awarded for clearing forests", other).send(); + + if (c.owner.isHuman) { + new MsgShowTemporaryPopup($"{shieldsAwarded} shields awarded for clearing forests", other).send(); + } + return; } } From 4605ea07ad446d5fd922f4f569b0fb3dba574f2c Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Wed, 11 Feb 2026 00:58:31 +0000 Subject: [PATCH 2/5] Push TemporaryPopup showing mechanism into the class --- C7/Game.cs | 7 +++---- C7/UIElements/Popups/TemporaryPopup.cs | 8 ++++++++ C7/UIElements/RightClickMenu.cs | 5 +---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/C7/Game.cs b/C7/Game.cs index 0a0e9179f..b26a7603c 100644 --- a/C7/Game.cs +++ b/C7/Game.cs @@ -278,10 +278,9 @@ public void HandleEngineMessage(MessageToUI msg) { turnsLeftToFastForward = 0; break; case MsgShowTemporaryPopup mSTP: - TemporaryPopup popup = new(mSTP.message, 1); - popup.SetPosition(mapView.screenLocationOfTile(mSTP.location, true) + new Vector2(0, -64)); - AddChild(popup); - popup.ShowPopup(); + Vector2 pos = mapView.screenLocationOfTile(mSTP.location, true); + Vector2 offset = new(0, -32); + TemporaryPopup.Show(this, mSTP.message, pos + offset); break; case MsgUnitMoved mUUAAB: EmitSignal(SignalName.UnitMoved, new ParameterWrapper(mUUAAB.Unit)); diff --git a/C7/UIElements/Popups/TemporaryPopup.cs b/C7/UIElements/Popups/TemporaryPopup.cs index d85e35208..c8379d18f 100644 --- a/C7/UIElements/Popups/TemporaryPopup.cs +++ b/C7/UIElements/Popups/TemporaryPopup.cs @@ -39,6 +39,14 @@ public static StyleBoxFlat PopupTechStyleBox() { return styleBox; } + public static void Show(Node parent, string msg, Vector2 rootPosition) { + TemporaryPopup popup = new(msg, 1); + Vector2 offset = new(0, -64); + popup.SetPosition(rootPosition + offset); + parent.AddChild(popup); + popup.ShowPopup(); + } + public async void ShowPopup() { // Wait until we hit our duration then destroy ourself. await Task.Delay(durationInMillis); diff --git a/C7/UIElements/RightClickMenu.cs b/C7/UIElements/RightClickMenu.cs index e0308e677..854dd06b6 100644 --- a/C7/UIElements/RightClickMenu.cs +++ b/C7/UIElements/RightClickMenu.cs @@ -102,10 +102,7 @@ public override void _Input(InputEvent @event) { } public void ShowCannotMovePopup() { - TemporaryPopup popup = new("This unit has already moved.", 1); - popup.SetPosition(position + new Vector2(0, -64)); - GetParent().AddChild(popup); - popup.ShowPopup(); + TemporaryPopup.Show(GetParent(), "This unit has already moved.", position); } } From 0fd4d53bf40076e9509e4abd6200f032bea73770 Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Wed, 11 Feb 2026 03:11:00 +0000 Subject: [PATCH 3/5] Adjust CanEnterTile logic to raise a warning when attempting an illegal move --- C7Engine/C7GameData/MapUnit.cs | 57 +++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/C7Engine/C7GameData/MapUnit.cs b/C7Engine/C7GameData/MapUnit.cs index 7e6c3dc26..ff4082eea 100644 --- a/C7Engine/C7GameData/MapUnit.cs +++ b/C7Engine/C7GameData/MapUnit.cs @@ -74,6 +74,10 @@ public bool CanDefendOnLand() { return IsLandUnit() && unitType.defense > 0; } + public bool IsNonCombatUnit() { + return unitType.attack == 0; + } + public bool HasRank() { return this.unitType.attack > 0 || this.unitType.defense > 0; } @@ -500,6 +504,14 @@ public bool CanEnterTile(Tile tile, bool allowCombat) { // Like above, but allows specifying that we want to handle the case // where a war declaration could be made before the move. public bool CanEnterTile(Tile tile, bool allowCombat, bool allowWarDeclaration) { + return CanEnterTile(tile, new TileProbe() { + AllowCombat = allowCombat, + AllowWarDeclaration = allowWarDeclaration + }); + } + + // Generalized check to see whether a given tile is accessible to the unit in a given context. + public bool CanEnterTile(Tile tile, TileProbe probe) { if (this.owner.isHuman && !this.owner.HasExploredTile(tile)) return true; @@ -521,10 +533,25 @@ public bool CanEnterTile(Tile tile, bool allowCombat, bool allowWarDeclaration) if (this.IsLandUnit() && !tile.IsLand()) return false; + if (IsNonCombatUnit()) { + if (HasHostileCity(tile, owner)) { + if (probe.RaiseNotice) { + new MsgShowTemporaryPopup($"Only combat units can capture cities and improvements.", tile).send(); + } + return false; + } + if (HasHostileUnits(tile, owner)) { + if (probe.RaiseNotice) { + new MsgShowTemporaryPopup($"Non-combat units may not attack.", tile).send(); + } + return false; + } + } + // If we allow declaring war on this move, then it doesn't matter if // there are units belonging to another player on the tile. // TODO: unbreakable alliances - if (allowWarDeclaration) { + if (probe.AllowWarDeclaration) { return true; } @@ -541,7 +568,7 @@ public bool CanEnterTile(Tile tile, bool allowCombat, bool allowWarDeclaration) foreach (MapUnit other in tile.unitsOnTile) { if (other.owner != owner) { if (!other.owner.IsAtPeaceWith(owner)) - return allowCombat && unitType.attack > 0; + return probe.AllowCombat && unitType.attack > 0; return false; } } @@ -550,13 +577,25 @@ public bool CanEnterTile(Tile tile, bool allowCombat, bool allowWarDeclaration) // Check for cities belonging to other civs. if (tile.cityAtTile != null && tile.cityAtTile.owner != owner) { if (!this.owner.isHuman || this.owner.tileKnowledge.isActiveTile(tile)) - return allowCombat; + return probe.AllowCombat; return false; } return true; } + private static bool HasHostileUnits(Tile tile, Player player) { + foreach (MapUnit other in tile.unitsOnTile) { + if (player != other.owner && !player.IsAtPeaceWith(other.owner)) + return true; + } + return false; + } + + private static bool HasHostileCity(Tile tile, Player player) { + return tile.HasCity && !player.IsAtPeaceWith(tile.cityAtTile.owner); + } + /// /// Moves the unit in the given direction /// @@ -568,7 +607,7 @@ public bool CanEnterTile(Tile tile, bool allowCombat, bool allowWarDeclaration) public async Task move(TileDirection dir, bool wait = false) { (int dx, int dy) = dir.toCoordDiff(); Tile newLoc = EngineStorage.gameData.map.tileAt(dx + location.XCoordinate, dy + location.YCoordinate); - if ((newLoc != Tile.NONE) && CanEnterTile(newLoc, true) && (movementPoints.canMove)) { + if ((newLoc != Tile.NONE) && CanEnterTile(newLoc, TileProbe.MoveProbe()) && (movementPoints.canMove)) { facingDirection = dir; wake(); @@ -857,4 +896,14 @@ public List GetAvailableActions() { return result; } } + + public class TileProbe { + public bool AllowCombat { get; set; } + public bool AllowWarDeclaration { get; set; } + public bool RaiseNotice { get; set; } + + public static TileProbe MoveProbe() { + return new TileProbe() { AllowCombat = true, RaiseNotice = true }; + } + } } From d75f0f428b8d27d16cb2d1201af2a1f2a507b9ba Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Thu, 12 Feb 2026 01:17:48 +0000 Subject: [PATCH 4/5] Standardise temporary popup presentation, add some transparency --- C7/Game.cs | 8 ++------ C7/UIElements/Advisors/TechBox.cs | 2 +- C7/UIElements/Popups/TemporaryPopup.cs | 23 +++++++++++++++++++---- C7/UIElements/UnitButtons/UnitButtons.cs | 2 +- C7Engine/C7GameData/MapUnit.cs | 4 ++-- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/C7/Game.cs b/C7/Game.cs index b26a7603c..8c49f3da5 100644 --- a/C7/Game.cs +++ b/C7/Game.cs @@ -279,8 +279,7 @@ public void HandleEngineMessage(MessageToUI msg) { break; case MsgShowTemporaryPopup mSTP: Vector2 pos = mapView.screenLocationOfTile(mSTP.location, true); - Vector2 offset = new(0, -32); - TemporaryPopup.Show(this, mSTP.message, pos + offset); + TemporaryPopup.Show(this, mSTP.message, pos); break; case MsgUnitMoved mUUAAB: EmitSignal(SignalName.UnitMoved, new ParameterWrapper(mUUAAB.Unit)); @@ -519,10 +518,7 @@ private void HandleUnitSelection(InputEventMouseButton eventMouseButton) { bool canMove = unitSelector.SetSelectedUnit(to_select); if (!canMove) { - TemporaryPopup popup = new("This unit has already moved.", 1); - popup.SetPosition(eventMouseButton.Position + new Vector2(0, -64)); - AddChild(popup); - popup.ShowPopup(); + TemporaryPopup.Show(this, "This unit has already moved.", eventMouseButton.Position); } } diff --git a/C7/UIElements/Advisors/TechBox.cs b/C7/UIElements/Advisors/TechBox.cs index 9aa5c9e6f..271660e4f 100644 --- a/C7/UIElements/Advisors/TechBox.cs +++ b/C7/UIElements/Advisors/TechBox.cs @@ -156,7 +156,7 @@ private void ShowTooltip() { smallFontTheme.SetColor("font_color", "Label", new Color(0.71f, 0.35f, 0.13f)); var customTheme = new Theme(); - customTheme.SetStylebox("panel", "TooltipPanel", TemporaryPopup.PopupTechStyleBox()); + customTheme.SetStylebox("panel", "TooltipPanel", TemporaryPopup.TooltipStyleBox()); customTheme.SetColor("font_color", "TooltipLabel", Colors.Black); customTheme.SetFontSize("font_size", "TooltipLabel", 12); this.Theme = customTheme; diff --git a/C7/UIElements/Popups/TemporaryPopup.cs b/C7/UIElements/Popups/TemporaryPopup.cs index c8379d18f..e0bb59570 100644 --- a/C7/UIElements/Popups/TemporaryPopup.cs +++ b/C7/UIElements/Popups/TemporaryPopup.cs @@ -14,7 +14,7 @@ public TemporaryPopup(string text, float durationInSec) { } // A stylebox that works well for temporary popups or tooltips in game. - public static StyleBoxFlat PopupStyleBox() { + public static StyleBoxFlat TooltipStyleBox() { StyleBoxFlat styleBox = new(); styleBox.ContentMarginLeft = 5; styleBox.ContentMarginRight = 5; @@ -24,6 +24,16 @@ public static StyleBoxFlat PopupStyleBox() { return styleBox; } + public static StyleBoxFlat PopupStyleBox() { + StyleBoxFlat styleBox = new(); + styleBox.ContentMarginLeft = 5; + styleBox.ContentMarginRight = 5; + styleBox.ContentMarginTop = 2; + styleBox.ContentMarginBottom = 2; + styleBox.BgColor = Color.FromHtml("1C1C1C99"); + return styleBox; + } + public static StyleBoxFlat PopupTechStyleBox() { StyleBoxFlat styleBox = new(); styleBox.ContentMarginLeft = 5; @@ -40,10 +50,15 @@ public static StyleBoxFlat PopupTechStyleBox() { } public static void Show(Node parent, string msg, Vector2 rootPosition) { - TemporaryPopup popup = new(msg, 1); - Vector2 offset = new(0, -64); - popup.SetPosition(rootPosition + offset); + TemporaryPopup popup = new(msg, 2); + popup.SetPosition(rootPosition); parent.AddChild(popup); + + // Center the text, adjust it slightly above the target (tile) position. + // Note: Label size isn't defined until node has been added to the tree. + Vector2 offset = new(-(popup.Size.X / 2), -64); + popup.SetPosition(rootPosition + offset); + popup.ShowPopup(); } diff --git a/C7/UIElements/UnitButtons/UnitButtons.cs b/C7/UIElements/UnitButtons/UnitButtons.cs index cf0662ecd..a4577891a 100644 --- a/C7/UIElements/UnitButtons/UnitButtons.cs +++ b/C7/UIElements/UnitButtons/UnitButtons.cs @@ -101,7 +101,7 @@ private void AddNewButton(HBoxContainer row, string action) { button.TooltipText = tooltipText; var customTheme = new Theme(); - customTheme.SetStylebox("panel", "TooltipPanel", TemporaryPopup.PopupStyleBox()); + customTheme.SetStylebox("panel", "TooltipPanel", TemporaryPopup.TooltipStyleBox()); customTheme.SetColor("font_color", "TooltipLabel", Colors.White); button.Theme = customTheme; } diff --git a/C7Engine/C7GameData/MapUnit.cs b/C7Engine/C7GameData/MapUnit.cs index ff4082eea..b685bf3f7 100644 --- a/C7Engine/C7GameData/MapUnit.cs +++ b/C7Engine/C7GameData/MapUnit.cs @@ -536,13 +536,13 @@ public bool CanEnterTile(Tile tile, TileProbe probe) { if (IsNonCombatUnit()) { if (HasHostileCity(tile, owner)) { if (probe.RaiseNotice) { - new MsgShowTemporaryPopup($"Only combat units can capture cities and improvements.", tile).send(); + new MsgShowTemporaryPopup($"Only combat units can capture cities and improvements.", location).send(); } return false; } if (HasHostileUnits(tile, owner)) { if (probe.RaiseNotice) { - new MsgShowTemporaryPopup($"Non-combat units may not attack.", tile).send(); + new MsgShowTemporaryPopup($"Non-combat units may not attack.", location).send(); } return false; } From cdeb06403aec40a6f1e5f85510cc39472533db74 Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Fri, 13 Feb 2026 00:34:28 +0000 Subject: [PATCH 5/5] Leveraging TileProbe more --- C7/Game.cs | 4 +- C7/Map/GotoLayer.cs | 2 +- .../AI/Pathing/PathingAlgorithmChooser.cs | 4 +- C7Engine/AI/PlayerAI.cs | 2 +- C7Engine/AI/UnitAI/CombatAI.cs | 2 +- C7Engine/AI/UnitAI/DefenderAI.cs | 2 +- C7Engine/AI/UnitAI/EscortAI.cs | 2 +- C7Engine/AI/UnitAI/ExplorerAI.cs | 2 +- C7Engine/AI/UnitAI/SettlerAI.cs | 2 +- C7Engine/AI/UnitAI/WorkerAI.cs | 2 +- C7Engine/AI/UnitAiExtension.cs | 6 +- C7Engine/C7GameData/MapUnit.cs | 63 +++++++++++-------- 12 files changed, 53 insertions(+), 40 deletions(-) diff --git a/C7/Game.cs b/C7/Game.cs index 8c49f3da5..e22c5333c 100644 --- a/C7/Game.cs +++ b/C7/Game.cs @@ -932,7 +932,7 @@ private GotoInfo GetGotoInfo(Vector2 mousePos) { // declare war with the move) mark the path. if (result.moveCost == -1 && unit.location.distanceTo(tile) == 1 - && unit.CanEnterTile(tile, allowCombat: true, allowWarDeclaration: true)) { + && unit.CanEnterTile(tile, TileProbe.DeclareWarProbe())) { Queue pathQueue = new(); pathQueue.Enqueue(tile); @@ -944,7 +944,7 @@ private GotoInfo GetGotoInfo(Vector2 mousePos) { // If we couldn't enter this tile without a war declaration, // record which civ we need to declare war on. - if (!unit.CanEnterTile(tile, allowCombat: true, allowWarDeclaration: false)) { + if (!unit.CanEnterTile(tile, TileProbe.CombatProbe())) { if (tile.cityAtTile != null) { result.requiresWarDeclarationOnPlayer = tile.cityAtTile.owner; } else { diff --git a/C7/Map/GotoLayer.cs b/C7/Map/GotoLayer.cs index 2a3829ee2..e72e3edff 100644 --- a/C7/Map/GotoLayer.cs +++ b/C7/Map/GotoLayer.cs @@ -85,7 +85,7 @@ public override void drawObject(LooseView looseView, GameData gameData, Tile til DrawStaticGoToCursor(looseView, tileCenter, 0, true); } - if (gotoInfo.path != null && unit.CanEnterTile(gotoInfo.destinationTile, true, true)) { + if (gotoInfo.path != null && unit.CanEnterTile(gotoInfo.destinationTile, TileProbe.GotoProbe())) { List tiles = new List(); tiles.Add(unitOriginTile); tiles.AddRange(gotoInfo.path.path); diff --git a/C7Engine/AI/Pathing/PathingAlgorithmChooser.cs b/C7Engine/AI/Pathing/PathingAlgorithmChooser.cs index 2f149c667..56801938d 100644 --- a/C7Engine/AI/Pathing/PathingAlgorithmChooser.cs +++ b/C7Engine/AI/Pathing/PathingAlgorithmChooser.cs @@ -30,8 +30,8 @@ public static PathingAlgorithm GetAlgorithm(MapUnit unit) { // to allow pathing to attack. We don't want to try and path // through opponents on the way to our destination though, // as we can get stuck. - bool allowCombat = neighbor == destination; - return unit.CanEnterTile(neighbor, allowCombat); + var probe = neighbor == destination ? TileProbe.PathCombatProbe() : TileProbe.PathProbe(); + return unit.CanEnterTile(neighbor, probe); } ); } diff --git a/C7Engine/AI/PlayerAI.cs b/C7Engine/AI/PlayerAI.cs index 663a2eab4..e1c7f1d7f 100644 --- a/C7Engine/AI/PlayerAI.cs +++ b/C7Engine/AI/PlayerAI.cs @@ -247,7 +247,7 @@ private static UnitAI GetCombatAIIfUnitCanAttackNearbyBarbCamp(MapUnit unit, Pla } List reachableBarbCampsTiles = player.tileKnowledge.AllKnownTiles() - .Where(t => unit.CanEnterTile(t, true) && t.hasBarbarianCamp).ToList(); + .Where(t => unit.CanEnterTile(t, TileProbe.AiCombatProbe()) && t.hasBarbarianCamp).ToList(); Tile closestBarbCamp = Tile.NONE; int closestBarbDistance = int.MaxValue; diff --git a/C7Engine/AI/UnitAI/CombatAI.cs b/C7Engine/AI/UnitAI/CombatAI.cs index 088eb37e7..2a78c7566 100644 --- a/C7Engine/AI/UnitAI/CombatAI.cs +++ b/C7Engine/AI/UnitAI/CombatAI.cs @@ -81,7 +81,7 @@ public C7GameData.UnitAI.MoveResult PlayTurnImpl(Player player, MapUnit unit) { } // Move along the path, initiating combat if we reach our target. - return this.TryToMoveAlongPath(unit, ref data.path, allowCombat: true); + return this.TryToMoveAlongPath(unit, ref data.path, TileProbe.AiCombatProbe()); } public string SummarizePlan() { diff --git a/C7Engine/AI/UnitAI/DefenderAI.cs b/C7Engine/AI/UnitAI/DefenderAI.cs index c45a784d9..3523e140f 100644 --- a/C7Engine/AI/UnitAI/DefenderAI.cs +++ b/C7Engine/AI/UnitAI/DefenderAI.cs @@ -53,7 +53,7 @@ C7GameData.UnitAI.MoveResult C7GameData.UnitAI.PlayTurnImpl(Player player, MapUn return C7GameData.UnitAI.Result.Done; } else { log.Debug("Moving defender towards " + data.destination); - return this.TryToMoveAlongPath(unit, ref data.pathToDestination, allowCombat: false); + return this.TryToMoveAlongPath(unit, ref data.pathToDestination, TileProbe.AiMoveProbe()); } } diff --git a/C7Engine/AI/UnitAI/EscortAI.cs b/C7Engine/AI/UnitAI/EscortAI.cs index 7070a2f01..b23272e8d 100644 --- a/C7Engine/AI/UnitAI/EscortAI.cs +++ b/C7Engine/AI/UnitAI/EscortAI.cs @@ -80,7 +80,7 @@ UnitAI.MoveResult UnitAI.PlayTurnImpl(Player player, MapUnit unit) { // Move to the unit we're escorting. TilePath path = PathingAlgorithmChooser.GetAlgorithm(unit).PathFrom(unit.location, data.unitToEscort.location, unit); - result = this.TryToMoveAlongPath(unit, ref path, allowCombat: false); + result = this.TryToMoveAlongPath(unit, ref path, TileProbe.AiMoveProbe()); if (result != UnitAI.Result.InProgress) { return result; } diff --git a/C7Engine/AI/UnitAI/ExplorerAI.cs b/C7Engine/AI/UnitAI/ExplorerAI.cs index 0c7344f86..be73ba980 100644 --- a/C7Engine/AI/UnitAI/ExplorerAI.cs +++ b/C7Engine/AI/UnitAI/ExplorerAI.cs @@ -51,7 +51,7 @@ UnitAI.MoveResult UnitAI.PlayTurnImpl(Player player, MapUnit unit) { return UnitAI.Result.Done; } - return this.TryToMoveAlongPath(unit, ref data.pathToDestination, allowCombat: false); + return this.TryToMoveAlongPath(unit, ref data.pathToDestination, TileProbe.AiMoveProbe()); } public string SummarizePlan() { diff --git a/C7Engine/AI/UnitAI/SettlerAI.cs b/C7Engine/AI/UnitAI/SettlerAI.cs index e90dfd033..011174963 100644 --- a/C7Engine/AI/UnitAI/SettlerAI.cs +++ b/C7Engine/AI/UnitAI/SettlerAI.cs @@ -66,7 +66,7 @@ C7GameData.UnitAI.MoveResult UnitAI.PlayTurnImpl(Player player, MapUnit unit) { unit.movementPoints.onConsumeAll(); return C7GameData.UnitAI.Result.InProgress; } else { - return this.TryToMoveAlongPath(unit, ref data.pathToDestination, allowCombat: false); + return this.TryToMoveAlongPath(unit, ref data.pathToDestination, TileProbe.AiMoveProbe()); } break; case SettlerAIData.SettlerGoal.JOIN_CITY: diff --git a/C7Engine/AI/UnitAI/WorkerAI.cs b/C7Engine/AI/UnitAI/WorkerAI.cs index 8746b3659..047a272cc 100644 --- a/C7Engine/AI/UnitAI/WorkerAI.cs +++ b/C7Engine/AI/UnitAI/WorkerAI.cs @@ -79,7 +79,7 @@ UnitAI.MoveResult UnitAI.PlayTurnImpl(Player player, MapUnit unit) { return PerformWorkerMove(unit, improvement); } - return this.TryToMoveAlongPath(unit, ref data.pathToDestination, allowCombat: false); + return this.TryToMoveAlongPath(unit, ref data.pathToDestination, TileProbe.AiMoveProbe()); } private static Terraform? GetTileImprovement(Tile t, MapUnit unit) { diff --git a/C7Engine/AI/UnitAiExtension.cs b/C7Engine/AI/UnitAiExtension.cs index 4cdbe11b4..b7757b8b0 100644 --- a/C7Engine/AI/UnitAiExtension.cs +++ b/C7Engine/AI/UnitAiExtension.cs @@ -13,17 +13,17 @@ public static class UnitAiExtension { // Attempts to move the supplied unit along the given path. // // `path` is a ref so that the path can be recalculated if necessary. - public static MoveResult TryToMoveAlongPath(this UnitAI unitAi, MapUnit unit, ref TilePath path, bool allowCombat) { + public static MoveResult TryToMoveAlongPath(this UnitAI unitAi, MapUnit unit, ref TilePath path, TileProbe probe) { if (!unit.movementPoints.canMove) { return UnitAI.Result.InProgress; } Tile nextTile = path.Next(); - if (nextTile == Tile.NONE || !unit.CanEnterTile(nextTile, allowCombat)) { + if (nextTile == Tile.NONE || !unit.CanEnterTile(nextTile, probe)) { log.Information($"Attempting to repath {unit} from {unit.location} to {path.destination}"); // Attempt to repath. If we succeed, return inprogress so we get // called again. path = PathingAlgorithmChooser.GetAlgorithm(unit).PathFrom(unit.location, path.destination, unit); - if ((path?.PathLength() ?? -1) == -1 || path.PeekNext() == Tile.NONE || !unit.CanEnterTile(path.PeekNext(), allowCombat)) { + if ((path?.PathLength() ?? -1) == -1 || path.PeekNext() == Tile.NONE || !unit.CanEnterTile(path.PeekNext(), probe)) { return UnitAI.Result.Error; } diff --git a/C7Engine/C7GameData/MapUnit.cs b/C7Engine/C7GameData/MapUnit.cs index b685bf3f7..a4c19f15d 100644 --- a/C7Engine/C7GameData/MapUnit.cs +++ b/C7Engine/C7GameData/MapUnit.cs @@ -74,10 +74,6 @@ public bool CanDefendOnLand() { return IsLandUnit() && unitType.defense > 0; } - public bool IsNonCombatUnit() { - return unitType.attack == 0; - } - public bool HasRank() { return this.unitType.attack > 0 || this.unitType.defense > 0; } @@ -354,7 +350,7 @@ public async Task fight(MapUnit defender) { // TODO: Defender retreat behavior requires some more work. There's an issue for it here: // https://github.com/C7-Game/Prototype/issues/274 Tile retreatDestination = defender.location.neighbors[attacker.facingDirection]; - if ((retreatDestination != Tile.NONE) && defender.CanEnterTile(retreatDestination, false)) { + if ((retreatDestination != Tile.NONE) && defender.CanEnterTile(retreatDestination, TileProbe.PathProbe())) { await defender.move(attacker.facingDirection, true); result = CombatResult.DefenderRetreated; break; @@ -497,19 +493,6 @@ public void OnEnterTile(Tile tile) { } } - public bool CanEnterTile(Tile tile, bool allowCombat) { - return CanEnterTile(tile, allowCombat, allowWarDeclaration: false); - } - - // Like above, but allows specifying that we want to handle the case - // where a war declaration could be made before the move. - public bool CanEnterTile(Tile tile, bool allowCombat, bool allowWarDeclaration) { - return CanEnterTile(tile, new TileProbe() { - AllowCombat = allowCombat, - AllowWarDeclaration = allowWarDeclaration - }); - } - // Generalized check to see whether a given tile is accessible to the unit in a given context. public bool CanEnterTile(Tile tile, TileProbe probe) { if (this.owner.isHuman && !this.owner.HasExploredTile(tile)) @@ -533,18 +516,20 @@ public bool CanEnterTile(Tile tile, TileProbe probe) { if (this.IsLandUnit() && !tile.IsLand()) return false; - if (IsNonCombatUnit()) { + if (!HasRank()) { if (HasHostileCity(tile, owner)) { if (probe.RaiseNotice) { new MsgShowTemporaryPopup($"Only combat units can capture cities and improvements.", location).send(); + return false; } - return false; + return true; } if (HasHostileUnits(tile, owner)) { if (probe.RaiseNotice) { new MsgShowTemporaryPopup($"Non-combat units may not attack.", location).send(); + return false; } - return false; + return true; } } @@ -897,13 +882,41 @@ public List GetAvailableActions() { } } - public class TileProbe { - public bool AllowCombat { get; set; } - public bool AllowWarDeclaration { get; set; } - public bool RaiseNotice { get; set; } + public struct TileProbe { + public bool AllowCombat { get; init; } + public bool AllowWarDeclaration { get; init; } + public bool RaiseNotice { get; init; } + + public static TileProbe AiMoveProbe() { + return new TileProbe() { AllowCombat = false, AllowWarDeclaration = false, RaiseNotice = false }; + } + + public static TileProbe AiCombatProbe() { + return new TileProbe() { AllowCombat = true, AllowWarDeclaration = false, RaiseNotice = false }; + } + + public static TileProbe PathProbe() { + return new TileProbe() { AllowCombat = false, RaiseNotice = false }; + } + + public static TileProbe PathCombatProbe() { + return new TileProbe() { AllowCombat = true, RaiseNotice = false }; + } public static TileProbe MoveProbe() { return new TileProbe() { AllowCombat = true, RaiseNotice = true }; } + + public static TileProbe CombatProbe() { + return new TileProbe() { AllowCombat = true, AllowWarDeclaration = false, RaiseNotice = false }; + } + + public static TileProbe GotoProbe() { + return new TileProbe() { AllowCombat = true, AllowWarDeclaration = true, RaiseNotice = false }; + } + + public static TileProbe DeclareWarProbe() { + return new TileProbe() { AllowCombat = true, AllowWarDeclaration = true, RaiseNotice = false }; + } } }