diff --git a/C7/Animations/AnimationManager.cs b/C7/Animations/AnimationManager.cs index 10196a1f8..b619d0726 100644 --- a/C7/Animations/AnimationManager.cs +++ b/C7/Animations/AnimationManager.cs @@ -40,6 +40,10 @@ public static string AnimationKey(MapUnit unit, MapUnit.AnimatedAction action, T return AnimationKey(BaseAnimationKey(unit, action), direction); } + public static string AnimationKey(AnimatedEffect effect, MapUnit.AnimatedAction action) { + return $"{effect.ToString()}_{action.ToString()}"; + } + public static readonly Dictionary AnimationThumbnails = new(); public static readonly Dictionary AnimationTintThumbnails = new(); @@ -112,6 +116,10 @@ public IniData getUnitINIData(string unitTypeName) { return getINIData(string.Format("Art/Units/{0}/{0}.INI", unitTypeName)); } + public string GetFlicFilePath(string rootPath, IniData iniData, MapUnit.AnimatedAction action) { + return rootPath + "/" + getFlicFileName(iniData, action); + } + public string getUnitFlicFilepath(MapUnit unit, MapUnit.AnimatedAction action) { string directory = string.Format("Art/Units/{0}", unit.GetArtName()); IniData ini = getUnitINIData(unit.GetArtName()); @@ -169,6 +177,23 @@ public static void loadFlicAnimation(string path, string name, ref SpriteFrames } } + public static void loadFlicEffectAnimation(string path, string name, ref SpriteFrames frames, ref SpriteFrames tint) { + Flic flic = Util.LoadFlic(path); + + for (int row = 0; row < flic.Images.GetLength(0); row++) { + string animationName = name; + frames.AddAnimation(animationName); + tint.AddAnimation(animationName); + + for (int col = 0; col < flic.Images.GetLength(1); col++) { + byte[] frame = flic.Images[row,col]; + (ImageTexture bl, ImageTexture tl) = Util.LoadTextureFromFlicData(frame, flic.Palette, flic.Width, flic.Height); + frames.AddFrame(animationName, bl, 0.5f); // TODO: frame duration is controlled by .ini + tint.AddFrame(animationName, tl, 0.5f); // TODO: frame duration is controlled by .ini + } + } + } + public bool LoadAnimation(MapUnit unit, MapUnit.AnimatedAction action) { string name = BaseAnimationKey(unit.GetArtName(), action); string testName = AnimationKey(name, TileDirection.NORTH); @@ -180,11 +205,20 @@ public bool LoadAnimation(MapUnit unit, MapUnit.AnimatedAction action) { return true; } + public bool LoadAnimation(AnimatedEffect effect, MapUnit.AnimatedAction action, string flicFilePath) { + string name = AnimationKey(effect, action); + if (spriteFrames.HasAnimation(name) && tintFrames.HasAnimation(name)) { + return false; + } + loadFlicEffectAnimation(flicFilePath, name, ref this.spriteFrames, ref this.tintFrames); + return true; + } + private Dictionary flicSheets = new Dictionary(); public Util.FlicSheet getFlicSheet(string rootPath, IniData iniData, MapUnit.AnimatedAction action) { Util.FlicSheet tr; - string pathKey = rootPath + "/" + getFlicFileName(iniData, action); + string pathKey = GetFlicFilePath(rootPath, iniData, action); if (!flicSheets.TryGetValue(pathKey, out tr)) { (tr, _) = Util.loadFlicSheet(pathKey); flicSheets.Add(pathKey, tr); @@ -229,6 +263,7 @@ public partial class C7Animation { public string folderPath { get; private set; } // For example "Art/Units/Warrior" or "Art/Animations/Trajectory" public string iniFileName { get; private set; } private MapUnit unit; + public AnimatedEffect effect; public MapUnit.AnimatedAction action { get; private set; } public C7Animation(AnimationManager civ3AnimData, MapUnit unit, MapUnit.AnimatedAction action) { @@ -264,6 +299,7 @@ public C7Animation(AnimationManager civ3AnimData, AnimatedEffect effect) { this.folderPath = "Art/Animations/" + effectCategories[effect]; this.iniFileName = effectINIFileNames[effect]; this.action = MapUnit.AnimatedAction.DEATH; + this.effect = effect; } public IniData getINIData() { @@ -278,6 +314,11 @@ public void loadSpriteAnimation() { this.animationManager.LoadAnimation(this.unit, this.action); } + public void loadEffectAnimation() { + var path = animationManager.GetFlicFilePath(folderPath, getINIData(), action); + this.animationManager.LoadAnimation(this.effect, this.action, path); + } + public void playSound() { animationManager.playSound(folderPath, getINIData(), action); } diff --git a/C7/Assets b/C7/Assets index 4a8289fe5..0bb74e964 160000 --- a/C7/Assets +++ b/C7/Assets @@ -1 +1 @@ -Subproject commit 4a8289fe522ff3dcb46f608f2de3f277d10186e0 +Subproject commit 0bb74e96472fe2c254dac2d0daacd56866c40720 diff --git a/C7/Game.cs b/C7/Game.cs index be1ba5909..c68c48932 100644 --- a/C7/Game.cs +++ b/C7/Game.cs @@ -51,6 +51,32 @@ public class GotoInfo { }; public GotoInfo gotoInfo = null; + public class BombardInfo { + public MapUnit bombardingUnit; + public Tile mouseTile; + + public BombardInfo(MapUnit bombardingUnit) { + this.bombardingUnit = bombardingUnit; + } + + public bool requiresWarDeclaration(Tile tile, out Player player) { + player = null; + + var bombarder = bombardingUnit.owner; + var foreignUnits = tile.unitsOnTile.Where(x => x.owner != bombarder).ToList(); + if (!foreignUnits.Any()) + return false; + + var targetPlayers = foreignUnits.Select(x => x.owner).Distinct(); + var friendly= targetPlayers.Where(p => bombarder.IsAtPeaceWith(p)).ToList(); + player = friendly.FirstOrDefault(); + return friendly.Any(); + + // TODO: handle complex scenarios arising from multiple civs co-located on tile + } + }; + public BombardInfo bombardInfo = null; + public class TileInfo { public Tile targetTile; public HashSet coveredTiles = []; @@ -525,6 +551,12 @@ private void OnSingleLeftMouseButtonClick(InputEventMouseButton eventMouseButton if (gotoInfo != null) { HandleGotoClick(gotoInfo); setGotoMode(false); + } else if (bombardInfo != null) { + Tile tile = PositionToTile(eventMouseButton.Position); + if (bombardInfo.bombardingUnit.canBombardTile(tile)) { + HandleBombardClick(bombardInfo, tile); + setBombard(null); + } } else { // Select unit on tile at mouse location HandleUnitSelection(eventMouseButton); @@ -581,6 +613,8 @@ private void HandleRightClickOnTile(Tile tile, InputEventMouseButton eventMouseB else if (!shiftDown && tile.cityAtTile?.owner == controller) // There are no units, but this is the player's city. new RightClickCityMenu(this, tile).Open(eventMouseButton.Position); + else if (bombardInfo != null) + setBombard(null); else ShowTileInfo(tile); @@ -636,6 +670,8 @@ private void HandleMouseMotionInput(InputEventMouseMotion eventMouseMotion) { OldPosition = eventMouseMotion.Position; } else if (gotoInfo != null) { gotoInfo = GetGotoInfo(eventMouseMotion.Position); + } else if (bombardInfo != null) { + bombardInfo.mouseTile = PositionToTile(eventMouseMotion.Position); } } @@ -793,6 +829,11 @@ private void ProcessAction(string currentAction) { return; } + if (currentAction == C7Action.Escape && bombardInfo != null) { + setBombard(null); + return; + } + // never poll for actions if UI elements are visible if (popupOverlay.Visible || cityScreen.Visible || advisor.Visible || diplomacy.Visible || palaceScene.Visible) { return; @@ -909,6 +950,15 @@ private void ProcessAction(string currentAction) { }); } + if (currentAction == C7Action.UnitBombard) { + if (CurrentlySelectedUnit != MapUnit.NONE && (CurrentlySelectedUnit?.canBombard() ?? false)) { + EngineStorage.ReadGameData((GameData gameData) => { + MapUnit currentUnit = gameData.GetUnit(CurrentlySelectedUnit.id); + setBombard(currentUnit); + }); + } + } + Terraform terraform = C7Action.ToTerraform(currentAction); if (CurrentlySelectedUnit == MapUnit.NONE || CurrentlySelectedUnit == null @@ -939,32 +989,41 @@ private void setGotoMode(bool isOn) { } } + private void setBombard(MapUnit bombardingUnit) { + bombardInfo = bombardingUnit == null ? null : new BombardInfo(bombardingUnit); + if (bombardingUnit == null) + Input.SetCustomMouseCursor(null); + } + private void HandleGotoClick(GotoInfo info) { if (info == null || info.moveCost == -1) { return; } EngineStorage.ReadGameData((GameData gameData) => { - int currentTurn = gameData.turn; - // If this move would require declaring war, display a popup that checks // if the player really wants to declare war. If they do, declare the // war for them, clear out the player, and call this method again. if (info.requiresWarDeclarationOnPlayer != null) { GotoInfo stashed = info; - popupOverlay.ShowPopup(new WarConfirmation(stashed.requiresWarDeclarationOnPlayer, - () => { - controller.DeclareWarOn(info.requiresWarDeclarationOnPlayer, currentTurn); - stashed.requiresWarDeclarationOnPlayer = null; - HandleGotoClick(stashed); - }), PopupOverlay.PopupCategory.Advisor); - return; + MaybeDeclareWar(stashed.requiresWarDeclarationOnPlayer, gameData.turn, () => { + stashed.requiresWarDeclarationOnPlayer = null; + HandleGotoClick(stashed); + }); + } else { + new MsgSetUnitPath(CurrentlySelectedUnit.id, info.path).send(); } - - new MsgSetUnitPath(CurrentlySelectedUnit.id, info.path).send(); }); } + private void MaybeDeclareWar(Player player, int currentTurn, Action callback) { + popupOverlay.ShowPopup(new WarConfirmation(player, + () => { + controller.DeclareWarOn(player, currentTurn); + callback(); + }), PopupOverlay.PopupCategory.Advisor); + } + private GotoInfo GetGotoInfo(Vector2 mousePos) { GotoInfo result = new(); @@ -1018,6 +1077,22 @@ private GotoInfo GetGotoInfo(Vector2 mousePos) { return result; } + private void HandleBombardClick(BombardInfo info, Tile tile) { + if (info == null || tile == null) { + return; + } + + EngineStorage.ReadGameData((GameData gameData) => { + if (info.requiresWarDeclaration(tile, out var player)) { + MaybeDeclareWar(player, gameData.turn, () => { + new MsgBombard(CurrentlySelectedUnit.id, tile).send(); + }); + } else { + new MsgBombard(CurrentlySelectedUnit.id, tile).send(); + } + }); + } + /** * User quit. We *may* want to do some things here like make a back-up save, or call the server and let it know we're bailing (esp. in MP). **/ diff --git a/C7/Lua/game_modes/base-ruleset.json b/C7/Lua/game_modes/base-ruleset.json index 0b7494c68..99852bdae 100644 --- a/C7/Lua/game_modes/base-ruleset.json +++ b/C7/Lua/game_modes/base-ruleset.json @@ -791,6 +791,8 @@ "attack": 0, "defense": 0, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -867,6 +869,8 @@ "attack": 0, "defense": 0, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -944,6 +948,8 @@ "attack": 0, "defense": 0, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Russia", @@ -988,6 +994,8 @@ "attack": 0, "defense": 0, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Rome", @@ -1054,6 +1062,8 @@ "attack": 12, "defense": 6, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -1124,6 +1134,8 @@ "attack": 6, "defense": 11, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -1194,6 +1206,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "A Barbarian Chiefdom", @@ -1262,6 +1276,8 @@ "attack": 2, "defense": 1, "bombard": 1, + "bombardRange": 0, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "Rome", @@ -1328,6 +1344,8 @@ "attack": 1, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -1392,6 +1410,8 @@ "attack": 3, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Egypt", @@ -1460,6 +1480,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Rome", @@ -1529,6 +1551,8 @@ "attack": 2, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "A Barbarian Chiefdom", @@ -1600,6 +1624,8 @@ "attack": 1, "defense": 3, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -1668,6 +1694,8 @@ "attack": 4, "defense": 1, "bombard": 2, + "bombardRange": 0, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "Rome", @@ -1735,6 +1763,8 @@ "attack": 2, "defense": 4, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -1805,6 +1835,8 @@ "attack": 4, "defense": 3, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Rome", @@ -1872,6 +1904,8 @@ "attack": 4, "defense": 6, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -1940,6 +1974,8 @@ "attack": 6, "defense": 3, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 3, "producibleBy": [ "Rome", @@ -2009,6 +2045,8 @@ "attack": 6, "defense": 10, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -2080,6 +2118,8 @@ "attack": 16, "defense": 8, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Rome", @@ -2151,6 +2191,8 @@ "attack": 12, "defense": 18, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Rome", @@ -2222,6 +2264,8 @@ "attack": 24, "defense": 16, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 3, "producibleBy": [ "Rome", @@ -2293,6 +2337,8 @@ "attack": 0, "defense": 0, "bombard": 4, + "bombardRange": 1, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "Rome", @@ -2362,6 +2408,8 @@ "attack": 0, "defense": 0, "bombard": 8, + "bombardRange": 1, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "Rome", @@ -2434,6 +2482,8 @@ "attack": 0, "defense": 0, "bombard": 12, + "bombardRange": 2, + "rateOfFire": 2, "movement": 1, "producibleBy": [ "Rome", @@ -2503,6 +2553,8 @@ "attack": 0, "defense": 0, "bombard": 16, + "bombardRange": 2, + "rateOfFire": 3, "movement": 2, "flags": [ "rotateBeforeAttack" @@ -2577,6 +2629,8 @@ "attack": 0, "defense": 0, "bombard": 16, + "bombardRange": 4, + "rateOfFire": 3, "movement": 1, "producibleBy": [ "Rome", @@ -2647,6 +2701,8 @@ "attack": 0, "defense": 0, "bombard": 0, + "bombardRange": 6, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -2719,6 +2775,8 @@ "attack": 0, "defense": 0, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -2790,6 +2848,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 3, "flags": [ "rotateBeforeAttack" @@ -2860,6 +2920,8 @@ "attack": 1, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 4, "flags": [ "rotateBeforeAttack" @@ -2930,6 +2992,8 @@ "attack": 2, "defense": 2, "bombard": 3, + "bombardRange": 1, + "rateOfFire": 2, "movement": 5, "flags": [ "rotateBeforeAttack" @@ -3004,6 +3068,8 @@ "attack": 1, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 4, "flags": [ "rotateBeforeAttack" @@ -3075,6 +3141,8 @@ "attack": 5, "defense": 6, "bombard": 6, + "bombardRange": 1, + "rateOfFire": 2, "movement": 3, "producibleBy": [ "Rome", @@ -3148,6 +3216,8 @@ "attack": 1, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 6, "producibleBy": [ "Rome", @@ -3218,6 +3288,8 @@ "attack": 1, "defense": 8, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 7, "producibleBy": [ "Rome", @@ -3288,6 +3360,8 @@ "attack": 8, "defense": 4, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 4, "producibleBy": [ "Rome", @@ -3358,6 +3432,8 @@ "attack": 12, "defense": 8, "bombard": 6, + "bombardRange": 1, + "rateOfFire": 2, "movement": 8, "producibleBy": [ "Rome", @@ -3429,6 +3505,8 @@ "attack": 18, "defense": 12, "bombard": 8, + "bombardRange": 2, + "rateOfFire": 2, "movement": 5, "flags": [ "rotateBeforeAttack" @@ -3503,6 +3581,8 @@ "attack": 15, "defense": 10, "bombard": 6, + "bombardRange": 2, + "rateOfFire": 2, "movement": 7, "producibleBy": [ "Rome", @@ -3575,6 +3655,8 @@ "attack": 8, "defense": 4, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 5, "producibleBy": [ "Rome", @@ -3645,6 +3727,8 @@ "attack": 4, "defense": 2, "bombard": 3, + "bombardRange": 0, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "Rome", @@ -3714,6 +3798,8 @@ "attack": 0, "defense": 2, "bombard": 12, + "bombardRange": 0, + "rateOfFire": 3, "movement": 1, "producibleBy": [ "Rome", @@ -3782,6 +3868,8 @@ "attack": 0, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -3851,6 +3939,8 @@ "attack": 8, "defense": 4, "bombard": 3, + "bombardRange": 0, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "Rome", @@ -3919,6 +4009,8 @@ "attack": 8, "defense": 6, "bombard": 6, + "bombardRange": 0, + "rateOfFire": 2, "movement": 1, "producibleBy": [ "Rome", @@ -3988,6 +4080,8 @@ "attack": 0, "defense": 5, "bombard": 18, + "bombardRange": 0, + "rateOfFire": 3, "movement": 1, "producibleBy": [ "Rome", @@ -4069,6 +4163,8 @@ "attack": 0, "defense": 0, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 3, "producibleBy": [ "Rome", @@ -4146,6 +4242,8 @@ "attack": 0, "defense": 0, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -4213,6 +4311,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Aztecs" @@ -4251,6 +4351,8 @@ "attack": 2, "defense": 2, "bombard": 1, + "bombardRange": 0, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "Babylon" @@ -4289,6 +4391,8 @@ "attack": 1, "defense": 3, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Greece" @@ -4327,6 +4431,8 @@ "attack": 1, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Zululand" @@ -4365,6 +4471,8 @@ "attack": 3, "defense": 3, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome" @@ -4406,6 +4514,8 @@ "attack": 4, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Persia" @@ -4447,6 +4557,8 @@ "attack": 2, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Egypt" @@ -4488,6 +4600,8 @@ "attack": 4, "defense": 3, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 3, "producibleBy": [ "China" @@ -4530,6 +4644,8 @@ "attack": 3, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Iroquois" @@ -4571,6 +4687,8 @@ "attack": 2, "defense": 5, "bombard": 2, + "bombardRange": 0, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "France" @@ -4612,6 +4730,8 @@ "attack": 4, "defense": 4, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Japan" @@ -4653,6 +4773,8 @@ "attack": 4, "defense": 3, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "India" @@ -4691,6 +4813,8 @@ "attack": 6, "defense": 3, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 3, "producibleBy": [ "Russia" @@ -4732,6 +4856,8 @@ "attack": 16, "defense": 8, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 3, "producibleBy": [ "Germany" @@ -4774,6 +4900,8 @@ "attack": 4, "defense": 2, "bombard": 4, + "bombardRange": 1, + "rateOfFire": 2, "movement": 5, "flags": [ "rotateBeforeAttack" @@ -4819,6 +4947,8 @@ "attack": 8, "defense": 4, "bombard": 6, + "bombardRange": 0, + "rateOfFire": 2, "movement": 1, "producibleBy": [ "America" @@ -4858,6 +4988,8 @@ "attack": 2, "defense": 1, "bombard": 3, + "bombardRange": 0, + "rateOfFire": 0, "movement": 5, "flags": [ "rotateBeforeAttack" @@ -4932,6 +5064,8 @@ "attack": 4, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Mongols" @@ -4973,6 +5107,8 @@ "attack": 3, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Spain" @@ -5013,6 +5149,8 @@ "attack": 6, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Scandinavia" @@ -5051,6 +5189,8 @@ "attack": 8, "defense": 3, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 3, "producibleBy": [ "Ottomans" @@ -5092,6 +5232,8 @@ "attack": 3, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Celts" @@ -5133,6 +5275,8 @@ "attack": 4, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 3, "producibleBy": [ "Arabia" @@ -5175,6 +5319,8 @@ "attack": 2, "defense": 3, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Carthage" @@ -5213,6 +5359,8 @@ "attack": 0, "defense": 0, "bombard": 8, + "bombardRange": 1, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "Korea" @@ -5255,6 +5403,8 @@ "attack": 4, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -5325,6 +5475,8 @@ "attack": 6, "defense": 6, "bombard": 3, + "bombardRange": 0, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "Rome", @@ -5392,6 +5544,8 @@ "attack": 0, "defense": 0, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [], "unproducible": true, @@ -5423,6 +5577,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5456,6 +5612,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5489,6 +5647,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5522,6 +5682,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5555,6 +5717,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5588,6 +5752,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5621,6 +5787,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5654,6 +5822,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5687,6 +5857,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5720,6 +5892,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5753,6 +5927,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5786,6 +5962,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5819,6 +5997,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5852,6 +6032,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5885,6 +6067,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5918,6 +6102,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5951,6 +6137,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -5984,6 +6172,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6017,6 +6207,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6050,6 +6242,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6083,6 +6277,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6116,6 +6312,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6149,6 +6347,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6182,6 +6382,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6215,6 +6417,8 @@ "attack": 1, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Sumeria" @@ -6253,6 +6457,8 @@ "attack": 2, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Hittites" @@ -6293,6 +6499,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6326,6 +6534,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6359,6 +6569,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6393,6 +6605,8 @@ "attack": 2, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 4, "flags": [ "rotateBeforeAttack" @@ -6434,6 +6648,8 @@ "attack": 1, "defense": 4, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Netherlands" @@ -6474,6 +6690,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6508,6 +6726,8 @@ "attack": 0, "defense": 0, "bombard": 6, + "bombardRange": 1, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "Rome", @@ -6576,6 +6796,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6609,6 +6831,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6642,6 +6866,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6675,6 +6901,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Inca" @@ -6713,6 +6941,8 @@ "attack": 2, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Maya" @@ -6751,6 +6981,8 @@ "attack": 2, "defense": 1, "bombard": 2, + "bombardRange": 1, + "rateOfFire": 2, "movement": 3, "producibleBy": [ "Byzantines" @@ -6790,6 +7022,8 @@ "attack": 15, "defense": 10, "bombard": 7, + "bombardRange": 1, + "rateOfFire": 2, "movement": 6, "producibleBy": [ "Rome", @@ -6861,6 +7095,8 @@ "attack": 5, "defense": 3, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [], "unproducible": true, @@ -6898,6 +7134,8 @@ "attack": 3, "defense": 2, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [], "unproducible": true, @@ -6933,6 +7171,8 @@ "attack": 1, "defense": 1, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "flags": [ "rotateBeforeAttack" @@ -7004,6 +7244,8 @@ "attack": 4, "defense": 9, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -7076,6 +7318,8 @@ "attack": 12, "defense": 14, "bombard": 6, + "bombardRange": 0, + "rateOfFire": 1, "movement": 1, "producibleBy": [ "Rome", @@ -7143,6 +7387,8 @@ "attack": 1, "defense": 6, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 1, "producibleBy": [ "Rome", @@ -7211,6 +7457,8 @@ "attack": 1, "defense": 6, "bombard": 0, + "bombardRange": 0, + "rateOfFire": 0, "movement": 2, "producibleBy": [ "Rome", diff --git a/C7/Lua/texture_configs/c7.lua b/C7/Lua/texture_configs/c7.lua index 03fbfc054..1cb8faf2e 100644 --- a/C7/Lua/texture_configs/c7.lua +++ b/C7/Lua/texture_configs/c7.lua @@ -103,6 +103,7 @@ local c7_texture_list = { "Art/Units/units_32.png", "Art/city screen/buildings-small.png", "Art/city screen/buildings-large.png", + "Art/Cursor.png" } -- Build lookup table from c7_texture_list without extensions diff --git a/C7/Lua/texture_configs/civ3.lua b/C7/Lua/texture_configs/civ3.lua index be5b70d00..3d8916d7a 100644 --- a/C7/Lua/texture_configs/civ3.lua +++ b/C7/Lua/texture_configs/civ3.lua @@ -20,6 +20,8 @@ local PALACE = "Art/PalaceView/" local POPUP_BORDERS = "Art/popupborders.pcx" +local CURSORS = "Art/Cursor.pcx" + -- Texture definitions local textures = {} @@ -153,6 +155,16 @@ textures.ui = { crop_region = { 144, 0, 72, 48 }, }, }, + cursor = { + bombard_deny = { + path = CURSORS, + crop_region = { 166, 1, 32, 32 }, + }, + bombard = { + path = CURSORS, + crop_region = { 199, 1, 32, 32 }, + } + } } textures.terrain = require "civ3.terrain" diff --git a/C7/Lua/texture_configs/civ3/unit_control.lua b/C7/Lua/texture_configs/civ3/unit_control.lua index 7739d8c1f..9f650c917 100644 --- a/C7/Lua/texture_configs/civ3/unit_control.lua +++ b/C7/Lua/texture_configs/civ3/unit_control.lua @@ -29,6 +29,8 @@ local unit_control = { unit_disband = make_entry(3, 0), unit_goto = make_entry(4, 0), unit_explore = make_entry(5, 0), + + unit_bombard = make_entry(3, 1), unit_build_city = make_entry(5, 2), unit_build_road = make_entry(6, 2), diff --git a/C7/Map/BombardLayer.cs b/C7/Map/BombardLayer.cs new file mode 100644 index 000000000..5a2fc25f7 --- /dev/null +++ b/C7/Map/BombardLayer.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.Linq; +using C7GameData; +using Godot; + +// The layer responsible for drawing the cursor and tile effects relating to bombardment. +public partial class BombardLayer : LooseLayer { + private readonly ImageTexture bombardCursorTexture; + private readonly ImageTexture bombardDenyCursorTexture; + + private TextureRect bombardCursorRect = null; + private TextureRect bombardDenyCursorRect = null; + + private Color bombardRed = Color.Color8(200, 0, 0, 225); + private float bombardGridLineWidth = (float)1.0; + + private Dictionary> tileSquareCache = new(); + + public BombardLayer() { + bombardCursorTexture = TextureLoader.Load("ui.cursor.bombard"); + bombardDenyCursorTexture = TextureLoader.Load("ui.cursor.bombard_deny"); + } + + public void DrawBombardCursor() { + Input.SetCustomMouseCursor(bombardCursorTexture, hotspot: bombardCursorTexture.Center()); + } + public void DrawBombardDenyCursor() { + Input.SetCustomMouseCursor(bombardDenyCursorTexture, hotspot: bombardDenyCursorTexture.Center()); + } + + public override void onBeginDraw(LooseView looseView, GameData gameData) { + bombardCursorRect?.Hide(); + bombardDenyCursorRect?.Hide(); + } + + public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) { + var bombardInfo = looseView.mapView.game.bombardInfo; + if (bombardInfo == null || bombardInfo.bombardingUnit.location != tile) + return; + + var unit = bombardInfo.bombardingUnit; + var range = unit.unitType.bombardRange; + var reachableTiles = GetTileSquare(tile, range); + var targetTiles = reachableTiles.Except([tile]).Where(t => unit.canBombardTile(t)); + var bombardTiles = targetTiles.ToHashSet(); + + // Choose one of two cursors depending on mouse tile hover + if (bombardInfo.mouseTile != null) { + var bombardable = bombardTiles.Contains(bombardInfo.mouseTile); + if (bombardable) { + DrawBombardCursor(); + drawTargetBombardTile(looseView, TileCenter(bombardInfo.mouseTile)); + } else + DrawBombardDenyCursor(); + } + + // Draw bombard grid + foreach (var bt in reachableTiles) { + drawBombardTile(looseView, TileCenter(bt)); + } + } + + private List GetTileSquare(Tile tile, int range) { + var key = $"{tile.Id}_{range}"; + if (tileSquareCache.TryGetValue(key, out var square)) + return square; + tileSquareCache[key] = tile.GetTilesWithinTileSquare(range); + return tileSquareCache[key]; + } + + private static Vector2 TileCenter(Tile bt) { + return MapView.cellSize * new Vector2(bt.XCoordinate + 1, bt.YCoordinate + 1); + } + + private void drawBombardTile(LooseView looseView, Vector2 tileCenter) { + var cS = MapView.cellSize; + var left = tileCenter + new Vector2(-cS.X, 0); + var top = tileCenter + new Vector2(0, -cS.Y); + var right = tileCenter + new Vector2(cS.X, 0); + var bottom = tileCenter + new Vector2(0, cS.Y); + DrawSquare(looseView, left, top, right, bottom); + } + + private void drawTargetBombardTile(LooseView looseView, Vector2 tileCenter) { + var cS = MapView.cellSize; + var inset = 10; + var left = tileCenter + new Vector2(-cS.X + inset, 0); + var top = tileCenter + new Vector2(0, -cS.Y + (inset/2f)); + var right = tileCenter + new Vector2(cS.X - inset, 0); + var bottom = tileCenter + new Vector2(0, cS.Y - (inset/2f)); + DrawSquare(looseView, left, top, right, bottom); + } + + private void DrawSquare(LooseView looseView, Vector2 left, Vector2 top, Vector2 right, Vector2 bottom) { + looseView.DrawLine(left, top, bombardRed, bombardGridLineWidth); + looseView.DrawLine(top, right, bombardRed, bombardGridLineWidth); + looseView.DrawLine(left, bottom, bombardRed, bombardGridLineWidth); + looseView.DrawLine(bottom, right, bombardRed, bombardGridLineWidth); + } +} diff --git a/C7/Map/UnitLayer.cs b/C7/Map/UnitLayer.cs index 0d37cfaa7..af6891aab 100644 --- a/C7/Map/UnitLayer.cs +++ b/C7/Map/UnitLayer.cs @@ -9,7 +9,7 @@ public partial class UnitLayer : LooseLayer { // The unit animations, effect animations, and cursor are all drawn as children attached to the looseView but aren't created and attached in // any particular order so we must use the ZIndex property to ensure they're properly layered. - public const int effectAnimZIndex = 1; + public const int effectAnimZIndex = 2; public const int unitAnimZIndex = 1; public const int cursorZIndex = -1; @@ -162,13 +162,19 @@ private Vector2 GetFramePosition(MapUnit.Appearance appearance, C7Animation unit } public void drawEffectAnimFrame(LooseView looseView, C7Animation anim, float progress, Vector2 tileCenter) { - // var flicSheet = anim.getFlicSheet(); - // var inst = getBlankAnimationInstance(looseView); - // setFlicShaderParams(inst.shaderMat, flicSheet, 0, progress); - // inst.shaderMat.SetShaderParameter("civColor", new Vector3(1, 1, 1)); - // inst.meshInst.Position = tileCenter; - // inst.meshInst.Scale = new Vector2(flicSheet.spriteWidth, -1 * flicSheet.spriteHeight); - // inst.meshInst.ZIndex = effectAnimZIndex; + AnimationInstance inst = getBlankAnimationInstance(looseView); + inst.sprite.ZIndex = effectAnimZIndex; + inst.spriteTint.ZIndex = effectAnimZIndex; + inst.SetPosition(tileCenter); + + anim.loadEffectAnimation(); + + string animName = AnimationManager.AnimationKey(anim.effect, anim.action); + int nextFrame = inst.GetNextFrameByProgress(animName, progress); + + inst.SetAnimation(animName); + inst.SetFrame(nextFrame); + inst.Show(); } private AnimatedSprite2D cursorSprite = null; @@ -204,21 +210,26 @@ public override void onBeginDraw(LooseView looseView, GameData gameData) { public MapUnit selectUnitToDisplay(LooseView looseView, List units) { // From the list, pick out which units are (1) the strongest defender vs the currently selected unit, (2) the currently selected unit // itself if it's in the list, and (3) any unit that is playing an animation that the player would want to see. - MapUnit bestDefender = units[0], - selected = null, - doingInterestingAnimation = null; + MapUnit bestDefender = units[0], selected = null, doingInterestingAnimation = null; var currentlySelectedUnit = looseView.mapView.game.CurrentlySelectedUnit; + foreach (var u in units) { - if (u == currentlySelectedUnit) + if (u == currentlySelectedUnit) { selected = u; + break; + } + + if (looseView.mapView.game.animationController.animTracker.getUnitAppearance(u).DeservesPlayerAttention()) { + doingInterestingAnimation = u; + break; + } + if (u.HasPriorityAsDefender(bestDefender, currentlySelectedUnit)) bestDefender = u; - if (looseView.mapView.game.animationController.animTracker.getUnitAppearance(u).DeservesPlayerAttention()) - doingInterestingAnimation = u; } // Prefer showing the selected unit, secondly show one doing a relevant animation, otherwise show the top defender - return selected != null ? selected : (doingInterestingAnimation != null ? doingInterestingAnimation : bestDefender); + return selected ?? doingInterestingAnimation ?? bestDefender; } public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) { diff --git a/C7/MapView.cs b/C7/MapView.cs index ebf419d60..e0e83076d 100644 --- a/C7/MapView.cs +++ b/C7/MapView.cs @@ -710,6 +710,7 @@ public MapView(Game game, int mapWidth, int mapHeight, bool wrapHorizontally, bo LooseView unitView = new(this); unitView.layers.Add(new GotoLayer()); + unitView.layers.Add(new BombardLayer()); LooseView otherView = new(this); otherView.layers.Add(new UnitLayer()); diff --git a/C7/Textures/TextureLoader.cs b/C7/Textures/TextureLoader.cs index 63a3a9de9..19ac505bf 100644 --- a/C7/Textures/TextureLoader.cs +++ b/C7/Textures/TextureLoader.cs @@ -466,4 +466,8 @@ public static void ClearCache() { animationCache.Clear(); colorCache.Clear(); } + + public static Vector2 Center(this ImageTexture tex) { + return new Vector2(tex.GetWidth(), tex.GetHeight()) / 2; + } } diff --git a/C7/UIElements/Popups/TemporaryPopup.cs b/C7/UIElements/Popups/TemporaryPopup.cs index e0bb59570..70eaa16e7 100644 --- a/C7/UIElements/Popups/TemporaryPopup.cs +++ b/C7/UIElements/Popups/TemporaryPopup.cs @@ -6,6 +6,7 @@ public partial class TemporaryPopup : Label { private int durationInMillis; public TemporaryPopup(string text, float durationInSec) { + ZIndex = 3; Text = text; this.durationInMillis = (int)(durationInSec * 1000); diff --git a/C7/UIElements/UnitButtons/UnitButtons.cs b/C7/UIElements/UnitButtons/UnitButtons.cs index a4577891a..86f91306c 100644 --- a/C7/UIElements/UnitButtons/UnitButtons.cs +++ b/C7/UIElements/UnitButtons/UnitButtons.cs @@ -59,7 +59,7 @@ private void SetUpControlButtons() { // AddNewButton(specializedControls, "load"); // AddNewButton(specializedControls, "unload"); // AddNewButton(specializedControls, "pillage"); - // AddNewButton(specializedControls, "bombard"); + AddNewButton(specializedControls, C7Action.UnitBombard); // AddNewButton(specializedControls, "autobombard"); // AddNewButton(specializedControls, "paradrop"); //superfortify? diff --git a/C7/project.godot b/C7/project.godot index aeb3e930f..80f5f679d 100644 --- a/C7/project.godot +++ b/C7/project.godot @@ -201,6 +201,11 @@ enable_temp_animations={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +unit_bombard={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":66,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} [mono] diff --git a/C7Engine/C7GameData/City.cs b/C7Engine/C7GameData/City.cs index acfce357f..04df44c8d 100644 --- a/C7Engine/C7GameData/City.cs +++ b/C7Engine/C7GameData/City.cs @@ -355,7 +355,7 @@ public void HandleCityGrowth(GameData gameData) { // Handle the city starving. if (foodStored < 0) { - RemoveCitizen(); + RemoveLastCitizen(); foodStored = 0; return; } @@ -624,9 +624,22 @@ public int FoodConsumedPerTurn() { return residents.Count * 2; } - private void RemoveCitizen() { - residents[residents.Count - 1].tileWorked.personWorkingTile = null; - residents.RemoveAt(residents.Count - 1); + + private void RemoveLastCitizen() { + RemoveCitizenAt(residents.Count - 1); + } + + public void RemoveRandomCitizen() { + if (residents.Count == 1) + return; // TODO: Handle extreme case + + var idx = GameData.rng.Next(residents.Count); + RemoveCitizenAt(idx); + } + + private void RemoveCitizenAt(int index) { + residents[index].tileWorked.personWorkingTile = null; + residents.RemoveAt(index); } public void AddCitizen(CityResident cr) { @@ -636,7 +649,7 @@ public void AddCitizen(CityResident cr) { public void RemoveCitizens(int number) { for (int i = 0; i < number; i++) { if (residents.Count > 0) { - RemoveCitizen(); + RemoveLastCitizen(); } else { Log.Warning("Trying to remove last citizen from " + name); break; @@ -646,7 +659,7 @@ public void RemoveCitizens(int number) { public void RemoveAllCitizens() { while (residents.Count > 0) { - RemoveCitizen(); + RemoveLastCitizen(); } } @@ -698,6 +711,9 @@ public void AddBuilding(Building building) { totalCulture = 0 }); } + public void RemoveBuilding(CityBuilding building) { + constructed_buildings.Remove(building); + } public void AddUnit(UnitPrototype proto, GameData gameData) { MapUnit newUnit = proto.GetInstance(gameData.GenerateID(proto.name), proto, owner, location: location); diff --git a/C7Engine/C7GameData/ImportCiv3.cs b/C7Engine/C7GameData/ImportCiv3.cs index eca11f470..53b5f71cd 100644 --- a/C7Engine/C7GameData/ImportCiv3.cs +++ b/C7Engine/C7GameData/ImportCiv3.cs @@ -1189,6 +1189,8 @@ private void ImportUnitPrototypes() { prototype.shieldCost = prto.ShieldCost; prototype.populationCost = prto.PopulationCost; prototype.bombard = prto.BombardStrength; + prototype.bombardRange = prto.BombardRange; + prototype.rateOfFire = prto.RateOfFire; if (prto.TurnToAttack) { prototype.flags.Add(SaveUnitPrototype.Flag.RotateBeforeAttack); } diff --git a/C7Engine/C7GameData/MapUnit.cs b/C7Engine/C7GameData/MapUnit.cs index 397442edc..b4c4683f1 100644 --- a/C7Engine/C7GameData/MapUnit.cs +++ b/C7Engine/C7GameData/MapUnit.cs @@ -269,17 +269,17 @@ public bool CanDefendAgainst(MapUnit attacker) { // priority. Otherwise it's just whoever is stronger on defense. public bool HasPriorityAsDefender(MapUnit otherDefender, MapUnit opponent) { Player opponentPlayer = opponent.owner; - bool weAreEnemy = (opponentPlayer != null) ? ! opponentPlayer.IsAtPeaceWith(owner) : false; - bool otherDefenderIsEnemy = (opponentPlayer != null) ? ! opponentPlayer.IsAtPeaceWith(otherDefender.owner) : false; + bool weAreEnemy = !opponentPlayer?.IsAtPeaceWith(owner) ?? false; + bool otherDefenderIsEnemy = !opponentPlayer?.IsAtPeaceWith(otherDefender.owner) ?? false; + if (weAreEnemy && !otherDefenderIsEnemy) return true; - else if (otherDefenderIsEnemy && !weAreEnemy) + if (otherDefenderIsEnemy && !weAreEnemy) return false; - else { - double ourTotalStrength = StrengthVersus(opponent, CombatRole.Defense, null) * hitPointsRemaining, - theirTotalStrength = otherDefender.StrengthVersus(opponent, CombatRole.Defense, null) * otherDefender.hitPointsRemaining; - return ourTotalStrength > theirTotalStrength; - } + + double ourTotalStrength = StrengthVersus(opponent, CombatRole.Defense, null) * hitPointsRemaining; + double theirTotalStrength = otherDefender.StrengthVersus(opponent, CombatRole.Defense, null) * otherDefender.hitPointsRemaining; + return ourTotalStrength > theirTotalStrength; } @@ -430,36 +430,223 @@ public async Task fight(MapUnit defender) { return result; } + public bool canBombard() { + return unitType.actions.Contains(UnitAction.Bombard); + } + + public bool canBombardTile(Tile tile) { + if (unitType.bombard == 0) + return false; + + if (tile.HasImprovements) + return true; + + MapUnit target = tile.FindTopDefender(this); + if (target.owner == owner) + return false; + + if (target != MapUnit.NONE) + return true; + + if (tile.HasCity && tile.cityAtTile.owner != owner) + return true; + + return false; + } + + public async Task bombard(Tile tile) { + // Could check canBombardTile(..) again, but no need really + MapUnit target = tile.FindTopDefender(this); - if ((unitType.bombard == 0) || (target == MapUnit.NONE)) - return; // Do nothing if we don't have a unit to attack. TODO: Attack city or tile improv if possible + + var hasTargetUnit = target != MapUnit.NONE && target.owner != owner; + var hasForeignCity = tile.HasCity && tile.cityAtTile.owner != owner; + var hasCityWalls = hasForeignCity && tile.cityAtTile.GetBuildings().Any(b => b.building.providesWalls); + var hasTileImprovements = tile.HasImprovements; + + if (!(hasTargetUnit || hasTileImprovements || hasForeignCity)) + return; // Nothing to bombard var unitOriginalOrientation = facingDirection; facingDirection = location.directionTo(tile); - // TODO: Figure out the bombard defense that walls grant. - double bombardStrength = StrengthVersus(target, CombatRole.Bombard, facingDirection); - double defenderStrength = target.StrengthVersus(this, CombatRole.BombardDefense, facingDirection); - double attackerOdds = bombardStrength / (bombardStrength + defenderStrength); - if (Double.IsNaN(attackerOdds)) - return; + if (hasCityWalls) + await bombardCityWalls(tile); + else if (hasTargetUnit) + await bombardUnits(tile, target); + else if (hasForeignCity) + await bombardCity(tile); + else + await bombardTileImprovements(tile); + + facingDirection = unitOriginalOrientation; + } + + private async Task bombardUnits(Tile tile, MapUnit target) { + // TODO: Make configurable + + var hitCount = 0; + + foreach (var fire in Enumerable.Range(0, unitType.rateOfFire)) { + // TODO: Figure out the bombard defense that walls grant. + double bombardStrength = StrengthVersus(target, CombatRole.Bombard, facingDirection); + double defenderStrength = target.StrengthVersus(this, CombatRole.BombardDefense, facingDirection); + double attackerOdds = bombardStrength / (bombardStrength + defenderStrength); + if (Double.IsNaN(attackerOdds)) + return; + + // TODO: Lethal/non-lethal bombardment + var isPotentiallyLethal = target.hitPointsRemaining == 1; + + await animateAsync(MapUnit.AnimatedAction.ATTACK1); + movementPoints.onUnitMove(1); + if (GameData.rng.NextDouble() < attackerOdds && !isPotentiallyLethal) { + hitCount += 1; + target.hitPointsRemaining -= 1; + await tile.AnimateAsync(AnimatedEffect.Hit3); + } else + await tile.AnimateAsync(AnimatedEffect.Miss); + + if (target.hitPointsRemaining <= 0) { + RollToPromote(target); + await target.animateAsync(MapUnit.AnimatedAction.DEATH); + target.RemoveFromPlay(); + break; // Target destroyed, skip remaining fire -- TODO: Re-target? + } + } + if (owner.isHuman) { + if (hitCount > 0) + new MsgShowTemporaryPopup($"Artillery bombardment successful! Enemy units injured.", tile).send(); + else + new MsgShowTemporaryPopup($"Artillery bombardment failed.", tile).send(); + } + } + + private async Task bombardCityWalls(Tile tile) { + // CF Civilopedia: City walls have a land bombardment defense of 8 + // CF Civilopedia: Coastal defences have a land bombardment defense of 8 + // Anecdotal: "City walls are hit first." + // TODO: Make configurable + + const int wallDefence = 8; + + var hitCount = 0; + + var walls = tile.cityAtTile.GetBuildings().First(b => b.building.providesWalls); + + foreach (var fire in Enumerable.Range(0, unitType.rateOfFire)) { + double bombardStrength = StrengthVersus(null, CombatRole.Bombard, facingDirection); + double defenderStrength = wallDefence; + double attackerOdds = bombardStrength / (bombardStrength + defenderStrength); + if (Double.IsNaN(attackerOdds)) + return; + + await RunAnimatedBombard(tile, attackerOdds, () => { + hitCount += 1; + tile.cityAtTile.RemoveBuilding(walls); + }); + } + + if (owner.isHuman) { + if (hitCount > 0) + new MsgShowTemporaryPopup($"Artillery bombardment successful! Walls destroyed.", tile).send(); + else + new MsgShowTemporaryPopup($"Artillery bombardment failed.", tile).send(); + } + } + + private async Task RunAnimatedBombard(Tile tile, double attackerOdds, Action callback) { await animateAsync(MapUnit.AnimatedAction.ATTACK1); movementPoints.onUnitMove(1); if (GameData.rng.NextDouble() < attackerOdds) { - target.hitPointsRemaining -= 1; - tile.Animate(AnimatedEffect.Hit3); + await tile.AnimateAsync(AnimatedEffect.Hit3); + callback(); } else - tile.Animate(AnimatedEffect.Miss); + await tile.AnimateAsync(AnimatedEffect.Miss); + } + + private async Task bombardCity(Tile tile) { + // Anecdotal: If there are no units left to hit, then citizens or buildings are hit, apparently with same probability. + // Anecdotal: "buildings (if I remember correctly) have a defense value of 16" + // Anecdotal: It seems population is killed off more quickly than buildings. + // TODO: Make configurable + + const int buildingDefence = 16; + const int populationDefence = 12; + const float buildingOrPopulationOdds = 0.5f; + + var targetBuildings = GameData.rng.NextDouble() <= buildingOrPopulationOdds; + var defence = targetBuildings ? buildingDefence : populationDefence; + var destroyMsg = targetBuildings ? "Destroyed city population." : "Destroyed a building."; + Action remover = targetBuildings + ? () => + { + var building = tile.cityAtTile.GetBuildings() + .OrderBy(x => GameData.rng.Next()).FirstOrDefault(); + tile.cityAtTile.RemoveBuilding(building); + } + : () => + { + tile.cityAtTile.RemoveRandomCitizen(); + }; + + var hitCount = 0; + + foreach (var fire in Enumerable.Range(0, unitType.rateOfFire)) { + double bombardStrength = StrengthVersus(null, CombatRole.Bombard, facingDirection); + double defenderStrength = defence; + double attackerOdds = bombardStrength / (bombardStrength + defenderStrength); + if (Double.IsNaN(attackerOdds)) + return; + + await RunAnimatedBombard(tile, attackerOdds, () => { + hitCount += 1; + remover(); + }); + } - if (target.hitPointsRemaining <= 0) { - RollToPromote(target); - await target.animateAsync(MapUnit.AnimatedAction.DEATH); - target.RemoveFromPlay(); + if (owner.isHuman) { + if (hitCount > 0) + new MsgShowTemporaryPopup($"Artillery bombardment successful! {destroyMsg}", tile).send(); + else + new MsgShowTemporaryPopup($"Artillery bombardment failed.", tile).send(); } + } - facingDirection = unitOriginalOrientation; + private async Task bombardTileImprovements(Tile tile) { + // Anecdotal: "arty seems to wipe out improvement on 75% or more of the shots" + // ==> Artillery.bombard : 12 --> TileImprovement.Defense : 3 + // TODO: Make configurable + + const int tileImprovementDefence = 3; + + var hitCount = 0; + + var improvement = tile.overlays.GetImprovements() + .OrderBy(x => GameData.rng.Next()).FirstOrDefault(); + + foreach (var fire in Enumerable.Range(0, unitType.rateOfFire)) { + double bombardStrength = StrengthVersus(null, CombatRole.Bombard, facingDirection); + double defenderStrength = tileImprovementDefence; + double attackerOdds = bombardStrength / (bombardStrength + defenderStrength); + if (Double.IsNaN(attackerOdds)) + return; + + await RunAnimatedBombard(tile, attackerOdds, () => { + hitCount += 1; + tile.overlays.Remove(improvement); + // TODO: Re-target? + }); + } + + if (owner.isHuman) { + if (hitCount > 0) + new MsgShowTemporaryPopup($"Artillery bombardment successful! Destroyed {improvement?.key}.", tile).send(); + else + new MsgShowTemporaryPopup($"Artillery bombardment failed.", tile).send(); + } } public int HealRateAt(Tile location) { @@ -663,9 +850,6 @@ public async Task move(TileDirection dir, bool wait = false) { movementPoints.onUnitMove(1); return true; } - } else if (unitType.bombard > 0) { - await bombard(newLoc); - return true; } else { return true; } diff --git a/C7Engine/C7GameData/Save/SaveUnitPrototype.cs b/C7Engine/C7GameData/Save/SaveUnitPrototype.cs index 2732e6c87..d5a1d7315 100644 --- a/C7Engine/C7GameData/Save/SaveUnitPrototype.cs +++ b/C7Engine/C7GameData/Save/SaveUnitPrototype.cs @@ -15,6 +15,8 @@ public enum Flag { public int attack { get; set; } public int defense { get; set; } public int bombard { get; set; } + public int bombardRange { get; set; } + public int rateOfFire { get; set; } public int movement { get; set; } public HashSet producibleBy = []; @@ -40,9 +42,9 @@ public SaveUnitPrototype() { } public SaveUnitPrototype(UnitPrototype proto) { (name, art, shieldCost, populationCost, unproducible, - attack, defense, bombard, movement) = + attack, defense, bombard, bombardRange, rateOfFire, movement) = (proto.name, proto.art, proto.shieldCost, proto.populationCost, proto.unproducible, - proto.attack, proto.defense, proto.bombard, proto.movement); + proto.attack, proto.defense, proto.bombard, proto.bombardRange, proto.rateOfFire, proto.movement); if (proto.requiredTech != null) requiredTech = proto.requiredTech.id; diff --git a/C7Engine/C7GameData/Tile.cs b/C7Engine/C7GameData/Tile.cs index b0ab0e88d..9811cb1dd 100644 --- a/C7Engine/C7GameData/Tile.cs +++ b/C7Engine/C7GameData/Tile.cs @@ -1,3 +1,5 @@ +using Serilog; + namespace C7GameData { using System; using System.Collections.Generic; @@ -702,6 +704,17 @@ public List GetTilesWithinRankDistance(int rank) { return result; } + // Same as GetTilesWithinRankDistance, but includes "corner tiles", + // i.e., returns perfect tile squares. + public List GetTilesWithinTileSquare(int rank) { + List result = new(); + for (int i = 0; i < (rank * 2 + 1) * (rank * 2 + 1); ++i) { + Tile t = GetTileAtNeighborIndex(i); + result.Add(t); + } + return result; + } + public MapUnit FindTopDefender(MapUnit opponent) { if (unitsOnTile.Count > 0) { IEnumerable potentialDefenders = unitsOnTile.Where(u => u.CanDefendAgainst(opponent)); @@ -775,6 +788,8 @@ public void Animate(AnimatedEffect effect) { public void ClearTerrainOverlay() { overlayTerrainType = baseTerrainType; } + + public bool HasImprovements => overlays.HasBeenImproved(); } public enum TileDirection { @@ -845,11 +860,23 @@ public void Add(TerrainImprovement improvement) { terrainImprovementByLayer[improvement.layer] = improvement; - // If a road is being added to a tile that previously had - // no road, invalidate the cached trade network - if (improvement.layer == TerrainImprovement.Layer.Roads && replacedImprovement == null) { - // Hack: don't do this if gamedata is null, which can - // be true in some unit tests. + ApplyTerrainImprovementChange(replacedImprovement, improvement); + } + + public void Remove(TerrainImprovement improvement) { + if (!terrainImprovementByLayer.Remove(improvement.layer)) + Log.Warning("Failed to remove terrain improvement."); + + ApplyTerrainImprovementChange(improvement, null); + } + + private static void ApplyTerrainImprovementChange(TerrainImprovement oldImprovement, TerrainImprovement newImprovement) { + var roadCreated = oldImprovement == null && newImprovement?.layer == TerrainImprovement.Layer.Roads; + var roadRemoved = oldImprovement?.layer == TerrainImprovement.Layer.Roads && newImprovement == null; + + // If there's a change in road coverage, invalidate the cached trade network + if (roadCreated || roadRemoved) { + // Hack: don't do this if gamedata is null, which can be true in some unit tests. EngineStorage.gameData?.InvalidateCachedTradeNetwork(); } } diff --git a/C7Engine/C7GameData/UnitPrototype.cs b/C7Engine/C7GameData/UnitPrototype.cs index f4f311bb8..9e93f9a48 100644 --- a/C7Engine/C7GameData/UnitPrototype.cs +++ b/C7Engine/C7GameData/UnitPrototype.cs @@ -59,6 +59,8 @@ public class UnitPrototype : IProducible { public int attack { get; set; } public int defense { get; set; } public int bombard { get; set; } + public int bombardRange { get; set; } + public int rateOfFire { get; set; } public int movement { get; set; } public HashSet producibleBy { get; set; } = []; public UnitPrototype upgradeTo; @@ -90,9 +92,9 @@ public bool rotateBeforeAttack { public UnitPrototype() { } public UnitPrototype(SaveUnitPrototype proto, IEnumerable terraforms) { - (name, art, shieldCost, populationCost, attack, defense, bombard, movement, unproducible) = + (name, art, shieldCost, populationCost, attack, defense, bombard, bombardRange, rateOfFire, movement, unproducible) = (proto.name, proto.art, proto.shieldCost, proto.populationCost, - proto.attack, proto.defense, proto.bombard, proto.movement, proto.unproducible); + proto.attack, proto.defense, proto.bombard, proto.bombardRange, proto.rateOfFire, proto.movement, proto.unproducible); categories = new HashSet(proto.categories); actions = proto.actions; diff --git a/C7Engine/EntryPoints/MessageToEngine.cs b/C7Engine/EntryPoints/MessageToEngine.cs index 77c4ce084..eb30898ef 100644 --- a/C7Engine/EntryPoints/MessageToEngine.cs +++ b/C7Engine/EntryPoints/MessageToEngine.cs @@ -78,6 +78,27 @@ public override async void process() { } } + public class MsgBombard : MessageToEngine { + private ID unitID; + private readonly int tileX; + private readonly int tileY; + + + public MsgBombard(ID unitID, Tile tile) { + this.unitID = unitID; + this.tileX = tile.XCoordinate; + this.tileY = tile.YCoordinate; + } + + public override async void process() { + MapUnit unit = EngineStorage.gameData.GetUnit(unitID); + Tile tile = EngineStorage.gameData.map.tileAt(tileX, tileY); + if (unit == null || tile == null) return; + + await unit.bombard(tile); + } + } + // A generic class that allows the UI to have the game engine run some // action, assumed to be on a unit. //