diff --git a/C7/Animations/AnimationManager.cs b/C7/Animations/AnimationManager.cs index 9dfab492..0e93aebb 100644 --- a/C7/Animations/AnimationManager.cs +++ b/C7/Animations/AnimationManager.cs @@ -40,6 +40,13 @@ public static string AnimationKey(UnitPrototype unit, MapUnit.AnimatedAction act return AnimationKey(BaseAnimationKey(unit, action), direction); } + public static readonly Dictionary AnimationThumbnails = new(); + public static readonly Dictionary AnimationTintThumbnails = new(); + + private const int thumbnailFrame = 0; + private const TileDirection thumbnailDirection = TileDirection.SOUTHEAST; + private const MapUnit.AnimatedAction thumbnailAction = MapUnit.AnimatedAction.DEFAULT; + private AudioStreamPlayer audioPlayer; public SpriteFrames spriteFrames; @@ -62,6 +69,32 @@ public IniData getINIData(string pathKey) { return tr; } + public static string GetUnitDefaultThumbnailKey(UnitPrototype unit) { + return $"{unit.artName}_{thumbnailDirection}_{thumbnailAction}_{thumbnailFrame}"; + } + + public (ImageTexture baseFrame, ImageTexture tintFrame) GetAnimationFrameAndTintTextures(UnitPrototype unit) { + + string key = GetUnitDefaultThumbnailKey(unit); + + if (AnimationThumbnails.TryGetValue(key, out ImageTexture baseImage) + && AnimationTintThumbnails.TryGetValue(key, out ImageTexture tintImage)) { + return (baseImage, tintImage); + } + + string filepath = getUnitFlicFilepath(unit, thumbnailAction); + + Flic flic = Util.LoadFlic(filepath); + + byte[] rawFrame = flic.Images[flicAnimationDirectionToRow(thumbnailDirection), thumbnailFrame]; + // This actually doesn't return the tint frame with the civ color applied. The shader still needs to be applied. + (ImageTexture baseFrame, ImageTexture tintFrame) = Util.LoadTextureFromFlicData(rawFrame, flic.Palette, flic.Width, flic.Height); + AnimationThumbnails[key] = baseFrame; + AnimationTintThumbnails[key] = tintFrame; + + return (baseFrame, tintFrame); + } + // Looks up the name of the flic file associated with a given action in an animation INI. If there is no flic file listed for the action, // returns instead the file name for the default action, and if that's missing too, throws an exception. public string getFlicFileName(IniData iniData, MapUnit.AnimatedAction action) { @@ -104,6 +137,20 @@ private static TileDirection flicRowToAnimationDirection(int row) { return TileDirection.NORTH; } + private static int flicAnimationDirectionToRow(TileDirection tileDirection) { + switch (tileDirection) { + case TileDirection.SOUTHWEST: return 0; + case TileDirection.SOUTH: return 1; + case TileDirection.SOUTHEAST: return 2; + case TileDirection.EAST: return 3; + case TileDirection.NORTHEAST: return 4; + case TileDirection.NORTH: return 5; + case TileDirection.NORTHWEST: return 6; + case TileDirection.WEST: return 7; + } + return 2; + } + public static void loadFlicAnimation(string path, string name, ref SpriteFrames frames, ref SpriteFrames tint) { Flic flic = Util.LoadFlic(path); @@ -170,6 +217,11 @@ public C7Animation forUnit(UnitPrototype unit, MapUnit.AnimatedAction action) { public C7Animation forEffect(AnimatedEffect effect) { return new C7Animation(this, effect); } + + public static void ClearCache() { + AnimationThumbnails.Clear(); + AnimationTintThumbnails.Clear(); + } } public partial class C7Animation { diff --git a/C7/MapView.cs b/C7/MapView.cs index b5729cb0..badb5f41 100644 --- a/C7/MapView.cs +++ b/C7/MapView.cs @@ -3,11 +3,9 @@ using System.Linq; using C7.Map; using Godot; -using ConvertCiv3Media; using C7GameData; using C7Engine; using Serilog; -using Serilog.Events; using System.Diagnostics; // Loose layers are for drawing things on the map on a per-tile basis. (Historical aside: There used to be another kind of layer called a TileLayer @@ -556,6 +554,8 @@ public Vector2 scaledCellSize { public Game game; + public MapView() { } + public int mapWidth { get; private set; } public int mapHeight { get; private set; } public bool wrapHorizontally { get; private set; } @@ -598,6 +598,16 @@ public int getRowStartX(int y) { const float MIN_SCALE = 0.1f; const float MAX_SCALE = 4.0f; + private LowerRightInfoBox lowerRightInfoBox; + public override void _Ready() { + lowerRightInfoBox = GetNode("/root/C7Game/CanvasLayer/Control/GameStatus/LowerRightInfoBox"); + lowerRightInfoBox.CenterCameraOnActiveUnit += OnCenterCameraOnUnit; + } + + public override void _ExitTree() { + lowerRightInfoBox.CenterCameraOnActiveUnit -= OnCenterCameraOnUnit; + } + public MapView(Game game, int mapWidth, int mapHeight, bool wrapHorizontally, bool wrapVertically) { this.game = game; this.mapWidth = mapWidth; @@ -804,4 +814,11 @@ public void centerCameraOnTile(Tile t) { var tileCenter = new Vector2(t.XCoordinate + 1, t.YCoordinate + 1) * scaledCellSize; setCameraLocation(tileCenter - (float)0.5 * getVisibleAreaSize()); } + + public void OnCenterCameraOnUnit() { + MapUnit currentlySelectedUnit = game.CurrentlySelectedUnit; + if (currentlySelectedUnit == MapUnit.NONE || currentlySelectedUnit == null) + return; + centerCameraOnTile(currentlySelectedUnit.location); + } } diff --git a/C7/Textures/TextureLoader.cs b/C7/Textures/TextureLoader.cs index e0a57a92..ccbd8a0b 100644 --- a/C7/Textures/TextureLoader.cs +++ b/C7/Textures/TextureLoader.cs @@ -76,6 +76,7 @@ public ConfigEntry() { private static Dictionary PcxCache = []; private static Dictionary PngCache = []; private static Dictionary colorCache = []; + private static Dictionary materialCache = []; private static Dictionary configKeyCache = []; private static Dictionary<(string configKey, object obj), ImageTexture> objectMappingCache = []; @@ -458,6 +459,18 @@ private static Image LoadPNG(string relPath) { return png; } + public static ShaderMaterial GetShaderMaterialForUnit(int civIndex) { + if (materialCache.TryGetValue(civIndex, out ShaderMaterial material)) { + return material; + } + material = new(); + material.Shader = GD.Load("res://UnitTint.gdshader"); + Color civColor = LoadColor(civIndex); + material.SetShaderParameter("tintColor", new Vector3(civColor.R, civColor.G, civColor.B)); + materialCache[civIndex] = material; + return material; + } + public static void ClearCache() { PcxCache.Clear(); PngCache.Clear(); @@ -466,5 +479,6 @@ public static void ClearCache() { objectMappingCache.Clear(); animationCache.Clear(); colorCache.Clear(); + materialCache.Clear(); } } diff --git a/C7/UIElements/CityScreen/CityScreen.cs b/C7/UIElements/CityScreen/CityScreen.cs index 5b161fc0..731eb6df 100644 --- a/C7/UIElements/CityScreen/CityScreen.cs +++ b/C7/UIElements/CityScreen/CityScreen.cs @@ -594,25 +594,9 @@ private void RenderProductionDetails(GameData gameData, City city) { } if (city.itemBeingProduced is UnitPrototype up) { - // Get the flic data for the unit we're producing. - string path = new AnimationManager(null).getUnitFlicFilepath(up, MapUnit.AnimatedAction.DEFAULT); - ConvertCiv3Media.Flic flic = Util.LoadFlic(path); - - // Set up a shader we can use to color the "tint" portion of the - // animation frame below. - ShaderMaterial material = new(); - material.Shader = GD.Load("res://UnitTint.gdshader"); - Color civColor = TextureLoader.LoadColor(city.owner.colorIndex); - material.SetShaderParameter("tintColor", new Vector3(civColor.R, civColor.G, civColor.B)); - - // See flicRowToAnimationDirection for the mapping, row 2 is facing - // southeast, and we're just grabbing frame 0. - byte[] frame = flic.Images[2, 0]; - - // Each frame is split in two parts, the base image, and the "tint" - // of the image, which is the part of the unit that has civ-specific - // colors. - (ImageTexture baseImage, ImageTexture imageTint) = Util.LoadTextureFromFlicData(frame, flic.Palette, flic.Width, flic.Height); + AnimationManager animationManager = mapView.game.animationController.civ3AnimData.forUnit(up, MapUnit.AnimatedAction.DEFAULT).animationManager; + ShaderMaterial material = TextureLoader.GetShaderMaterialForUnit(city.owner.colorIndex); + (ImageTexture baseImage, ImageTexture imageTint) = animationManager.GetAnimationFrameAndTintTextures(up); // Add the base sprite. Sprite2D baseImageSprite = new(); diff --git a/C7/UIElements/GameStatus/LowerRightInfoBox.cs b/C7/UIElements/GameStatus/LowerRightInfoBox.cs index 991955ca..dd948f63 100644 --- a/C7/UIElements/GameStatus/LowerRightInfoBox.cs +++ b/C7/UIElements/GameStatus/LowerRightInfoBox.cs @@ -1,5 +1,4 @@ using Godot; -using ConvertCiv3Media; using C7GameData; using Serilog; using C7Engine; @@ -9,35 +8,68 @@ public partial class LowerRightInfoBox : Civ3TextureRect { private ILogger log = LogManager.ForContext(); - TextureButton nextTurnButton = new TextureButton(); - ImageTexture nextTurnOnTexture; - ImageTexture nextTurnOffTexture; - ImageTexture nextTurnBlinkTexture; + private const int fontSize = 12; + private const float offsetUnitThumbnailX = 14; + private const float offsetUnitThumbnailY = 16; - Label civAndGovt = new(); - Label lblUnitSelected = new Label(); - Label attackDefenseMovement = new Label(); - Label terrainType = new Label(); - Label yearAndGold = new Label(); - Label scienceProgress = new(); + private MapUnit activeUnit; - Timer blinkingTimer = new Timer(); - bool timerStarted = false; //This "isStopped" returns false if it's never been started. So we need this to know if we've ever started it. + private TextureRect boxRightRectangle = new(); + private TextureButton boxRightRectangleButton = new(); + + private TextureButton nextTurnButton = new(); + private ImageTexture nextTurnOnTexture; + private ImageTexture nextTurnOffTexture; + private ImageTexture nextTurnBlinkTexture; + + private Label unitRank = new(); + private Label unitType = new(); + private Label attackDefenseMovement = new(); + private Label terrainType = new(); + + private Label civAndGovt = new(); + private Label yearAndGold = new(); + private Label scienceProgress = new(); + + private Label suggestion = new(); + + private Sprite2D unitPlaceholder = new(); + private Sprite2D unitTintPlaceholder = new(); + + private Timer blinkingTimer = new Timer(); + private bool timerStarted = false; //This "isStopped" returns false if it's never been started. So we need this to know if we've ever started it. [Signal] public delegate void BlinkyEndTurnButtonPressedEventHandler(); + [Signal] public delegate void CenterCameraOnActiveUnitEventHandler(); // Called when the node enters the scene tree for the first time. public override void _Ready() { + unitRank.AddThemeFontSizeOverride("font_size", fontSize); + unitType.AddThemeFontSizeOverride("font_size", fontSize); + attackDefenseMovement.AddThemeFontSizeOverride("font_size", fontSize); + terrainType.AddThemeFontSizeOverride("font_size", fontSize); + civAndGovt.AddThemeFontSizeOverride("font_size", fontSize); + yearAndGold.AddThemeFontSizeOverride("font_size", fontSize); + scienceProgress.AddThemeFontSizeOverride("font_size", fontSize); + suggestion.AddThemeFontSizeOverride("font_size", fontSize); + this.CreateUI(); } private void CreateUI() { ImageTexture boxRight = TextureLoader.Load("lower_right_infobox.box"); - TextureRect boxRightRectangle = new TextureRect(); + boxRightRectangle = new TextureRect(); boxRightRectangle.Texture = boxRight; boxRightRectangle.SetPosition(new Vector2(0, 0)); AddChild(boxRightRectangle); + // An "invisible" button covering the inside area of the box so we can register the click + // and center the camera on the unit or end the turn + boxRightRectangleButton.SetSize(new Vector2(228, 108)); + boxRightRectangleButton.SetPosition(new Vector2(40, 17)); + AddChild(boxRightRectangleButton); + boxRightRectangleButton.Pressed += HandleBoxClick; + nextTurnOffTexture = TextureLoader.Load("lower_right_infobox.next_turn.off"); nextTurnOnTexture = TextureLoader.Load("lower_right_infobox.next_turn.on"); nextTurnBlinkTexture = TextureLoader.Load("lower_right_infobox.next_turn.blink"); @@ -49,40 +81,56 @@ private void CreateUI() { nextTurnButton.Pressed += turnEnded; - //Labels and whatnot in this text box - lblUnitSelected.Text = "Settler"; - lblUnitSelected.HorizontalAlignment = HorizontalAlignment.Right; - lblUnitSelected.SetPosition(new Vector2(0, 20)); - lblUnitSelected.AnchorRight = 1.0f; - lblUnitSelected.OffsetRight = -30; - boxRightRectangle.AddChild(lblUnitSelected); + // Unit info + unitType.Text = "Settler"; + unitType.HorizontalAlignment = HorizontalAlignment.Right; + unitType.SetPosition(new Vector2(0, 18)); + unitType.AnchorRight = 1.0f; + unitType.OffsetRight = -35; + boxRightRectangle.AddChild(unitType); + + unitRank.Text = "Regular"; + unitRank.HorizontalAlignment = HorizontalAlignment.Right; + unitRank.SetPosition(new Vector2(0, 32)); + unitRank.AnchorRight = 1.0f; + unitRank.OffsetRight = -35; + unitRank.Visible = false; + boxRightRectangle.AddChild(unitRank); attackDefenseMovement.Text = "0.0. 1/1"; attackDefenseMovement.HorizontalAlignment = HorizontalAlignment.Right; - attackDefenseMovement.SetPosition(new Vector2(0, 35)); + attackDefenseMovement.SetPosition(new Vector2(0, 32)); attackDefenseMovement.AnchorRight = 1.0f; - attackDefenseMovement.OffsetRight = -30; + attackDefenseMovement.OffsetRight = -35; boxRightRectangle.AddChild(attackDefenseMovement); terrainType.Text = "Grassland"; terrainType.HorizontalAlignment = HorizontalAlignment.Right; - terrainType.SetPosition(new Vector2(0, 50)); + terrainType.SetPosition(new Vector2(0, 46)); terrainType.AnchorRight = 1.0f; - terrainType.OffsetRight = -30; + terrainType.OffsetRight = -35; boxRightRectangle.AddChild(terrainType); - civAndGovt.SetPosition(new Vector2(0, 75)); + // Player info + civAndGovt.SetPosition(new Vector2(0, 80)); boxRightRectangle.AddChild(civAndGovt); civAndGovt.SetTextAndCenterLabel("Carthage - Despotism (5.5.0)"); - yearAndGold.SetPosition(new Vector2(0, 90)); + yearAndGold.SetPosition(new Vector2(0, 94)); boxRightRectangle.AddChild(yearAndGold); yearAndGold.SetTextAndCenterLabel("Turn 0 10 Gold (+0 per turn)"); - scienceProgress.SetPosition(new Vector2(0, 105)); + scienceProgress.SetPosition(new Vector2(0, 108)); boxRightRectangle.AddChild(scienceProgress); scienceProgress.SetTextAndCenterLabel(""); + // End of turn suggestions + suggestion.HorizontalAlignment = HorizontalAlignment.Right; + suggestion.SetPosition(new Vector2(0, 25)); + suggestion.AnchorRight = 1.0f; + suggestion.OffsetRight = -45; + boxRightRectangle.AddChild(suggestion); + //Setup up, but do not start, the timer. blinkingTimer.OneShot = false; blinkingTimer.WaitTime = 0.6f; @@ -91,7 +139,11 @@ private void CreateUI() { } private void SetEndOfTurnStatus() { - lblUnitSelected.Text = "ENTER or SPACEBAR for next turn"; + UpdateUnitGraphic(MapUnit.NONE); + suggestion.Text = "ENTER or SPACEBAR for next turn"; + suggestion.Visible = true; + unitRank.Visible = false; + unitType.Visible = false; attackDefenseMovement.Visible = false; terrainType.Visible = false; @@ -108,17 +160,17 @@ private void SetEndOfTurnStatus() { private void toggleEndTurnButton() { if (nextTurnButton.TextureNormal == nextTurnOnTexture) { nextTurnButton.TextureNormal = nextTurnBlinkTexture; - lblUnitSelected.Visible = true; + suggestion.Visible = true; } else { nextTurnButton.TextureNormal = nextTurnOnTexture; - lblUnitSelected.Visible = false; + suggestion.Visible = false; } } private void StopToggling() { nextTurnButton.TextureNormal = nextTurnOffTexture; - lblUnitSelected.Text = "Please wait..."; - lblUnitSelected.Visible = true; + suggestion.Text = "Please wait..."; + suggestion.Visible = true; blinkingTimer.Stop(); timerStarted = false; } @@ -128,19 +180,33 @@ private void turnEnded() { EmitSignal(SignalName.BlinkyEndTurnButtonPressed); } - private void UpdateUnitInfo(MapUnit NewUnit, TerrainType terrain) { + private void UpdateUnitInfo(MapUnit unit, TerrainType terrain) { StopToggling(); terrainType.Text = terrain.DisplayName; + if (unit.location.HasCity && unit.owner == unit.location.cityAtTile.owner) { + terrainType.Text = unit.location.cityAtTile.name; + } + if (unit.unitType.attack > 0 || unit.unitType.defense > 0) { + unitRank.Visible = true; + unitRank.Text = unit.experienceLevel.displayName; + attackDefenseMovement.SetPosition(new Vector2(0, 46)); + terrainType.SetPosition(new Vector2(0, 60)); + } else { + unitRank.Visible = false; + attackDefenseMovement.SetPosition(new Vector2(0, 32)); + terrainType.SetPosition(new Vector2(0, 46)); + } + suggestion.Visible = false; terrainType.Visible = true; - lblUnitSelected.Text = NewUnit.unitType.name; - lblUnitSelected.Visible = true; - string movementPointsRemaining = NewUnit.movementPoints.canMove ? "" + $"{(NewUnit.movementPoints.getMixedNumber())}" : "0"; + unitType.Text = unit.unitType.name; + unitType.Visible = true; + string movementPointsRemaining = unit.movementPoints.canMove ? "" + $"{(unit.movementPoints.getMixedNumber())}" : "0"; string bombardText = ""; - if (NewUnit.unitType.bombard > 0) { - bombardText = $"({NewUnit.unitType.bombard})"; + if (unit.unitType.bombard > 0) { + bombardText = $"({unit.unitType.bombard})"; } - attackDefenseMovement.Text = $"{NewUnit.unitType.attack}{bombardText}.{NewUnit.unitType.defense} {movementPointsRemaining}/{NewUnit.unitType.movement}"; + attackDefenseMovement.Text = $"{unit.unitType.attack}{bombardText}.{unit.unitType.defense} {movementPointsRemaining}/{unit.unitType.movement}"; attackDefenseMovement.Visible = true; } @@ -174,11 +240,61 @@ public override void _Process(double delta) { }); base._Process(delta); + + if (activeUnit == MapUnit.NONE || activeUnit == null) + return; + + UpdateUnitInfo(activeUnit, activeUnit.location.overlayTerrainType); } private void OnNewUnitSelected(ParameterWrapper wrappedMapUnit) { - MapUnit newUnit = wrappedMapUnit.Value; - log.Information("Selected unit: " + newUnit + " at " + newUnit.location); - UpdateUnitInfo(newUnit, newUnit.location.overlayTerrainType); + MapUnit unit = wrappedMapUnit.Value; + activeUnit = unit; + log.Information("Selected unit: " + unit + " at " + unit.location); + UpdateUnitGraphic(unit); + UpdateUnitInfo(unit, unit.location.overlayTerrainType); + } + + private void UpdateUnitGraphic(MapUnit unit) { + if (this.GetChildren().Contains(unitPlaceholder)) + this.RemoveChild(unitPlaceholder); + if (this.GetChildren().Contains(unitTintPlaceholder)) + this.RemoveChild(unitTintPlaceholder); + + if (unit == MapUnit.NONE || unit == null) { + activeUnit = MapUnit.NONE; + return; + } + + string key = AnimationManager.GetUnitDefaultThumbnailKey(unit.unitType); + ImageTexture baseFrame = AnimationManager.AnimationThumbnails[key]; + ImageTexture tintFrame = AnimationManager.AnimationTintThumbnails[key]; + + ShaderMaterial material = TextureLoader.GetShaderMaterialForUnit(unit.owner.colorIndex); + + // Add the base sprite. + unitPlaceholder = new Sprite2D(); + unitPlaceholder.Texture = baseFrame; + unitPlaceholder.Position = new Vector2( + boxRightRectangle.Texture.GetWidth() / 2f - offsetUnitThumbnailX, + boxRightRectangle.Texture.GetHeight() / 2f - offsetUnitThumbnailY); + this.AddChild(unitPlaceholder); + + // Add the tint sprite, hooking up the shader. + unitTintPlaceholder = new Sprite2D(); + unitTintPlaceholder.Texture = tintFrame; + unitTintPlaceholder.Material = material; + unitTintPlaceholder.Position = unitPlaceholder.Position; + this.AddChild(unitTintPlaceholder); + } + + private void HandleBoxClick() { + // When the turn can be ended, the click on the box is like clicking on the blinky button + if (timerStarted) { + turnEnded(); + return; + } + // Otherwise we can center the camera to the unit currently active + EmitSignal(SignalName.CenterCameraOnActiveUnit); } } diff --git a/C7/UnitSelector.cs b/C7/UnitSelector.cs index ef7aaab4..bb48c478 100644 --- a/C7/UnitSelector.cs +++ b/C7/UnitSelector.cs @@ -103,6 +103,7 @@ public bool SetSelectedUnit(MapUnit unit) { if (CurrentlySelectedUnit != MapUnit.NONE) { NoMoreAutoselectableUnitsEmitted = false; ParameterWrapper wrappedUnit = new(CurrentlySelectedUnit); + PreLoadUnitAnimationThumbnail(wrappedUnit.Value.unitType); EmitSignal(SignalName.NewAutoselectedUnit, wrappedUnit); return true; } @@ -114,4 +115,8 @@ public bool SetSelectedUnit(MapUnit unit) { return false; } + + private void PreLoadUnitAnimationThumbnail(UnitPrototype unit) { + game.animationController.civ3AnimData.GetAnimationFrameAndTintTextures(unit); + } } diff --git a/C7/Util.cs b/C7/Util.cs index 64b7fa23..29d48891 100644 --- a/C7/Util.cs +++ b/C7/Util.cs @@ -392,5 +392,6 @@ public static void ApplyNoSaveFlag(Godot.Collections.Dictionary property, HashSe public static void ClearCaches() { flicCache.Clear(); TextureLoader.ClearCache(); + AnimationManager.ClearCache(); } }