diff --git a/.gitignore b/.gitignore index f6c4cef6..3635d19d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ data_*/ # Below here are ignores imported from Puppeteer's project sav/ -*.png *.pcx *.flc *.mp4 @@ -43,7 +42,7 @@ project.lock.json *.sln.docstates C7.ini log.txt -*.csproj.old +*.csproj.old* # Build results [Dd]ebug/ diff --git a/C7/AnimationManager.cs b/C7/AnimationManager.cs index d60376be..ec953894 100644 --- a/C7/AnimationManager.cs +++ b/C7/AnimationManager.cs @@ -90,19 +90,19 @@ public string getUnitFlicFilepath(UnitPrototype unit, MapUnit.AnimatedAction act // The flic loading code parses the animations into a 2D array, where each row is an animation // corresponding to a tile direction. flicRowToAnimationDirection maps row number -> direction. private static TileDirection flicRowToAnimationDirection(int row) { - switch (row) { - case 0: return TileDirection.SOUTHWEST; - case 1: return TileDirection.SOUTH; - case 2: return TileDirection.SOUTHEAST; - case 3: return TileDirection.EAST; - case 4: return TileDirection.NORTHEAST; - case 5: return TileDirection.NORTH; - case 6: return TileDirection.NORTHWEST; - case 7: return TileDirection.WEST; - } // TODO: I wanted to add a TileDirection.INVALID enum value when implementing this, // but adding an INVALID value broke stuff: https://github.com/C7-Game/Prototype/issues/397 - return TileDirection.NORTH; + return row switch { + 0 => TileDirection.SOUTHWEST, + 1 => TileDirection.SOUTH, + 2 => TileDirection.SOUTHEAST, + 3 => TileDirection.EAST, + 4 => TileDirection.NORTHEAST, + 5 => TileDirection.NORTH, + 6 => TileDirection.NORTHWEST, + 7 => TileDirection.WEST, + _ => TileDirection.NORTH, + }; } public static void loadFlicAnimation(string path, string name, ref SpriteFrames frames, ref SpriteFrames tint) { @@ -123,10 +123,9 @@ public static void loadFlicAnimation(string path, string name, ref SpriteFrames } } - public static void loadCursorAnimation(string path, ref SpriteFrames frames) { + public static void loadCursorAnimation(string path, string name, ref SpriteFrames frames) { Flic flic = Util.LoadFlic(path); int row = 0; - string name = "cursor"; frames.AddAnimation(name); for (int col = 0; col < flic.Images.GetLength(1); col++) { @@ -247,4 +246,8 @@ public double getDuration() { double frameCount = flicSheet.indices.GetWidth() / flicSheet.spriteWidth; return frameCount / 20.0; // Civ 3 anims often run at 20 FPS TODO: Do they all? How could we tell? Is it exactly 20 FPS? } + + public override string ToString() { + return $"{unit.name}: {action}"; + } } diff --git a/C7/AnimationTracker.cs b/C7/AnimationTracker.cs index 17ac100c..1bc266de 100644 --- a/C7/AnimationTracker.cs +++ b/C7/AnimationTracker.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Linq; using C7GameData; -using C7Engine; public partial class AnimationTracker { private AnimationManager civ3AnimData; @@ -22,12 +21,9 @@ public struct ActiveAnimation { public C7Animation anim; } - private Dictionary activeAnims = new Dictionary(); + public Dictionary activeAnims = new Dictionary(); - public long getCurrentTimeMS() - { - return DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; - } + public long getCurrentTimeMS() => DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; private string getTileID(Tile tile) { @@ -45,8 +41,9 @@ private void startAnimation(string id, C7Animation anim, AutoResetEvent completi if (activeAnims.TryGetValue(id, out aa)) { // If there's already an animation playing for this unit, end it first before replacing it // TODO: Consider instead queueing up the new animation until after the first one is completed - if (aa.completionEvent != null) + if (aa.completionEvent is not null) { aa.completionEvent.Set(); + } } aa = new ActiveAnimation { startTimeMS = currentTimeMS, endTimeMS = currentTimeMS + animDurationMS, completionEvent = completionEvent, ending = ending, anim = anim }; @@ -110,19 +107,19 @@ public bool hasCurrentAction(MapUnit unit) public void update() { - long currentTimeMS = (! endAllImmediately) ? getCurrentTimeMS() : long.MaxValue; + long currentTimeMS = !endAllImmediately ? getCurrentTimeMS() : long.MaxValue; var keysToRemove = new List(); foreach (var guidAAPair in activeAnims.Where(guidAAPair => guidAAPair.Value.endTimeMS <= currentTimeMS)) { var (id, aa) = (guidAAPair.Key, guidAAPair.Value); - if (aa.completionEvent != null) { + if (aa.completionEvent is not null) { aa.completionEvent.Set(); aa.completionEvent = null; // So event is only triggered once } - if (aa.ending == AnimationEnding.Stop) + if (aa.ending == AnimationEnding.Stop) { keysToRemove.Add(id); + } } - foreach (var key in keysToRemove) - activeAnims.Remove(key); + keysToRemove.ForEach(key => activeAnims.Remove(key)); } public MapUnit.Appearance getUnitAppearance(MapUnit unit) @@ -158,9 +155,6 @@ public MapUnit.Appearance getUnitAppearance(MapUnit unit) public C7Animation getTileEffect(Tile tile) { ActiveAnimation aa; - if (activeAnims.TryGetValue(getTileID(tile), out aa)) - return aa.anim; - else - return null; + return activeAnims.TryGetValue(getTileID(tile), out aa) ? aa.anim : null; } } diff --git a/C7/Art/Title_Screen.jpg.import b/C7/Art/Title_Screen.jpg.import index 5f577051..869beee3 100644 --- a/C7/Art/Title_Screen.jpg.import +++ b/C7/Art/Title_Screen.jpg.import @@ -2,7 +2,7 @@ importer="texture" type="CompressedTexture2D" -uid="uid://ds3dwrouk7g55" +uid="uid://bkxkefpbld468" path="res://.godot/imported/Title_Screen.jpg-067f940f7a89fae79632e2159c786062.ctex" metadata={ "vram_texture": false diff --git a/C7/Art/grid.png b/C7/Art/grid.png new file mode 100644 index 00000000..8c3e5f58 Binary files /dev/null and b/C7/Art/grid.png differ diff --git a/C7/C7Game.tscn b/C7/C7Game.tscn index 501f8f06..e53baff9 100644 --- a/C7/C7Game.tscn +++ b/C7/C7Game.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=14 format=3 uid="uid://cldl5nk4n61m2"] +[gd_scene load_steps=15 format=3 uid="uid://cldl5nk4n61m2"] [ext_resource type="Script" path="res://Game.cs" id="1"] +[ext_resource type="Script" path="res://Map/MapViewCamera.cs" id="2_xrlqh"] [ext_resource type="Script" path="res://UIElements/Advisors/Advisors.cs" id="3"] [ext_resource type="Script" path="res://UIElements/UpperLeftNav/MenuButton.cs" id="4"] [ext_resource type="Script" path="res://UIElements/UpperLeftNav/CivilopediaButton.cs" id="5"] @@ -61,12 +62,19 @@ _data = { [node name="C7Game" type="Node2D"] script = ExtResource("1") +[node name="MapViewCamera" type="Camera2D" parent="."] +script = ExtResource("2_xrlqh") + [node name="CanvasLayer" type="CanvasLayer" parent="."] [node name="Advisor" type="CenterContainer" parent="CanvasLayer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 +offset_left = -574.0 +offset_top = -322.0 +offset_right = -574.0 +offset_bottom = -322.0 grow_horizontal = 2 grow_vertical = 2 theme = ExtResource("10") @@ -86,6 +94,7 @@ script = ExtResource("9") [node name="Control" type="Control" parent="CanvasLayer"] layout_mode = 3 +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 10.0 @@ -173,12 +182,14 @@ text = "End Turn" [node name="SlideOutBar" type="Control" parent="CanvasLayer/Control"] layout_mode = 1 +anchors_preset = 11 anchor_left = 1.0 anchor_right = 1.0 anchor_bottom = 1.0 -offset_top = 50.0 -offset_right = -10.0 -offset_bottom = -200.0 +offset_left = -108.0 +offset_top = 80.0 +offset_right = -108.0 +offset_bottom = -170.0 grow_horizontal = 0 grow_vertical = 2 mouse_filter = 1 @@ -248,7 +259,6 @@ libraries = { [connection signal="TurnStarted" from="." to="CanvasLayer/Control/GameStatus" method="OnTurnStarted"] [connection signal="BuildCity" from="CanvasLayer/PopupOverlay" to="." method="OnBuildCity"] [connection signal="HidePopup" from="CanvasLayer/PopupOverlay" to="CanvasLayer/PopupOverlay" method="OnHidePopup"] -[connection signal="Quit" from="CanvasLayer/PopupOverlay" to="." method="OnQuitTheGame"] [connection signal="UnitDisbanded" from="CanvasLayer/PopupOverlay" to="." method="OnUnitDisbanded"] [connection signal="BlinkyEndTurnButtonPressed" from="CanvasLayer/Control/GameStatus" to="." method="OnPlayerEndTurn"] [connection signal="pressed" from="CanvasLayer/Control/ToolBar/MarginContainer/HBoxContainer/AdvisorButton" to="CanvasLayer/Advisor" method="ShowLatestAdvisor"] diff --git a/C7/Civ3Map/Civ3Map.cs b/C7/Civ3Map/Civ3Map.cs deleted file mode 100644 index 883108df..00000000 --- a/C7/Civ3Map/Civ3Map.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Godot; -using System.Collections; -using System.Collections.Generic; -using ConvertCiv3Media; -using C7GameData; - -public partial class Civ3Map : Node2D -{ - public List Civ3Tiles; - public int[,] Map { get; protected set; } - TileMap TM; - public TileSet TS { get; protected set; } - private int[,] TileIDLookup; - // NOTE: The following two must be set externally before running TerrainAsTileMap - public int MapWidth; - public int MapHeight; - // If a mod is in effect, set this, otherwise set to "" or "Conquests" - public string ModRelPath = ""; - public Civ3Map(int mapWidth, int mapHeight, string modRelPath = "") - { - MapWidth = mapWidth; - MapHeight = mapHeight; - ModRelPath = modRelPath; - } - public void TerrainAsTileMap() { - if (TM != null) { RemoveChild(TM); } - // Although tiles appear isometric, they are logically laid out as a checkerboard pattern on a square grid - TM = new TileMap(); - TM.TileSet.TileSize = new Vector2I(64,32); - // TM.CenteredTextures = true; - TS = TM.TileSet; - - TileIDLookup = new int[9,81]; - - // int id = TS.GetLastUnusedTileId(); - - // Make blank default tile - // TODO: Make red tile or similar - // NOTE: Need an unused tile at 0, anyway, to test to see if real tile has been loaded yet - - // TS.CreateTile(id); - // id++; - - Map = new int[MapWidth,MapHeight]; - - // Populate map values - if(Civ3Tiles != null) - { - foreach (Tile tile in Civ3Tiles) - { - // If tile media file not loaded yet - if (TileIDLookup[tile.ExtraInfo.BaseTerrainFileID, 1] == 0) { LoadTileSet(tile.ExtraInfo.BaseTerrainFileID); } - // var _ = TileIDLookup[tile.ExtraInfo.BaseTerrainFileID, tile.ExtraInfo.BaseTerrainImageID]; - Map[tile.xCoordinate, tile.yCoordinate] = 0; - Map[tile.xCoordinate, tile.yCoordinate] = TileIDLookup[tile.ExtraInfo.BaseTerrainFileID,tile.ExtraInfo.BaseTerrainImageID]; - } - } - /* This code sets the tiles for display, but that is being done by MapView now - for (int y = 0; y < MapHeight; y++) { - for (int x = y % 2; x < MapWidth; x+=2) { - TM.SetCellv(new Vector2(x, y), Map[x,y]); - } - } - AddChild(TM); - */ - } - private void LoadTileSet(int fileID) - { - Hashtable FileNameLookup = new Hashtable - { - { 0, "Art/Terrain/xtgc.pcx" }, - { 1, "Art/Terrain/xpgc.pcx" }, - { 2, "Art/Terrain/xdgc.pcx" }, - { 3, "Art/Terrain/xdpc.pcx" }, - { 4, "Art/Terrain/xdgp.pcx" }, - { 5, "Art/Terrain/xggc.pcx" }, - { 6, "Art/Terrain/wCSO.pcx" }, - { 7, "Art/Terrain/wSSS.pcx" }, - { 8, "Art/Terrain/wOOO.pcx" }, - }; - // int id = TS.GetLastUnusedTileId(); - // temp if - if (FileNameLookup[fileID] != null) { - Pcx PcxTxtr = new Pcx(Util.Civ3MediaPath(FileNameLookup[fileID].ToString()));//, ModRelPath)); - ImageTexture Txtr = PCXToGodot.getImageTextureFromPCX(PcxTxtr); - - for (int y = 0; y < PcxTxtr.Height; y += 64) { - for (int x = 0; x < PcxTxtr.Width; x+= 128/*, id++*/) { - // TS.Create - // TS.CreateTile(id); - // TS.TileSetTexture(id, Txtr); - // TS.TileSetRegion(id, new Rect2(x, y, 128, 64)); - // TileIDLookup[fileID, (x / 128) + (y / 64) * (PcxTxtr.Width / 128)] = id; - } - } - } - } -} diff --git a/C7/Game.cs b/C7/Game.cs index 7c973592..8bb7efba 100644 --- a/C7/Game.cs +++ b/C7/Game.cs @@ -5,6 +5,7 @@ using C7Engine; using C7GameData; using Serilog; +using C7.Map; public partial class Game : Node2D { [Signal] public delegate void TurnStartedEventHandler(); @@ -22,7 +23,6 @@ enum GameState { } public Player controller; // Player that's controlling the UI. - private MapView mapView; public AnimationManager civ3AnimData; public AnimationTracker animTracker; @@ -41,12 +41,10 @@ enum GameState { public bool KeepCSUWhenFortified = false; Control Toolbar; - private bool IsMovingCamera; - private Vector2 OldPosition; Stopwatch loadTimer = new Stopwatch(); GlobalSingleton Global; - + public MapViewCamera camera; bool errorOnLoad = false; public override void _EnterTree() { @@ -57,6 +55,7 @@ public override void _EnterTree() { // The catch should always catch any error, as it's the general catch // that gives an error if we fail to load for some reason. public override void _Ready() { + GetTree().AutoAcceptQuit = false; Global = GetNode("/root/GlobalSingleton"); try { var animSoundPlayer = new AudioStreamPlayer(); @@ -66,35 +65,34 @@ public override void _Ready() { controller = CreateGame.createGame(Global.LoadGamePath, Global.DefaultBicPath); // Spawns engine thread Global.ResetLoadGamePath(); + camera = GetNode("MapViewCamera"); using (var gameDataAccess = new UIGameDataAccess()) { GameMap map = gameDataAccess.gameData.map; Util.setModPath(gameDataAccess.gameData.scenarioSearchPath); log.Debug("RelativeModPath ", map.RelativeModPath); - mapView = new MapView(this, map.numTilesWide, map.numTilesTall, map.wrapHorizontally, map.wrapVertically); - AddChild(mapView); - mapView.cameraZoom = (float)1.0; - mapView.gridLayer.visible = false; + mapView = new MapView(this, gameDataAccess.gameData); // Set initial camera location. If the UI controller has any cities, focus on their capital. Otherwise, focus on their // starting settler. if (controller.cities.Count > 0) { City capital = controller.cities.Find(c => c.IsCapital()); - if (capital != null) - mapView.centerCameraOnTile(capital.location); + if (capital is not null) { + camera.centerOnTile(capital.location, mapView); + } } else { MapUnit startingSettler = controller.units.Find(u => u.unitType.actions.Contains(C7Action.UnitBuildCity)); - if (startingSettler != null) - mapView.centerCameraOnTile(startingSettler.location); + if (startingSettler is not null) { + camera.centerOnTile(startingSettler.location, mapView); + } } + camera.attachToMapView(mapView); + AddChild(mapView); } Toolbar = GetNode("CanvasLayer/Control/ToolBar/MarginContainer/HBoxContainer"); - //TODO: What was this supposed to do? It throws errors and occasinally causes crashes now, because _OnViewportSizeChanged doesn't exist - // GetTree().Root.Connect("size_changed",new Callable(this,"_OnViewportSizeChanged")); - // Hide slideout bar on startup _on_SlideToggle_toggled(false); @@ -139,9 +137,7 @@ public void processEngineMessages(GameData gameData) { } break; case MsgStartEffectAnimation mSEA: - int x, y; - gameData.map.tileIndexToCoords(mSEA.tileIndex, out x, out y); - Tile tile = gameData.map.tileAt(x, y); + Tile tile = gameData.map.tileAtIndex(mSEA.tileIndex); if (tile != Tile.NONE && controller.tileKnowledge.isTileKnown(tile)) animTracker.startAnimation(tile, mSEA.effect, mSEA.completionEvent, mSEA.ending); else { @@ -152,21 +148,32 @@ public void processEngineMessages(GameData gameData) { case MsgStartTurn mST: OnPlayerStartTurn(); break; + + case MsgCityBuilt mBC: + Tile cityTile = gameData.map.tiles[mBC.tileIndex]; + City city = cityTile.cityAtTile; + mapView.addCity(city, cityTile); + break; + + case MsgTileDiscovered mTD: + Tile discoveredTile = gameData.map.tileAtIndex(mTD.tileIndex); + mapView.discoverTile(discoveredTile, controller.tileKnowledge); + break; } } } - // Instead of Game calling animTracker.update periodically (this used to happen in _Process), this method gets called as necessary to bring - // the animations up to date. Right now it's called from UnitLayer right before it draws the units on the map. This method also processes all - // waiting messages b/c some of them might pertain to animations. TODO: Consider processing only the animation messages here. - // Must only be called while holding the game data mutex + // updateAnimations updates animation states in the tracker and then their corresponding + // sprites in the MapView. It must be called when holding the game data mutex. + // TODO: before switching to tilemap, this was only called by the old UnitLayer _Draw + // method. It only really needs to be called as frequently as animations update... public void updateAnimations(GameData gameData) { - processEngineMessages(gameData); animTracker.update(); + mapView.updateAnimations(); } public override void _Process(double delta) { - this.processActions(); + processActions(); // TODO: Is it necessary to keep the game data mutex locked for this entire method? using (var gameDataAccess = new UIGameDataAccess()) { @@ -188,13 +195,13 @@ public override void _Process(double delta) { (CurrentlySelectedUnit.isFortified && !KeepCSUWhenFortified))) GetNextAutoselectedUnit(gameData); } - //Listen to keys. There is a C# Mono Godot bug where e.g. Godot.Key.F1 (etc.) doesn't work - //without a manual cast to int. - //https://github.com/godotengine/godot/issues/16388 + // Listen to keys. TODO: move this if (Input.IsKeyPressed(Godot.Key.F1)) { EmitSignal("ShowSpecificAdvisor", "F1"); } } + + updateAnimations(gameDataAccess.gameData); } } @@ -219,9 +226,9 @@ public override void _Process(double delta) { // If "location" is not already near the center of the screen, moves the camera to bring it into view. public void ensureLocationIsInView(Tile location) { if (controller.tileKnowledge.isTileKnown(location) && location != Tile.NONE) { - Vector2 relativeScreenLocation = mapView.screenLocationOfTile(location, true) / mapView.getVisibleAreaSize(); - if (relativeScreenLocation.DistanceTo(new Vector2((float)0.5, (float)0.5)) > 0.30) - mapView.centerCameraOnTile(location); + if (!camera.isTileInView(location, mapView)) { + camera.centerOnTile(location, mapView); + } } } @@ -289,42 +296,23 @@ private void OnPlayerEndTurn() { } public void _on_QuitButton_pressed() { - // This apparently exits the whole program - // GetTree().Quit(); - // ChangeSceneToFile deletes the current scene and frees its memory, so this is quitting to main menu GetTree().ChangeSceneToFile("res://MainMenu.tscn"); } - public void _on_Zoom_value_changed(float value) { - mapView.setCameraZoomFromMiddle(value); - } - public void AdjustZoomSlider(int numSteps, Vector2 zoomCenter) { VSlider slider = GetNode("CanvasLayer/Control/SlideOutBar/VBoxContainer/Zoom"); double newScale = slider.Value + slider.Step * (double)numSteps; - if (newScale < slider.MinValue) - newScale = slider.MinValue; - else if (newScale > slider.MaxValue) - newScale = slider.MaxValue; + newScale = Mathf.Clamp(newScale, slider.MinValue, slider.MaxValue); // Note we must set the camera zoom before setting the new slider value since setting the value will trigger the callback which will // adjust the zoom around a center we don't want. - mapView.setCameraZoom((float)newScale, zoomCenter); + // camera.scaleZoom(zoom) slider.Value = newScale; } - public void _on_RightButton_pressed() { - mapView.cameraLocation += new Vector2(128, 0); - } - public void _on_LeftButton_pressed() { - mapView.cameraLocation += new Vector2(-128, 0); - } - public void _on_UpButton_pressed() { - mapView.cameraLocation += new Vector2(0, -64); - } - public void _on_DownButton_pressed() { - mapView.cameraLocation += new Vector2(0, 64); + private void onSliderZoomChanged(float value) { + camera.setZoom(value); } public override void _Input(InputEvent @event) { @@ -336,13 +324,14 @@ public override void _Input(InputEvent @event) { public override void _UnhandledInput(InputEvent @event) { // Control node must not be in the way and/or have mouse pass enabled if (@event is InputEventMouseButton eventMouseButton) { + Vector2 globalMousePosition = GetGlobalMousePosition(); if (eventMouseButton.ButtonIndex == MouseButton.Left) { GetViewport().SetInputAsHandled(); if (eventMouseButton.IsPressed()) { if (inUnitGoToMode) { setGoToMode(false); using (var gameDataAccess = new UIGameDataAccess()) { - var tile = mapView.tileOnScreenAt(gameDataAccess.gameData.map, eventMouseButton.Position); + var tile = mapView.tileAt(gameDataAccess.gameData.map, globalMousePosition); if (tile != null) { new MsgSetUnitPath(CurrentlySelectedUnit.guid, tile).send(); } @@ -350,19 +339,14 @@ public override void _UnhandledInput(InputEvent @event) { } else { // Select unit on tile at mouse location using (var gameDataAccess = new UIGameDataAccess()) { - var tile = mapView.tileOnScreenAt(gameDataAccess.gameData.map, eventMouseButton.Position); + var tile = mapView.tileAt(gameDataAccess.gameData.map, globalMousePosition); if (tile != null) { MapUnit to_select = tile.unitsOnTile.Find(u => u.movementPoints.canMove); if (to_select != null && to_select.owner == controller) setSelectedUnit(to_select); } } - - OldPosition = eventMouseButton.Position; - IsMovingCamera = true; } - } else { - IsMovingCamera = false; } } else if (eventMouseButton.ButtonIndex == MouseButton.WheelUp) { GetViewport().SetInputAsHandled(); @@ -370,10 +354,10 @@ public override void _UnhandledInput(InputEvent @event) { } else if (eventMouseButton.ButtonIndex == MouseButton.WheelDown) { GetViewport().SetInputAsHandled(); AdjustZoomSlider(-1, GetViewport().GetMousePosition()); - } else if ((eventMouseButton.ButtonIndex == MouseButton.Right) && (!eventMouseButton.IsPressed())) { + } else if (eventMouseButton.ButtonIndex == MouseButton.Right && !eventMouseButton.IsPressed()) { setGoToMode(false); using (var gameDataAccess = new UIGameDataAccess()) { - var tile = mapView.tileOnScreenAt(gameDataAccess.gameData.map, eventMouseButton.Position); + var tile = mapView.tileAt(gameDataAccess.gameData.map, globalMousePosition); if (tile != null) { bool shiftDown = Input.IsKeyPressed(Godot.Key.Shift); if (shiftDown && tile.cityAtTile?.owner == controller) @@ -403,10 +387,8 @@ public override void _UnhandledInput(InputEvent @event) { } } } - } else if (@event is InputEventMouseMotion eventMouseMotion && IsMovingCamera) { + } else if (@event is InputEventMouseMotion eventMouseMotion) { GetViewport().SetInputAsHandled(); - mapView.cameraLocation += OldPosition - eventMouseMotion.Position; - OldPosition = eventMouseMotion.Position; } else if (@event is InputEventKey eventKeyDown && eventKeyDown.Pressed) { if (eventKeyDown.Keycode == Godot.Key.O && eventKeyDown.ShiftPressed && eventKeyDown.IsCommandOrControlPressed() && eventKeyDown.AltPressed) { using (UIGameDataAccess gameDataAccess = new UIGameDataAccess()) { @@ -424,17 +406,6 @@ public override void _UnhandledInput(InputEvent @event) { } } } - } else if (@event is InputEventMagnifyGesture magnifyGesture) { - // UI slider has the min/max zoom settings for now - VSlider slider = GetNode("CanvasLayer/Control/SlideOutBar/VBoxContainer/Zoom"); - double newScale = mapView.cameraZoom * magnifyGesture.Factor; - if (newScale < slider.MinValue) - newScale = slider.MinValue; - else if (newScale > slider.MaxValue) - newScale = slider.MaxValue; - mapView.setCameraZoom((float)newScale, magnifyGesture.Position); - // Update the UI slider - slider.Value = newScale; } } @@ -476,7 +447,7 @@ private void processActions() { } if (Input.IsActionJustPressed(C7Action.ToggleGrid)) { - this.mapView.gridLayer.visible = !this.mapView.gridLayer.visible; + mapView.toggleGrid(); } if (Input.IsActionJustPressed(C7Action.Escape) && !this.inUnitGoToMode) { @@ -486,15 +457,7 @@ private void processActions() { } if (Input.IsActionJustPressed(C7Action.ToggleZoom)) { - if (mapView.cameraZoom != 1) { - mapView.setCameraZoomFromMiddle(1.0f); - VSlider slider = GetNode("CanvasLayer/Control/SlideOutBar/VBoxContainer/Zoom"); - slider.Value = 1.0f; - } else { - mapView.setCameraZoomFromMiddle(0.5f); - VSlider slider = GetNode("CanvasLayer/Control/SlideOutBar/VBoxContainer/Zoom"); - slider.Value = 0.5f; - } + camera.setZoom(camera.zoomFactor != 1 ? 1.0f : 0.5f); } if (Input.IsActionJustPressed(C7Action.ToggleAnimations)) { @@ -558,7 +521,6 @@ private void processActions() { if (Input.IsActionJustPressed(C7Action.UnitBuildRoad) && CurrentlySelectedUnit.canBuildRoad()) { new MsgBuildRoad(CurrentlySelectedUnit.guid).send(); } - } private void GetNextAutoselectedUnit(GameData gameData) { @@ -583,11 +545,13 @@ private void OnUnitDisbanded() { } /** - * 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). + * 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). **/ - private void OnQuitTheGame() { - log.Information("Goodbye!"); - GetTree().Quit(); + public override void _Notification(int what) { + if (what == NotificationWMCloseRequest) { + log.Information("Goodbye!"); + GetTree().Quit(); + } } private void OnBuildCity(string name) { diff --git a/C7/LogManager.cs b/C7/LogManager.cs index a686286a..e18f80b2 100644 --- a/C7/LogManager.cs +++ b/C7/LogManager.cs @@ -38,7 +38,6 @@ public override void _Notification(int what) { GD.Print("Goodbye logger!"); Log.ForContext().Debug("Goodbye!"); Log.CloseAndFlush(); - GetTree().Quit(); } } diff --git a/C7/Map/CityLabelScene.cs b/C7/Map/CityLabelScene.cs index 86c8abc9..421c4230 100644 --- a/C7/Map/CityLabelScene.cs +++ b/C7/Map/CityLabelScene.cs @@ -11,7 +11,6 @@ public partial class CityLabelScene : Node2D { private City city; private Tile tile; - private Vector2I tileCenter; private ImageTexture cityTexture; @@ -37,6 +36,11 @@ public partial class CityLabelScene : Node2D { private static Theme popSizeTheme = new Theme(); private int lastLabelWidth = 0; + private int lastTurnsUntilGrowth; + private int lastTurnsUntilProductFinished; + private int lastCitySize; + private string lastProductionName; + private bool wasCapital; static CityLabelScene() { smallFontTheme.DefaultFont = smallFont; @@ -54,94 +58,105 @@ static CityLabelScene() { nonEmbassyStar = PCXToGodot.getImageFromPCX(cityIcons, 20, 1, 18, 18); } - public CityLabelScene(City city, Tile tile, Vector2I tileCenter) { + public CityLabelScene(City city, Tile tile) { this.city = city; this.tile = tile; - this.tileCenter = tileCenter; - labelTextureRect.MouseFilter = Control.MouseFilterEnum.Ignore; cityNameLabel.MouseFilter = Control.MouseFilterEnum.Ignore; productionLabel.MouseFilter = Control.MouseFilterEnum.Ignore; popSizeLabel.MouseFilter = Control.MouseFilterEnum.Ignore; + cityNameLabel.Text = city.name; AddChild(labelTextureRect); AddChild(cityNameLabel); AddChild(productionLabel); AddChild(popSizeLabel); + + cityNameLabel.Theme = smallFontTheme; + cityNameLabel.OffsetTop = 24; + + productionLabel.Theme = smallFontTheme; + productionLabel.OffsetTop = 36; + + popSizeLabel.Theme = popSizeTheme; + popSizeLabel.OffsetTop = 28; + + lastTurnsUntilGrowth = city.TurnsUntilGrowth(); + lastTurnsUntilProductFinished = city.TurnsUntilProductionFinished(); + lastCitySize = city.size; + lastProductionName = city.itemBeingProduced.name; + wasCapital = city.IsCapital(); + + resize(lastTurnsUntilGrowth, lastTurnsUntilProductFinished, lastCitySize, lastProductionName, wasCapital); } - public override void _Draw() { - base._Draw(); + private string getCityNameAndGrowthString(int turnsUntilGrowth) { + string turnsUntilGrowthText = turnsUntilGrowth == int.MaxValue || turnsUntilGrowth < 0 ? "- -" : turnsUntilGrowth.ToString(); + return $"{city.name} : {turnsUntilGrowthText}"; + } - int turnsUntilGrowth = city.TurnsUntilGrowth(); - string turnsUntilGrowthText = turnsUntilGrowth == int.MaxValue || turnsUntilGrowth < 0 ? "- -" : "" + turnsUntilGrowth; - string cityNameAndGrowth = $"{city.name} : {turnsUntilGrowthText}"; - string productionDescription = city.itemBeingProduced.name + " : " + city.TurnsUntilProductionFinished(); + private void resize(int turnsUntilGrowth, int turnsUntilProductionFinished, int citySize, string productionName, bool isCapital) { + string productionDescription = $"{productionName} : {turnsUntilProductionFinished}"; + int productionDescriptionWidth = (int)smallFont.GetStringSize(productionDescription).X; + string cityNameAndGrowth = getCityNameAndGrowthString(turnsUntilGrowth); int cityNameAndGrowthWidth = (int)smallFont.GetStringSize(cityNameAndGrowth).X; - int productionDescriptionWidth = (int)smallFont.GetStringSize(productionDescription).X; int maxTextWidth = Math.Max(cityNameAndGrowthWidth, productionDescriptionWidth); + int cityLabelWidth = maxTextWidth + (isCapital? 70 : 45); //TODO: Is 65 right? 70? Will depend on whether it's capital, too - int cityLabelWidth = maxTextWidth + (city.IsCapital()? 70 : 45); //TODO: Is 65 right? 70? Will depend on whether it's capital, too - int textAreaWidth = cityLabelWidth - (city.IsCapital() ? 50 : 25); + int textAreaWidth = cityLabelWidth - (isCapital ? 50 : 25); if (log.IsEnabled(LogEventLevel.Verbose)) { - log.Verbose("Width of city name = " + maxTextWidth); log.Verbose("City label width: " + cityLabelWidth); log.Verbose("Text area width: " + textAreaWidth); } - if (cityLabelWidth != lastLabelWidth) { Image labelBackground = CreateLabelBackground(cityLabelWidth, city, textAreaWidth); - cityLabel = ImageTexture.CreateFromImage(labelBackground); + cityLabel = ImageTexture.CreateFromImage(labelBackground); lastLabelWidth = cityLabelWidth; + labelTextureRect.Texture = cityLabel; + labelTextureRect.OffsetLeft = (cityLabelWidth / -2); + labelTextureRect.OffsetTop = 24; } - - DrawLabelOnScreen(tileCenter, cityLabelWidth, city, cityLabel); - DrawTextOnLabel(tileCenter, cityNameAndGrowthWidth, productionDescriptionWidth, city, cityNameAndGrowth, productionDescription, cityLabelWidth); - } - - private void DrawLabelOnScreen(Vector2I tileCenter, int cityLabelWidth, City city, ImageTexture cityLabel) - { - labelTextureRect.OffsetLeft = tileCenter.X + (cityLabelWidth / -2); - labelTextureRect.OffsetTop = tileCenter.Y + 24; - labelTextureRect.Texture = cityLabel; - } - - private void DrawTextOnLabel(Vector2I tileCenter, int cityNameAndGrowthWidth, int productionDescriptionWidth, City city, string cityNameAndGrowth, string productionDescription, int cityLabelWidth) { - - //Destination for font is based on lower-left of baseline of font, not upper left as for blitted rectangles - int cityNameOffset = cityNameAndGrowthWidth / -2; - int prodDescriptionOffset = productionDescriptionWidth / -2; - if (!city.IsCapital()) { + int cityNameOffset = cityNameAndGrowthWidth / -2 - 8; + int prodDescriptionOffset = productionDescriptionWidth / -2 - 8; + if (!isCapital) { cityNameOffset += 12; prodDescriptionOffset += 12; } - - cityNameLabel.Theme = smallFontTheme; cityNameLabel.Text = cityNameAndGrowth; - cityNameLabel.OffsetLeft = tileCenter.X + cityNameOffset; - cityNameLabel.OffsetTop = tileCenter.Y + 22; + cityNameLabel.OffsetLeft = cityNameOffset; - productionLabel.Theme = smallFontTheme; productionLabel.Text = productionDescription; - productionLabel.OffsetLeft = tileCenter.X + prodDescriptionOffset; - productionLabel.OffsetTop = tileCenter.Y + 32; + productionLabel.OffsetLeft = prodDescriptionOffset; - //City pop size - string popSizeString = "" + city.size; + string popSizeString = citySize.ToString(); int popSizeWidth = (int)midSizedFont.GetStringSize(popSizeString).X; int popSizeOffset = LEFT_RIGHT_BOXES_WIDTH / 2 - popSizeWidth / 2; - popSizeLabel.Theme = popSizeTheme; - - if (city.TurnsUntilGrowth() < 0) { + if (turnsUntilGrowth < 0) { popSizeLabel.Theme = popThemeRed; } popSizeLabel.Text = popSizeString; - popSizeLabel.OffsetLeft = tileCenter.X + cityLabelWidth / -2 + popSizeOffset; - popSizeLabel.OffsetTop = tileCenter.Y + 22; + popSizeLabel.OffsetLeft = cityLabelWidth / -2 + popSizeOffset; + } + + public override void _Process(double delta) { + int turnsUntilGrowth = city.TurnsUntilGrowth(); + int turnsUntilProductionFinished = city.TurnsUntilProductionFinished(); + int citySize = city.size; + string productionName = city.itemBeingProduced.name; + bool isCaptial = city.IsCapital(); + + if (turnsUntilGrowth != lastTurnsUntilGrowth || turnsUntilProductionFinished != lastTurnsUntilProductFinished || citySize != lastCitySize || productionName != lastProductionName || isCaptial != wasCapital) { + lastTurnsUntilGrowth = turnsUntilGrowth; + lastTurnsUntilProductFinished = turnsUntilProductionFinished; + lastCitySize = citySize; + lastProductionName = productionName; + wasCapital = isCaptial; + resize(turnsUntilGrowth, turnsUntilProductionFinished, citySize, productionName, isCaptial); + } } private Image CreateLabelBackground(int cityLabelWidth, City city, int textAreaWidth) diff --git a/C7/Map/CityLayer.cs b/C7/Map/CityLayer.cs deleted file mode 100644 index 074454ed..00000000 --- a/C7/Map/CityLayer.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using C7GameData; -using Godot; -using Serilog; - -namespace C7.Map { - public class CityLayer : LooseLayer { - - private ILogger log = LogManager.ForContext(); - - private ImageTexture cityTexture; - private Dictionary cityLabels = new Dictionary(); - - private List citiesWithScenes = new List(); - private Dictionary citySceneLookup = new Dictionary(); - - public CityLayer() - { - } - - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) - { - if (tile.cityAtTile is null) { - return; - } - - City city = tile.cityAtTile; - if (!citySceneLookup.ContainsKey(city)) { - CityScene cityScene = new CityScene(city, tile, new Vector2I((int)tileCenter.X, (int)tileCenter.Y)); - looseView.AddChild(cityScene); - citySceneLookup[city] = cityScene; - } else { - CityScene scene = citySceneLookup[city]; - scene._Draw(); - } - } - } -} diff --git a/C7/Map/CityScene.cs b/C7/Map/CityScene.cs index 6e5263dd..ab47f2fb 100644 --- a/C7/Map/CityScene.cs +++ b/C7/Map/CityScene.cs @@ -4,17 +4,17 @@ using Serilog; namespace C7.Map { - public partial class CityScene : Node2D { + public partial class CityScene : Sprite2D { private ILogger log = LogManager.ForContext(); private readonly Vector2 citySpriteSize; private ImageTexture cityTexture; - private TextureRect cityGraphics = new TextureRect(); private CityLabelScene cityLabelScene; - public CityScene(City city, Tile tile, Vector2I tileCenter) { - cityLabelScene = new CityLabelScene(city, tile, tileCenter); + public CityScene(City city, Tile tile) { + ZIndex = MapZIndex.Cities; + cityLabelScene = new CityLabelScene(city, tile); //TODO: Generalize, support multiple city types, etc. Pcx pcx = Util.LoadPCX("Art/Cities/rMIDEAST.PCX"); @@ -23,18 +23,9 @@ public CityScene(City city, Tile tile, Vector2I tileCenter) { cityTexture = Util.LoadTextureFromPCX("Art/Cities/rMIDEAST.PCX", 0, 0, width, height); citySpriteSize = new Vector2(width, height); - cityGraphics.OffsetLeft = tileCenter.X - (float)0.5 * citySpriteSize.X; - cityGraphics.OffsetTop = tileCenter.Y - (float)0.5 * citySpriteSize.Y; - cityGraphics.MouseFilter = Control.MouseFilterEnum.Ignore; - cityGraphics.Texture = cityTexture; + Texture = cityTexture; - AddChild(cityGraphics); AddChild(cityLabelScene); } - - public override void _Draw() { - base._Draw(); - cityLabelScene._Draw(); - } } } diff --git a/C7/Map/Civ3TerrainTileset.cs b/C7/Map/Civ3TerrainTileset.cs new file mode 100644 index 00000000..ba344cc6 --- /dev/null +++ b/C7/Map/Civ3TerrainTileset.cs @@ -0,0 +1,103 @@ +using Godot; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace C7.Map { + + class TerrainPcx { + private static Random prng = new Random(); + private string name; + // abc refers to the layout of the terrain tiles in the pcx based on + // the positions of each terrain at the corner of 4 tiles. + // - https://forums.civfanatics.com/threads/terrain-editing.622999/ + // - https://forums.civfanatics.com/threads/editing-terrain-pcx-files.102840/ + private string[] abc; + public int atlas; + public TerrainPcx(string name, string[] abc, int atlas) { + this.name = name; + this.abc = abc; + this.atlas = atlas; + } + public bool validFor(string[] corner) { + return corner.All(tile => abc.Contains(tile)); + } + private int abcIndex(string terrain) { + List indices = new List(); + for (int i = 0; i < abc.Count(); i++) { + if (abc[i] == terrain) { + indices.Add(i); + } + } + return indices[prng.Next(indices.Count)]; + } + + // getTextureCoords looks up the correct texture index in the pcx + // for the given position of each corner terrain type + public Vector2I getTextureCoords(string[] corner) { + int top = abcIndex(corner[0]); + int right = abcIndex(corner[1]); + int bottom = abcIndex(corner[2]); + int left = abcIndex(corner[3]); + int index = top + (left * 3) + (right * 9) + (bottom * 27); + return new Vector2I(index % 9, index / 9); + } + } + + // Civ3TerrainTileSet loads civ3 terrain pcx files and generates a tileset + class Civ3TerrainTileSet { + // same order as terrainPcxList + private static readonly List terrainPcxFiles = new List { + "Art/Terrain/xtgc.pcx", "Art/Terrain/xpgc.pcx", "Art/Terrain/xdgc.pcx", + "Art/Terrain/xdpc.pcx", "Art/Terrain/xdgp.pcx", "Art/Terrain/xggc.pcx", + "Art/Terrain/wCSO.pcx", "Art/Terrain/wSSS.pcx", "Art/Terrain/wOOO.pcx", + }; + + // same order as terrainPcxFiles + private static readonly List terrainPcxList = new List() { + new TerrainPcx("tgc", new string[]{"tundra", "grassland", "coast"}, 0), + new TerrainPcx("pgc", new string[]{"plains", "grassland", "coast"}, 1), + new TerrainPcx("dgc", new string[]{"desert", "grassland", "coast"}, 2), + new TerrainPcx("dpc", new string[]{"desert", "plains", "coast"}, 3), + new TerrainPcx("dgp", new string[]{"desert", "grassland", "plains"}, 4), + new TerrainPcx("ggc", new string[]{"grassland", "grassland", "coast"}, 5), + new TerrainPcx("cso", new string[]{"coast", "sea", "ocean"}, 6), + new TerrainPcx("sss", new string[]{"sea", "sea", "sea"}, 7), + new TerrainPcx("ooo", new string[]{"ocean", "ocean", "ocean"}, 8), + }; + + private static readonly Vector2I terrainTileSize = new Vector2I(128, 64); + + public static TileSet Generate() { + List textures = terrainPcxFiles.ConvertAll(path => Util.LoadTextureFromPCX(path)); + TileSet tileset = new TileSet{ + TileShape = TileSet.TileShapeEnum.Isometric, + TileLayout = TileSet.TileLayoutEnum.Stacked, + TileOffsetAxis = TileSet.TileOffsetAxisEnum.Horizontal, + TileSize = terrainTileSize, + }; + foreach (ImageTexture texture in textures) { + TileSetAtlasSource source = new TileSetAtlasSource{ + Texture = texture, + TextureRegionSize = terrainTileSize, + }; + for (int x = 0; x < 9; x++) { + for (int y = 0; y < 9; y++) { + source.CreateTile(new Vector2I(x, y)); + } + } + tileset.AddSource(source); + } + return tileset; + } + + public static TerrainPcx GetPcxFor(string[] corners) { + if (corners.Length != 4) { + throw new ArgumentException($"terrain corner must be of 4 tiles but got {corners.Length}"); + } + return terrainPcxList.Find(pcx => pcx.validFor(corners)); + } + + } + +} diff --git a/C7/Map/FogOfWarLayer.cs b/C7/Map/FogOfWarLayer.cs deleted file mode 100644 index eb88eab9..00000000 --- a/C7/Map/FogOfWarLayer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using C7GameData; -using ConvertCiv3Media; -using Godot; - -namespace C7.Map { - public partial class FogOfWarLayer : LooseLayer { - - private readonly ImageTexture fogOfWarTexture; - private readonly Vector2 tileSize; - - public FogOfWarLayer() { - Pcx fogOfWarPcx = new Pcx(Util.Civ3MediaPath("Art/Terrain/FogOfWar.pcx")); - fogOfWarTexture = PCXToGodot.getPureAlphaFromPCX(fogOfWarPcx); - tileSize = fogOfWarTexture.GetSize() / 9; - } - - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) { - Rect2 screenTarget = new Rect2(tileCenter - tileSize / 2, tileSize); - TileKnowledge tileKnowledge = gameData.GetHumanPlayers()[0].tileKnowledge; - //N.B. FogOfWar.pcx handles both totally unknown and fogged tiles, indexed in the same file. - //Hence the trinary math rather than the more commonplace binary. - if (!tileKnowledge.isTileKnown(tile)) { - int sum = 0; - if (tileKnowledge.isTileKnown(tile.neighbors[TileDirection.NORTH]) || tileKnowledge.isTileKnown(tile.neighbors[TileDirection.NORTHWEST]) || tileKnowledge.isTileKnown(tile.neighbors[TileDirection.NORTHEAST])) - sum += 1 * 2; - if (tileKnowledge.isTileKnown(tile.neighbors[TileDirection.WEST]) || tileKnowledge.isTileKnown(tile.neighbors[TileDirection.NORTHWEST]) || tileKnowledge.isTileKnown(tile.neighbors[TileDirection.SOUTHWEST])) - sum += 3 * 2; - if (tileKnowledge.isTileKnown(tile.neighbors[TileDirection.EAST]) || tileKnowledge.isTileKnown(tile.neighbors[TileDirection.NORTHEAST]) || tileKnowledge.isTileKnown(tile.neighbors[TileDirection.SOUTHEAST])) - sum += 9 * 2; - if (tileKnowledge.isTileKnown(tile.neighbors[TileDirection.SOUTH]) || tileKnowledge.isTileKnown(tile.neighbors[TileDirection.SOUTHWEST]) || tileKnowledge.isTileKnown(tile.neighbors[TileDirection.SOUTHEAST])) - sum += 27 * 2; - if (sum != 0) { - looseView.DrawTextureRectRegion(fogOfWarTexture, screenTarget, getRect(sum)); - } - } - //do nothing if the tile is known (equiv to the lower-right) - } - - private Rect2 getRect(int sum) { - int row = sum / 9; - int col = sum % 9; - return new Rect2(col * tileSize.X, row * tileSize.Y, tileSize); - } - } -} diff --git a/C7/Map/MapView.cs b/C7/Map/MapView.cs new file mode 100644 index 00000000..13513324 --- /dev/null +++ b/C7/Map/MapView.cs @@ -0,0 +1,676 @@ +using Godot; +using C7GameData; +using System; +using Serilog; +using System.Linq; +using System.Collections.Generic; +using C7Engine; + +namespace C7.Map { + + public static class MapZIndex { + public static readonly int Tiles = 10; + public static readonly int Cities = 20; + public static readonly int Cursor = 29; + public static readonly int Units = 30; + public static readonly int FogOfWar = 100; + } + + public enum HorizontalWrapState { + Left, // beyond left edge of the map is visible + Right, // beyond right edge of the map is visible + None, // camera is entirely over the map + } + + public partial class MapView : Node2D { + private string[,]terrain; + private TileMap terrainTilemap; + private TileMap wrappingTerrainTilemap; + private TileSet terrainTileset; + private TileMap tilemap; + private TileMap wrappingTilemap; + private TileSet tileset; + public Vector2I tileSize {get; private set;} = new Vector2I(128, 64); + private ILogger log = LogManager.ForContext(); + public bool wrapHorizontally {get; private set;} + public int worldEdgeRight {get; private set;} + public int worldEdgeLeft {get; private set;} + private int width; + private int height; + private bool showGrid = false; + private void setShowGrid(bool value) { + bool update = showGrid != value; + showGrid = value; + if (update) { + updateGridLayer(); + } + } + public void toggleGrid() { + setShowGrid(!showGrid); + } + public int pixelWidth {get; private set;} + private Game game; + private GameData data; + private GameMap gameMap; + + private Dictionary unitSprites = new Dictionary(); + private Dictionary cityScenes = new Dictionary(); + private CursorSprite cursor; + + private UnitSprite spriteFor(MapUnit unit) { + UnitSprite sprite = unitSprites.GetValueOrDefault(unit, null); + if (sprite is null) { + sprite = new UnitSprite(game.civ3AnimData); + unitSprites.Add(unit, sprite); + AddChild(sprite); + } + return sprite; + } + + private Vector2 getSpriteLocalPosition(Tile tile, MapUnit.Appearance appearance) { + Vector2 position = tilemap.MapToLocal(stackedCoords(tile)); + Vector2 offset = tileSize * new Vector2(appearance.offsetX, appearance.offsetY) / 2; + return position + offset; + } + + public void addCity(City city, Tile tile) { + log.Debug($"adding city at tile ({tile.xCoordinate}, {tile.yCoordinate})"); + CityScene scene = new CityScene(city, tile); + scene.Position = tilemap.MapToLocal(stackedCoords(tile)); + AddChild(scene); + cityScenes.Add(tile, scene); + } + + private Vector2I horizontalWrapOffset(HorizontalWrapState wrap) { + return wrap switch { + HorizontalWrapState.Left => Vector2I.Left, + HorizontalWrapState.Right => Vector2I.Right, + _ => Vector2I.Zero, + } * pixelWidth; + } + + private void animateUnit(Tile tile, MapUnit unit, HorizontalWrapState wrap) { + // TODO: simplify AnimationManager and drawing animations it is unnecessarily complex + // - also investigate if the custom offset tracking and SetFrame can be replaced by + // engine functionality + MapUnit.Appearance appearance = game.animTracker.getUnitAppearance(unit); + string name = AnimationManager.AnimationKey(unit.unitType, appearance.action, appearance.direction); + C7Animation animation = game.civ3AnimData.forUnit(unit.unitType, appearance.action); + animation.loadSpriteAnimation(); + UnitSprite sprite = spriteFor(unit); + int frame = sprite.GetNextFrameByProgress(name, appearance.progress); + float yOffset = sprite.FrameSize(name).Y / 4f; // TODO: verify actual value + Vector2 position = getSpriteLocalPosition(tile, appearance); + sprite.Position = position - new Vector2(0, yOffset); + Color civColor = new Color(unit.owner.color); + sprite.SetColor(civColor); + sprite.SetAnimation(name); + sprite.SetFrame(frame); + sprite.Show(); + + Vector2 wrapOffset = horizontalWrapOffset(wrap); + sprite.Translate(wrapOffset); + + if (unit == game.CurrentlySelectedUnit) { + // TODO: just noticed cursor position maybe should not be + // on sprite position which has a potential offset? + cursor.Position = position + wrapOffset; + cursor.Show(); + } + } + + private MapUnit selectUnitToDisplay(List units) { + if (units.Count == 0) { + return MapUnit.NONE; + } + MapUnit bestDefender = units[0], selected = null, interesting = null; + MapUnit currentlySelected = game.CurrentlySelectedUnit; + foreach (MapUnit unit in units) { + if (unit == currentlySelected) { + selected = unit; + } + if (unit.HasPriorityAsDefender(bestDefender, currentlySelected)) { + bestDefender = unit; + } + if (game.animTracker.getUnitAppearance(unit).DeservesPlayerAttention()) { + interesting = unit; + } + } + // Prefer showing the selected unit, secondly show one doing a relevant animation, otherwise show the top defender + return selected ?? interesting ?? bestDefender; + } + + public List<(Tile, HorizontalWrapState)> getVisibleTiles() { + List<(Tile, HorizontalWrapState)> tiles = new List<(Tile, HorizontalWrapState)>(); + Rect2 bounds = game.camera.getVisibleWorld(); + Vector2I topLeft = tilemap.LocalToMap(ToLocal(bounds.Position)); + Vector2I bottomRight = tilemap.LocalToMap(ToLocal(bounds.End)); + for (int x = topLeft.X - 1; x < bottomRight.X + 1; x++) { + for (int y = topLeft.Y - 1; y < bottomRight.Y + 1; y++) { + (int usX, int usY) = unstackedCoords(new Vector2I(x, y)); + HorizontalWrapState wrap = x switch { + _ when x < 0 => HorizontalWrapState.Left, + _ when x >= width => HorizontalWrapState.Right, + _ => HorizontalWrapState.None, + }; + tiles.Add((data.map.tileAt(usX, usY), wrap)); + } + } + return tiles; + } + + public void updateAnimations() { + foreach (UnitSprite s in unitSprites.Values) { + s.Hide(); + } + cursor.Hide(); + foreach ((Tile tile, HorizontalWrapState wrap) in getVisibleTiles()) { + MapUnit unit = selectUnitToDisplay(tile.unitsOnTile); + if (unit != MapUnit.NONE) { + animateUnit(tile, unit, wrap); + } + if (cityScenes.ContainsKey(tile)) { + CityScene scene = cityScenes[tile]; + Vector2 position = tilemap.MapToLocal(stackedCoords(tile)) + horizontalWrapOffset(wrap); + scene.Position = position; + } + } + } + + public void setHorizontalWrap(HorizontalWrapState state) { + if (state != HorizontalWrapState.None) { + Vector2I offset = horizontalWrapOffset(state); + wrappingTerrainTilemap.Position = terrainTilemap.Position + offset; + wrappingTilemap.Position = tilemap.Position + offset; + } + } + + private void initializeTileMap() { + terrainTilemap = new TileMap(); + wrappingTerrainTilemap = new TileMap(); + terrainTileset = Civ3TerrainTileSet.Generate(); + terrainTilemap.TileSet = terrainTileset; + terrainTilemap.Position += Vector2I.Right * (tileSize.X / 2); + wrappingTerrainTilemap.TileSet = terrainTileset; + + tilemap = new TileMap{ YSortEnabled = true }; + wrappingTilemap = new TileMap { YSortEnabled = true }; + + tileset = TileSetLoader.LoadCiv3TileSet(); + tilemap.TileSet = tileset; + wrappingTilemap.TileSet = tileset; + + // create tilemap layers + foreach (Layer layer in Enum.GetValues(typeof(Layer))) { + if (layer != Layer.Invalid) { + tilemap.AddLayer(layer.Index()); + wrappingTilemap.AddLayer(layer.Index()); + if (layer != Layer.FogOfWar) { + tilemap.SetLayerYSortEnabled(layer.Index(), true); + wrappingTilemap.SetLayerYSortEnabled(layer.Index(), true); + } else { + tilemap.SetLayerZIndex(layer.Index(), MapZIndex.FogOfWar); + wrappingTilemap.SetLayerZIndex(layer.Index(), MapZIndex.FogOfWar); + } + } + } + + setHorizontalWrap(HorizontalWrapState.Right); // just put it somewhere + + tilemap.ZIndex = MapZIndex.Tiles; // need to figure out a good way to order z indices + wrappingTilemap.ZIndex = MapZIndex.Tiles; + AddChild(tilemap); + AddChild(wrappingTilemap); + AddChild(terrainTilemap); + AddChild(wrappingTerrainTilemap); + } + + private void setTerrainTiles() { + string[] corners(int x, int y) { + string left = terrain[x, y]; + string right = terrain[(x + 1) % width, y]; + bool even = y % 2 == 0; + string top = "coast"; + if (y > 0) { + top = even ? terrain[x, y - 1] : terrain[(x + 1) % width, y - 1]; + } + string bottom = "coast"; + if (y < height - 1) { + bottom = even ? terrain[x, y + 1] : terrain[(x + 1) % width, y + 1]; + } + return new string[4]{top, right, bottom, left}; + } + void lookupAndSetTerrainTile(int x, int y, int cellX, int cellY) { + Vector2I cell = new Vector2I(cellX, cellY); + string[] corner = corners(x, y); + TerrainPcx pcx = Civ3TerrainTileSet.GetPcxFor(corner); + Vector2I texCoords = pcx.getTextureCoords(corner); + setTerrainTile(cell, pcx.atlas, texCoords); + } + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + lookupAndSetTerrainTile(x, y, x, y); + } + } + for (int y = 0; y < height; y++) { + lookupAndSetTerrainTile(width - 1, y, -1, y); + } + } + + void setTerrainTile(Vector2I cell, int atlas, Vector2I texCoords) { + terrainTilemap.SetCell(0, cell, atlas, texCoords); + wrappingTerrainTilemap.SetCell(0, cell, atlas, texCoords); + } + + private Vector2I stackedCoords(Tile tile) { + int x = tile.xCoordinate; + int y = tile.yCoordinate; + x = y % 2 == 0 ? x / 2 : (x - 1) / 2; + return new Vector2I(x, y); + } + + private (int, int) unstackedCoords(Vector2I stacked) { + (int x, int y) = (stacked.X, stacked.Y); + x = y % 2 == 0 ? x * 2 : (x * 2) + 1; + return (x, y); + } + + public MapView(Game game, GameData data) { + this.data = data; + this.game = game; + this.data = data; + this.gameMap = data.map; + cursor = new CursorSprite(); + AddChild(cursor); + width = gameMap.numTilesWide / 2; + pixelWidth = width * tileSize.X; + height = gameMap.numTilesTall; + initializeTileMap(); + wrapHorizontally = gameMap.wrapHorizontally; + terrain = new string[width, height]; + worldEdgeRight = (int)ToGlobal(tilemap.MapToLocal(new Vector2I(width - 1, 1))).X + tileSize.X / 2; + worldEdgeLeft = (int)ToGlobal(tilemap.MapToLocal(new Vector2I(0, 0))).X - tileSize.X / 2; + + // Convert coordinates from current save coordinates to + // stacked coordinates used by Godot's TileMap, and + // write terrain types to 2D array for generating corners + // TODO in the future convert civ3 coordinates to stacked + // coordinates when reading from the civ3 save so Tile has + // stacked coordinates + foreach (Tile t in gameMap.tiles) { + Vector2I coords = stackedCoords(t); + terrain[coords.X, coords.Y] = t.baseTerrainTypeKey; + } + setTerrainTiles(); + + // update each tile once to add all initial layers + TileKnowledge tk = data.GetHumanPlayers().First()?.tileKnowledge; + foreach (Tile tile in gameMap.tiles) { + updateTile(tile, tk); + } + } + + public Tile tileAt(GameMap gameMap, Vector2 globalMousePosition) { + Vector2I tilemapCoord = tilemap.LocalToMap(ToLocal(globalMousePosition)); + (int x, int y) = unstackedCoords(tilemapCoord); + return gameMap.tileAt(x, y); + } + + public Vector2 tileToLocal(Tile tile) => tilemap.MapToLocal(stackedCoords(tile)); + + private void setCell(Layer layer, Atlas atlas, Tile tile, Vector2I atlasCoords) { + if (!tileset.HasSource(atlas.Index())) { + log.Warning($"atlas id {atlas} is not a valid tileset source"); + } + if (!tileset.GetSource(atlas.Index()).HasTile(atlasCoords)) { + log.Warning($"atlas id {atlas} does not have tile at {atlasCoords}"); + } + tilemap.SetCell(layer.Index(), stackedCoords(tile), atlas.Index(), atlasCoords); + wrappingTilemap.SetCell(layer.Index(), stackedCoords(tile), atlas.Index(), atlasCoords); + } + + private void eraseCell(Layer layer, Tile tile) { + tilemap.EraseCell(layer.Index(), stackedCoords(tile)); + wrappingTilemap.EraseCell(layer.Index(), stackedCoords(tile)); + } + + private void updateRoadLayer(Tile tile, bool center) { + if (!tile.overlays.road) { + eraseCell(Layer.Road, tile); + eraseCell(Layer.Rail, tile); + return; + } + if (!tile.overlays.railroad) { + // road + int index = 0; + foreach ((TileDirection direction, Tile neighbor) in tile.neighbors) { + if (neighbor.overlays.road) { + index |= roadFlag(direction); + } + } + eraseCell(Layer.Rail, tile); + setCell(Layer.Road, Atlas.Road, tile, roadIndexTo2D(index)); + } else { + // railroad + int roadIndex = 0; + int railIndex = 0; + foreach ((TileDirection direction, Tile neighbor) in tile.neighbors) { + if (neighbor.overlays.railroad) { + railIndex |= roadFlag(direction); + } else if (neighbor.overlays.road) { + roadIndex |= roadFlag(direction); + } + } + if (roadIndex != 0) { + setCell(Layer.Road, Atlas.Road, tile, roadIndexTo2D(roadIndex)); + } else { + eraseCell(Layer.Road, tile); + } + setCell(Layer.Rail, Atlas.Rail, tile, roadIndexTo2D(railIndex)); + } + + if (center) { + // updating a tile may change neighboring tiles + foreach (Tile neighbor in tile.neighbors.Values) { + updateRoadLayer(neighbor, false); + } + } + } + + private Vector2I roadIndexTo2D(int index) => new Vector2I(index & 0xF, index >> 4); + + private static int roadFlag(TileDirection direction) { + return direction switch { + TileDirection.NORTHEAST => 0x1, + TileDirection.EAST => 0x2, + TileDirection.SOUTHEAST => 0x4, + TileDirection.SOUTH => 0x8, + TileDirection.SOUTHWEST => 0x10, + TileDirection.WEST => 0x20, + TileDirection.NORTHWEST => 0x40, + TileDirection.NORTH => 0x80, + _ => throw new ArgumentOutOfRangeException("Invalid TileDirection") + }; + } + + private void updateRiverLayer(Tile tile) { + // The "point" is the easternmost point of the current tile. + // The river graphic is determined by the tiles neighboring that point. + + + + Tile northOfPoint = tile.neighbors[TileDirection.NORTHEAST]; + Tile eastOfPoint = tile.neighbors[TileDirection.EAST]; + Tile westOfPoint = tile; + Tile southOfPoint = tile.neighbors[TileDirection.SOUTHEAST]; + + List riverNeighbors = new List () {northOfPoint, eastOfPoint, westOfPoint, southOfPoint}; + + int coastCount = riverNeighbors.Sum(tile => tile.IsWater()== true ? 1 : 0); + + int index = 0; + index += northOfPoint.riverSouthwest ? 1 : 0; + index += eastOfPoint.riverNorthwest ? 2 : 0; + index += westOfPoint.riverSoutheast ? 4 : 0; + index += southOfPoint.riverNortheast ? 8 : 0; + + if (index == 0) { + eraseCell(Layer.River, tile); + } else { + // We might eventually want a more sophisticated delta algorithm. Maybe check if *all four* are coastal and then delete the river entirely? Maybe check if the river is *ending* at a coast, etc. Might also want deltas in wetland tiles like marshes. Just sticking with this as it was the issue spec. + if(coastCount >= 2) + { + setCell(Layer.RiverDelta, Atlas.RiverDelta, tile, new Vector2I(index % 4, index / 4)); + } + else + { + setCell(Layer.River, Atlas.River, tile, new Vector2I(index % 4, index / 4)); + } + } + } + + private void updateHillLayer(Tile tile) { + if (!tile.overlayTerrainType.isHilly()) { + return; + } + Vector2I texCoord = getHillTextureCoordinate(tile); + TerrainType nearbyVegitation = getDominantVegetationNearHillyTile(tile); + Atlas atlas = tile.overlayTerrainType.Key switch { + "hills" => nearbyVegitation.Key switch { + "forest" => Atlas.ForestHill, + "jungle" => Atlas.JungleHill, + _ => Atlas.Hill, + }, + "mountains" => nearbyVegitation.Key switch { + _ when tile.isSnowCapped => Atlas.SnowMountain, + "forest" => Atlas.ForestMountain, + "jungle" => Atlas.JungleMountain, + _ => Atlas.Mountain, + }, + "volcano" => nearbyVegitation.Key switch { + "forest" => Atlas.ForestVolcano, + "jungle" => Atlas.JungleVolcano, + _ => Atlas.Volcano, + }, + _ => Atlas.Invalid, + }; + if (atlas != Atlas.Invalid) { + setCell(Layer.TerrainOverlay, atlas, tile, texCoord); + } + } + + private Vector2I getHillTextureCoordinate(Tile tile) { + int index = 0; + if (tile.neighbors[TileDirection.NORTHWEST].overlayTerrainType.isHilly()) { + index++; + } + if (tile.neighbors[TileDirection.NORTHEAST].overlayTerrainType.isHilly()) { + index+=2; + } + if (tile.neighbors[TileDirection.SOUTHWEST].overlayTerrainType.isHilly()) { + index+=4; + } + if (tile.neighbors[TileDirection.SOUTHEAST].overlayTerrainType.isHilly()) { + index+=8; + } + return new Vector2I(index % 4, index / 4); + } + + private TerrainType getDominantVegetationNearHillyTile(Tile center) { + TerrainType northeastType = center.neighbors[TileDirection.NORTHEAST].overlayTerrainType; + TerrainType northwestType = center.neighbors[TileDirection.NORTHWEST].overlayTerrainType; + TerrainType southeastType = center.neighbors[TileDirection.SOUTHEAST].overlayTerrainType; + TerrainType southwestType = center.neighbors[TileDirection.SOUTHWEST].overlayTerrainType; + + TerrainType[] neighborTerrains = { northeastType, northwestType, southeastType, southwestType }; + + int hills = neighborTerrains.Where(tt => tt.isHilly()).Count(); + TerrainType forest = neighborTerrains.FirstOrDefault(tt => tt.Key == "forest", null); + int forests = neighborTerrains.Where(tt => tt.Key == "forest").Count(); + TerrainType jungle = neighborTerrains.FirstOrDefault(tt => tt.Key == "jungle", null); + int jungles = neighborTerrains.Where(tt => tt.Key == "jungle").Count(); + + if (hills + forests + jungles < 4) { // some surrounding tiles are neither forested nor hilly + return TerrainType.NONE; + } + if (forests == 0 && jungles == 0) { + return TerrainType.NONE; // all hills + } + if (forests == jungles) { + // deterministically choose one on a tie so it doesn't change if the tile is updated + return center.xCoordinate % 2 == 0 ? forest : jungle; + } + return forests > jungles ? forest : jungle; + } + + private void updateForestLayer(Tile tile) { + if (tile.overlayTerrainType.Key == "forest") { + (int row, int col) = (0, 0); + if (tile.isPineForest) { + row = 8 + tile.xCoordinate % 2; // pine starts at row 8 in atlas + col = tile.xCoordinate % 6; // pine has 6 columns + } else { + bool small = tile.numWaterEdges() > 0; + // this technically omits one large and one small tile but the math is simpler + if (small) { + row = 6 + tile.xCoordinate % 2; + col = 1 + tile.xCoordinate % 4; + } else { + row = 4 + tile.xCoordinate % 2; + col = tile.xCoordinate % 4; + } + } + Atlas atlas = tile.baseTerrainType.Key switch { + "plains" => Atlas.PlainsForest, + "grassland" => Atlas.GrasslandsForest, + "tundra" => Atlas.TundraForest, + _ => Atlas.PlainsForest, + }; + setCell(Layer.TerrainOverlay, atlas, tile, new Vector2I(col, row)); + } else if (tile.overlayTerrainType.Key == "jungle") { + // Randomly, but predictably, choose a large jungle graphic + // More research is needed on when to use large vs small jungles. Probably, small is used when neighboring fewer jungles. + // For the first pass, we're just always using large jungles. + (int row, int col) = (tile.xCoordinate % 2, tile.xCoordinate % 4); + if (tile.numWaterEdges() > 0) { + (row, col) = (2 + tile.xCoordinate % 2, 1 + tile.xCoordinate % 5); + } + setCell(Layer.TerrainOverlay, Atlas.GrasslandsForest, tile, new Vector2I(col, row)); + } + } + + private void updateMarshLayer(Tile tile) { + if (tile.overlayTerrainType.Key != "marsh") { + return; + } + (int row, int col) = (tile.xCoordinate % 2, tile.xCoordinate % 4); + if (tile.numWaterEdges() > 0) { + (row, col) = (2 + tile.xCoordinate % 2, 1 + tile.xCoordinate % 4); + } + setCell(Layer.TerrainOverlay, Atlas.Marsh, tile, new Vector2I(col, row)); + } + + private static bool isForest(Tile tile) { + return tile.overlayTerrainType.Key == "forest" || tile.overlayTerrainType.Key == "jungle"; + } + + private void updateTerrainOverlayLayer(Tile tile) { + if (!tile.overlayTerrainType.isHilly() && !isForest(tile) && tile.overlayTerrainType.Key != "marsh") { + eraseCell(Layer.TerrainOverlay, tile); + return; + } + if (tile.overlayTerrainType.isHilly()) { + updateHillLayer(tile); + } else if (isForest(tile)) { + updateForestLayer(tile); + } else { + updateMarshLayer(tile); + } + } + + private void updateBuildingLayer(Tile tile) { + // TODO: add goody huts here once they are stored in the save and Tile class + if (tile.hasBarbarianCamp) { + setCell(Layer.Building, Atlas.TerrainBuilding, tile, new Vector2I(2, 0)); + } else { + eraseCell(Layer.Building, tile); + } + } + + // updateFogOfWarLayer returns true if the tile is visible or + // semi-visible, indicating other layers should be updated. + + private static int fogOfWarIndex(Tile tile, TileKnowledge tk) { + if (tk.isTileKnown(tile)) { + return 0; + } + int sum = 0; + // HACK: edge tiles have missing directions in neighbors map + if (tile.neighbors.Values.Count != 8) { + return sum; + } + if (tk.isTileKnown(tile.neighbors[TileDirection.NORTH]) || tk.isTileKnown(tile.neighbors[TileDirection.NORTHWEST]) || tk.isTileKnown(tile.neighbors[TileDirection.NORTHEAST])) { + sum += 1 * 2; + } + if (tk.isTileKnown(tile.neighbors[TileDirection.WEST]) || tk.isTileKnown(tile.neighbors[TileDirection.NORTHWEST]) || tk.isTileKnown(tile.neighbors[TileDirection.SOUTHWEST])) { + sum += 3 * 2; + } + if (tk.isTileKnown(tile.neighbors[TileDirection.EAST]) || tk.isTileKnown(tile.neighbors[TileDirection.NORTHEAST]) || tk.isTileKnown(tile.neighbors[TileDirection.SOUTHEAST])) { + sum += 9 * 2; + } + if (tk.isTileKnown(tile.neighbors[TileDirection.SOUTH]) || tk.isTileKnown(tile.neighbors[TileDirection.SOUTHWEST]) || tk.isTileKnown(tile.neighbors[TileDirection.SOUTHEAST])) { + sum += 27 * 2; + } + return sum; + } + + private bool updateFogOfWarLayer(Tile tile, TileKnowledge tk) { + if (tk.isTileKnown(tile)) { + eraseCell(Layer.FogOfWar, tile); + return true; + } + int index = fogOfWarIndex(tile, tk); + setCell(Layer.FogOfWar, Atlas.FogOfWar, tile, new Vector2I(index % 9, index / 9)); + return index > 0; // partially visible + } + + public void updateTile(Tile tile, TileKnowledge tk) { + if (tile == Tile.NONE || tile is null) { + string msg = tile is null ? "null tile" : "Tile.NONE"; + log.Warning($"attempting to update {msg}"); + return; + } + + if (tk is not null) { + bool visible = updateFogOfWarLayer(tile, tk); + if (!visible) { + return; + } + } + + updateRoadLayer(tile, true); + + if (tile.Resource != C7GameData.Resource.NONE) { + int index = tile.Resource.Icon; + Vector2I texCoord = new Vector2I(index % 6, index / 6); + setCell(Layer.Resource, Atlas.Resource, tile, texCoord); + } else { + eraseCell(Layer.Resource, tile); + } + + if (tile.baseTerrainType.Key == "grassland" && tile.isBonusShield) { + setCell(Layer.TerrainYield, Atlas.TerrainYield, tile, new Vector2I(0, 3)); + } else { + eraseCell(Layer.TerrainYield, tile); + } + + updateTerrainOverlayLayer(tile); + + updateRiverLayer(tile); + + updateBuildingLayer(tile); + } + + private void updateGridLayer() { + if (showGrid) { + foreach (Tile tile in data.map.tiles) { + setCell(Layer.Grid, Atlas.Grid, tile, Vector2I.Zero); + } + } else { + tilemap.ClearLayer(Layer.Grid.Index()); + } + } + + public void discoverTile(Tile tile, TileKnowledge tk) { + HashSet update = new HashSet(tile.neighbors.Values); + foreach (Tile n in tile.neighbors.Values) { + foreach (Tile nn in n.neighbors.Values) { + update.Add(nn); + } + } + foreach (Tile t in update) { + updateTile(t, tk); + } + } + } +} diff --git a/C7/Map/MapViewCamera.cs b/C7/Map/MapViewCamera.cs new file mode 100644 index 00000000..403e7607 --- /dev/null +++ b/C7/Map/MapViewCamera.cs @@ -0,0 +1,122 @@ +using Godot; +using C7GameData; + +namespace C7.Map { + + // MapViewCamera position and zoom should only be modified through the + // following methods: + // - scaleZoom + // - setZoom + // - setPosition + // This is because these methods will ensure that MapViewCamera handles + // world wrapping on the MapView automatically. + public partial class MapViewCamera : Camera2D { + private readonly float maxZoom = 3.0f; + private readonly float minZoom = 0.2f; + public float zoomFactor { get; private set; } = 1.0f; + private MapView map; + private int wrapEdgeTileMargin = 2; // margin in number of tiles to trigger map wrapping + private HorizontalWrapState hwrap = HorizontalWrapState.None; + + public async void attachToMapView(MapView map) { + this.map = map; + wrapEdgeMargin = wrapEdgeTileMargin * map.tileSize.X; + map.updateAnimations(); + + // Awaiting a 0 second timer is a workaround to force GlobalCanvasTransform to be updated. + // This is necessary when the camera's starting position is close to the edge of the map. + // Without it, the GlobalCanvasTransform will not be updated until the camera is moved, + // resulting in broken map wrapping. I tried awaiting "process_frame" but it does not seem + // to work, although I believe that is what we want to do here. GPT-4 suggested waiting for + // a 0 second timer, which seems to work. + await ToSignal(GetTree().CreateTimer(0), "timeout"); + checkWorldWrap(); + } + + public override void _Ready() { + base._Ready(); + scaleZoom(zoomFactor); + } + + public void scaleZoom(float factor) { + zoomFactor = zoomFactor * factor; + zoomFactor = Mathf.Clamp(zoomFactor, minZoom, maxZoom); + Zoom = Vector2.One * zoomFactor; + checkWorldWrap(); + } + + public void setZoom(float factor) { + zoomFactor = Mathf.Clamp(factor, minZoom, maxZoom); + Zoom = Vector2.One * zoomFactor; + checkWorldWrap(); + } + + public void setPosition(Vector2 position) { + Position = position; + checkWorldWrap(); + } + private float wrapEdgeMargin = 0; + float wrapRightEdge {get => map.worldEdgeRight - wrapEdgeMargin; } + float wrapLeftEdge {get => map.worldEdgeLeft + wrapEdgeMargin; } + private bool enteringRightWrap(Rect2 v) => hwrap != HorizontalWrapState.Right && v.End.X >= wrapRightEdge; + private bool enteringLeftWrap(Rect2 v) => hwrap != HorizontalWrapState.Left && v.Position.X <= wrapLeftEdge; + private bool atEdgeOfRightWrap(Rect2 v) => hwrap == HorizontalWrapState.Right && v.End.X >= wrapRightEdge + map.pixelWidth; + private bool atEdgeOfLeftWrap(Rect2 v) => hwrap == HorizontalWrapState.Left && v.Position.X <= wrapLeftEdge - map.pixelWidth; + private HorizontalWrapState currentHwrap(Rect2 v) { + return v.Position.X <= wrapLeftEdge ? HorizontalWrapState.Left : (v.End.X >= wrapRightEdge ? HorizontalWrapState.Right : HorizontalWrapState.None); + } + + // checkWorldWrap determines if the camera is about to spill over the world map and will: + // - move the second "wrap" tilemap to the appropriate edge + // to give the illusion of true wrapping tilemap + // - teleport the camera one world-width to the left or right when + // only the "wrap" tilemap is in view + private void checkWorldWrap() { + if (map is null || !map.wrapHorizontally) { + // TODO: for maps that do not wrap horizontally restrict movement + return; + } + Rect2 visibleWorld = getVisibleWorld(); + if (enteringRightWrap(visibleWorld)) { + map.setHorizontalWrap(HorizontalWrapState.Right); + } else if (enteringLeftWrap(visibleWorld)) { + map.setHorizontalWrap(HorizontalWrapState.Left); + } + if (atEdgeOfRightWrap(visibleWorld)) { + Translate(Vector2.Left * map.pixelWidth); + } else if (atEdgeOfLeftWrap(visibleWorld)) { + Translate(Vector2.Right * map.pixelWidth); + } + hwrap = currentHwrap(visibleWorld); + } + + public override void _UnhandledInput(InputEvent @event) { + switch (@event) { + case InputEventMouseMotion mouseDrag when mouseDrag.ButtonMask == MouseButtonMask.Left: + setPosition(Position - mouseDrag.Relative / Zoom); + break; + case InputEventMagnifyGesture magnifyGesture: + scaleZoom(magnifyGesture.Factor); + break; + } + } + + private Transform2D viewportToGlobalTransform => (GetViewport().GlobalCanvasTransform * GetCanvasTransform()).AffineInverse(); + + public Rect2 getVisibleWorld() => viewportToGlobalTransform * GetViewportRect(); + + public void centerOnTile(Tile tile, MapView map) { + Vector2 target = map.tileToLocal(tile); + setPosition(target); + } + + public bool isTileInView(Tile tile, MapView map) { + Rect2 visible = getVisibleWorld(); + Vector2 target = map.tileToLocal(tile); + float size = 30; + target -= Vector2.One * (size / 2); + Rect2 boundingBox = new Rect2(target, size, size); + return visible.Encloses(boundingBox); + } + } +} diff --git a/C7/Map/ResourceLayer.cs b/C7/Map/ResourceLayer.cs deleted file mode 100644 index 75454f9a..00000000 --- a/C7/Map/ResourceLayer.cs +++ /dev/null @@ -1,38 +0,0 @@ -using C7GameData; -using Godot; -using Resource = C7GameData.Resource; -using Serilog; - -namespace C7.Map -{ - public partial class ResourceLayer : LooseLayer - { - private ILogger log = LogManager.ForContext(); - - private static readonly Vector2 resourceSize = new Vector2(50, 50); - private int maxRow; - private ImageTexture resourceTexture; - - public ResourceLayer() - { - resourceTexture = Util.LoadTextureFromPCX("Art/resources.pcx"); - maxRow = (resourceTexture.GetHeight() / 50) - 1; - } - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) - { - Resource resource = tile.Resource; - if (resource != Resource.NONE) { - int resourceIcon = tile.Resource.Icon; - int row = resourceIcon / 6; - int col = resourceIcon % 6; - if (row > maxRow) { - log.Warning("Resource icon for " + resource.Name + " is too high"); - return; - } - Rect2 resourceRectangle = new Rect2(col * resourceSize.X, row * resourceSize.Y, resourceSize); - Rect2 screenTarget = new Rect2(tileCenter - 0.5f * resourceSize, resourceSize); - looseView.DrawTextureRectRegion(resourceTexture, screenTarget, resourceRectangle); - } - } - } -} diff --git a/C7/Map/RoadLayer.cs b/C7/Map/RoadLayer.cs deleted file mode 100644 index 9fe2cfe9..00000000 --- a/C7/Map/RoadLayer.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using C7GameData; -using Godot; - -namespace C7.Map -{ - public partial class RoadLayer : LooseLayer { - private readonly ImageTexture roadTexture; - private readonly ImageTexture railroadTexture; - private readonly Vector2 tileSize; - - public RoadLayer() { - roadTexture = Util.LoadTextureFromPCX("Art/Terrain/roads.pcx"); - railroadTexture = Util.LoadTextureFromPCX("Art/Terrain/railroads.pcx"); - tileSize = roadTexture.GetSize() / 16; - // grid 16x16 tiles - // assume that roads and railroads textures have the same size - } - - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) { - if (!hasRoad(tile)) return; - - Rect2 screenTarget = new Rect2(tileCenter - tileSize / 2, tileSize); - - if (!hasRailRoad(tile)) { - int roadIndex = 0; - foreach (KeyValuePair dirToTile in tile.neighbors) { - if (hasRoad(dirToTile.Value)) { - roadIndex |= getFlag(dirToTile.Key); - } - } - looseView.DrawTextureRectRegion(roadTexture, screenTarget, getRect(roadIndex)); - } else { - // has railroad - int roadIndex = 0; - int railroadIndex = 0; - foreach (KeyValuePair dirToTile in tile.neighbors) { - if (hasRailRoad(dirToTile.Value)) { - railroadIndex |= getFlag(dirToTile.Key); - } else if (hasRoad(dirToTile.Value)) { - roadIndex |= getFlag(dirToTile.Key); - } - } - if (roadIndex != 0) { - looseView.DrawTextureRectRegion(roadTexture, screenTarget, getRect(roadIndex)); - } - looseView.DrawTextureRectRegion(railroadTexture, screenTarget, getRect(railroadIndex)); - } - } - - private Rect2 getRect(int index) { - int row = index >> 4; - int column = index & 0xF; - return new Rect2(column * tileSize.X, row * tileSize.Y, tileSize); - } - - private static int getFlag(TileDirection direction) { - return direction switch { - TileDirection.NORTHEAST => 0x1, - TileDirection.EAST => 0x2, - TileDirection.SOUTHEAST => 0x4, - TileDirection.SOUTH => 0x8, - TileDirection.SOUTHWEST => 0x10, - TileDirection.WEST => 0x20, - TileDirection.NORTHWEST => 0x40, - TileDirection.NORTH => 0x80, - _ => throw new ArgumentOutOfRangeException("Invalid TileDirection") - }; - } - - private static bool hasRoad(Tile tile) { - return tile.overlays.road; - } - - private static bool hasRailRoad(Tile tile) { - return tile.overlays.railroad; - } - } -} diff --git a/C7/Map/TileSetLoader.cs b/C7/Map/TileSetLoader.cs new file mode 100644 index 00000000..03abb55b --- /dev/null +++ b/C7/Map/TileSetLoader.cs @@ -0,0 +1,237 @@ +using Godot; +using System.Collections.Generic; + +namespace C7.Map { + + public enum Layer { + TerrainOverlay, + River, + RiverDelta, + Road, + Rail, + Resource, + TerrainYield, + Building, + Grid, + FogOfWar, + Invalid, + }; + + public static class LayerExtensions { + public static int Index(this Layer layer) { + return (int)layer; + } + } + + public enum Atlas { + Hill, + ForestHill, + JungleHill, + Mountain, + SnowMountain, + ForestMountain, + JungleMountain, + Volcano, + ForestVolcano, + JungleVolcano, + PlainsForest, + GrasslandsForest, + TundraForest, + Marsh, + River, + RiverDelta, + Road, + Rail, + Resource, + TerrainYield, + TerrainBuilding, + GoodyHut, + Grid, + FogOfWar, + Invalid, + } + + public static class AtlasExtensions { + public static int Index(this Atlas atlas) { + return (int)atlas; + } + } + + class AtlasLoader { + string path; + protected int width; + protected int height; + Vector2I regionSize; + Vector2I textureOrigin; + protected TileSetAtlasSource source; + bool loaded = false; + + public AtlasLoader(string p, int w, int h, Vector2I rs, int y = 0) { + path = p; + width = w; + height = h; + regionSize = rs; + textureOrigin = new Vector2I(0, y); + source = new TileSetAtlasSource{ + Texture = p.EndsWith("FogOfWar.pcx") ? Util.LoadFogOfWarPCX(path) : Util.LoadTextureFromPCX(path), + TextureRegionSize = regionSize, + }; + } + + protected void createTile(int x, int y, bool doOffset = true) { + Vector2I atlasCoords = new Vector2I(x, y); + source.CreateTile(atlasCoords); + if (doOffset && textureOrigin.Y != 0) { + source.GetTileData(atlasCoords, 0).TextureOrigin = textureOrigin; + } + } + + public TileSetAtlasSource Load() { + if (!loaded) { + load(); + loaded = true; + } + return source; + } + + protected virtual void load() { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + createTile(x, y); + } + } + } + } + + class ForestAtlasLoader : AtlasLoader { + bool jungle; + public ForestAtlasLoader(string p, Vector2I rs, bool j = false) : base(p, -1, -1, rs, 12) { + jungle = j; + } + + protected override void load() { + for (int x = 0; x < 6; x++) { + for (int y = 0; y < 10; y++) { + if ((y < 4 && !jungle) || (y < 2 && x > 3)) { + continue; // first 4 rows are for jungle tiles + } + if ((y > 3 && y < 6 && x > 3) || (y > 5 && y < 8 && x > 4)) { + continue; // forest tilemap is shaped like this + } + bool shouldDoOffset = y == 1 || y == 2 || y == 4 || y == 5; + createTile(x, y, shouldDoOffset); + } + } + } + }; + + class NonSquareAtlasLoader : AtlasLoader { + int lastRowWidth; + public NonSquareAtlasLoader(string p, int w, int h, int lastRowWidth, Vector2I rs) : base(p, w, h, rs) { + this.lastRowWidth = lastRowWidth; + } + + protected override void load() { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (y == height - 1 && x >= lastRowWidth) { + continue; + } + createTile(x, y); + } + } + } + } + + class MarshAtlasLoader : AtlasLoader { + public MarshAtlasLoader(string p, Vector2I rs) : base(p, -1, -1, rs, 12) { } + + protected override void load() { + // TODO: incomplete + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 5; x++) { + if (y < 2 && x > 3) { + continue; + } + bool shouldDoOffset = y == 0 || y == 1; + createTile(x, y, shouldDoOffset); + } + } + } + } + + // TileSetLoader loads tileset atlas sources + // In the future, it will be configured to set the path property of each + // atlas loader depending on which custom terrain or graphics are used. + class TileSetLoader { + private static readonly Vector2I tileSize = new Vector2I(128, 64); + private static readonly Vector2I hillSize = new Vector2I(128, 72); + private static readonly Vector2I mountainSize = new Vector2I(128, 88); + private static readonly Vector2I volcanoSize = new Vector2I(128, 88); + + private static readonly Vector2I forestSize = new Vector2I(128, 88); + private static readonly Vector2I marshSize = new Vector2I(128, 88); + + private static readonly Vector2I resourceSize = new Vector2I(50, 50); + private static readonly Vector2I buildingSize = new Vector2I(128, 64); + + private static readonly Dictionary civ3PcxForAtlas = new Dictionary { + {Atlas.Resource, new NonSquareAtlasLoader("Conquests/Art/resources.pcx", 6, 4, 4, resourceSize)}, + + {Atlas.Road, new AtlasLoader("Art/Terrain/roads.pcx", 16, 16, tileSize)}, + {Atlas.Rail, new AtlasLoader("Art/Terrain/railroads.pcx", 16, 16, tileSize)}, + + {Atlas.TerrainYield, new AtlasLoader("Art/Terrain/tnt.pcx", 3, 6, tileSize)}, + + {Atlas.River, new AtlasLoader("Art/Terrain/mtnRivers.pcx", 4, 4, tileSize)}, + {Atlas.RiverDelta, new AtlasLoader("Art/Terrain/deltaRivers.pcx", 4, 4, tileSize)}, + + {Atlas.Hill, new AtlasLoader("Art/Terrain/xhills.pcx", 4, 4, hillSize, 4)}, + {Atlas.ForestHill, new AtlasLoader("Art/Terrain/hill forests.pcx", 4, 4, hillSize, 4)}, + {Atlas.JungleHill, new AtlasLoader("Art/Terrain/hill jungle.pcx", 4, 4, hillSize, 4)}, + + {Atlas.Mountain, new AtlasLoader("Art/Terrain/Mountains.pcx", 4, 4, mountainSize, 12)}, + {Atlas.SnowMountain, new AtlasLoader("Art/Terrain/Mountains-snow.pcx", 4, 4, mountainSize, 12)}, + {Atlas.ForestMountain, new AtlasLoader("Art/Terrain/mountain forests.pcx", 4, 4, mountainSize, 12)}, + {Atlas.JungleMountain, new AtlasLoader("Art/Terrain/mountain jungles.pcx", 4, 4, mountainSize, 12)}, + + {Atlas.Volcano, new AtlasLoader("Art/Terrain/Volcanos.pcx", 4, 4, mountainSize, 12)}, + {Atlas.ForestVolcano, new AtlasLoader("Art/Terrain/Volcanos forests.pcx", 4, 4, mountainSize, 12)}, + {Atlas.JungleVolcano, new AtlasLoader("Art/Terrain/Volcanos jungles.pcx", 4, 4, mountainSize, 12)}, + + {Atlas.PlainsForest, new ForestAtlasLoader("Art/Terrain/plains forests.pcx", forestSize)}, + {Atlas.GrasslandsForest, new ForestAtlasLoader("Art/Terrain/grassland forests.pcx", forestSize, true)}, + {Atlas.TundraForest, new ForestAtlasLoader("Art/Terrain/tundra forests.pcx", forestSize)}, + + {Atlas.Marsh, new MarshAtlasLoader("Art/Terrain/marsh.pcx", marshSize)}, + + {Atlas.TerrainBuilding, new AtlasLoader("Art/Terrain/TerrainBuildings.pcx", 4, 4, buildingSize)}, + {Atlas.GoodyHut, new NonSquareAtlasLoader("Art/Terrain/goodyhuts.pcx", 3, 3, 2, buildingSize)}, + + {Atlas.FogOfWar, new NonSquareAtlasLoader("Art/Terrain/FogOfWar.pcx", 9, 9, 8, tileSize)}, + }; + + public static TileSet LoadCiv3TileSet() { + TileSet tileset = new TileSet { + TileShape = TileSet.TileShapeEnum.Isometric, + TileLayout = TileSet.TileLayoutEnum.Stacked, + TileOffsetAxis = TileSet.TileOffsetAxisEnum.Horizontal, + TileSize = tileSize, + }; + + foreach ((Atlas atlas, AtlasLoader loader) in civ3PcxForAtlas) { + TileSetAtlasSource source = loader.Load(); + tileset.AddSource(source, atlas.Index()); + } + + TileSetAtlasSource gridSource = new TileSetAtlasSource{ + Texture = Util.LoadTextureFromC7JPG("Art/grid.png"), + TextureRegionSize = tileSize, + }; + gridSource.CreateTile(Vector2I.Zero); + tileset.AddSource(gridSource, Atlas.Grid.Index()); + + return tileset; + } + } +} diff --git a/C7/Map/TntLayer.cs b/C7/Map/TntLayer.cs deleted file mode 100644 index cfd0a20a..00000000 --- a/C7/Map/TntLayer.cs +++ /dev/null @@ -1,44 +0,0 @@ -using C7GameData; -using Godot; -using Resource = C7GameData.Resource; -using Serilog; - -namespace C7.Map -{ - /// - /// Displays terrain yield overlays (from the tnt.pcx file). These are most well known for letting you know where - /// there are bonus grasslands. - /// Note: I don't know why it's called tnt. - /// - public partial class TntLayer : LooseLayer - { - private ILogger log = LogManager.ForContext(); - - private static readonly Vector2 tntSize = new Vector2(128, 64); - private ImageTexture tntTexture; - - //Each row corresponds to a terrain. For now we're only adding one, maybe someday we'll add full TNT support -#pragma warning disable CS0414 - private readonly int GRASSLAND_ROW = 0; - private readonly int BONUS_GRASSLAND_ROW = 1; - private readonly int PLAINS_ROW = 2; - private readonly int DESERT_ROW = 3; - private readonly int BONUS_GRASSLAND_TNT_OFF_ROW = 3; - private readonly int TUNDRA_ROW = 4; - private readonly int FLOOD_PLAIN_ROW = 5; -#pragma warning restore CS0414 - - public TntLayer() - { - tntTexture = Util.LoadTextureFromPCX("Art/Terrain/tnt.pcx"); - } - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) - { - if (tile.overlayTerrainType.Key == "grassland" && tile.isBonusShield) { - Rect2 tntRectangle = new Rect2(0, BONUS_GRASSLAND_TNT_OFF_ROW * tntSize.Y, tntSize); - Rect2 screenTarget = new Rect2(tileCenter - 0.5f * tntSize, tntSize); - looseView.DrawTextureRectRegion(tntTexture, screenTarget, tntRectangle); - } - } - } -} diff --git a/C7/Map/UnitLayer.cs b/C7/Map/UnitLayer.cs deleted file mode 100644 index 4cc35faa..00000000 --- a/C7/Map/UnitLayer.cs +++ /dev/null @@ -1,289 +0,0 @@ -using System; -using System.Collections.Generic; -using C7GameData; -using C7Engine; -using ConvertCiv3Media; -using Godot; - -public partial class UnitLayer : LooseLayer { - private ImageTexture unitIcons; - private int unitIconsWidth; - private ImageTexture unitMovementIndicators; - - // 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 = 150; - public const int unitAnimZIndex = 100; - public const int cursorZIndex = 50; - - public UnitLayer() { - var iconPCX = new Pcx(Util.Civ3MediaPath("Art/Units/units_32.pcx")); - unitIcons = PCXToGodot.getImageTextureFromPCX(iconPCX); - unitIconsWidth = (unitIcons.GetWidth() - 1) / 33; - - var moveIndPCX = new Pcx(Util.Civ3MediaPath("Art/interface/MovementLED.pcx")); - unitMovementIndicators = PCXToGodot.getImageTextureFromPCX(moveIndPCX); - } - - // Creates a plane mesh facing the positive Z-axis with the given shader attached. The quad is 1.0 units long on both sides, - // intended to be scaled to the appropriate size when used. - public static (ShaderMaterial, MeshInstance2D) createShadedQuad(Shader shader) { - PlaneMesh mesh = new PlaneMesh(); - mesh.SubdivideDepth = 1; - mesh.Orientation = PlaneMesh.OrientationEnum.Z; - mesh.Size = new Vector2(1, 1); - - ShaderMaterial shaderMat = new ShaderMaterial(); - shaderMat.Shader = shader; - - MeshInstance2D meshInst = new MeshInstance2D(); - meshInst.Material = shaderMat; - meshInst.Mesh = mesh; - - return (shaderMat, meshInst); - } - - public Color getHPColor(float fractionRemaining) { - if (fractionRemaining >= 0.67f) { - return Color.Color8(0, 255, 0); - } else if (fractionRemaining >= 0.34f) { - return Color.Color8(255, 255, 0); - } else { - return Color.Color8(255, 0, 0); - } - } - - // AnimationInstance represents an animation appearing on the screen. It's specific to a unit, action, and direction. AnimationInstances have - // two components: a ShaderMaterial and a MeshInstance2D. The ShaderMaterial runs the unit shader (created by UnitLayer.getShader) with all - // the parameters set to a particular texture, civ color, direction, etc. The MeshInstance2D is what's actually drawn by Godot, i.e., what's - // added to the node tree. AnimationInstances are only active for one frame at a time but they live as long as the UnitLayer. They are - // retrieved or created as needed by getBlankAnimationInstance during the drawing of units and are hidden & requeued for use at the beginning - // of each frame. - - // should hold animation players instead of animations - public partial class AnimationInstance { - - public AnimatedSprite2D sprite; - public AnimatedSprite2D spriteTint; - public ShaderMaterial material; - - public void SetPosition(Vector2 position) { - this.sprite.Position = position; - this.spriteTint.Position = position; - } - - public int GetNextFrameByProgress(string animation, float progress) { - // AnimatedSprite2D has a settable FrameProgress field, which I expected to - // update the current frame of the animation upon setting, but it did not - // when I tried it, so instead, calculate what the next frame should be - // based on the progress. - int frameCount = this.sprite.SpriteFrames.GetFrameCount(animation); - int nextFrame = (int)((float)frameCount * progress); - return nextFrame >= frameCount ? frameCount - 1 : (nextFrame < 0 ? 0 : nextFrame); - } - - public void SetFrame(int frame) { - this.sprite.Frame = frame; - this.spriteTint.Frame = frame; - } - - public void SetAnimation(string name) { - this.sprite.Animation = name; - this.spriteTint.Animation = name; - } - - public void Show() { - this.sprite.Show(); - this.spriteTint.Show(); - } - - public void Hide() { - this.sprite.Hide(); - this.spriteTint.Hide(); - } - - public Vector2 FrameSize(string animation) { - return this.sprite.SpriteFrames.GetFrameTexture(animation, 0).GetSize(); - } - - public AnimationInstance(LooseView looseView) { - AnimationManager manager = looseView.mapView.game.civ3AnimData; - - this.sprite = new AnimatedSprite2D(); - this.sprite.ZIndex = unitAnimZIndex; - this.sprite.SpriteFrames = manager.spriteFrames; - - this.spriteTint = new AnimatedSprite2D(); - this.spriteTint.ZIndex = unitAnimZIndex; - this.spriteTint.SpriteFrames = manager.tintFrames; - - this.material = new ShaderMaterial(); - this.material.Shader = GD.Load("res://UnitTint.gdshader"); - this.spriteTint.Material = this.material; - - looseView.AddChild(sprite); - looseView.AddChild(spriteTint); - } - } - - private List animInsts = new List(); - private int nextBlankAnimInst = 0; - - // Returns the next unused AnimationInstance or creates & returns a new one if none are available. - public AnimationInstance getBlankAnimationInstance(LooseView looseView) { - if (nextBlankAnimInst >= animInsts.Count) { - animInsts.Add(new AnimationInstance(looseView)); - } - AnimationInstance inst = animInsts[nextBlankAnimInst]; - nextBlankAnimInst++; - return inst; - } - - public void drawUnitAnimFrame(LooseView looseView, MapUnit unit, MapUnit.Appearance appearance, Vector2 tileCenter) { - AnimationInstance inst = getBlankAnimationInstance(looseView); - looseView.mapView.game.civ3AnimData.forUnit(unit.unitType, appearance.action).loadSpriteAnimation(); - string animName = AnimationManager.AnimationKey(unit.unitType, appearance.action, appearance.direction); - - // Need to move the sprites upward a bit so that their feet are at the center of the tile. I don't know if spriteHeight/4 is the right - var animOffset = MapView.cellSize * new Vector2(appearance.offsetX, appearance.offsetY); - Vector2 position = tileCenter + animOffset - new Vector2(0, inst.FrameSize(animName).Y / 4); - inst.SetPosition(position); - - var civColor = new Color(unit.owner.color); - int nextFrame = inst.GetNextFrameByProgress(animName, appearance.progress); - inst.material.SetShaderParameter("tintColor", new Vector3(civColor.R, civColor.G, civColor.B)); - - inst.SetAnimation(animName); - inst.SetFrame(nextFrame); - inst.Show(); - } - - 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; - } - - private AnimatedSprite2D cursorSprite = null; - - public void drawCursor(LooseView looseView, Vector2 position) { - // Initialize cursor if necessary - if (cursorSprite == null) { - cursorSprite = new AnimatedSprite2D(); - SpriteFrames frames = new SpriteFrames(); - cursorSprite.SpriteFrames = frames; - AnimationManager.loadCursorAnimation("Art/Animations/Cursor/Cursor.flc", ref frames); - cursorSprite.Animation = "cursor"; // hardcoded in loadCursorAnimation - looseView.AddChild(cursorSprite); - } - - const double period = 2.5; // TODO: Just eyeballing this for now. Read the actual period from the INI or something. - double repCount = (double)Time.GetTicksMsec() / 1000.0 / period; - float progress = (float)(repCount - Math.Floor(repCount)); - cursorSprite.Position = position; - int frameCount = cursorSprite.SpriteFrames.GetFrameCount("cursor"); - int nextFrame = (int)((float)frameCount * progress); - nextFrame = nextFrame >= frameCount ? frameCount - 1 : (nextFrame < 0 ? 0 : nextFrame); - cursorSprite.Frame = nextFrame; - cursorSprite.Show(); - } - - public override void onBeginDraw(LooseView looseView, GameData gameData) { - // Reset animation instances - for (int n = 0; n < nextBlankAnimInst; n++) { - animInsts[n].Hide(); - } - nextBlankAnimInst = 0; - - // Hide cursor if it's been initialized - cursorSprite?.Hide(); - - looseView.mapView.game.updateAnimations(gameData); - } - - // Returns which unit should be drawn from among a list of units. The list is assumed to be non-empty. - 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; - var currentlySelectedUnit = looseView.mapView.game.CurrentlySelectedUnit; - foreach (var u in units) { - if (u == currentlySelectedUnit) - selected = u; - if (u.HasPriorityAsDefender(bestDefender, currentlySelectedUnit)) - bestDefender = u; - if (looseView.mapView.game.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); - } - - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) { - // First draw animated effects. These will always appear over top of units regardless of draw order due to z-index. - C7Animation tileEffect = looseView.mapView.game.animTracker.getTileEffect(tile); - if (tileEffect != null) { - (_, float progress) = looseView.mapView.game.animTracker.getCurrentActionAndProgress(tile); - drawEffectAnimFrame(looseView, tileEffect, progress, tileCenter); - } - - if (tile.unitsOnTile.Count == 0) { - return; - } - - var white = Color.Color8(255, 255, 255); - - MapUnit unit = selectUnitToDisplay(looseView, tile.unitsOnTile); - MapUnit.Appearance appearance = looseView.mapView.game.animTracker.getUnitAppearance(unit); - Vector2 animOffset = new Vector2(appearance.offsetX, appearance.offsetY) * MapView.cellSize; - - // If the unit we're about to draw is currently selected, draw the cursor first underneath it - if ((unit != MapUnit.NONE) && (unit == looseView.mapView.game.CurrentlySelectedUnit)) { - drawCursor(looseView, tileCenter + animOffset); - } - - drawUnitAnimFrame(looseView, unit, appearance, tileCenter); - - Vector2 indicatorLoc = tileCenter - new Vector2(26, 40) + animOffset; - - int moveIndIndex = (!unit.movementPoints.canMove) ? 4 : ((unit.movementPoints.remaining >= unit.unitType.movement) ? 0 : 2); - Vector2 moveIndUpperLeft = new Vector2(1 + 7 * moveIndIndex, 1); - Rect2 moveIndRect = new Rect2(moveIndUpperLeft, new Vector2(6, 6)); - var screenRect = new Rect2(indicatorLoc, new Vector2(6, 6)); - looseView.DrawTextureRectRegion(unitMovementIndicators, screenRect, moveIndRect); - - int hpIndHeight = 6 * (unit.maxHitPoints <= 5 ? unit.maxHitPoints : 5), hpIndWidth = 6; - Rect2 hpIndBackgroundRect = new Rect2(indicatorLoc + new Vector2(-1, 8), new Vector2(hpIndWidth, hpIndHeight)); - if ((unit.unitType.attack > 0) || (unit.unitType.defense > 0)) { - float hpFraction = (float)unit.hitPointsRemaining / unit.maxHitPoints; - looseView.DrawRect(hpIndBackgroundRect, Color.Color8(0, 0, 0)); - float hpHeight = hpFraction * (hpIndHeight - 2); - if (hpHeight < 1) - hpHeight = 1; - var hpContentsRect = new Rect2(hpIndBackgroundRect.Position + new Vector2(1, hpIndHeight - 1 - hpHeight), // position - new Vector2(hpIndWidth - 2, hpHeight)); // size - looseView.DrawRect(hpContentsRect, getHPColor(hpFraction)); - if (unit.isFortified) - looseView.DrawRect(hpIndBackgroundRect, white, false); - } - - // Draw lines to show that there are more units on this tile - if (tile.unitsOnTile.Count > 1) { - int lineCount = tile.unitsOnTile.Count; - if (lineCount > 5) - lineCount = 5; - for (int n = 0; n < lineCount; n++) { - var lineStart = indicatorLoc + new Vector2(-2, hpIndHeight + 12 + 4 * n); - looseView.DrawLine(lineStart, lineStart + new Vector2(8, 0), white); - looseView.DrawLine(lineStart + new Vector2(0, 1), lineStart + new Vector2(8, 1), Color.Color8(75, 75, 75)); - } - } - } -} diff --git a/C7/Map/UnitSprites.cs b/C7/Map/UnitSprites.cs new file mode 100644 index 00000000..247b421b --- /dev/null +++ b/C7/Map/UnitSprites.cs @@ -0,0 +1,198 @@ +using System; +using C7GameData; +using ConvertCiv3Media; +using Godot; +using C7.Map; + +// UnitSprite represents an animated unit. It's specific to a unit, action, and direction. +// UnitSprite comprises two sprites: a base sprite and a civ color-tinted sprite. The +// shading is done in the UnitTint.gdshader shader. +// TODO: once https://github.com/godotengine/godot/issues/62943 is solved, UnitSprite should +// use a single instance of a material and UnitSprite use a per instance uniform +public partial class UnitSprite : Node2D { + + private readonly int unitAnimZIndex = MapZIndex.Units; + private readonly string unitShaderPath = "res://UnitTint.gdshader"; + private readonly string unitColorShaderParameter = "tintColor"; + private Shader unitShader; + + public AnimatedSprite2D sprite; + public AnimatedSprite2D spriteTint; + public ShaderMaterial material; + + public int GetNextFrameByProgress(string animation, float progress) { + if (!sprite.SpriteFrames.HasAnimation(animation)) { + throw new ArgumentException($"no such animation: {animation}"); + } + int frameCount = sprite.SpriteFrames.GetFrameCount(animation); + int nextFrame = (int)((float)frameCount * progress); + return Mathf.Clamp(nextFrame, 0, frameCount - 1); + } + + public void SetFrame(int frame) { + sprite.Frame = frame; + spriteTint.Frame = frame; + } + + public void SetAnimation(string name) { + sprite.Animation = name; + spriteTint.Animation = name; + } + + public void Play(string name) { + sprite.Play(name); + spriteTint.Play(name); + } + + public void SetColor(Color color) { + material.SetShaderParameter(unitColorShaderParameter, new Vector3(color.R, color.G, color.B)); + } + + public Vector2 FrameSize(string animation) { + return sprite.SpriteFrames.GetFrameTexture(animation, 0).GetSize(); + } + + public UnitSprite(AnimationManager manager) { + ZIndex = unitAnimZIndex; + sprite = new AnimatedSprite2D{ + SpriteFrames = manager.spriteFrames, + }; + spriteTint = new AnimatedSprite2D{ + SpriteFrames= manager.tintFrames, + }; + + material = new ShaderMaterial(); + unitShader = GD.Load(unitShaderPath); + material.Shader = unitShader; + spriteTint.Material = material; + + AddChild(sprite); + AddChild(spriteTint); + } +} + +public partial class CursorSprite : Node2D { + private readonly string animationPath = "Art/Animations/Cursor/Cursor.flc"; + private readonly string animationName = "cursor"; + private readonly double period = 2.5; + private readonly int cursorAnimZIndex = MapZIndex.Cursor; + private AnimatedSprite2D sprite; + private int frameCount; + + public CursorSprite() { + ZIndex = cursorAnimZIndex; + SpriteFrames frames = new SpriteFrames(); + AnimationManager.loadCursorAnimation(animationPath, animationName, ref frames); + sprite = new AnimatedSprite2D{ + SpriteFrames = frames, + Animation = animationName, + }; + frameCount = sprite.SpriteFrames.GetFrameCount(animationName); + AddChild(sprite); + } + + public override void _Process(double delta) { + double repCount = (double)Time.GetTicksMsec() / 1000.0 / period; + float progress = (float)(repCount - Math.Floor(repCount)); + int nextFrame = (int)((float)frameCount * progress); + nextFrame = Mathf.Clamp(nextFrame, 0, frameCount - 1); + sprite.Frame = nextFrame; + base._Process(delta); + } +} + +public partial class UnitLayer { + private ImageTexture unitIcons; + private int unitIconsWidth; + private ImageTexture unitMovementIndicators; + + public UnitLayer() { + var iconPCX = new Pcx(Util.Civ3MediaPath("Art/Units/units_32.pcx")); + unitIcons = PCXToGodot.getImageTextureFromPCX(iconPCX); + unitIconsWidth = (unitIcons.GetWidth() - 1) / 33; + + var moveIndPCX = new Pcx(Util.Civ3MediaPath("Art/interface/MovementLED.pcx")); + unitMovementIndicators = PCXToGodot.getImageTextureFromPCX(moveIndPCX); + } + + public Color getHPColor(float fractionRemaining) { + if (fractionRemaining >= 0.67f) { + return Color.Color8(0, 255, 0); + } else if (fractionRemaining >= 0.34f) { + return Color.Color8(255, 255, 0); + } else { + return Color.Color8(255, 0, 0); + } + } + + public void drawEffectAnimFrame(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; + } + + public void drawObject(GameData gameData, Tile tile, Vector2 tileCenter) { + // First draw animated effects. These will always appear over top of units regardless of draw order due to z-index. + // C7Animation tileEffect = looseView.mapView.game.animTracker.getTileEffect(tile); + // if (tileEffect != null) { + // (_, float progress) = looseView.mapView.game.animTracker.getCurrentActionAndProgress(tile); + // drawEffectAnimFrame(looseView, tileEffect, progress, tileCenter); + // } + + if (tile.unitsOnTile.Count == 0) { + return; + } + + var white = Color.Color8(255, 255, 255); + + // MapUnit unit = selectUnitToDisplay(looseView, tile.unitsOnTile); + // MapUnit.Appearance appearance = looseView.mapView.game.animTracker.getUnitAppearance(unit); + // Vector2 animOffset = new Vector2(appearance.offsetX, appearance.offsetY) * OldMapView.cellSize; + + // // If the unit we're about to draw is currently selected, draw the cursor first underneath it + // if ((unit != MapUnit.NONE) && (unit == looseView.mapView.game.CurrentlySelectedUnit)) { + // drawCursor(looseView, tileCenter + animOffset); + // } + + // drawUnitAnimFrame(looseView, unit, appearance, tileCenter); + + // Vector2 indicatorLoc = tileCenter - new Vector2(26, 40) + animOffset; + + // int moveIndIndex = (!unit.movementPoints.canMove) ? 4 : ((unit.movementPoints.remaining >= unit.unitType.movement) ? 0 : 2); + // Vector2 moveIndUpperLeft = new Vector2(1 + 7 * moveIndIndex, 1); + // Rect2 moveIndRect = new Rect2(moveIndUpperLeft, new Vector2(6, 6)); + // var screenRect = new Rect2(indicatorLoc, new Vector2(6, 6)); + // looseView.DrawTextureRectRegion(unitMovementIndicators, screenRect, moveIndRect); + + // int hpIndHeight = 6 * (unit.maxHitPoints <= 5 ? unit.maxHitPoints : 5), hpIndWidth = 6; + // Rect2 hpIndBackgroundRect = new Rect2(indicatorLoc + new Vector2(-1, 8), new Vector2(hpIndWidth, hpIndHeight)); + // if ((unit.unitType.attack > 0) || (unit.unitType.defense > 0)) { + // float hpFraction = (float)unit.hitPointsRemaining / unit.maxHitPoints; + // looseView.DrawRect(hpIndBackgroundRect, Color.Color8(0, 0, 0)); + // float hpHeight = hpFraction * (hpIndHeight - 2); + // if (hpHeight < 1) + // hpHeight = 1; + // var hpContentsRect = new Rect2(hpIndBackgroundRect.Position + new Vector2(1, hpIndHeight - 1 - hpHeight), // position + // new Vector2(hpIndWidth - 2, hpHeight)); // size + // looseView.DrawRect(hpContentsRect, getHPColor(hpFraction)); + // if (unit.isFortified) + // looseView.DrawRect(hpIndBackgroundRect, white, false); + // } + + // // Draw lines to show that there are more units on this tile + // if (tile.unitsOnTile.Count > 1) { + // int lineCount = tile.unitsOnTile.Count; + // if (lineCount > 5) + // lineCount = 5; + // for (int n = 0; n < lineCount; n++) { + // var lineStart = indicatorLoc + new Vector2(-2, hpIndHeight + 12 + 4 * n); + // looseView.DrawLine(lineStart, lineStart + new Vector2(8, 0), white); + // looseView.DrawLine(lineStart + new Vector2(0, 1), lineStart + new Vector2(8, 1), Color.Color8(75, 75, 75)); + // } + // } + } +} diff --git a/C7/Map/notes.md b/C7/Map/notes.md new file mode 100644 index 00000000..09422529 --- /dev/null +++ b/C7/Map/notes.md @@ -0,0 +1,14 @@ +# state +currently the new MapView is stateful, for example when a city is built: + +build city in ui -> msg to engine -> city added to GameData -> msg to ui -> Game calls MapView.addCity + +This is hard to manage because Game must make the correct calls into MapView to ensure MapView is in sync with GameData. This pattern of updating MapView is also wherein lies the performance boost, for example when a road is built: MapView calculates what road texture to use, and it is added to TileMap. This is only done once when the road is built, then Godot manages drawing the TileMap contents instead of doing it every frame in c#. + +I think the current city approach will become too complex as more elements must be rendered, and will introduce bugs where MapView does not reflect GameData. Potential compromises: + +1. When GameData changes (the game map changes), recalculate everything in MapView - expensive, but not every frame. This may be worse than the non-TileMap implementation because updating TileMap might not be cheap... +2. When GameData changes, determine all tiles that could possibly have changed appearance (ie. creating or destroying a road changes the appearance of that tile and 0 or more of its neighbors visually, so recalculate everything for those 9 tiles) - this is roughly speaking what updateTile currently does for all things rendered in the TileMap (currently everything except for cities, units, and terrain which has its own TileMap instance). + +# wrapping +the easiest way to implement wrapping is to have 2 copies of the tilemap and translate the second copy to wherever the camera is "hanging" over the edge of the main tilemap. Depending on how expensive this is (mainly in memory consumption? need to profile) a better approach may be to split the TileMap in half (left/right for horizontally wrapping maps, top/bottom for vertically) or in four (corners for maps that wrap both ways) and move them around to cover the entire screen. neither approach accounts for the edge case where the camera is zoomed out far enough / display large enough that tiles are visible multiple times on screen. diff --git a/C7/MapView.cs b/C7/MapView.cs deleted file mode 100644 index 2202fb6b..00000000 --- a/C7/MapView.cs +++ /dev/null @@ -1,781 +0,0 @@ -using System.Collections.Generic; -using System; -using System.Linq; -using C7.Map; -using Godot; -using ConvertCiv3Media; -using C7GameData; -using C7Engine; -using Serilog; -using Serilog.Events; - -// 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 -// that was intended to draw regularly tiled objects like terrain sprites but using LooseLayers for everything was found to be a prefereable -// approach.) LooseLayer is effectively the standard map layer. The MapView contains a list of loose layers, inside a LooseView object. Right now to -// add a new layer you must modify the MapView constructor to add it to the list, but (TODO) eventually that will be made moddable. -public abstract class LooseLayer { - // drawObject draws the things this layer is supposed to draw that are associated with the given tile. Its parameters are: - // looseView: The Node2D to actually draw to, e.g., use looseView.DrawCircle(...) to draw a circle. This object also contains a reference to - // the MapView in case you need it. - // gameData: A reference to the game data so each layer doesn't have to redundantly request access. - // tile: The game tile whose contents are to be drawn. This function gets called for each tile in view of the camera and none out of - // view. The same tile may be drawn multiple times at different locations due to edge wrapping. - // tileCenter: The location to draw to. You should draw around this location without adjusting for the camera location or zoom since the - // MapView already transforms the looseView node to account for those things. - public abstract void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter); - - public virtual void onBeginDraw(LooseView looseView, GameData gameData) {} - public virtual void onEndDraw(LooseView looseView, GameData gameData) {} - - // The layer will be skipped during map drawing if visible is false - public bool visible = true; -} - -public partial class TerrainLayer : LooseLayer { - - public static readonly Vector2 terrainSpriteSize = new Vector2(128, 64); - - // A triple sheet is a sprite sheet containing sprites for three different terrain types including transitions between. - private List tripleSheets; - - // TileToDraw stores the arguments passed to drawObject so the draws can be sorted by texture before being submitted. This significantly - // reduces the number of draw calls Godot must generate (1483 to 312 when fully zoomed out on our test map) and modestly improves framerate - // (by about 14% on my system). - private class TileToDraw : IComparable - { - public Tile tile; - public Vector2 tileCenter; - - public TileToDraw(Tile tile, Vector2 tileCenter) - { - this.tile = tile; - this.tileCenter = tileCenter; - } - - public int CompareTo(TileToDraw other) - { - // "other" might be null, in which case we should return a positive value. CompareTo(null) will do this. - try { - return this.tile.ExtraInfo.BaseTerrainFileID.CompareTo(other?.tile.ExtraInfo.BaseTerrainFileID); - } catch (Exception) { - //It also could be Tile.NONE. In which case, also return a positive value. - return 1; - } - } - } - - private List tilesToDraw = new List(); - - public TerrainLayer() - { - tripleSheets = loadTerrainTripleSheets(); - } - - public List loadTerrainTripleSheets() - { - List fileNames = new List { - "Art/Terrain/xtgc.pcx", - "Art/Terrain/xpgc.pcx", - "Art/Terrain/xdgc.pcx", - "Art/Terrain/xdpc.pcx", - "Art/Terrain/xdgp.pcx", - "Art/Terrain/xggc.pcx", - "Art/Terrain/wCSO.pcx", - "Art/Terrain/wSSS.pcx", - "Art/Terrain/wOOO.pcx", - }; - return fileNames.ConvertAll(name => Util.LoadTextureFromPCX(name)); - } - - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) - { - tilesToDraw.Add(new TileToDraw(tile, tileCenter)); - tilesToDraw.Add(new TileToDraw(tile.neighbors[TileDirection.SOUTH], tileCenter + new Vector2(0, 64))); - tilesToDraw.Add(new TileToDraw(tile.neighbors[TileDirection.SOUTHWEST], tileCenter + new Vector2(-64, 32))); - tilesToDraw.Add(new TileToDraw(tile.neighbors[TileDirection.SOUTHEAST], tileCenter + new Vector2(64, 32))); - } - - public override void onEndDraw(LooseView looseView, GameData gameData) { - tilesToDraw.Sort(); - foreach (TileToDraw tTD in tilesToDraw) { - if (tTD.tile != Tile.NONE) { - int xSheet = tTD.tile.ExtraInfo.BaseTerrainImageID % 9, ySheet = tTD.tile.ExtraInfo.BaseTerrainImageID / 9; - Rect2 texRect = new Rect2(new Vector2(xSheet, ySheet) * terrainSpriteSize, terrainSpriteSize); - Vector2 terrainOffset = new Vector2(0, -1 * MapView.cellSize.Y); - // Multiply size by 100.1% so avoid "seams" in the map. See issue #106. - // Jim's option of a whole-map texture is less hacky, but this is quicker and seems to be working well. - Rect2 screenRect = new Rect2(tTD.tileCenter - (float)0.5 * terrainSpriteSize + terrainOffset, terrainSpriteSize * 1.001f); - looseView.DrawTextureRectRegion(tripleSheets[tTD.tile.ExtraInfo.BaseTerrainFileID], screenRect, texRect); - } - } - tilesToDraw.Clear(); - } -} - -public partial class HillsLayer : LooseLayer { - public static readonly Vector2 mountainSize = new Vector2(128, 88); - public static readonly Vector2 volcanoSize = new Vector2(128, 88); //same as mountain - public static readonly Vector2 hillsSize = new Vector2(128, 72); - private ImageTexture mountainTexture; - private ImageTexture snowMountainTexture; - private ImageTexture forestMountainTexture; - private ImageTexture jungleMountainTexture; - private ImageTexture hillsTexture; - private ImageTexture forestHillsTexture; - private ImageTexture jungleHillsTexture; - private ImageTexture volcanosTexture; - private ImageTexture forestVolcanoTexture; - private ImageTexture jungleVolcanoTexture; - - public HillsLayer() { - mountainTexture = Util.LoadTextureFromPCX("Art/Terrain/Mountains.pcx"); - snowMountainTexture = Util.LoadTextureFromPCX("Art/Terrain/Mountains-snow.pcx"); - forestMountainTexture = Util.LoadTextureFromPCX("Art/Terrain/mountain forests.pcx"); - jungleMountainTexture = Util.LoadTextureFromPCX("Art/Terrain/mountain jungles.pcx"); - hillsTexture = Util.LoadTextureFromPCX("Art/Terrain/xhills.pcx"); - forestHillsTexture = Util.LoadTextureFromPCX("Art/Terrain/hill forests.pcx"); - jungleHillsTexture = Util.LoadTextureFromPCX("Art/Terrain/hill jungle.pcx"); - volcanosTexture = Util.LoadTextureFromPCX("Art/Terrain/Volcanos.pcx"); - forestVolcanoTexture = Util.LoadTextureFromPCX("Art/Terrain/Volcanos forests.pcx"); - jungleVolcanoTexture = Util.LoadTextureFromPCX("Art/Terrain/Volcanos jungles.pcx"); - } - - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) - { - if (tile.overlayTerrainType.isHilly()) { - int pcxIndex = getMountainIndex(tile); - int row = pcxIndex/4; - int column = pcxIndex % 4; - if (tile.overlayTerrainType.Key == "mountains") { - Rect2 mountainRectangle = new Rect2(column * mountainSize.X, row * mountainSize.Y, mountainSize); - Rect2 screenTarget = new Rect2(tileCenter - (float)0.5 * mountainSize + new Vector2(0, -12), mountainSize); - ImageTexture mountainGraphics; - if (tile.isSnowCapped) { - mountainGraphics = snowMountainTexture; - } - else { - TerrainType dominantVegetation = getDominantVegetationNearHillyTile(tile); - if (dominantVegetation.Key == "forest") { - mountainGraphics = forestMountainTexture; - } - else if (dominantVegetation.Key == "jungle") { - mountainGraphics = jungleMountainTexture; - } - else { - mountainGraphics = mountainTexture; - } - } - looseView.DrawTextureRectRegion(mountainGraphics, screenTarget, mountainRectangle); - } - else if (tile.overlayTerrainType.Key == "hills") { - Rect2 hillsRectangle = new Rect2(column * hillsSize.X, row * hillsSize.Y, hillsSize); - Rect2 screenTarget = new Rect2(tileCenter - (float)0.5 * hillsSize + new Vector2(0, -4), hillsSize); - ImageTexture hillGraphics; - TerrainType dominantVegetation = getDominantVegetationNearHillyTile(tile); - if (dominantVegetation.Key == "forest") { - hillGraphics = forestHillsTexture; - } - else if (dominantVegetation.Key == "jungle") { - hillGraphics = jungleHillsTexture; - } - else { - hillGraphics = hillsTexture; - } - looseView.DrawTextureRectRegion(hillGraphics, screenTarget, hillsRectangle); - } - else if (tile.overlayTerrainType.Key == "volcano") { - Rect2 volcanoRectangle = new Rect2(column * volcanoSize.X, row * volcanoSize.Y, volcanoSize); - Rect2 screenTarget = new Rect2(tileCenter - (float)0.5 * volcanoSize + new Vector2(0, -12), volcanoSize); - ImageTexture volcanoGraphics; - TerrainType dominantVegetation = getDominantVegetationNearHillyTile(tile); - if (dominantVegetation.Key == "forest") { - volcanoGraphics = forestVolcanoTexture; - } - else if (dominantVegetation.Key == "jungle") { - volcanoGraphics = jungleVolcanoTexture; - } - else { - volcanoGraphics = volcanosTexture; - } - looseView.DrawTextureRectRegion(volcanoGraphics, screenTarget, volcanoRectangle); - } - } - } - - private TerrainType getDominantVegetationNearHillyTile(Tile center) - { - TerrainType northeastType = center.neighbors[TileDirection.NORTHEAST].overlayTerrainType; - TerrainType northwestType = center.neighbors[TileDirection.NORTHWEST].overlayTerrainType; - TerrainType southeastType = center.neighbors[TileDirection.SOUTHEAST].overlayTerrainType; - TerrainType southwestType = center.neighbors[TileDirection.SOUTHWEST].overlayTerrainType; - - TerrainType[] neighborTerrains = { northeastType, northwestType, southeastType, southwestType }; - - int hills = 0; - int forests = 0; - int jungles = 0; - //These references are so we can return the appropriate type, and because we don't have a good way - //to grab them directly at this point in time. - TerrainType forest = null; - TerrainType jungle = null; - foreach (TerrainType type in neighborTerrains) { - if (type.isHilly()) { - hills++; - } - else if (type.Key == "forest") { - forests++; - forest = type; - } - else if (type.Key == "jungle") { - jungles++; - jungle = type; - } - } - - if (hills + forests + jungles < 4) { //some surrounding tiles are neither forested nor hilly - return TerrainType.NONE; - } - if (forests == 0 && jungles == 0) { - return TerrainType.NONE; //all hills - } - if (forests > jungles) { - return forest; - } - if (jungles > forests) { - return jungle; - } - - //If we get here, it's a tie between forest and jungle. Deterministically choose one so it doesn't change on every render - if (center.xCoordinate % 2 == 0) { - return forest; - } - return jungle; - } - - private int getMountainIndex(Tile tile) { - int index = 0; - if (tile.neighbors[TileDirection.NORTHWEST].overlayTerrainType.isHilly()) { - index++; - } - if (tile.neighbors[TileDirection.NORTHEAST].overlayTerrainType.isHilly()) { - index+=2; - } - if (tile.neighbors[TileDirection.SOUTHWEST].overlayTerrainType.isHilly()) { - index+=4; - } - if (tile.neighbors[TileDirection.SOUTHEAST].overlayTerrainType.isHilly()) { - index+=8; - } - return index; - } -} - -public partial class ForestLayer : LooseLayer { - public static readonly Vector2 forestJungleSize = new Vector2(128, 88); - - private ImageTexture largeJungleTexture; - private ImageTexture smallJungleTexture; - private ImageTexture largeForestTexture; - private ImageTexture largePlainsForestTexture; - private ImageTexture largeTundraForestTexture; - private ImageTexture smallForestTexture; - private ImageTexture smallPlainsForestTexture; - private ImageTexture smallTundraForestTexture; - private ImageTexture pineForestTexture; - private ImageTexture pinePlainsTexture; - private ImageTexture pineTundraTexture; - - public ForestLayer() { - largeJungleTexture = Util.LoadTextureFromPCX("Art/Terrain/grassland forests.pcx", 0, 0, 512, 176); - smallJungleTexture = Util.LoadTextureFromPCX("Art/Terrain/grassland forests.pcx", 0, 176, 768, 176); - largeForestTexture = Util.LoadTextureFromPCX("Art/Terrain/grassland forests.pcx", 0, 352, 512, 176); - largePlainsForestTexture = Util.LoadTextureFromPCX("Art/Terrain/plains forests.pcx", 0, 352, 512, 176); - largeTundraForestTexture = Util.LoadTextureFromPCX("Art/Terrain/tundra forests.pcx", 0, 352, 512, 176); - smallForestTexture = Util.LoadTextureFromPCX("Art/Terrain/grassland forests.pcx", 0, 528, 640, 176); - smallPlainsForestTexture = Util.LoadTextureFromPCX("Art/Terrain/plains forests.pcx", 0, 528, 640, 176); - smallTundraForestTexture = Util.LoadTextureFromPCX("Art/Terrain/tundra forests.pcx", 0, 528, 640, 176); - pineForestTexture = Util.LoadTextureFromPCX("Art/Terrain/grassland forests.pcx", 0, 704, 768, 176); - pinePlainsTexture = Util.LoadTextureFromPCX("Art/Terrain/plains forests.pcx" , 0, 704, 768, 176); - pineTundraTexture = Util.LoadTextureFromPCX("Art/Terrain/tundra forests.pcx" , 0, 704, 768, 176); - } - - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) { - if (tile.overlayTerrainType.Key == "jungle") { - //Randomly, but predictably, choose a large jungle graphic - //More research is needed on when to use large vs small jungles. Probably, small is used when neighboring fewer jungles. - //For the first pass, we're just always using large jungles. - int randomJungleRow = tile.yCoordinate % 2; - int randomJungleColumn; - ImageTexture jungleTexture; - if (tile.getEdgeNeighbors().Any(t => t.IsWater())) { - randomJungleColumn = tile.xCoordinate % 6; - jungleTexture = smallJungleTexture; - } - else { - randomJungleColumn = tile.xCoordinate % 4; - jungleTexture = largeJungleTexture; - } - Rect2 jungleRectangle = new Rect2(randomJungleColumn * forestJungleSize.X, randomJungleRow * forestJungleSize.Y, forestJungleSize); - Rect2 screenTarget = new Rect2(tileCenter - (float)0.5 * forestJungleSize + new Vector2(0, -12), forestJungleSize); - looseView.DrawTextureRectRegion(jungleTexture, screenTarget, jungleRectangle); - } - if (tile.overlayTerrainType.Key == "forest") { - int forestRow = 0; - int forestColumn = 0; - ImageTexture forestTexture; - if (tile.isPineForest) { - forestRow = tile.yCoordinate % 2; - forestColumn = tile.xCoordinate % 6; - if (tile.baseTerrainType.Key == "grassland") { - forestTexture = pineForestTexture; - } - else if (tile.baseTerrainType.Key == "plains") { - forestTexture = pinePlainsTexture; - } - else { //Tundra - forestTexture = pineTundraTexture; - } - } - else { - forestRow = tile.yCoordinate % 2; - if (tile.getEdgeNeighbors().Any(t => t.IsWater())) { - forestColumn = tile.xCoordinate % 5; - if (tile.baseTerrainType.Key == "grassland") { - forestTexture = smallForestTexture; - } - else if (tile.baseTerrainType.Key == "plains") { - forestTexture = smallPlainsForestTexture; - } - else { //tundra - forestTexture = smallTundraForestTexture; - } - } - else { - forestColumn = tile.xCoordinate % 4; - if (tile.baseTerrainType.Key == "grassland") { - forestTexture = largeForestTexture; - } - else if (tile.baseTerrainType.Key == "plains") { - forestTexture = largePlainsForestTexture; - } - else { //tundra - forestTexture = largeTundraForestTexture; - } - } - } - Rect2 forestRectangle = new Rect2(forestColumn * forestJungleSize.X, forestRow * forestJungleSize.Y, forestJungleSize); - Rect2 screenTarget = new Rect2(tileCenter - (float)0.5 * forestJungleSize + new Vector2(0, -12), forestJungleSize); - looseView.DrawTextureRectRegion(forestTexture, screenTarget, forestRectangle); - } - } -} -public partial class MarshLayer : LooseLayer { - public static readonly Vector2 marshSize = new Vector2(128, 88); - //Because the marsh graphics are 88 pixels tall instead of the 64 of a tile, we also need an addition 12 pixel offset to the top - //88 - 64 = 24; 24/2 = 12. This keeps the marsh centered with half the extra 24 pixels above the tile and half below. - readonly Vector2 MARSH_OFFSET = (float)0.5 * marshSize + new Vector2(0, -12); - - private ImageTexture largeMarshTexture; - private ImageTexture smallMarshTexture; - - public MarshLayer() { - largeMarshTexture = Util.LoadTextureFromPCX("Art/Terrain/marsh.pcx", 0, 0, 512, 176); - smallMarshTexture = Util.LoadTextureFromPCX("Art/Terrain/marsh.pcx", 0, 176, 640, 176); - } - - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) { - if (tile.overlayTerrainType.Key == "marsh") { - int randomJungleRow = tile.yCoordinate % 2; - int randomMarshColumn; - ImageTexture marshTexture; - if (tile.getEdgeNeighbors().Any(t => t.IsWater())) { - randomMarshColumn = tile.xCoordinate % 5; - marshTexture = smallMarshTexture; - } - else { - randomMarshColumn = tile.xCoordinate % 4; - marshTexture = largeMarshTexture; - } - Rect2 jungleRectangle = new Rect2(randomMarshColumn * marshSize.X, randomJungleRow * marshSize.Y, marshSize); - Rect2 screenTarget = new Rect2(tileCenter - MARSH_OFFSET, marshSize); - looseView.DrawTextureRectRegion(marshTexture, screenTarget, jungleRectangle); - } - } -} - -public partial class RiverLayer : LooseLayer -{ - public static readonly Vector2 riverSize = new Vector2(128, 64); - public static readonly Vector2 riverCenterOffset = new Vector2(riverSize.X / 2, 0); - private ImageTexture riverTexture; - - public RiverLayer() { - riverTexture = Util.LoadTextureFromPCX("Art/Terrain/mtnRivers.pcx"); - } - - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) - { - //The "point" is the easternmost point of the tile for which we are drawing rivers. - //Which river graphics to used is calculated by evaluating the tiles that neighbor - //that point. - Tile northOfPoint = tile.neighbors[TileDirection.NORTHEAST]; - Tile eastOfPoint = tile.neighbors[TileDirection.EAST]; - Tile westOfPoint = tile; - Tile southOfPoint = tile.neighbors[TileDirection.SOUTHEAST]; - - int riverGraphicsIndex = 0; - - if (northOfPoint.riverSouthwest) { - riverGraphicsIndex++; - } - if (eastOfPoint.riverNorthwest) { - riverGraphicsIndex+=2; - } - if (westOfPoint.riverSoutheast) { - riverGraphicsIndex+=4; - } - if (southOfPoint.riverNortheast) { - riverGraphicsIndex+=8; - } - if (riverGraphicsIndex == 0) { - return; - } - int riverRow = riverGraphicsIndex / 4; - int riverColumn = riverGraphicsIndex % 4; - - Rect2 riverRectangle = new Rect2(riverColumn * riverSize.X, riverRow * riverSize.Y, riverSize); - Rect2 screenTarget = new Rect2(tileCenter - (float)0.5 * riverSize + riverCenterOffset, riverSize); - looseView.DrawTextureRectRegion(riverTexture, screenTarget, riverRectangle); - } -} - -public partial class GridLayer : LooseLayer { - public Color color = Color.Color8(50, 50, 50, 150); - public float lineWidth = (float)1.0; - - public GridLayer() {} - - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) - { - Vector2 cS = MapView.cellSize; - Vector2 left = tileCenter + new Vector2(-cS.X, 0 ); - Vector2 top = tileCenter + new Vector2( 0 , -cS.Y); - Vector2 right = tileCenter + new Vector2( cS.X, 0 ); - looseView.DrawLine(left, top , color, lineWidth); - looseView.DrawLine(top , right, color, lineWidth); - } -} - -public partial class BuildingLayer : LooseLayer { - private ImageTexture buildingsTex; - private Vector2 buildingSpriteSize; - - public BuildingLayer() - { - var buildingsPCX = new Pcx(Util.Civ3MediaPath("Art/Terrain/TerrainBuildings.PCX")); - buildingsTex = PCXToGodot.getImageTextureFromPCX(buildingsPCX); - //In Conquests, this graphic is 4x4, and the search path will now find the Conquests one first - buildingSpriteSize = new Vector2((float)buildingsTex.GetWidth() / 4, (float)buildingsTex.GetHeight() / 4); - } - - public override void drawObject(LooseView looseView, GameData gameData, Tile tile, Vector2 tileCenter) - { - if (tile.hasBarbarianCamp) { - var texRect = new Rect2(buildingSpriteSize * new Vector2 (2, 0), buildingSpriteSize); //(2, 0) is the offset in the TerrainBuildings.PCX file (top row, third in) - // TODO: Modify this calculation so it doesn't assume buildingSpriteSize is the same as the size of the terrain tiles - var screenRect = new Rect2(tileCenter - (float)0.5 * buildingSpriteSize, buildingSpriteSize); - looseView.DrawTextureRectRegion(buildingsTex, screenRect, texRect); - } - } -} - -public partial class LooseView : Node2D { - public MapView mapView; - public List layers = new List(); - - public LooseView(MapView mapView) - { - this.mapView = mapView; - } - - private struct VisibleTile - { - public Tile tile; - public Vector2 tileCenter; - } - - public override void _Draw() - { - base._Draw(); - - using (var gameDataAccess = new UIGameDataAccess()) { - GameData gD = gameDataAccess.gameData; - - // Iterating over visible tiles is unfortunately pretty expensive. Assemble a list of Tile references and centers first so we don't - // have to reiterate for each layer. Doing this improves framerate significantly. - MapView.VisibleRegion visRegion = mapView.getVisibleRegion(); - List visibleTiles = new List(); - for (int y = visRegion.upperLeftY; y < visRegion.lowerRightY; y++) { - if (gD.map.isRowAt(y)) { - for (int x = visRegion.getRowStartX(y); x < visRegion.lowerRightX; x += 2) { - Tile tile = gD.map.tileAt(x, y); - if (IsTileKnown(tile, gameDataAccess)) { - visibleTiles.Add(new VisibleTile { tile = tile, tileCenter = MapView.cellSize * new Vector2(x + 1, y + 1) }); - } - } - } - } - - foreach (LooseLayer layer in layers.FindAll(L => L.visible && !(L is FogOfWarLayer))) { - layer.onBeginDraw(this, gD); - foreach (VisibleTile vT in visibleTiles) { - layer.drawObject(this, gD, vT.tile, vT.tileCenter); - } - layer.onEndDraw(this, gD); - } - - if (!gD.observerMode) { - foreach (LooseLayer layer in layers.FindAll(layer => layer is FogOfWarLayer)) { - for (int y = visRegion.upperLeftY; y < visRegion.lowerRightY; y++) - if (gD.map.isRowAt(y)) - for (int x = visRegion.getRowStartX(y); x < visRegion.lowerRightX; x += 2) { - Tile tile = gD.map.tileAt(x, y); - if (tile != Tile.NONE) { - VisibleTile invisibleTile = new VisibleTile { tile = tile, tileCenter = MapView.cellSize * new Vector2(x + 1, y + 1) }; - layer.drawObject(this, gD, tile, invisibleTile.tileCenter); - } - } - } - } - } - } - private static bool IsTileKnown(Tile tile, UIGameDataAccess gameDataAccess) { - if (gameDataAccess.gameData.observerMode) { - return true; - } - return tile != Tile.NONE && gameDataAccess.gameData.GetHumanPlayers()[0].tileKnowledge.isTileKnown(tile); - } -} - -public partial class MapView : Node2D { - // cellSize is half the size of the tile sprites, or the amount of space each tile takes up when they are packed on the grid (note tiles are - // staggered and half overlap). - public static readonly Vector2 cellSize = new Vector2(64, 32); - public Vector2 scaledCellSize { - get { return cellSize * new Vector2(cameraZoom, cameraZoom); } - } - - public Game game; - - public int mapWidth { get; private set; } - public int mapHeight { get; private set; } - public bool wrapHorizontally { get; private set; } - public bool wrapVertically { get; private set; } - - private Vector2 internalCameraLocation = new Vector2(0, 0); - public Vector2 cameraLocation { - get { - return internalCameraLocation; - } - set { - setCameraLocation(value); - } - } - public float internalCameraZoom = 1; - public float cameraZoom { - get { return internalCameraZoom; } - set { setCameraZoomFromMiddle(value); } - } - - private LooseView looseView; - - // Specifies a rectangular block of tiles that are currently potentially on screen. Accessible through getVisibleRegion(). Tile coordinates - // are "virtual", i.e. "unwrapped", so there isn't necessarily a tile at each location. The region is intended to include the upper left - // coordinates but not the lower right ones. When iterating over all tiles in the region you must account for the fact that map rows are - // staggered, see LooseView._Draw for an example. - public struct VisibleRegion { - public int upperLeftX, upperLeftY; - public int lowerRightX, lowerRightY; - - public int getRowStartX(int y) - { - return upperLeftX + (y - upperLeftY)%2; - } - } - - public GridLayer gridLayer { get; private set; } - - public ImageTexture civColorWhitePalette = null; - - public MapView(Game game, int mapWidth, int mapHeight, bool wrapHorizontally, bool wrapVertically) - { - this.game = game; - this.mapWidth = mapWidth; - this.mapHeight = mapHeight; - this.wrapHorizontally = wrapHorizontally; - this.wrapVertically = wrapVertically; - - looseView = new LooseView(this); - looseView.layers.Add(new TerrainLayer()); - looseView.layers.Add(new RiverLayer()); - looseView.layers.Add(new ForestLayer()); - looseView.layers.Add(new MarshLayer()); - looseView.layers.Add(new HillsLayer()); - looseView.layers.Add(new TntLayer()); - looseView.layers.Add(new RoadLayer()); - looseView.layers.Add(new ResourceLayer()); - this.gridLayer = new GridLayer(); - looseView.layers.Add(this.gridLayer); - looseView.layers.Add(new BuildingLayer()); - looseView.layers.Add(new UnitLayer()); - looseView.layers.Add(new CityLayer()); - looseView.layers.Add(new FogOfWarLayer()); - - (civColorWhitePalette, _) = Util.loadPalettizedPCX("Art/Units/Palettes/ntp00.pcx"); - - AddChild(looseView); - } - - public override void _Process(double delta) - { - // Redraw everything. This is necessary so that animations play. Maybe we could only update the unit layer but long term I think it's - // better to redraw everything every frame like a typical modern video game. - looseView.QueueRedraw(); - } - - // Returns the size in pixels of the area in which the map will be drawn. This is the viewport size or, if that's null, the window size. - public Vector2 getVisibleAreaSize() - { - return GetViewport() != null ? GetViewportRect().Size : DisplayServer.WindowGetSize(); - } - - public VisibleRegion getVisibleRegion() - { - (int x0, int y0) = tileCoordsOnScreenAt(new Vector2(0, 0)); - Vector2 mapViewSize = new Vector2(2, 4) + getVisibleAreaSize() / scaledCellSize; - return new VisibleRegion { upperLeftX = x0 - 2, upperLeftY = y0 - 2, - lowerRightX = x0 + (int)mapViewSize.X, lowerRightY = y0 + (int)mapViewSize.Y }; - } - - // "center" is the screen location around which the zoom is centered, e.g., if center is (0, 0) the tile in the top left corner will be the - // same after the zoom level is changed, and if center is screenSize/2, the tile in the center of the window won't change. - public void setCameraZoom(float newScale, Vector2 center) - { - Vector2 v2NewZoom = new Vector2(newScale, newScale); - Vector2 v2OldZoom = new Vector2(cameraZoom, cameraZoom); - if (v2NewZoom != v2OldZoom) { - internalCameraZoom = newScale; - looseView.Scale = v2NewZoom; - setCameraLocation ((v2NewZoom / v2OldZoom) * (cameraLocation + center) - center); - } - } - - // Zooms in or out centered on the middle of the screen - public void setCameraZoomFromMiddle(float newScale) - { - setCameraZoom(newScale, getVisibleAreaSize() / 2); - } - - public void moveCamera(Vector2 offset) - { - setCameraLocation(cameraLocation + offset); - } - - public void setCameraLocation(Vector2 location) - { - // Prevent the camera from moving beyond an unwrapped edge of the map. One complication here is that the viewport might actually be - // larger than the map (if we're zoomed far out) so in that case we must apply the constraint the other way around, i.e. constrain the - // map to the viewport rather than the viewport to the map. - Vector2 visAreaSize = getVisibleAreaSize(); - Vector2 mapPixelSize = new Vector2(cameraZoom, cameraZoom) * (new Vector2(cellSize.X * (mapWidth + 1), cellSize.Y * (mapHeight + 1))); - if (!wrapHorizontally) { - float leftLim, rightLim; - { - if (mapPixelSize.X >= visAreaSize.X) { - leftLim = 0; - rightLim = mapPixelSize.X - visAreaSize.X; - } else { - leftLim = mapPixelSize.X - visAreaSize.X; - rightLim = 0; - } - } - if (location.X < leftLim) - location.X = leftLim; - else if (location.X > rightLim) - location.X = rightLim; - } - if (!wrapVertically) { - // These margins allow the player to move the camera that far off those map edges so that the UI controls don't cover up the - // map. TODO: These values should be read from the sizes of the UI elements instead of hardcoded. - float topMargin = 70, bottomMargin = 140; - float topLim, bottomLim; - { - if (mapPixelSize.Y >= visAreaSize.Y) { - topLim = -topMargin; - bottomLim = mapPixelSize.Y - visAreaSize.Y + bottomMargin; - } else { - topLim = mapPixelSize.Y - visAreaSize.Y; - bottomLim = 0; - } - } - if (location.Y < topLim) - location.Y = topLim; - else if (location.Y > bottomLim) - location.Y = bottomLim; - } - - internalCameraLocation = location; - looseView.Position = -location; - } - - public Vector2 screenLocationOfTileCoords(int x, int y, bool center = true) - { - // Add one to x & y to get the tile center b/c in Civ 3 the tile at (x, y) is a diamond centered on (x+1, y+1). - Vector2 centeringOffset = center ? new Vector2(1, 1) : new Vector2(0, 0); - - var mapLoc = (new Vector2(x, y) + centeringOffset) * cellSize; - return mapLoc * cameraZoom - cameraLocation; - } - - // Returns the location of tile (x, y) on the screen, if "center" is true returns the location of the tile center and otherwise returns the - // upper left. Works even if (x, y) is off screen or out of bounds. - public Vector2 screenLocationOfTile(Tile tile, bool center = true) - { - return screenLocationOfTileCoords(tile.xCoordinate, tile.yCoordinate, center); - } - - // Returns the virtual tile coordinates on screen at the given location. "Virtual" meaning the coordinates are unwrapped and there isn't - // necessarily a tile there at all. - public (int, int) tileCoordsOnScreenAt(Vector2 screenLocation) - { - Vector2 mapLoc = (screenLocation + cameraLocation) / scaledCellSize; - Vector2 intMapLoc = mapLoc.Floor(); - Vector2 fracMapLoc = mapLoc - intMapLoc; - int x = (int)intMapLoc.X, y = (int)intMapLoc.Y; - bool evenColumn = x%2 == 0, evenRow = y%2 == 0; - if (evenColumn ^ evenRow) { - if (fracMapLoc.Y > fracMapLoc.X) - x -= 1; - else - y -= 1; - } else { - if (fracMapLoc.Y < 1 - fracMapLoc.X) { - x -= 1; - y -= 1; - } - } - return (x, y); - } - - public Tile tileOnScreenAt(GameMap map, Vector2 screenLocation) - { - (int x, int y) = tileCoordsOnScreenAt(screenLocation); - return map.tileAt(x, y); - } - - public void centerCameraOnTile(Tile t) - { - var tileCenter = new Vector2(t.xCoordinate + 1, t.yCoordinate + 1) * scaledCellSize; - setCameraLocation(tileCenter - (float)0.5 * getVisibleAreaSize()); - } -} diff --git a/C7/OldMapView.cs b/C7/OldMapView.cs new file mode 100644 index 00000000..f6a88424 --- /dev/null +++ b/C7/OldMapView.cs @@ -0,0 +1,194 @@ +using C7.Map; +using Godot; +using C7GameData; + +public partial class GridLayer { + public Color color = Color.Color8(50, 50, 50, 150); + public float lineWidth = (float)1.0; + + public GridLayer() {} + + public void drawObject(Tile tile, Vector2 tileCenter) + { + Vector2 cS = OldMapView.cellSize; + Vector2 left = tileCenter + new Vector2(-cS.X, 0 ); + Vector2 top = tileCenter + new Vector2( 0 , -cS.Y); + Vector2 right = tileCenter + new Vector2( cS.X, 0 ); + // DrawLine(left, top , color, lineWidth); + // DrawLine(top , right, color, lineWidth); + } +} + +public partial class OldMapView : Node2D { + // cellSize is half the size of the tile sprites, or the amount of space each tile takes up when they are packed on the grid (note tiles are + // staggered and half overlap). + public static readonly Vector2 cellSize = new Vector2(64, 32); + public Vector2 scaledCellSize { + get { return cellSize * new Vector2(cameraZoom, cameraZoom); } + } + + public Game game; + + public int mapWidth { get; private set; } + public int mapHeight { get; private set; } + public bool wrapHorizontally { get; private set; } + public bool wrapVertically { get; private set; } + + private Vector2 internalCameraLocation = new Vector2(0, 0); + public Vector2 cameraLocation { + get { + return internalCameraLocation; + } + set { + setCameraLocation(value); + } + } + public float internalCameraZoom = 1; + public float cameraZoom { + get { return internalCameraZoom; } + set { setCameraZoomFromMiddle(value); } + } + + public struct VisibleRegion { + public int upperLeftX, upperLeftY; + public int lowerRightX, lowerRightY; + + public int getRowStartX(int y) + { + return upperLeftX + (y - upperLeftY)%2; + } + } + + public Vector2 getVisibleAreaSize() + { + return GetViewport() != null ? GetViewportRect().Size : DisplayServer.WindowGetSize(); + } + + public VisibleRegion getVisibleRegion() + { + (int x0, int y0) = tileCoordsOnScreenAt(new Vector2(0, 0)); + Vector2 mapViewSize = new Vector2(2, 4) + getVisibleAreaSize() / scaledCellSize; + return new VisibleRegion { upperLeftX = x0 - 2, upperLeftY = y0 - 2, + lowerRightX = x0 + (int)mapViewSize.X, lowerRightY = y0 + (int)mapViewSize.Y }; + } + + // "center" is the screen location around which the zoom is centered, e.g., if center is (0, 0) the tile in the top left corner will be the + // same after the zoom level is changed, and if center is screenSize/2, the tile in the center of the window won't change. + public void setCameraZoom(float newScale, Vector2 center) + { + Vector2 v2NewZoom = new Vector2(newScale, newScale); + Vector2 v2OldZoom = new Vector2(cameraZoom, cameraZoom); + if (v2NewZoom != v2OldZoom) { + internalCameraZoom = newScale; + setCameraLocation ((v2NewZoom / v2OldZoom) * (cameraLocation + center) - center); + } + } + + // Zooms in or out centered on the middle of the screen + public void setCameraZoomFromMiddle(float newScale) + { + setCameraZoom(newScale, getVisibleAreaSize() / 2); + } + + public void moveCamera(Vector2 offset) + { + setCameraLocation(cameraLocation + offset); + } + + public void setCameraLocation(Vector2 location) + { + // Prevent the camera from moving beyond an unwrapped edge of the map. One complication here is that the viewport might actually be + // larger than the map (if we're zoomed far out) so in that case we must apply the constraint the other way around, i.e. constrain the + // map to the viewport rather than the viewport to the map. + Vector2 visAreaSize = getVisibleAreaSize(); + Vector2 mapPixelSize = new Vector2(cameraZoom, cameraZoom) * (new Vector2(cellSize.X * (mapWidth + 1), cellSize.Y * (mapHeight + 1))); + if (!wrapHorizontally) { + float leftLim, rightLim; + { + if (mapPixelSize.X >= visAreaSize.X) { + leftLim = 0; + rightLim = mapPixelSize.X - visAreaSize.X; + } else { + leftLim = mapPixelSize.X - visAreaSize.X; + rightLim = 0; + } + } + if (location.X < leftLim) + location.X = leftLim; + else if (location.X > rightLim) + location.X = rightLim; + } + if (!wrapVertically) { + // These margins allow the player to move the camera that far off those map edges so that the UI controls don't cover up the + // map. TODO: These values should be read from the sizes of the UI elements instead of hardcoded. + float topMargin = 70, bottomMargin = 140; + float topLim, bottomLim; + { + if (mapPixelSize.Y >= visAreaSize.Y) { + topLim = -topMargin; + bottomLim = mapPixelSize.Y - visAreaSize.Y + bottomMargin; + } else { + topLim = mapPixelSize.Y - visAreaSize.Y; + bottomLim = 0; + } + } + if (location.Y < topLim) + location.Y = topLim; + else if (location.Y > bottomLim) + location.Y = bottomLim; + } + + internalCameraLocation = location; + } + + public Vector2 screenLocationOfTileCoords(int x, int y, bool center = true) + { + // Add one to x & y to get the tile center b/c in Civ 3 the tile at (x, y) is a diamond centered on (x+1, y+1). + Vector2 centeringOffset = center ? new Vector2(1, 1) : new Vector2(0, 0); + + var mapLoc = (new Vector2(x, y) + centeringOffset) * cellSize; + return mapLoc * cameraZoom - cameraLocation; + } + + // Returns the location of tile (x, y) on the screen, if "center" is true returns the location of the tile center and otherwise returns the + // upper left. Works even if (x, y) is off screen or out of bounds. + public Vector2 screenLocationOfTile(Tile tile, bool center = true) + { + return screenLocationOfTileCoords(tile.xCoordinate, tile.yCoordinate, center); + } + + // Returns the virtual tile coordinates on screen at the given location. "Virtual" meaning the coordinates are unwrapped and there isn't + // necessarily a tile there at all. + public (int, int) tileCoordsOnScreenAt(Vector2 screenLocation) + { + Vector2 mapLoc = (screenLocation + cameraLocation) / scaledCellSize; + Vector2 intMapLoc = mapLoc.Floor(); + Vector2 fracMapLoc = mapLoc - intMapLoc; + int x = (int)intMapLoc.X, y = (int)intMapLoc.Y; + bool evenColumn = x%2 == 0, evenRow = y%2 == 0; + if (evenColumn ^ evenRow) { + if (fracMapLoc.Y > fracMapLoc.X) + x -= 1; + else + y -= 1; + } else { + if (fracMapLoc.Y < 1 - fracMapLoc.X) { + x -= 1; + y -= 1; + } + } + return (x, y); + } + + public Tile tileOnScreenAt(GameMap map, Vector2 screenLocation) + { + (int x, int y) = tileCoordsOnScreenAt(screenLocation); + return map.tileAt(x, y); + } + + public void centerCameraOnTile(Tile t) + { + var tileCenter = new Vector2(t.xCoordinate + 1, t.yCoordinate + 1) * scaledCellSize; + setCameraLocation(tileCenter - (float)0.5 * getVisibleAreaSize()); + } +} diff --git a/C7/PCXToGodot.cs b/C7/PCXToGodot.cs index b3621c91..6c86dd7e 100644 --- a/C7/PCXToGodot.cs +++ b/C7/PCXToGodot.cs @@ -16,6 +16,20 @@ public static ImageTexture getImageTextureFromPCX(Pcx pcx, int leftStart, int to return getImageTextureFromImage(image); } + public static ImageTexture getImageTextureFromFogOfWarPCX(Pcx pcx) { + Image ImgTxtr = ByteArrayToImage(pcx.ColorIndices, pcx.Palette, pcx.Width, pcx.Height); + for (int x = 0; x < ImgTxtr.GetWidth(); x++) { + for (int y = 0; y < ImgTxtr.GetHeight(); y++) { + Color pixel = ImgTxtr.GetPixel(x, y); + if (pixel.A > 0) { + Color transparent = new Color(pixel.R, pixel.G, pixel.B, 1f - pixel.R); + ImgTxtr.SetPixel(x, y, transparent); + } + } + } + return getImageTextureFromImage(ImgTxtr); + } + /** * This method is for cases where we want to use components of multiple PCXs in a texture, such as for the popup background. **/ diff --git a/C7/Util.cs b/C7/Util.cs index d72cb60f..ba1a3e26 100644 --- a/C7/Util.cs +++ b/C7/Util.cs @@ -221,6 +221,8 @@ static public ImageTexture LoadTextureFromPCX(string relPath, int leftStart, int return texture; } + static public ImageTexture LoadFogOfWarPCX(string relPath) => PCXToGodot.getImageTextureFromFogOfWarPCX(LoadPCX(relPath)); + private static Dictionary PcxCache = new Dictionary(); /** diff --git a/C7/icon.png.import b/C7/icon.png.import index 0265838e..06f7374a 100644 --- a/C7/icon.png.import +++ b/C7/icon.png.import @@ -2,7 +2,7 @@ importer="texture" type="CompressedTexture2D" -uid="uid://bk71ywmbfvg35" +uid="uid://cg00nurhpvuk5" path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" metadata={ "vram_texture": false diff --git a/C7/project.godot b/C7/project.godot index 3caefaa0..023bff70 100644 --- a/C7/project.godot +++ b/C7/project.godot @@ -196,4 +196,7 @@ limits/debugger_stdout/max_chars_per_second=65535 textures/canvas_textures/default_texture_filter=0 environment/defaults/default_clear_color=Color(0.301961, 0.301961, 0.301961, 1) +2d/snap/snap_2d_transforms_to_pixel=true +2d/snap/snap_2d_vertices_to_pixel=true +lights_and_shadows/positional_shadow/atlas_quadrant_2_subdiv=0 environment/default_environment="res://default_env.tres" diff --git a/C7/tests/TestUnit.cs b/C7/tests/TestUnit.cs index 64254d3d..97520381 100644 --- a/C7/tests/TestUnit.cs +++ b/C7/tests/TestUnit.cs @@ -1,53 +1,26 @@ using Godot; -using System; -using ConvertCiv3Media; +using C7GameData; public partial class TestUnit : Node2D { - - // Called when the node enters the scene tree for the first time. public override void _Ready() { - //AudioStreamPlayer player = GetNode("CanvasLayer/SoundEffectPlayer"); - - AnimatedSprite2D sprite = new AnimatedSprite2D(); - SpriteFrames frames = new SpriteFrames(); - sprite.SpriteFrames = frames; - - AnimatedSprite2D spriteTint = new AnimatedSprite2D(); - SpriteFrames framesTint = new SpriteFrames(); - spriteTint.SpriteFrames = framesTint; - - AnimationManager.loadFlicAnimation("Art/Units/warrior/warriorRun.flc", "run", ref frames, ref framesTint); - - ShaderMaterial material = new ShaderMaterial(); - material.Shader = GD.Load("res://UnitTint.gdshader"); - material.SetShaderParameter("tintColor", new Vector3(1f,1f,1f)); - spriteTint.Material = material; - + AnimationManager manager = new AnimationManager(null); + UnitSprite sprite = new UnitSprite(manager); + UnitPrototype prototype = new UnitPrototype{name="warrior"}; + manager.forUnit(prototype, MapUnit.AnimatedAction.RUN).loadSpriteAnimation(); + string name = AnimationManager.AnimationKey(prototype, MapUnit.AnimatedAction.RUN, TileDirection.EAST); AddChild(sprite); - AddChild(spriteTint); + sprite.Play(name); - sprite.Play("run_EAST"); - spriteTint.Play("run_EAST"); - sprite.Position = new Vector2(30, 30); - spriteTint.Position = new Vector2(30, 30); - - float SCALE = 6; - this.Scale = new Vector2(SCALE, SCALE); + float scale = 6; + this.Scale = new Vector2(scale, scale); + sprite.SetColor(new Color(1, 1, 1)); + sprite.Position = new Vector2(30, 30); - AnimatedSprite2D cursor = new AnimatedSprite2D(); - SpriteFrames cursorFrames = new SpriteFrames(); - cursor.SpriteFrames = cursorFrames; - AnimationManager.loadCursorAnimation("Art/Animations/Cursor/Cursor.flc", ref cursorFrames); + CursorSprite cursor = new CursorSprite(); cursor.Position = new Vector2(120, 30); - cursor.Play("cursor"); AddChild(cursor); } - - // Called every frame. 'delta' is the elapsed time since the previous frame. - public override void _Process(double delta) - { - } } diff --git a/C7Engine/EntryPoints/CityInteractions.cs b/C7Engine/EntryPoints/CityInteractions.cs index 62be1c62..fddf88b7 100644 --- a/C7Engine/EntryPoints/CityInteractions.cs +++ b/C7Engine/EntryPoints/CityInteractions.cs @@ -1,4 +1,3 @@ -using System.Linq; using C7Engine.AI; namespace C7Engine @@ -23,6 +22,7 @@ public static void BuildCity(int x, int y, string playerGuid, string name) owner.cities.Add(newCity); tileWithNewCity.cityAtTile = newCity; tileWithNewCity.overlays.road = true; + new MsgCityBuilt(tileWithNewCity).send(); // UI will add city to the map view } public static void DestroyCity(int x, int y) { diff --git a/C7Engine/EntryPoints/MessageToUI.cs b/C7Engine/EntryPoints/MessageToUI.cs index e4339c33..7750863d 100644 --- a/C7Engine/EntryPoints/MessageToUI.cs +++ b/C7Engine/EntryPoints/MessageToUI.cs @@ -1,7 +1,8 @@ +using System.Threading; +using C7GameData; + namespace C7Engine { - using System.Threading; - using C7GameData; public class MessageToUI { public void send() @@ -40,6 +41,22 @@ public MsgStartEffectAnimation(Tile tile, AnimatedEffect effect, AutoResetEvent } } + public class MsgCityBuilt : MessageToUI { + public int tileIndex; + + public MsgCityBuilt(Tile tile) { + this.tileIndex = EngineStorage.gameData.map.tileCoordsToIndex(tile.xCoordinate, tile.yCoordinate); + } + } + public class MsgStartTurn : MessageToUI {} + public class MsgTileDiscovered : MessageToUI { + public int tileIndex; + + public MsgTileDiscovered(Tile tile) { + this.tileIndex = EngineStorage.gameData.map.tileCoordsToIndex(tile.xCoordinate, tile.yCoordinate); + } + } + } diff --git a/C7Engine/MapUnitExtensions.cs b/C7Engine/MapUnitExtensions.cs index 82dbbc4f..f1e0d28d 100644 --- a/C7Engine/MapUnitExtensions.cs +++ b/C7Engine/MapUnitExtensions.cs @@ -274,7 +274,9 @@ public static void OnBeginTurn(this MapUnit unit) { public static void OnEnterTile(this MapUnit unit, Tile tile) { //Add to player knowledge of tiles - unit.owner.tileKnowledge.AddTilesToKnown(tile); + if (unit.owner.tileKnowledge.AddTilesToKnown(tile)) { + new MsgTileDiscovered(tile).send(); + } // Disperse barb camp if (tile.hasBarbarianCamp && (!unit.owner.isBarbarians)) { @@ -366,9 +368,9 @@ public static bool move(this MapUnit unit, TileDirection dir, bool wait = false) throw new System.Exception("Failed to remove unit from tile it's supposed to be on"); unit.OnEnterTile(newLoc); newLoc.unitsOnTile.Add(unit); + unit.animate(MapUnit.AnimatedAction.RUN, wait); unit.location = newLoc; unit.movementPoints.onUnitMove(movementCost); - unit.animate(MapUnit.AnimatedAction.RUN, wait); } return true; } diff --git a/C7GameData/AIData/TileKnowledge.cs b/C7GameData/AIData/TileKnowledge.cs index cab2e024..a4db9f98 100644 --- a/C7GameData/AIData/TileKnowledge.cs +++ b/C7GameData/AIData/TileKnowledge.cs @@ -7,11 +7,12 @@ public class TileKnowledge HashSet knownTiles = new HashSet(); HashSet visibleTiles = new HashSet(); - public void AddTilesToKnown(Tile unitLocation) { - knownTiles.Add(unitLocation); + public bool AddTilesToKnown(Tile unitLocation) { + bool added = knownTiles.Add(unitLocation); foreach (Tile t in unitLocation.neighbors.Values) { - knownTiles.Add(t); + added |= knownTiles.Add(t); } + return added; } public bool isTileKnown(Tile t) { diff --git a/C7GameData/GameMap.cs b/C7GameData/GameMap.cs index 30fa3a88..a92fb8ca 100644 --- a/C7GameData/GameMap.cs +++ b/C7GameData/GameMap.cs @@ -87,14 +87,13 @@ public bool isRowAt(int y) public bool isTileAt(int x, int y) { bool evenRow = y%2 == 0; - bool xInBounds; { - if (wrapHorizontally) - xInBounds = true; - else if (evenRow) - xInBounds = (x >= 0) && (x <= numTilesWide - 2); - else - xInBounds = (x >= 1) && (x <= numTilesWide - 1); - } + bool xInBounds; + if (wrapHorizontally) + xInBounds = true; + else if (evenRow) + xInBounds = (x >= 0) && (x <= numTilesWide - 2); + else + xInBounds = (x >= 1) && (x <= numTilesWide - 1); return xInBounds && isRowAt(y) && (evenRow ? (x%2 == 0) : (x%2 != 0)); } @@ -118,10 +117,13 @@ public int wrapTileY(int y) public Tile tileAt(int x, int y) { - if (isTileAt(x, y)) - return tiles[tileCoordsToIndex(wrapTileX(x), wrapTileY(y))]; - else - return Tile.NONE; + return isTileAt(x, y) ? tiles[tileCoordsToIndex(wrapTileX(x), wrapTileY(y))] : Tile.NONE; + } + + public Tile tileAtIndex(int index) { + int x, y; + tileIndexToCoords(index, out x, out y); + return tileAt(x, y); } /** diff --git a/C7GameData/Tile.cs b/C7GameData/Tile.cs index d62b85d2..61e202aa 100644 --- a/C7GameData/Tile.cs +++ b/C7GameData/Tile.cs @@ -97,6 +97,10 @@ public Tile[] getEdgeNeighbors() { return edgeNeighbors; } + public int numWaterEdges() { + return getEdgeNeighbors().Count(t => t.IsWater()); + } + public override string ToString() { return "[" + xCoordinate + ", " + yCoordinate + "] (" + overlayTerrainType.DisplayName + " on " + baseTerrainType.DisplayName + ")"; diff --git a/QueryCiv3/QueryCiv3.cs b/QueryCiv3/QueryCiv3.cs index 1bfaa072..24d2450f 100644 --- a/QueryCiv3/QueryCiv3.cs +++ b/QueryCiv3/QueryCiv3.cs @@ -35,16 +35,7 @@ public Civ3File(byte[] fileBytes) } public Boolean SectionExists(string sectionName) { - bool result = false; - foreach (Civ3Section section in Sections) - { - if (section.Name == sectionName) - { - result = true; - break; - } - } - return result; + return Array.Exists(Sections, section => section.Name == sectionName); } protected internal Civ3Section[] PopulateSections(byte[] Data) {