diff --git a/MapMaker/localization.json b/MapMaker/localization.json index 6abab65..07c21e3 100644 --- a/MapMaker/localization.json +++ b/MapMaker/localization.json @@ -102,5 +102,9 @@ }, "mapmaker_numbers_only":{ "English": "Only numbers are allowed." + }, + "mapmaker_tile_picked":{ + "English": "Tile picked into brush.", + "Russian": "Клетка взята в кисть." } } \ No newline at end of file diff --git a/src/Brush.cs b/src/Brush.cs index 8564031..1a3bf8b 100644 --- a/src/Brush.cs +++ b/src/Brush.cs @@ -1,12 +1,17 @@ using HarmonyLib; using Polytopia.Data; using PolytopiaBackendBase.Common; +using PolytopiaMapManager.UI; +using PolytopiaMapManager.UI.Picker; using UnityEngine; +using System.Collections.Generic; +using System.Linq; namespace PolytopiaMapManager; public static class Brush { + internal static int brushSize = 0; internal static int chosenClimate = 0; internal static SkinType chosenSkinType = SkinType.Default; internal static Polytopia.Data.TerrainData.Type chosenTerrain = Polytopia.Data.TerrainData.Type.None; @@ -23,103 +28,308 @@ private static void Tile_OnHoverStart(Tile __instance) { if (GameManager.Instance.isLevelLoaded) { - if(chosenTerrain != Polytopia.Data.TerrainData.Type.None) - __instance.data.terrain = chosenTerrain; - if(chosenResource == ResourceData.Type.None) + // Shift + right-click = pick tile under cursor into the brush (only once per mouse-down) + if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) { - __instance.data.resource = null; - ActionUtils.CheckSurroundingArea(GameManager.GameState, GameManager.LocalPlayer.Id, __instance.data); + if (Input.GetKeyDown(KeyCode.Mouse1)) + { + var start = __instance.data; + chosenTerrain = start.terrain; + chosenResource = start.resource != null ? start.resource.type : Polytopia.Data.ResourceData.Type.None; + chosenTileEffect = start.effects.Count > 0 ? start.effects.ToArray().ToList().LastOrDefault(TileData.EffectType.None) : TileData.EffectType.None; + chosenBuilding = start.improvement != null ? start.improvement.type : ImprovementData.Type.None; + chosenClimate = start.climate; + chosenSkinType = start.Skin; + var gld = GameManager.GameState.GameLogicData; + Manager.climatePicker.Update(gld); + Manager.mapPicker.Update(gld); + Manager.resourcePicker.Update(gld); + Manager.terrainPicker.Update(gld); + Manager.tileEffectPicker.Update(gld); + Manager.improvementPicker.Update(gld); + NotificationManager.Notify(Localization.Get("mapmaker.tile.picked")); + } + return; } - else if(GameManager.GameState.GameLogicData.TryGetData(chosenResource, out ResourceData data)) + + bool excludeDiagonals = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); // Hold Ctrl to EXCLUDE diagonals + foreach (var tileData in GameManager.GameState.Map.GetArea(__instance.Coordinates, brushSize, !excludeDiagonals)) { - if(data.resourceTerrainRequirements.Contains(__instance.data.terrain)) + var tile = tileData.GetInstance(); + + if(chosenTerrain != Polytopia.Data.TerrainData.Type.None) + tile.data.terrain = chosenTerrain; + if(chosenResource == ResourceData.Type.None) + { + tile.data.resource = null; + ActionUtils.CheckSurroundingArea(GameManager.GameState, GameManager.LocalPlayer.Id, tile.data); + } + else if(GameManager.GameState.GameLogicData.TryGetData(chosenResource, out ResourceData data)) { - __instance.data.resource = new ResourceState + if(data.resourceTerrainRequirements.Contains(tile.data.terrain)) { - type = chosenResource - }; - ActionUtils.CheckSurroundingArea(GameManager.GameState, GameManager.LocalPlayer.Id, __instance.data); + tile.data.resource = new ResourceState + { + type = chosenResource + }; + ActionUtils.CheckSurroundingArea(GameManager.GameState, GameManager.LocalPlayer.Id, tile.data); + } + else if(Time.time >= nextAllowedTimeForPopup) + { + NotificationManager.Notify(Localization.Get("mapmaker.failed.creation", new Il2CppSystem.Object[] { Localization.Get(data.displayName, new Il2CppSystem.Object[] {} ),Localization.Get(tile.data.terrain.GetDisplayName(), new Il2CppSystem.Object[] {} )})); + nextAllowedTimeForPopup = Time.time + 1f; + } } else if(Time.time >= nextAllowedTimeForPopup) { - NotificationManager.Notify(Localization.Get("mapmaker.failed.creation", new Il2CppSystem.Object[] { Localization.Get(data.displayName, new Il2CppSystem.Object[] {} ),Localization.Get(__instance.data.terrain.GetDisplayName(), new Il2CppSystem.Object[] {} )})); + NotificationManager.Notify(Localization.Get("mapmaker.failed.retrieve", new Il2CppSystem.Object[] { tile.data.resource})); nextAllowedTimeForPopup = Time.time + 1f; } + // if(!tile.data.HasEffect(chosenTileEffect)) + // { + // if(chosenTileEffect != TileData.EffectType.None) + // tile.data.AddEffect(chosenTileEffect); + // } + // else + // { + // tile.data.RemoveEffect(chosenTileEffect); + // } + if(chosenTileEffect != TileData.EffectType.None) + { + if(!tile.data.HasEffect(chosenTileEffect)) + { + if(chosenTileEffect == TileData.EffectType.Flooded) + { + if(tile.data.isFloodable()) + { + tile.data.AddEffect(chosenTileEffect); + if(chosenSkinType == SkinType.Swamp) + tile.data.AddEffect(TileData.EffectType.Swamped); + } + else if(Time.time >= nextAllowedTimeForPopup) + { + NotificationManager.Notify(Localization.Get("mapmaker.failed.creation", new Il2CppSystem.Object[] { Localization.Get($"tile.effect.{EnumCache.GetName(chosenTileEffect)}"), Localization.Get(tile.data.terrain.GetDisplayName(), new Il2CppSystem.Object[] {} )})); + nextAllowedTimeForPopup = Time.time + 1f; + } + } + else if(chosenTileEffect == TileData.EffectType.Algae) + { + if(tile.data.IsWater) + { + tile.data.AddEffect(chosenTileEffect); + if(chosenSkinType == SkinType.Cute) + tile.data.AddEffect(TileData.EffectType.Foam); + } + else if(Time.time >= nextAllowedTimeForPopup) + { + NotificationManager.Notify(Localization.Get("mapmaker.failed.creation", new Il2CppSystem.Object[] { Localization.Get($"tile.effect.{EnumCache.GetName(chosenTileEffect)}"), Localization.Get(tile.data.terrain.GetDisplayName(), new Il2CppSystem.Object[] {} )})); + nextAllowedTimeForPopup = Time.time + 1f; + } + } + } + } + else + { + TileData.EffectType lastEffect = tile.data.effects.ToArray().ToList().LastOrDefault(TileData.EffectType.None); + tile.data.RemoveEffect(lastEffect); + } + if(!tile.data.HasImprovement(ImprovementData.Type.LightHouse)) + { + if(chosenBuilding == ImprovementData.Type.None) + { + GameManager.Client.ActionManager.ExecuteCommand(new DestroyCommand(GameManager.LocalPlayer.Id, tile.data.coordinates), out string error); + } + else + { + bool succeded = GameManager.Client.ActionManager.ExecuteCommand(new BuildCommand(GameManager.LocalPlayer.Id, chosenBuilding, tile.data.coordinates), out string error); + // if(!succeded && Time.time >= nextAllowedTimeForPopup) + // { + // NotificationManager.Notify(Localization.Get("mapmaker.failed.creation", new Il2CppSystem.Object[] { Localization.Get(chosenBuilding.GetDisplayName()), Localization.Get(tile.data.terrain.GetDisplayName(), new Il2CppSystem.Object[] {} )})); + // } + } + } + if(chosenClimate != 0) + { + tile.data.climate = chosenClimate; + tile.data.Skin = chosenSkinType; + } + tile.Render(); } - else if(Time.time >= nextAllowedTimeForPopup) + } + } + // Middle mouse = bucket (flood fill) + if (Input.GetKey(KeyCode.Mouse2)) + { + var map = GameManager.GameState!.Map; + var start = map.GetTile(__instance.Coordinates); + if (start == null) + return; + + bool matchTerrain = chosenTerrain != Polytopia.Data.TerrainData.Type.None; + bool matchResource = chosenResource != Polytopia.Data.ResourceData.Type.None; + bool matchTileEffect = chosenTileEffect != TileData.EffectType.None; + bool matchBuilding = chosenBuilding != ImprovementData.Type.None; + bool matchClimate = chosenClimate != 0; + bool matchSkin = chosenSkinType != SkinType.Default; + + var refTerrain = matchTerrain ? start.terrain : Polytopia.Data.TerrainData.Type.None; + ResourceData.Type? refResource = null; + if (matchResource && start.resource != null) refResource = start.resource.type; + bool refHasEffect = matchTileEffect && start.HasEffect(chosenTileEffect); + bool refHasBuilding = matchBuilding && start.HasImprovement(chosenBuilding); + int refClimate = matchClimate ? start.climate : 0; + SkinType refSkin = matchSkin ? start.Skin : SkinType.Default; + + bool Matches(TileData t) + { + if (matchTerrain && t.terrain != refTerrain) + return false; + + if (matchResource) { - NotificationManager.Notify(Localization.Get("mapmaker.failed.retrieve", new Il2CppSystem.Object[] { __instance.data.resource})); - nextAllowedTimeForPopup = Time.time + 1f; + if (refResource.HasValue) + { + if (t.resource == null) return false; + if (t.resource.type != refResource.Value) return false; + } + else + { + if (t.resource != null) return false; + } } - // if(!__instance.data.HasEffect(chosenTileEffect)) - // { - // if(chosenTileEffect != TileData.EffectType.None) - // __instance.data.AddEffect(chosenTileEffect); - // } - // else - // { - // __instance.data.RemoveEffect(chosenTileEffect); - // } - if(chosenTileEffect != TileData.EffectType.None) + + if (matchTileEffect) { - if(!__instance.data.HasEffect(chosenTileEffect)) + if (t.HasEffect(chosenTileEffect) != refHasEffect) return false; + } + + if (matchBuilding) + { + if (t.HasImprovement(chosenBuilding) != refHasBuilding) return false; + } + + if (matchClimate && t.climate != refClimate) + return false; + + if (matchSkin && t.Skin != refSkin) + return false; + + return true; + } + + var toProcess = new Queue(); + var visited = new HashSet(); + bool includeDiagonals = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); // Hold Ctrl to include diagonals + if (Matches(start)) + { + toProcess.Enqueue(start); + visited.Add(start); + } + + while (toProcess.Count > 0) + { + var td = toProcess.Dequeue(); + var tile = td.GetInstance(); + + // apply only the attributes the user explicitly selected + if (matchTerrain) + tile.data.terrain = chosenTerrain; + + if (matchResource) + { + if (chosenResource == ResourceData.Type.None) + { + tile.data.resource = null; + ActionUtils.CheckSurroundingArea(GameManager.GameState, GameManager.LocalPlayer.Id, tile.data); + } + else if (GameManager.GameState.GameLogicData.TryGetData(chosenResource, out ResourceData data)) + { + if (data.resourceTerrainRequirements.Contains(tile.data.terrain)) + { + tile.data.resource = new ResourceState { type = chosenResource }; + ActionUtils.CheckSurroundingArea(GameManager.GameState, GameManager.LocalPlayer.Id, tile.data); + } + else if (Time.time >= nextAllowedTimeForPopup) + { + NotificationManager.Notify(Localization.Get("mapmaker.failed.creation", new Il2CppSystem.Object[] { Localization.Get(data.displayName, new Il2CppSystem.Object[] {} ),Localization.Get(tile.data.terrain.GetDisplayName(), new Il2CppSystem.Object[] {} )})); + nextAllowedTimeForPopup = Time.time + 1f; + } + } + else if (Time.time >= nextAllowedTimeForPopup) + { + NotificationManager.Notify(Localization.Get("mapmaker.failed.retrieve", new Il2CppSystem.Object[] { tile.data.resource})); + nextAllowedTimeForPopup = Time.time + 1f; + } + } + + if (matchTileEffect && chosenTileEffect != TileData.EffectType.None) + { + if(!tile.data.HasEffect(chosenTileEffect)) { if(chosenTileEffect == TileData.EffectType.Flooded) { - if(__instance.data.isFloodable()) + if(tile.data.isFloodable()) { - __instance.data.AddEffect(chosenTileEffect); + tile.data.AddEffect(chosenTileEffect); if(chosenSkinType == SkinType.Swamp) - __instance.data.AddEffect(TileData.EffectType.Swamped); + tile.data.AddEffect(TileData.EffectType.Swamped); } else if(Time.time >= nextAllowedTimeForPopup) { - NotificationManager.Notify(Localization.Get("mapmaker.failed.creation", new Il2CppSystem.Object[] { Localization.Get($"tile.effect.{EnumCache.GetName(chosenTileEffect)}"), Localization.Get(__instance.data.terrain.GetDisplayName(), new Il2CppSystem.Object[] {} )})); + NotificationManager.Notify(Localization.Get("mapmaker.failed.creation", new Il2CppSystem.Object[] { Localization.Get($"tile.effect.{EnumCache.GetName(chosenTileEffect)}"), Localization.Get(tile.data.terrain.GetDisplayName(), new Il2CppSystem.Object[] {} )})); nextAllowedTimeForPopup = Time.time + 1f; } } else if(chosenTileEffect == TileData.EffectType.Algae) { - if(__instance.data.IsWater) + if(tile.data.IsWater) { - __instance.data.AddEffect(chosenTileEffect); + tile.data.AddEffect(chosenTileEffect); if(chosenSkinType == SkinType.Cute) - __instance.data.AddEffect(TileData.EffectType.Foam); + tile.data.AddEffect(TileData.EffectType.Foam); } else if(Time.time >= nextAllowedTimeForPopup) { - NotificationManager.Notify(Localization.Get("mapmaker.failed.creation", new Il2CppSystem.Object[] { Localization.Get($"tile.effect.{EnumCache.GetName(chosenTileEffect)}"), Localization.Get(__instance.data.terrain.GetDisplayName(), new Il2CppSystem.Object[] {} )})); + NotificationManager.Notify(Localization.Get("mapmaker.failed.creation", new Il2CppSystem.Object[] { Localization.Get($"tile.effect.{EnumCache.GetName(chosenTileEffect)}"), Localization.Get(tile.data.terrain.GetDisplayName(), new Il2CppSystem.Object[] {} )})); nextAllowedTimeForPopup = Time.time + 1f; } } } } - else - { - TileData.EffectType lastEffect = __instance.data.effects.ToArray().ToList().LastOrDefault(TileData.EffectType.None); - __instance.data.RemoveEffect(lastEffect); - } - if(!__instance.data.HasImprovement(ImprovementData.Type.LightHouse)) + + if (matchBuilding && !tile.data.HasImprovement(ImprovementData.Type.LightHouse)) { if(chosenBuilding == ImprovementData.Type.None) { - GameManager.Client.ActionManager.ExecuteCommand(new DestroyCommand(GameManager.LocalPlayer.Id, __instance.data.coordinates), out string error); + GameManager.Client.ActionManager.ExecuteCommand(new DestroyCommand(GameManager.LocalPlayer.Id, tile.data.coordinates), out string error); } else { - bool succeded = GameManager.Client.ActionManager.ExecuteCommand(new BuildCommand(GameManager.LocalPlayer.Id, chosenBuilding, __instance.data.coordinates), out string error); - // if(!succeded && Time.time >= nextAllowedTimeForPopup) - // { - // NotificationManager.Notify(Localization.Get("mapmaker.failed.creation", new Il2CppSystem.Object[] { Localization.Get(chosenBuilding.GetDisplayName()), Localization.Get(__instance.data.terrain.GetDisplayName(), new Il2CppSystem.Object[] {} )})); - // } + bool succeded = GameManager.Client.ActionManager.ExecuteCommand(new BuildCommand(GameManager.LocalPlayer.Id, chosenBuilding, tile.data.coordinates), out string error); } } - if(chosenClimate != 0) + + if (matchClimate) + { + tile.data.climate = chosenClimate; + } + if (matchSkin) + { + tile.data.Skin = chosenSkinType; + } + + tile.Render(); + + foreach (var nb in map.GetArea(td.coordinates, 1, includeDiagonals)) { - __instance.data.climate = chosenClimate; - __instance.data.Skin = chosenSkinType; + if (nb == null || visited.Contains(nb) || nb == td) + continue; + if (Matches(nb)) + { + visited.Add(nb); + toProcess.Enqueue(nb); + } } - __instance.Render(); } } } diff --git a/src/Loader.cs b/src/Loader.cs index 4950c90..97aef5c 100644 --- a/src/Loader.cs +++ b/src/Loader.cs @@ -12,6 +12,7 @@ public static class Loader { internal const uint MAX_MAP_SIZE = 100; internal const uint MIN_MAP_SIZE = 3; + internal const uint MAX_BRUSH_SIZE = MAX_MAP_SIZE / 2; internal static List maps = new(); internal static Data.MapInfo? chosenMap; internal const string DEFAULT_MAP_NAME_KEY = "mapmaker.map.untitled"; diff --git a/src/UI/Editor.cs b/src/UI/Editor.cs index f6ff547..a3f121a 100644 --- a/src/UI/Editor.cs +++ b/src/UI/Editor.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using PolytopiaMapManager; using PolytopiaMapManager.Data; using PolytopiaMapManager.Popup; using TMPro; @@ -140,6 +141,45 @@ private static void HudScreen_OnMatchStart(HudScreen __instance) mapSizeButton.OnClicked = (UIButtonBase.ButtonAction)ShowMapPopup; mapSizeButton.text = string.Empty; + UIRoundButton brushSizeButton = GameObject.Instantiate(__instance.replayInterface.viewmodeSelectButton, __instance.transform); + brushSizeButton.transform.position = mapSizeButton.transform.position + new Vector3(60, 0, 0); + brushSizeButton.gameObject.SetActive(true); + brushSizeButton.OnClicked = (UIButtonBase.ButtonAction)ShowBrushPopup; + brushSizeButton.text = string.Empty; + + void ShowBrushPopup(int id, BaseEventData eventData) + { + BasicPopup popup = PopupManager.GetBasicPopup(); + popup.Header = "Brush Size"; + popup.Description = "Set brush radius (0 = single tile, 1 = 3x3, etc)."; + popup.buttonData = new PopupBase.PopupButtonData[] + { + new PopupBase.PopupButtonData("buttons.exit", PopupBase.PopupButtonData.States.None, (UIButtonBase.ButtonAction)Exit, -1, true, null), + new PopupBase.PopupButtonData("buttons.set", PopupBase.PopupButtonData.States.None, (UIButtonBase.ButtonAction)SetBrush, -1, true, null) + }; + void Exit(int id, BaseEventData eventData) + { + CustomInput.RemoveInputFromPopup(popup); + } + void SetBrush(int id, BaseEventData eventData) + { + var input = CustomInput.GetInputFromPopup(popup); + if(input != null && int.TryParse(input.text, out int size)) + { + if(size < 0) size = 0; + if(size > Loader.MAX_BRUSH_SIZE) size = (int)Loader.MAX_BRUSH_SIZE; + Brush.brushSize = size; + NotificationManager.Notify($"Brush radius set to {2*size+1}x{2*size+1}", "Brush"); + } + else + { + NotificationManager.Notify("Only numbers are allowed", "Error"); + } + CustomInput.RemoveInputFromPopup(popup); + } + CustomInput.AddInputToPopup(popup, Brush.brushSize.ToString()); + } + void ShowMapPopup(int id, BaseEventData eventData) { BasicPopup popup = PopupManager.GetBasicPopup();