diff --git a/WorldEditor.sln.DotSettings.user b/WorldEditor.sln.DotSettings.user index 667658e..bd2566a 100644 --- a/WorldEditor.sln.DotSettings.user +++ b/WorldEditor.sln.DotSettings.user @@ -73,6 +73,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -91,6 +92,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -134,12 +136,14 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/WorldEditor/Architect/Architect.cs b/WorldEditor/Architect/Architect.cs index a85ce6f..41d5f14 100644 --- a/WorldEditor/Architect/Architect.cs +++ b/WorldEditor/Architect/Architect.cs @@ -15,6 +15,7 @@ using Modding; using Satchel; using UnityEngine; +using UnityEngine.UI; using Object = UnityEngine.Object; namespace Architect; @@ -112,6 +113,8 @@ public WorldEditorGlobalSettings OnSaveGlobal() private static void InitializeLayout() { _editorLayout = new LayoutRoot(true, "Architect Editor"); + _editorLayout.Canvas.GetComponent().screenMatchMode = CanvasScaler.ScreenMatchMode.Expand; + EditorUIManager.Initialize(_editorLayout); _menuLayout = new LayoutRoot(true, "Architect Menu") diff --git a/WorldEditor/Architect/Architect.csproj b/WorldEditor/Architect/Architect.csproj index f8490d0..23dda74 100644 --- a/WorldEditor/Architect/Architect.csproj +++ b/WorldEditor/Architect/Architect.csproj @@ -8,8 +8,8 @@ Architect A mod to add platforms, enemies and more to change areas in the game with an in-game level editor. Copyright © Arun Kapila 2025 - 1.16.15.0 - 1.16.15.0 + 1.18.0.3 + 1.18.0.3 bin\$(Configuration)\ latest diff --git a/WorldEditor/Architect/Category/PrefabsCategory.cs b/WorldEditor/Architect/Category/PrefabsCategory.cs index d1fd7e5..d0462a6 100644 --- a/WorldEditor/Architect/Category/PrefabsCategory.cs +++ b/WorldEditor/Architect/Category/PrefabsCategory.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Architect.Objects; using Architect.Storage; using Architect.UI; @@ -12,7 +13,8 @@ internal class PrefabsCategory : ObjectCategory public PrefabsCategory() : base("Prefabs") { - foreach (var obj in SceneSaveLoader.Load("Prefabs")) + foreach (var obj in SceneSaveLoader.Load("Prefabs").Placements + .Where(obj => obj?.GetPlaceableObject() != null)) { Prefabs.Add(new PrefabObject(obj)); Objects.Add(obj); @@ -26,7 +28,7 @@ public static void TryAddPrefab() var placement = placeable.MakePlacement(); Objects.Add(placement); - SceneSaveLoader.Save("Prefabs", Objects); + SceneSaveLoader.Save("Prefabs", new LevelData(Objects, [])); Prefabs.Add(new PrefabObject(placement)); @@ -38,7 +40,7 @@ public static void RemovePrefab(PrefabObject obj) { Prefabs.Remove(obj); Objects.RemoveAll(placement => placement.GetId() == obj.GetId()); - SceneSaveLoader.Save("Prefabs", Objects); + SceneSaveLoader.Save("Prefabs", new LevelData(Objects, [])); EditorUIManager.RefreshObjects(); EditorUIManager.RefreshButtons(); diff --git a/WorldEditor/Architect/Content/Elements/Custom/Behaviour/ZoteTrophy.cs b/WorldEditor/Architect/Content/Elements/Custom/Behaviour/ZoteTrophy.cs index 523cfa0..6d146a9 100644 --- a/WorldEditor/Architect/Content/Elements/Custom/Behaviour/ZoteTrophy.cs +++ b/WorldEditor/Architect/Content/Elements/Custom/Behaviour/ZoteTrophy.cs @@ -1,14 +1,12 @@ using Architect.MultiplayerHook; -using HutongGames.PlayMaker; -using Modding; +using SFCore.Utils; using UnityEngine; +using FsmUtil = Satchel.FsmUtil; namespace Architect.Content.Elements.Custom.Behaviour; public class ZoteTrophy : MonoBehaviour { - private static GameObject _titleCard; - private static FsmString _titleCardData; private bool _collected; private float _collectedTime; private bool _particlesStopped; @@ -77,19 +75,33 @@ private void OnTriggerEnter2D(Collider2D other) public static void Init() { - _titleCard = GameCameras.instance.hudCamera.transform.GetChild(10).GetChild(0).gameObject; - _titleCardData = _titleCard.LocateMyFSM("Area Title Control").FsmVariables.FindFsmString("Area Event"); - - ModHooks.LanguageGetHook += (key, _, orig) => + On.PlayMakerFSM.Awake += (orig, fsm) => { - if (key.EndsWith("_RawText_SUPER") || key.EndsWith("_RawText_SUB")) return ""; - return key.EndsWith("_RawText_MAIN") ? key.Replace("_RawText_MAIN", "") : orig; + orig(fsm); + + if (fsm.FsmName != "Area Title Control") return; + + FsmUtil.InsertCustomAction(fsm, "Visited Check", f => + { + if (_overrideAreaText) + { + f.SendEvent("UNVISITED"); + f.FsmVariables.FindFsmString("Title Main").Value = _areaBody; + f.FsmVariables.FindFsmString("Title Sup").Value = ""; + f.FsmVariables.FindFsmString("Title Sub").Value = ""; + } + }, 0); }; } + + private static bool _overrideAreaText; + private static string _areaBody; public static void WinScreen(string name) { - _titleCardData.Value = name + "_RawText"; - _titleCard.SetActive(true); + _overrideAreaText = true; + _areaBody = name; + + AreaTitle.instance.gameObject.SetActive(true); } } \ No newline at end of file diff --git a/WorldEditor/Architect/Content/Elements/Custom/RoomObjects.cs b/WorldEditor/Architect/Content/Elements/Custom/RoomObjects.cs index 22dfb03..0e5abc8 100644 --- a/WorldEditor/Architect/Content/Elements/Custom/RoomObjects.cs +++ b/WorldEditor/Architect/Content/Elements/Custom/RoomObjects.cs @@ -84,6 +84,9 @@ public static void Initialize() CreateObjectRemover("door_remover", "Remove Transition", FindObjectsToDisable) .WithConfigGroup(ConfigGroup.Invisible) .WithReceiverGroup(ReceiverGroup.Invisible), + CreateObjectRemover("renderer_remover", "Remove Renderer", FindObjectsToDisable) + .WithConfigGroup(ConfigGroup.Invisible) + .WithReceiverGroup(ReceiverGroup.Invisible), CreateObjectRemover("enemy_remover", "Remove Enemy", FindObjectsToDisable) .WithConfigGroup(ConfigGroup.Invisible) .WithReceiverGroup(ReceiverGroup.Invisible), @@ -98,7 +101,22 @@ public static void Initialize() if (config) try { - point = o.scene.FindGameObject(config.objectPath); + // First try the current scene + point = o.scene.FindGameObject(config.objectPath); + + // If not found, search all other loaded scenes + if (point == null) + { + for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) + { + var scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); + if (scene != o.scene) // Skip the current scene since we already checked it + { + point = scene.FindGameObject(config.objectPath); + if (point != null) break; + } + } + } } catch (ArgumentException) { @@ -112,7 +130,7 @@ public static void Initialize() ContentPacks.RegisterPack(edits); } - private static Disabler[] FindObjectsToDisable(GameObject disabler) where T : UnityEngine.Behaviour + private static Disabler[] FindObjectsToDisable(GameObject disabler) where T : Component { var objects = disabler.scene.GetRootGameObjects() .Where(obj => !obj.name.StartsWith("[Architect] ")) diff --git a/WorldEditor/Architect/Content/Groups/ConfigGroup.cs b/WorldEditor/Architect/Content/Groups/ConfigGroup.cs index 5acebdb..16705cf 100644 --- a/WorldEditor/Architect/Content/Groups/ConfigGroup.cs +++ b/WorldEditor/Architect/Content/Groups/ConfigGroup.cs @@ -437,19 +437,19 @@ public static void Initialize() GeoChest = new ConfigGroup(Generic, Attributes.ConfigManager.RegisterConfigType( - new IntConfigType("Large Geo", + new IntConfigType("Large Geo", (o, value) => { o.LocateMyFSM("Chest Control").FsmVariables.FindFsmInt("Geo Large").Value = value.GetValue(); }), "chest_large_geo"), Attributes.ConfigManager.RegisterConfigType( - new IntConfigType("Medium Geo", + new IntConfigType("Medium Geo", (o, value) => { o.LocateMyFSM("Chest Control").FsmVariables.FindFsmInt("Geo Med").Value = value.GetValue(); }), "chest_med_geo"), Attributes.ConfigManager.RegisterConfigType( - new IntConfigType("Small Geo", + new IntConfigType("Small Geo", (o, value) => { o.LocateMyFSM("Chest Control").FsmVariables.FindFsmInt("Geo Small").Value = value.GetValue(); @@ -490,6 +490,41 @@ public static void Initialize() }) .WithCondition(o => o.GetComponentInChildren() || o.transform.Find("Alert Range")), "alert_range_trigger"), + Attributes.ConfigManager.RegisterConfigType( + new BoolConfigType("Can Attack", + (o, value) => + { + var attackRange = o.transform.Find("Attack Range"); + var attackRegion = o.transform.Find("Attack Region"); + var alertRegion = o.transform.Find("Alert Region"); + var evadeCheck = o.transform.Find("Evade Check"); + var runCheck = o.transform.Find("Run Check"); + var wallCheck = o.transform.Find("Wall Check"); + var HeadBox = o.transform.Find("Head Box"); + + if (attackRange != null) + attackRange.gameObject.SetActive(value.GetValue()); + + if (attackRegion != null) + attackRegion.gameObject.SetActive(value.GetValue()); + + if (alertRegion != null) + alertRegion.gameObject.SetActive(value.GetValue()); + + if (evadeCheck != null) + evadeCheck.gameObject.SetActive(value.GetValue()); + + if (runCheck != null) + runCheck.gameObject.SetActive(value.GetValue()); + + if (wallCheck != null) + wallCheck.gameObject.SetActive(value.GetValue()); + + if (HeadBox != null) + HeadBox.gameObject.SetActive(value.GetValue()); + }) + .WithCondition(o => o.transform.Find("Attack Range") != null || o.transform.Find("Attack Region") != null), + "attack_range_trigger"), Attributes.ConfigManager.RegisterConfigType( new BoolConfigType("Contact Damage", (o, value) => @@ -520,19 +555,19 @@ public static void Initialize() KillableEnemies = new ConfigGroup(Enemies, Attributes.ConfigManager.RegisterConfigType( - new IntConfigType("Health", + new IntConfigType("Health", (o, value) => { o.GetComponent().hp = Mathf.Abs(value.GetValue()); }), "enemy_health"), Attributes.ConfigManager.RegisterConfigType( - new IntConfigType("Large Geo Drops", + new IntConfigType("Large Geo Drops", (o, value) => { o.GetComponent().SetGeoLarge(Mathf.Abs(value.GetValue())); }), "enemy_large_geo"), Attributes.ConfigManager.RegisterConfigType( - new IntConfigType("Medium Geo Drops", + new IntConfigType("Medium Geo Drops", (o, value) => { o.GetComponent().SetGeoMedium(Mathf.Abs(value.GetValue())); }), "enemy_med_geo"), Attributes.ConfigManager.RegisterConfigType( - new IntConfigType("Small Geo Drops", + new IntConfigType("Small Geo Drops", (o, value) => { o.GetComponent().SetGeoSmall(Mathf.Abs(value.GetValue())); }), "enemy_small_geo"), Attributes.ConfigManager.RegisterConfigType(MakePersistenceConfigType("Stay Dead", o => @@ -667,7 +702,7 @@ public static void Initialize() Cocoon = new ConfigGroup(Breakable, Attributes.ConfigManager.RegisterConfigType( - new IntConfigType("Lifeseed Count", + new IntConfigType("Lifeseed Count", (o, value) => { o.GetComponent()?.SetScuttlerAmount(Mathf.Abs(value.GetValue())); }), "lifeseed_count") ); @@ -740,7 +775,7 @@ public static void Initialize() ); TollBench = new ConfigGroup(Levers, - Attributes.ConfigManager.RegisterConfigType(new IntConfigType("Cost", (o, value) => + Attributes.ConfigManager.RegisterConfigType(new IntConfigType("Cost", (o, value) => { foreach (var fsm in o.GetComponents()) { @@ -778,7 +813,7 @@ public static void Initialize() }), "disable_collision"); Thorns = new ConfigGroup(Colours, Attributes.ConfigManager.RegisterConfigType( - new IntConfigType("Damage Amount", + new IntConfigType("Damage Amount", (o, value) => { o.GetOrAddComponent().damageAmount = value.GetValue(); }), "thorns_damage"), disableCollision @@ -1263,7 +1298,7 @@ public static void Initialize() new FloatConfigType("Repeat Delay", (o, value) => { o.GetComponent().repeatDelay = value.GetValue(); }), "timer_delay"), Attributes.ConfigManager.RegisterConfigType( - new IntConfigType("Max Calls", (o, value) => { o.GetComponent().maxCalls = value.GetValue(); }), + new IntConfigType("Max Calls", (o, value) => { o.GetComponent().maxCalls = value.GetValue(); }), "timer_max_calls") ); @@ -1473,7 +1508,7 @@ public static void Initialize() Choice = new ConfigGroup(Invisible, tc, Attributes.ConfigManager.RegisterConfigType( - new IntConfigType("Cost", (o, value) => { o.GetComponent().cost = value.GetValue(); }) + new IntConfigType("Cost", (o, value) => { o.GetComponent().cost = value.GetValue(); }) .WithDefaultValue(0), "choice_cost") ); diff --git a/WorldEditor/Architect/MultiplayerHook/HkmpHook.cs b/WorldEditor/Architect/MultiplayerHook/HkmpHook.cs index 5594658..ef61f16 100644 --- a/WorldEditor/Architect/MultiplayerHook/HkmpHook.cs +++ b/WorldEditor/Architect/MultiplayerHook/HkmpHook.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; using Architect.Objects; using Hkmp.Api.Client; @@ -37,6 +38,11 @@ public static void Erase(string guid, string scene) _clientAddon.Erase(guid, scene); } + public static void TilemapChange(string scene, List<(int, int)> changes, bool empty) + { + _clientAddon.TilemapChange(scene, changes, empty); + } + public static void ClearRoom(string scene) { _clientAddon.ClearRoom(scene); diff --git a/WorldEditor/Architect/MultiplayerHook/Packets/PacketId.cs b/WorldEditor/Architect/MultiplayerHook/Packets/PacketId.cs index 4b7f0c8..90eb57a 100644 --- a/WorldEditor/Architect/MultiplayerHook/Packets/PacketId.cs +++ b/WorldEditor/Architect/MultiplayerHook/Packets/PacketId.cs @@ -5,6 +5,7 @@ public enum PacketId Refresh, Edit, Erase, + Tile, Update, Win, Relay, diff --git a/WorldEditor/Architect/MultiplayerHook/Packets/TilePacketData.cs b/WorldEditor/Architect/MultiplayerHook/Packets/TilePacketData.cs new file mode 100644 index 0000000..ded5093 --- /dev/null +++ b/WorldEditor/Architect/MultiplayerHook/Packets/TilePacketData.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Hkmp.Networking.Packet; + +namespace Architect.MultiplayerHook.Packets; + +public class TilePacketData : IPacketData +{ + public List<(int, int)> Tiles; + public bool Empty; + public string SceneName; + + public void WriteData(IPacket packet) + { + packet.Write(SceneName); + packet.Write(Empty); + packet.Write(Tiles.Count); + foreach (var r in Tiles) + { + packet.Write(r.Item1); + packet.Write(r.Item2); + } + } + + public void ReadData(IPacket packet) + { + SceneName = packet.ReadString(); + Empty = packet.ReadBool(); + Tiles = []; + var count = packet.ReadInt(); + for (var i = 0; i < count; i++) + { + Tiles.Add((packet.ReadInt(), packet.ReadInt())); + } + } + + public bool IsReliable => true; + public bool DropReliableDataIfNewerExists => false; +} \ No newline at end of file diff --git a/WorldEditor/Architect/MultiplayerHook/WeClientAddon.cs b/WorldEditor/Architect/MultiplayerHook/WeClientAddon.cs index c403cee..642352e 100644 --- a/WorldEditor/Architect/MultiplayerHook/WeClientAddon.cs +++ b/WorldEditor/Architect/MultiplayerHook/WeClientAddon.cs @@ -97,8 +97,7 @@ public override void Initialize(IClientApi clientApi) } else { - SceneSaveLoader.ScheduleEdit(packet.SceneName, - edit); // This will store it in a list to be added when the scene is next loaded, or saved when the game is next closed + SceneSaveLoader.ScheduleEdit(packet.SceneName, edit); } }); @@ -123,8 +122,33 @@ public override void Initialize(IClientApi clientApi) } else { - SceneSaveLoader.ScheduleErase(packet.SceneName, - packet.Id); // This will store it in a list to be added when the scene is next loaded, or saved when the game is next closed + SceneSaveLoader.ScheduleErase(packet.SceneName, packet.Id); + } + }); + + netReceiver.RegisterPacketHandler(PacketId.Tile, packet => + { + if (!Architect.GlobalSettings.CollaborationMode) return; + Logger.Info("Receiving Tile Change Packet [CLIENT]"); + + if (packet.SceneName == GameManager.instance.sceneName) + { + var map = PlacementManager.GetTilemap(); + foreach (var (x, y) in packet.Tiles) + { + PlacementManager.GetCurrentLevel().ToggleTile((x, y)); + if (EditorManager.IsEditing) + { + if (!map) continue; + if (packet.Empty) map.ClearTile(x, y, 0); + else map.SetTile(x, y, 0, 0); + } + } + if (map) map.Build(); + } + else + { + SceneSaveLoader.ScheduleTileChange(packet.SceneName, packet.Tiles); } }); @@ -192,6 +216,21 @@ public void Erase(string guid, string scene) }); } + public void TilemapChange(string scene, List<(int, int)> changes, bool empty) + { + if (!_api.NetClient.IsConnected) return; + + Logger.Info("Sending Tile Change Packet"); + + _api.NetClient.GetNetworkSender(this) + .SendSingleData(PacketId.Tile, new TilePacketData + { + SceneName = scene, + Tiles = changes, + Empty = empty + }); + } + public void ClearRoom(string scene) { if (!_api.NetClient.IsConnected) return; @@ -242,7 +281,7 @@ public async void Refresh() Logger.Info("Sending Refresh Packets"); var scene = GameManager.instance.sceneName; - var json = SceneSaveLoader.SerializeSceneData(PlacementManager.GetCurrentPlacements()); + var json = SceneSaveLoader.SerializeSceneData(PlacementManager.GetCurrentLevel()); var bytes = Split(ZipUtils.Zip(json), SplitSize); @@ -300,6 +339,7 @@ public IPacketData InstantiatePacket(PacketId packetId) PacketId.Win => new WinPacketData(), PacketId.Edit => new EditPacketData(), PacketId.Erase => new ErasePacketData(), + PacketId.Tile => new TilePacketData(), PacketId.Update => new UpdatePacketData(), PacketId.Relay => new RelayPacketData(), PacketId.Clear => new ClearPacketData(), diff --git a/WorldEditor/Architect/MultiplayerHook/WeServerAddon.cs b/WorldEditor/Architect/MultiplayerHook/WeServerAddon.cs index c73e70f..9abf992 100644 --- a/WorldEditor/Architect/MultiplayerHook/WeServerAddon.cs +++ b/WorldEditor/Architect/MultiplayerHook/WeServerAddon.cs @@ -20,6 +20,7 @@ public IPacketData InstantiatePacket(PacketId packetId) PacketId.Win => new WinPacketData(), PacketId.Edit => new EditPacketData(), PacketId.Erase => new ErasePacketData(), + PacketId.Tile => new TilePacketData(), PacketId.Update => new UpdatePacketData(), PacketId.Relay => new RelayPacketData(), PacketId.Clear => new ClearPacketData(), @@ -54,6 +55,16 @@ public override void Initialize(IServerApi serverApi) ); }); + netReceiver.RegisterPacketHandler(PacketId.Tile, (id, packet) => + { + Logger.Info("Receiving Tile Change Packet [SERVER]"); + sender.SendSingleData(PacketId.Tile, packet, serverApi.ServerManager.Players + .Where(player => player.Id != id) + .Select(player => player.Id) + .ToArray() + ); + }); + netReceiver.RegisterPacketHandler(PacketId.Clear, (id, packet) => { Logger.Info("Receiving Clear Packet [SERVER]"); @@ -88,8 +99,10 @@ public override void Initialize(IServerApi serverApi) { Logger.Info("Receiving Win Packet [SERVER]"); sender.SendSingleData(PacketId.Win, packet, serverApi.ServerManager.Players - .Where(player => player.Id != id) - .Select(player => player.Id).ToArray()); + .Where(player => player.Id != id) + .Select(player => player.Id) + .ToArray() + ); }); netReceiver.RegisterPacketHandler(PacketId.Relay, (id, packet) => diff --git a/WorldEditor/Architect/Objects/LockObject.cs b/WorldEditor/Architect/Objects/LockObject.cs new file mode 100644 index 0000000..10163d2 --- /dev/null +++ b/WorldEditor/Architect/Objects/LockObject.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; +using Architect.MultiplayerHook; +using Architect.Util; +using UnityEngine; + +namespace Architect.Objects; + +internal class LockObject : SelectableObject +{ + internal static readonly LockObject Instance = new(); + + private readonly Sprite _sprite; + + private LockObject() : base("Lock") + { + _sprite = PrepareSprite(); + } + + private static Sprite PrepareSprite() + { + return ResourceUtils.LoadInternal("lock"); + } + + public override void OnClickInWorld(Vector3 pos, bool first) + { + if (!first) return; + var placement = PlacementManager.FindClickedObject(pos, true); + placement?.ToggleLock(); + } + + public override bool IsFavourite() + { + return false; + } + + public override Sprite GetSprite() + { + return _sprite; + } + + public override int GetWeight() + { + return 0; + } +} \ No newline at end of file diff --git a/WorldEditor/Architect/Objects/ObjectPlacement.cs b/WorldEditor/Architect/Objects/ObjectPlacement.cs index 4a69df3..f593fd3 100644 --- a/WorldEditor/Architect/Objects/ObjectPlacement.cs +++ b/WorldEditor/Architect/Objects/ObjectPlacement.cs @@ -28,6 +28,7 @@ public class ObjectPlacement public readonly EventReceiver[] Receivers; public readonly float Rotation; public readonly float Scale; + public bool Locked; private Color _defaultColor; private bool _dragging; @@ -42,6 +43,7 @@ public ObjectPlacement( string name, Vector3 pos, bool flipped, + bool locked, float rotation, float scale, string id, @@ -52,6 +54,7 @@ public ObjectPlacement( Name = name; _pos = pos; Flipped = flipped; + Locked = locked; Scale = scale; _id = id; Rotation = rotation; @@ -99,6 +102,7 @@ public bool Touching(Vector2 pos) public bool IsWithinZone(Vector2 pos1, Vector2 pos2) { + if (Locked) return false; var withinX = (_pos.x > pos1.x && _pos.x < pos2.x) || (_pos.x < pos1.x && _pos.x > pos2.x); var withinY = (_pos.y > pos1.y && _pos.y < pos2.y) || (_pos.y < pos1.y && _pos.y > pos2.y); return withinX && withinY; @@ -162,7 +166,7 @@ internal void PlaceGhost() var r = 1f; var g = 1f; var b = 1f; - var a = 0.5f; + var a = Locked ? 0.2f : 0.5f; var renderer = _previewObject.AddComponent(); @@ -191,10 +195,6 @@ internal void PlaceGhost() { b = blue.GetValue(); } - else if (config.GetName() == "a" && config is FloatConfigValue alpha) - { - a *= Mathf.Max(0.15f, alpha.GetValue()); - } else if (config.GetName() == "layer" && config is IntConfigValue layer) { renderer.sortingOrder = layer.GetValue(); @@ -353,6 +353,9 @@ private static void WritePlacementInfo(JsonWriter writer, ObjectPlacement placem writer.WritePropertyName("flipped"); writer.WriteValue(placement.Flipped); + writer.WritePropertyName("locked"); + writer.WriteValue(placement.Locked); + if (placement.Rotation != 0) { writer.WritePropertyName("rotation"); @@ -376,6 +379,7 @@ public override ObjectPlacement ReadJson(JsonReader reader, Type objectType, Obj string id = null; var pos = Vector3.zero; var flipped = true; + var locked = false; var rotation = 0f; var scale = 1f; @@ -413,6 +417,10 @@ public override ObjectPlacement ReadJson(JsonReader reader, Type objectType, Obj reader.ReadAsBoolean(); flipped = (bool)reader.Value; break; + case "locked": + reader.ReadAsBoolean(); + locked = (bool)reader.Value; + break; case "rotation": reader.ReadAsDouble(); rotation = (float)(double)reader.Value; @@ -450,7 +458,7 @@ public override ObjectPlacement ReadJson(JsonReader reader, Type objectType, Obj name = Updater.UpdateObject(name); id ??= Guid.NewGuid().ToString().Substring(0, 8); var placement = - new ObjectPlacement(name, pos, flipped, rotation, scale, id, broadcasters, receivers, config); + new ObjectPlacement(name, pos, flipped, locked, rotation, scale, id, broadcasters, receivers, config); return placement; } @@ -482,4 +490,11 @@ private static ConfigValue[] DeserializeConfig(Dictionary data) return config; } } + + public void ToggleLock() + { + Locked = !Locked; + _defaultColor.a = Locked ? 0.2f : 0.5f; + if (_previewObject) _previewObject.GetComponent().color = _defaultColor; + } } \ No newline at end of file diff --git a/WorldEditor/Architect/Objects/PlaceableObject.cs b/WorldEditor/Architect/Objects/PlaceableObject.cs index d99ccf6..452b59d 100644 --- a/WorldEditor/Architect/Objects/PlaceableObject.cs +++ b/WorldEditor/Architect/Objects/PlaceableObject.cs @@ -37,7 +37,7 @@ public PlaceableObject(AbstractPackElement element) : base(element.GetName()) public override void OnClickInWorld(Vector3 pos, bool first) { if (!first) return; - + var placement = MakePlacement(pos); PlacementManager.GetCurrentPlacements().Add(placement); @@ -61,6 +61,7 @@ public ObjectPlacement MakePlacement(Vector3 pos = default) GetName(), pos, EditorManager.IsFlipped, + false, EditorManager.Rotation, EditorManager.Scale, Guid.NewGuid().ToString().Substring(0, 8), diff --git a/WorldEditor/Architect/Objects/PlacementManager.cs b/WorldEditor/Architect/Objects/PlacementManager.cs index 2f29da6..eae1baf 100644 --- a/WorldEditor/Architect/Objects/PlacementManager.cs +++ b/WorldEditor/Architect/Objects/PlacementManager.cs @@ -12,11 +12,23 @@ namespace Architect.Objects; public static class PlacementManager { private static string _sceneName; - private static List _currentPlacements; + private static LevelData _currentPlacements; + private static tk2dTileMap _tileMap; public static readonly Dictionary Objects = []; public static List GetCurrentPlacements() + { + return GetCurrentLevel().Placements; + } + + public static tk2dTileMap GetTilemap() + { + if (!_tileMap) _tileMap = Object.FindObjectOfType(); + return _tileMap; + } + + public static LevelData GetCurrentLevel() { var sceneName = GameManager.instance.sceneName; if (_sceneName == sceneName) return _currentPlacements; @@ -37,10 +49,20 @@ private static void LoadPlacements() { Objects.Clear(); CustomObjects.PlayerListeners.Clear(); - foreach (var placement in GetCurrentPlacements().Where(placement => placement.GetPlaceableObject() != null)) + var ld = GetCurrentLevel(); + foreach (var placement in ld.Placements.Where(placement => placement.GetPlaceableObject() != null)) if (EditorManager.IsEditing) placement.PlaceGhost(); else Objects[placement.GetId()] = placement.SpawnObject(); + + var map = GetTilemap(); + if (!map) return; + foreach (var (x, y) in ld.TileChanges) + { + if (map.GetTile(x, y, 0) == -1) map.SetTile(x, y, 0, 0); + else map.ClearTile(x, y, 0); + } + map.Build(); } internal static void Initialize() @@ -59,8 +81,9 @@ internal static void Initialize() } [CanBeNull] - public static ObjectPlacement FindClickedObject(Vector3 mousePos) + public static ObjectPlacement FindClickedObject(Vector3 mousePos, bool ignoreLock = false) { - return GetCurrentPlacements().FirstOrDefault(placement => placement.Touching(mousePos)); + return GetCurrentPlacements().FirstOrDefault(placement => (ignoreLock || !placement.Locked) + && placement.Touching(mousePos)); } } \ No newline at end of file diff --git a/WorldEditor/Architect/Objects/ResetObject.cs b/WorldEditor/Architect/Objects/ResetObject.cs index ac3ccb1..6a2a123 100644 --- a/WorldEditor/Architect/Objects/ResetObject.cs +++ b/WorldEditor/Architect/Objects/ResetObject.cs @@ -78,13 +78,27 @@ public static void ResetRoom(string sceneName) { if (sceneName == GameManager.instance.sceneName) { - var placements = PlacementManager.GetCurrentPlacements(); + var level = PlacementManager.GetCurrentLevel(); + var placements = level.Placements; while (placements.Count > 0) placements[0].Destroy(); + + var map = PlacementManager.GetTilemap(); + if (map) + { + foreach (var (x, y) in level.TileChanges) + { + if (map.GetTile(x, y, 0) == -1) map.SetTile(x, y, 0, 0); + else map.ClearTile(x, y, 0); + } + map.Build(); + } + level.TileChanges.Clear(); + UndoManager.ClearHistory(); } else { - SceneSaveLoader.SaveScene(sceneName, []); + SceneSaveLoader.SaveScene(sceneName, new LevelData([], [])); } } } \ No newline at end of file diff --git a/WorldEditor/Architect/Objects/TilemapObject.cs b/WorldEditor/Architect/Objects/TilemapObject.cs new file mode 100644 index 0000000..5477506 --- /dev/null +++ b/WorldEditor/Architect/Objects/TilemapObject.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Linq; +using Architect.MultiplayerHook; +using Architect.Util; +using UnityEngine; + +namespace Architect.Objects; + +internal class TilemapObject : SelectableObject +{ + internal static readonly TilemapObject Instance = new(); + + private static readonly List<(int, int)> TileFlips = []; + + private static (int, int) _lastPos = (-1, -1); + private static bool _lastEmpty; + + private readonly Sprite _sprite; + + private TilemapObject() : base("Tilemap Editor") + { + _sprite = PrepareSprite(); + } + + private static Sprite PrepareSprite() + { + return ResourceUtils.LoadInternal("tilemap"); + } + + public override void OnClickInWorld(Vector3 pos, bool first) + { + var map = PlacementManager.GetTilemap(); + if (!map || !map.GetTileAtPosition(pos, out var x, out var y)) return; + + var xyPos = (x, y); + if (_lastPos == xyPos && !first) return; + _lastPos = xyPos; + + var empty = map.GetTile(x, y, 0) == -1; + if (first) _lastEmpty = empty; + else if (_lastEmpty != empty) return; + + if (empty) map.SetTile(x, y, 0, 0); + else map.ClearTile(x, y, 0); + map.Build(); + + PlacementManager.GetCurrentLevel().ToggleTile(xyPos); + + TileFlips.Add(xyPos); + } + + public static void Release() + { + if (TileFlips.Count == 0) return; + UndoManager.PerformAction(new ToggleTile(TileFlips.ToList(), _lastEmpty)); + + if (Architect.UsingMultiplayer && Architect.GlobalSettings.CollaborationMode) + { + HkmpHook.TilemapChange(GameManager.instance.sceneName, TileFlips, _lastEmpty); + } + + TileFlips.Clear(); + } + + public override bool IsFavourite() + { + return false; + } + + public override Sprite GetSprite() + { + return _sprite; + } + + public override int GetWeight() + { + return 0; + } +} \ No newline at end of file diff --git a/WorldEditor/Architect/Resources/ScatteredAndLost/dream_block_exit2.wav b/WorldEditor/Architect/Resources/ScatteredAndLost/dream_block_exit2.wav deleted file mode 100644 index 844817f..0000000 Binary files a/WorldEditor/Architect/Resources/ScatteredAndLost/dream_block_exit2.wav and /dev/null differ diff --git a/WorldEditor/Architect/Resources/lock.png b/WorldEditor/Architect/Resources/lock.png new file mode 100644 index 0000000..391e1b7 Binary files /dev/null and b/WorldEditor/Architect/Resources/lock.png differ diff --git a/WorldEditor/Architect/Resources/renderer_remover.png b/WorldEditor/Architect/Resources/renderer_remover.png new file mode 100644 index 0000000..16d0018 Binary files /dev/null and b/WorldEditor/Architect/Resources/renderer_remover.png differ diff --git a/WorldEditor/Architect/Resources/tilemap.png b/WorldEditor/Architect/Resources/tilemap.png new file mode 100644 index 0000000..f4f4565 Binary files /dev/null and b/WorldEditor/Architect/Resources/tilemap.png differ diff --git a/WorldEditor/Architect/Storage/LevelData.cs b/WorldEditor/Architect/Storage/LevelData.cs new file mode 100644 index 0000000..0ed799f --- /dev/null +++ b/WorldEditor/Architect/Storage/LevelData.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using Architect.Objects; +using Newtonsoft.Json; + +namespace Architect.Storage; + +[JsonConverter(typeof(LevelDataConverter))] +public class LevelData(List placements, List<(int, int)> tileChanges) +{ + public readonly List Placements = placements; + public readonly List<(int, int)> TileChanges = tileChanges; + + public void ToggleTile((int, int) pos) + { + if (!TileChanges.Remove(pos)) TileChanges.Add(pos); + } + + public class LevelDataConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, LevelData value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("placements"); + serializer.Serialize(writer, value.Placements); + + writer.WritePropertyName("tilemap"); + serializer.Serialize(writer, value.TileChanges); + writer.WriteEndObject(); + } + + public override LevelData ReadJson(JsonReader reader, Type objectType, LevelData existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.StartArray) + { + return new LevelData(serializer.Deserialize>(reader), []); + } + + List placements = []; + List<(int, int)> tiles = []; + + while (reader.Read()) + { + if (reader.Value is not string value) break; + switch (value) + { + case "placements": + reader.Read(); + placements = serializer.Deserialize>(reader); + break; + case "tilemap": + reader.Read(); + tiles = serializer.Deserialize>(reader); + break; + } + } + + return new LevelData(placements, tiles); + } + } +} \ No newline at end of file diff --git a/WorldEditor/Architect/Storage/SceneSaveLoader.cs b/WorldEditor/Architect/Storage/SceneSaveLoader.cs index b0a6ee2..105da54 100644 --- a/WorldEditor/Architect/Storage/SceneSaveLoader.cs +++ b/WorldEditor/Architect/Storage/SceneSaveLoader.cs @@ -21,6 +21,7 @@ public static class SceneSaveLoader public static string DataPath; private static readonly Dictionary> ScheduledErases = new(); + private static readonly Dictionary> ScheduledTileChanges = new(); private static readonly Dictionary> ScheduledUpdates = new(); private static readonly Dictionary> ScheduledEdits = new(); @@ -34,53 +35,69 @@ internal static void Initialize() List scenes = []; scenes.AddRange(ScheduledErases.Keys); + scenes.AddRange(ScheduledTileChanges.Keys); scenes.AddRange(ScheduledEdits.Keys); scenes.AddRange(ScheduledUpdates.Keys); foreach (var scene in scenes) { - var placements = LoadScene(scene); + var levelData = LoadScene(scene); if (ScheduledErases.TryGetValue(scene, out var erases)) - placements.RemoveAll(obj => erases.Contains(obj.GetId())); - if (ScheduledEdits.TryGetValue(scene, out var edits)) placements.AddRange(edits); + levelData.Placements.RemoveAll(obj => erases.Contains(obj.GetId())); + if (ScheduledEdits.TryGetValue(scene, out var edits)) levelData.Placements.AddRange(edits); + if (ScheduledTileChanges.TryGetValue(scene, out var tileChanges)) + { + foreach (var tile in tileChanges) levelData.ToggleTile(tile); + } if (ScheduledUpdates.TryGetValue(scene, out var updates)) foreach (var pair in updates) - placements.First(obj => obj.GetId() == pair.Item1).Move(pair.Item2); - SaveScene(scene, placements); + levelData.Placements.First(obj => obj.GetId() == pair.Item1).Move(pair.Item2); + SaveScene(scene, levelData); } + ScheduledTileChanges.Clear(); + ScheduledErases.Clear(); + ScheduledEdits.Clear(); + ScheduledUpdates.Clear(); + orig(self); }; } - public static List LoadScene(string name) + public static LevelData LoadScene(string name) { return Load("Architect/" + name); } - public static List Load(string name) + public static LevelData Load(string name) { var path = DataPath + name + ".architect.json"; - if (!File.Exists(path)) return []; + if (!File.Exists(path)) return new LevelData([], []); var content = File.ReadAllText(path); var data = DeserializeSceneData(content); if (ScheduledErases.TryGetValue(name, out var erase)) { - data.RemoveAll(obj => erase.Contains(obj.GetId())); + data.Placements.RemoveAll(obj => erase.Contains(obj.GetId())); ScheduledErases.Remove(name); } if (ScheduledEdits.TryGetValue(name, out var edit)) { - data.AddRange(edit); - ScheduledErases.Remove(name); + data.Placements.AddRange(edit); + ScheduledEdits.Remove(name); + } + + if (ScheduledTileChanges.TryGetValue(name, out var tileChanges)) + { + foreach (var tile in tileChanges) data.ToggleTile(tile); + ScheduledTileChanges.Remove(name); } if (ScheduledUpdates.TryGetValue(name, out var scheduledUpdate)) { - foreach (var update in scheduledUpdate) data.First(obj => obj.GetId() == update.Item1).Move(update.Item2); + foreach (var update in scheduledUpdate) data.Placements.First(obj => obj.GetId() == update.Item1).Move(update.Item2); ScheduledUpdates.Remove(name); } @@ -88,12 +105,12 @@ public static List Load(string name) return data; } - public static void SaveScene(string name, List placements) + public static void SaveScene(string name, LevelData placements) { Save("Architect/" + name, placements); } - public static void Save(string name, List placements) + public static void Save(string name, LevelData placements) { var data = SerializeSceneData(placements); Save(name, data); @@ -124,12 +141,12 @@ public static void WipeAllScenes() foreach (var file in Directory.GetFiles(DataPath + "Architect/")) File.Delete(file); } - public static List DeserializeSceneData(string data) + public static LevelData DeserializeSceneData(string data) { - return JsonConvert.DeserializeObject>(data); + return JsonConvert.DeserializeObject(data); } - public static string SerializeSceneData(List placements) + public static string SerializeSceneData(LevelData placements) { return JsonConvert.SerializeObject(placements, Opc, @@ -178,7 +195,7 @@ public static IEnumerator LoadAllScenes(Dictionary placements) private static async Task LoadEdits(string data) { var passed = true; - foreach (var config in DeserializeSceneData(data).Select(obj => obj.Config + foreach (var config in DeserializeSceneData(data).Placements.Select(obj => obj.Config .ToDictionary(conf => conf.GetName()))) { if (!await TryDownload(config, "Source URL", CustomAssetLoader.GetSpritePath)) passed = false; @@ -209,6 +226,12 @@ public static void ScheduleErase(string scene, string id) ScheduledErases[scene].Add(id); } + public static void ScheduleTileChange(string scene, List<(int, int)> tiles) + { + if (!ScheduledTileChanges.ContainsKey(scene)) ScheduledTileChanges[scene] = []; + ScheduledTileChanges[scene].AddRange(tiles); + } + public static void ScheduleEdit(string scene, ObjectPlacement placement) { if (!ScheduledEdits.ContainsKey(scene)) ScheduledEdits[scene] = []; diff --git a/WorldEditor/Architect/Storage/WorldEditorGlobalSettings.cs b/WorldEditor/Architect/Storage/WorldEditorGlobalSettings.cs index f5b7272..d19969e 100644 --- a/WorldEditor/Architect/Storage/WorldEditorGlobalSettings.cs +++ b/WorldEditor/Architect/Storage/WorldEditorGlobalSettings.cs @@ -18,5 +18,7 @@ public class WorldEditorGlobalSettings [JsonConverter(typeof(PlayerActionSetConverter))] public WorldEditorKeyBinds Keybinds = new(); + public float UIScaleFactor { get; set; } = 1.0f; + public bool TestMode = false; } \ No newline at end of file diff --git a/WorldEditor/Architect/UI/ConfigurationScreen.cs b/WorldEditor/Architect/UI/ConfigurationScreen.cs index 82d476f..f477c6b 100644 --- a/WorldEditor/Architect/UI/ConfigurationScreen.cs +++ b/WorldEditor/Architect/UI/ConfigurationScreen.cs @@ -19,6 +19,19 @@ public static MenuScreen GetScreen(MenuScreen modListMenu, WorldEditorGlobalSett "True", "False" ]; + + // UI Scale options + string[] scaleValues = + [ + "1.0x", + "1.25x", + "1.5x", + "1.75x", + "2.0x", + ]; + + float[] scaleFactors = [1.0f, 1.25f, 1.5f, 1.75f, 2.0f]; + var elements = new List { new TextPanel("Editor"), @@ -36,82 +49,78 @@ public static MenuScreen GetScreen(MenuScreen modListMenu, WorldEditorGlobalSett i => { globalSettings.TestMode = i == 0; }, () => globalSettings.TestMode ? 0 : 1 ), + new HorizontalOption( + "UI Scale Factor", + "Adjusts the size of the editor UI. Changes apply immediately.", + scaleValues, + i => + { + globalSettings.UIScaleFactor = scaleFactors[i]; + EditorUIManager.ApplyUIScale(globalSettings.UIScaleFactor); + }, + () => + { + // Find the closest matching scale factor index + for (int i = 0; i < scaleFactors.Length; i++) + { + if (Mathf.Approximately(globalSettings.UIScaleFactor, scaleFactors[i])) + return i; + } + return 2; + } + ), new TextPanel(""), new TextPanel("Keybinds"), new KeyBind( "Toggle Editor", globalSettings.Keybinds.ToggleEditor ), - new MenuRow([ - new KeyBind( - "Undo", - globalSettings.Keybinds.Undo - ), - new KeyBind( - "Redo", - globalSettings.Keybinds.Redo - ) + new MenuRow( + [ + new KeyBind("Undo", globalSettings.Keybinds.Undo), + new KeyBind("Redo", globalSettings.Keybinds.Redo) ], - "undo_redo"), - new MenuRow([ - new KeyBind( - "Copy Selection", - globalSettings.Keybinds.Copy - ), - new KeyBind( - "Paste Selection", - globalSettings.Keybinds.Paste - ) + "undo_redo" + ), + new MenuRow( + [ + new KeyBind("Copy Selection", globalSettings.Keybinds.Copy), + new KeyBind("Paste Selection", globalSettings.Keybinds.Paste) ], - "copy_paste"), - new MenuRow([ - new KeyBind( - "Rotate", - globalSettings.Keybinds.RotateItem - ), - new KeyBind( - "Use Unsafe Rotations", - globalSettings.Keybinds.UnsafeRotation - ) + "copy_paste" + ), + new MenuRow( + [ + new KeyBind("Rotate", globalSettings.Keybinds.RotateItem), + new KeyBind("Use Unsafe Rotations", globalSettings.Keybinds.UnsafeRotation) ], - "rotations"), - new MenuRow([ - new KeyBind( - "Decrease Scale", - globalSettings.Keybinds.DecreaseScale - ), - new KeyBind( - "Increase Scale", - globalSettings.Keybinds.IncreaseScale - ) + "rotations" + ), + new MenuRow( + [ + new KeyBind("Decrease Scale", globalSettings.Keybinds.DecreaseScale), + new KeyBind("Increase Scale", globalSettings.Keybinds.IncreaseScale) ], - "scale"), - new MenuRow([ - new KeyBind( - "Flip", - globalSettings.Keybinds.FlipItem - ), - new KeyBind( - "Lock Axis", - globalSettings.Keybinds.LockAxis - ) + "scale" + ), + new MenuRow( + [ + new KeyBind("Flip", globalSettings.Keybinds.FlipItem), + new KeyBind("Lock Axis", globalSettings.Keybinds.LockAxis) ], - "flip_lock"), - new MenuRow([ - new KeyBind( - "Moving Object Preview", - globalSettings.Keybinds.TogglePreview - ), - new KeyBind( - "Add Prefab", - globalSettings.Keybinds.AddPrefab - ) + "flip_lock" + ), + new MenuRow( + [ + new KeyBind("Moving Object Preview", globalSettings.Keybinds.TogglePreview), + new KeyBind("Add Prefab", globalSettings.Keybinds.AddPrefab) ], - "preview_prefab"), - + "preview_prefab" + ), new TextPanel(""), new TextPanel("Management"), - new MenuButton("Delete All Edits", + new MenuButton( + "Delete All Edits", "", _ => { @@ -120,20 +129,22 @@ public static MenuScreen GetScreen(MenuScreen modListMenu, WorldEditorGlobalSett ResetObject.ResetRoom(GameManager.instance.sceneName); PlacementManager.InvalidateCache(); } - SceneSaveLoader.WipeAllScenes(); - }), - + } + ), new TextPanel(""), new TextPanel("Content Pack Toggles"), new TextPanel( - "Disable packs you don't need to reduce startup time and memory usage, changes are applied when game is rebooted.\n\nThings will break if you disable a pack that is in use!") + "Disable packs you don't need to reduce startup time and memory usage, changes are applied when game is rebooted.\n\nThings will break if you disable a pack that is in use!" + ) { FontSize = 20, Anchor = TextAnchor.UpperCenter } }; + if (Architect.UsingMultiplayer) + { elements.Insert(3, new HorizontalOption( "Collaboration Mode", "Shares edits across of an HKMP server as soon as they're made, to allow working together online", @@ -141,9 +152,16 @@ public static MenuScreen GetScreen(MenuScreen modListMenu, WorldEditorGlobalSett i => { globalSettings.CollaborationMode = i == 0; }, () => globalSettings.CollaborationMode ? 0 : 1 )); - elements.AddRange(ContentPacks.Packs.Select(pack => new HorizontalOption(pack.GetName(), pack.GetDescription(), - values, i => { globalSettings.ContentPackSettings[pack.GetName()] = i == 0; }, - () => globalSettings.ContentPackSettings[pack.GetName()] ? 0 : 1))); + } + + elements.AddRange(ContentPacks.Packs.Select(pack => new HorizontalOption( + pack.GetName(), + pack.GetDescription(), + values, + i => { globalSettings.ContentPackSettings[pack.GetName()] = i == 0; }, + () => globalSettings.ContentPackSettings[pack.GetName()] ? 0 : 1 + ))); + _menuRef ??= new Menu( Architect.Instance.Name, elements.ToArray() diff --git a/WorldEditor/Architect/UI/EditorUIManager.cs b/WorldEditor/Architect/UI/EditorUIManager.cs index 0f45af1..4efc4a2 100644 --- a/WorldEditor/Architect/UI/EditorUIManager.cs +++ b/WorldEditor/Architect/UI/EditorUIManager.cs @@ -10,6 +10,7 @@ using Architect.Content; using Architect.MultiplayerHook; using Architect.Objects; +using Architect.Storage; using Architect.Util; using JetBrains.Annotations; using MagicUI.Core; @@ -32,6 +33,13 @@ public static class EditorUIManager private const string Nothing = " "; + // UI Scaling + private static float _uiScaleFactor = 1.0f; + public static float UI_SCALE_FACTOR => _uiScaleFactor; + private const int BASE_BUTTON_SIZE = 80; + private const int BASE_IMAGE_SIZE = 40; + private const int BASE_PADDING = 15; + // The current page, item index and filter info private static int _groupIndex; private static int _index; @@ -42,6 +50,7 @@ public static class EditorUIManager private static List _categories; // Info about current selected item and buttons to change item + private static TextObject _editorEnabledText; private static TextObject _selectionInfo; private static TextObject _sceneInfo; private static TextObject _extraInfo; @@ -193,6 +202,12 @@ private static void UpdateSelectedItem() case -5: SelectedItem = ResetObject.Instance; break; + case -6: + SelectedItem = LockObject.Instance; + break; + case -7: + SelectedItem = TilemapObject.Instance; + break; default: { var index = _groupIndex * ItemsPerGroup + _index; @@ -226,44 +241,60 @@ public static void RefreshSelectedItem(bool useDefaultConfigValues) // Initializes the UI internal static void Initialize(LayoutRoot layout) { - _layout = layout; + try + { + _layout = layout; - _selectionButtons = []; - PauseOptions = []; + _selectionButtons = []; + PauseOptions = []; - layout.VisibilityCondition = () => EditorManager.IsEditing; + layout.VisibilityCondition = () => EditorManager.IsEditing; - SetupTextDisplay(layout); - SetupLeftSide(layout); - SetupObjectOptions(layout); - SetupFilter(layout); - SetupTools(layout); + SetupTextDisplay(layout); + SetupLeftSide(layout); + SetupObjectOptions(layout); + SetupFilter(layout); + SetupTools(layout); + + On.HeroController.SceneInit += (orig, self) => + { + orig(self); + _sceneInfo.Text = "Scene: " + GameManager.instance.sceneName; + }; + } - On.HeroController.SceneInit += (orig, self) => + catch (Exception e) { - orig(self); - _sceneInfo.Text = "Scene: " + GameManager.instance.sceneName; - }; + Debug.LogError($"Failed to initialize Editor UI: {e}"); + throw; // Re-throw to see the actual error + } } private static void SetupLeftSide(LayoutRoot layout) { + var basePadding = 20; + var baseRow1Height = 120; + var baseRow2Height = 280; + var baseRow3Height = 25; + var baseColumnWidth = 150; + var categoriesVerticalOffset = 40; + _leftSideGrid = new GridLayout(layout, "Left Side") { - Padding = new Padding(20, 15), + Padding = new Padding((int)(basePadding * _uiScaleFactor), (int)((basePadding + categoriesVerticalOffset) * _uiScaleFactor)), VerticalAlignment = VerticalAlignment.Bottom, HorizontalAlignment = HorizontalAlignment.Left, RowDefinitions = { - new GridDimension(120, GridUnit.AbsoluteMin), - new GridDimension(320, GridUnit.AbsoluteMin), - new GridDimension(24, GridUnit.AbsoluteMin) + new GridDimension((int)(baseRow1Height * _uiScaleFactor), GridUnit.AbsoluteMin), + new GridDimension((int)(baseRow2Height * _uiScaleFactor), GridUnit.AbsoluteMin), + new GridDimension((int)(baseRow3Height * _uiScaleFactor), GridUnit.AbsoluteMin) }, ColumnDefinitions = { - new GridDimension(160, GridUnit.AbsoluteMin), - new GridDimension(160, GridUnit.AbsoluteMin), - new GridDimension(160, GridUnit.AbsoluteMin) + new GridDimension((int)(baseColumnWidth * _uiScaleFactor), GridUnit.AbsoluteMin), + new GridDimension((int)(baseColumnWidth * _uiScaleFactor), GridUnit.AbsoluteMin), + new GridDimension((int)(baseColumnWidth * _uiScaleFactor), GridUnit.AbsoluteMin) } }; @@ -281,13 +312,21 @@ private static void SetupLeftSide(LayoutRoot layout) private static GridLayout SetupExtraControls(LayoutRoot layout) { + var baseInputWidth = 80; + var basePadding = 20; + var baseFontSize = 12; + + RotationChoice?.Destroy(); + ScaleChoice?.Destroy(); + RotationChoice = new TextInput(layout, "Rotation Input") { ContentType = InputField.ContentType.DecimalNumber, HorizontalAlignment = HorizontalAlignment.Right, - MinWidth = 80, + MinWidth = (int)(baseInputWidth * _uiScaleFactor), Text = "0", - Padding = new Padding(20, 10) + Padding = new Padding((int)(basePadding * _uiScaleFactor), (int)(10 * _uiScaleFactor)), + FontSize = (int)(baseFontSize * _uiScaleFactor) }.WithProp(GridLayout.Column, 1); RotationChoice.TextEditFinished += (_, s) => { @@ -299,9 +338,10 @@ private static GridLayout SetupExtraControls(LayoutRoot layout) { ContentType = InputField.ContentType.DecimalNumber, HorizontalAlignment = HorizontalAlignment.Right, - MinWidth = 80, + MinWidth = (int)(baseInputWidth * _uiScaleFactor), Text = "1", - Padding = new Padding(20, 10) + Padding = new Padding((int)(basePadding * _uiScaleFactor), (int)(10 * _uiScaleFactor)), + FontSize = (int)(baseFontSize * _uiScaleFactor) }.WithProp(GridLayout.Column, 1).WithProp(GridLayout.Row, 1); ScaleChoice.TextEditFinished += (_, s) => { @@ -313,8 +353,8 @@ private static GridLayout SetupExtraControls(LayoutRoot layout) { RowDefinitions = { - new GridDimension(1, GridUnit.Proportional), - new GridDimension(1, GridUnit.Proportional) + new GridDimension((int)(40 * _uiScaleFactor), GridUnit.AbsoluteMin), + new GridDimension((int)(40 * _uiScaleFactor), GridUnit.AbsoluteMin) }, ColumnDefinitions = { @@ -327,13 +367,17 @@ private static GridLayout SetupExtraControls(LayoutRoot layout) { Text = "Rotation", HorizontalAlignment = HorizontalAlignment.Left, - Padding = new Padding(0, 10) + VerticalAlignment = VerticalAlignment.Center, + Padding = new Padding(0, 10), + FontSize = (int)(baseFontSize * _uiScaleFactor) }, new TextObject(layout, "Scale Text") { Text = "Scale", HorizontalAlignment = HorizontalAlignment.Left, - Padding = new Padding(0, 10) + VerticalAlignment = VerticalAlignment.Center, + Padding = new Padding(0, 10), + FontSize = (int)(baseFontSize * _uiScaleFactor) }.WithProp(GridLayout.Row, 1), RotationChoice, ScaleChoice @@ -346,12 +390,23 @@ private static GridLayout SetupExtraControls(LayoutRoot layout) private static void SetupConfigArea(LayoutRoot layout) { + _configButton?.Destroy(); + _broadcasterButton?.Destroy(); + _receiverButton?.Destroy(); + + var baseButtonWidth = 140; + var baseFontSize = 12; + var configButtonsTopPadding = 10; + _configButton = new Button(layout, "Config Choice") { Content = "Config", HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Bottom, - MinWidth = 160, + MinWidth = (int)(baseButtonWidth * _uiScaleFactor), + MinHeight = (int)(25 * _uiScaleFactor), + FontSize = (int)(baseFontSize * _uiScaleFactor), + Padding = new Padding(0, (int)(configButtonsTopPadding * _uiScaleFactor)), Enabled = false }.WithProp(GridLayout.Row, 2); _configButton.Click += _ => @@ -365,8 +420,10 @@ private static void SetupConfigArea(LayoutRoot layout) Content = "Events", HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Bottom, - MinWidth = 160, - Padding = new Padding(20, 0), + MinWidth = (int)(baseButtonWidth * _uiScaleFactor), + MinHeight = (int)(25 * _uiScaleFactor), + FontSize = (int)(baseFontSize * _uiScaleFactor), + Padding = new Padding((int)(20 * _uiScaleFactor), (int)(configButtonsTopPadding * _uiScaleFactor)), Enabled = false }.WithProp(GridLayout.Column, 1).WithProp(GridLayout.Row, 2); _broadcasterButton.Click += _ => @@ -380,7 +437,10 @@ private static void SetupConfigArea(LayoutRoot layout) Content = "Listeners", HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Bottom, - MinWidth = 160, + MinWidth = (int)(baseButtonWidth * _uiScaleFactor), + MinHeight = (int)(25 * _uiScaleFactor), + FontSize = (int)(baseFontSize * _uiScaleFactor), + Padding = new Padding(0, (int)(configButtonsTopPadding * _uiScaleFactor)), Enabled = false }.WithProp(GridLayout.Column, 2).WithProp(GridLayout.Row, 2); _receiverButton.Click += _ => @@ -396,52 +456,67 @@ private static void SetupConfigArea(LayoutRoot layout) private static void SetupTextDisplay(LayoutRoot layout) { - _ = new TextObject(layout) + var baseFontSize = 12; + + _editorEnabledText?.Destroy(); + _selectionInfo?.Destroy(); + _sceneInfo?.Destroy(); + _extraInfo?.Destroy(); + _bigText?.Destroy(); + + var basePadding = 80; + var paddingIncrement = 35; + + _editorEnabledText = new TextObject(layout) { HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top, - MaxWidth = 200, - MaxHeight = 20, - Padding = new Padding(0, 60), - Text = "Editor Enabled" + MaxWidth = (int)(250 * _uiScaleFactor), + MaxHeight = (int)(25 * _uiScaleFactor), + Padding = new Padding(0, (int)(basePadding * _uiScaleFactor)), + Text = "Editor Enabled", + FontSize = (int)(baseFontSize * _uiScaleFactor) }; _selectionInfo = new TextObject(layout) { HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top, - Padding = new Padding(0, 90), - MaxWidth = 1000, - MaxHeight = 20, - Text = "Current Item: None" + Padding = new Padding(0, (int)((basePadding + paddingIncrement) * _uiScaleFactor)), + MaxWidth = (int)(1200 * _uiScaleFactor), + MaxHeight = (int)(20 * _uiScaleFactor), + Text = "Current Item: None", + FontSize = (int)(baseFontSize * _uiScaleFactor) }; _sceneInfo = new TextObject(layout) { HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top, - Padding = new Padding(0, 120), - MaxWidth = 1000, - MaxHeight = 20, - Text = "Scene: None" + Padding = new Padding(0, (int)((basePadding + paddingIncrement * 2) * _uiScaleFactor)), + MaxWidth = (int)(1200 * _uiScaleFactor), + MaxHeight = (int)(25 * _uiScaleFactor), + Text = "Scene: None", + FontSize = (int)(baseFontSize * _uiScaleFactor) }; _extraInfo = new TextObject(layout) { HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top, - Padding = new Padding(0, 150), - MaxWidth = 1000, - MaxHeight = 20, + Padding = new Padding(0, (int)((basePadding + paddingIncrement * 3) * _uiScaleFactor)), + MaxWidth = (int)(1200 * _uiScaleFactor), + MaxHeight = (int)(25 * _uiScaleFactor), Text = "ID: None", - Visibility = Visibility.Hidden + Visibility = Visibility.Hidden, + FontSize = (int)(baseFontSize * _uiScaleFactor) }; _bigText = new TextObject(layout) { HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, - FontSize = 128, + FontSize = (int)(128 * _uiScaleFactor), Text = "" }; } @@ -462,27 +537,57 @@ public static async Task DisplayExtraInfo(string info) private static GridLayout SetupCategories(LayoutRoot layout) { + var existingCategories = _leftSideGrid?.Children?.FirstOrDefault(c => c.Name == "Categories"); + + existingCategories?.Destroy(); + _categories = []; SetCategory(FavouritesCategory.Instance); Dictionary normalCategories = new(); - foreach (var element in ContentPacks.Packs.Where(pack => pack.IsEnabled()).SelectMany(pack => pack)) + + if (ContentPacks.Packs != null) { - if (!normalCategories.ContainsKey(element.GetCategory())) + foreach (var element in ContentPacks.Packs.Where(pack => pack?.IsEnabled() == true).SelectMany(pack => pack)) { - var normalCategory = new NormalCategory(element.GetCategory()); - normalCategories.Add(element.GetCategory(), normalCategory); - _categories.Add(normalCategory); - } + if (element?.GetCategory() == null) continue; + + if (!normalCategories.ContainsKey(element.GetCategory())) + { + var normalCategory = new NormalCategory(element.GetCategory()); + normalCategories.Add(element.GetCategory(), normalCategory); + _categories.Add(normalCategory); + } - normalCategories[element.GetCategory()].AddObject(PlaceableObject.Create(element)); + var placeable = PlaceableObject.Create(element); + if (placeable != null) + { + normalCategories[element.GetCategory()].AddObject(placeable); + } + } } foreach (var category in normalCategories.Values) category.Sort(); - _categories.Add(new BlankCategory()); - _categories.Add(new PrefabsCategory()); + try + { + _categories.Add(new BlankCategory()); + } + catch (Exception e) + { + Debug.LogError($"Failed to create BlankCategory: {e}"); + } + + try + { + _categories.Add(new PrefabsCategory()); + } + catch (Exception e) + { + Debug.LogError($"Failed to create PrefabsCategory: {e}"); + } + _categories.Add(_category); var grid = new GridLayout(layout, "Categories") @@ -497,13 +602,15 @@ private static GridLayout SetupCategories(LayoutRoot layout) { columnCount -= 1; grid.RowDefinitions.Add(new GridDimension(1, GridUnit.Proportional)); - if (!category.CreateButton()) continue; + if (category == null || !category.CreateButton()) continue; var button = new Button(layout, category.GetName()) { VerticalAlignment = VerticalAlignment.Bottom, Padding = new Padding(0, 2), - Content = category.GetName() + Content = category.GetName(), + FontSize = (int)(12 * _uiScaleFactor), + MinHeight = (int)(30 * _uiScaleFactor) }.WithProp(GridLayout.Row, columnCount); button.Click += _ => { SetCategory(category); }; grid.Children.Add(button); @@ -514,31 +621,68 @@ private static GridLayout SetupCategories(LayoutRoot layout) private static void SetupObjectOptions(LayoutRoot layout) { + var baseButtonSpacingX = 100; + var baseButtonSpacingY = 100; + var baseGridOffsetX = 30; + var baseGridOffsetY = 30; + + foreach (var (button, favButton, image) in _selectionButtons) + { + button?.Destroy(); + favButton?.Destroy(); + image?.Destroy(); + } + + _selectionButtons.Clear(); + for (var i = 0; i < ItemsPerGroup; i++) { - var j = 2 - i / 3; + var row = 2 - i / 3; + var col = i % 3; + + var horizontalPos = baseGridOffsetX + col * baseButtonSpacingX * _uiScaleFactor; + var verticalPos = baseGridOffsetY + row * baseButtonSpacingY * _uiScaleFactor; var (button, image) = - CreateImagedButton(layout, Architect.BlankSprite, i.ToString(), (2 - i % 3) * 100, j * 100, i); + CreateImagedButton(layout, Architect.BlankSprite, i.ToString(), (2 - i % 3) * 100, row * 100, i); var favourite = new Button(layout, i + " Favourite") { Content = EmptyStar, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Bottom, - MinHeight = 20, - MinWidth = 20, + MinHeight = (int)(20 * _uiScaleFactor), + MinWidth = (int)(20 * _uiScaleFactor), + FontSize = (int)(12 * _uiScaleFactor), Borderless = true, - Padding = new Padding((2 - i % 3) * 100 + 92, j * 100 + 45) + Padding = new Padding( + (int)((horizontalPos + 50) * _uiScaleFactor), + (int)((verticalPos + 50) * _uiScaleFactor) + ), }; var k = i; favourite.Click += _ => { ToggleFavourite(k); }; PauseOptions.Add(favourite); _selectionButtons.Add((button, favourite, image)); + + PauseOptions.Add(button); + PauseOptions.Add(image); } - CreateImagedButton(layout, ResetObject.Instance.GetSprite(), "Reset", 0, 360, -5); + var resetHorizontalPos = baseGridOffsetX; + var resetVerticalPos = baseGridOffsetY + 3 * baseButtonSpacingY; + + var existingReset = PauseOptions.FirstOrDefault(x => x.Name?.Contains("Reset Button") == true); + if (existingReset == null) + { + var (resetButton, resetImage) = CreateImagedButton(layout, ResetObject.Instance.GetSprite(), "Reset", + resetHorizontalPos, resetVerticalPos, -5); + + // Add reset button and image to PauseOptions + PauseOptions.Add(resetButton); + PauseOptions.Add(resetImage); + } RefreshObjects(); RefreshButtons(); @@ -546,13 +690,18 @@ private static void SetupObjectOptions(LayoutRoot layout) private static void SetupFilter(LayoutRoot layout) { + var baseFilterWidth = 200; + var baseFilterX = 100; + var baseFilterY = 350; + var filter = new TextInput(layout, "Search") { ContentType = InputField.ContentType.Standard, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Bottom, - MinWidth = 200, - Padding = new Padding(55, 315) + MinWidth = (int)(baseFilterWidth * _uiScaleFactor), + FontSize = (int)(12 * _uiScaleFactor), + Padding = new Padding((int)(baseFilterX * _uiScaleFactor), (int)(baseFilterY * _uiScaleFactor)) }; filter.TextChanged += (_, s) => { @@ -565,15 +714,105 @@ private static void SetupFilter(LayoutRoot layout) PauseOptions.Add(filter); } + public static void ApplyUIScale(float scaleFactor) + { + _uiScaleFactor = scaleFactor; + + if (_layout != null && EditorManager.IsEditing) + { + var currentSelectedItem = SelectedItem; + var currentIndex = _index; + var currentGroupIndex = _groupIndex; + var currentCategory = _category; + var currentFilter = _filter; + + // Destroy current UI elements + foreach (var element in PauseOptions.ToArray()) + { + if (element != null) + { + element.Destroy(); + } + } + + PauseOptions.Clear(); + + if (_selectionButtons != null) + { + foreach (var (button, favButton, image) in _selectionButtons) + { + button?.Destroy(); + favButton?.Destroy(); + image?.Destroy(); + } + _selectionButtons.Clear(); + } + else + { + _selectionButtons = new List<(Button, Button, Image)>(); + } + + _configGrid?.Destroy(); + _broadcastersGrid?.Destroy(); + _receiversGrid?.Destroy(); + _editorEnabledText?.Destroy(); + _selectionInfo?.Destroy(); + _sceneInfo?.Destroy(); + _extraInfo?.Destroy(); + _bigText?.Destroy(); + _leftSideGrid?.Destroy(); + _configButton?.Destroy(); + _broadcasterButton?.Destroy(); + _receiverButton?.Destroy(); + RotationChoice?.Destroy(); + ScaleChoice?.Destroy(); + + _configGrid = null; + _broadcastersGrid = null; + _receiversGrid = null; + _editorEnabledText = null; + _selectionInfo = null; + _sceneInfo = null; + _extraInfo = null; + _bigText = null; + _leftSideGrid = null; + _configButton = null; + _broadcasterButton = null; + _receiverButton = null; + RotationChoice = null; + ScaleChoice = null; + + Initialize(_layout); + + _category = currentCategory; + _groupIndex = currentGroupIndex; + _index = currentIndex; + _filter = currentFilter; + SelectedItem = currentSelectedItem; + + RefreshObjects(); + RefreshButtons(); + RefreshSelectedItem(false); + } + } + private static void SetupTools(LayoutRoot layout) { + var baseToolsPaddingX = 300; + var baseToolsPaddingY = 30; + var toolsRightPadding = 80; + var toolSpacing = 70; + var extraSettings = new GridLayout(layout, "Extra Settings") { HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Bottom, - Padding = new Padding(335, 0), + Padding = new Padding((int)((baseToolsPaddingX + toolsRightPadding) * _uiScaleFactor), + (int)(baseToolsPaddingY * _uiScaleFactor)), ColumnDefinitions = { + new GridDimension(1, GridUnit.Proportional), + new GridDimension(1, GridUnit.Proportional), new GridDimension(1, GridUnit.Proportional), new GridDimension(1, GridUnit.Proportional), new GridDimension(1, GridUnit.Proportional), @@ -581,7 +820,7 @@ private static void SetupTools(LayoutRoot layout) }, RowDefinitions = { - new GridDimension(1, GridUnit.Proportional) + new GridDimension((int)(toolSpacing * _uiScaleFactor), GridUnit.AbsoluteMin) } }; @@ -589,41 +828,55 @@ private static void SetupTools(LayoutRoot layout) if (Architect.UsingMultiplayer) { - extraSettings.RowDefinitions.Add(new GridDimension(0.2f, GridUnit.Proportional)); + extraSettings.RowDefinitions.Add(new GridDimension((int)(40 * _uiScaleFactor), GridUnit.AbsoluteMin)); var multiplayerRefresh = new Button(layout, "Refresh") { VerticalAlignment = VerticalAlignment.Bottom, HorizontalAlignment = HorizontalAlignment.Center, Content = "Reshare Level (HKMP)" - }.WithProp(GridLayout.Column, 0).WithProp(GridLayout.ColumnSpan, 4).WithProp(GridLayout.Row, correctRow); + }.WithProp(GridLayout.Column, 0).WithProp(GridLayout.ColumnSpan, 6).WithProp(GridLayout.Row, correctRow); multiplayerRefresh.Click += _ => { HkmpHook.Refresh(); }; extraSettings.Children.Add(multiplayerRefresh); correctRow++; } - var cursorImagedButton = CreateImagedButton(layout, CursorObject.Instance.GetSprite(), "Cursor", 0, 0, -1); + var toolOffset = 0; + + var cursorImagedButton = CreateImagedButton(layout, CursorObject.Instance.GetSprite(), "Cursor", toolOffset, toolOffset, -1); cursorImagedButton.Item1.WithProp(GridLayout.Column, 0).WithProp(GridLayout.Row, correctRow); - cursorImagedButton.Item2.WithProp(GridLayout.Column, 0).WithProp(GridLayout.Row, correctRow); + cursorImagedButton.Item2.WithProp(GridLayout.Column, 0).WithProp(GridLayout.Row, correctRow); extraSettings.Children.Add(cursorImagedButton.Item1); - extraSettings.Children.Add(cursorImagedButton.Item2); + extraSettings.Children.Add(cursorImagedButton.Item2); - var dragImagedButton = CreateImagedButton(layout, DragObject.Instance.GetSprite(), "Drag", 0, 0, -3); + var dragImagedButton = CreateImagedButton(layout, DragObject.Instance.GetSprite(), "Drag", toolOffset, toolOffset, -3); dragImagedButton.Item1.WithProp(GridLayout.Column, 1).WithProp(GridLayout.Row, correctRow); - dragImagedButton.Item2.WithProp(GridLayout.Column, 1).WithProp(GridLayout.Row, correctRow); + dragImagedButton.Item2.WithProp(GridLayout.Column, 1).WithProp(GridLayout.Row, correctRow); extraSettings.Children.Add(dragImagedButton.Item1); - extraSettings.Children.Add(dragImagedButton.Item2); + extraSettings.Children.Add(dragImagedButton.Item2); - var pickImagedButton = CreateImagedButton(layout, PickObject.Instance.GetSprite(), "Pick", 0, 0, -4); + var pickImagedButton = CreateImagedButton(layout, PickObject.Instance.GetSprite(), "Pick", toolOffset, toolOffset, -4); pickImagedButton.Item1.WithProp(GridLayout.Column, 2).WithProp(GridLayout.Row, correctRow); - pickImagedButton.Item2.WithProp(GridLayout.Column, 2).WithProp(GridLayout.Row, correctRow); + pickImagedButton.Item2.WithProp(GridLayout.Column, 2).WithProp(GridLayout.Row, correctRow); extraSettings.Children.Add(pickImagedButton.Item1); - extraSettings.Children.Add(pickImagedButton.Item2); + extraSettings.Children.Add(pickImagedButton.Item2); - var eraserImagedButton = CreateImagedButton(layout, EraserObject.Instance.GetSprite(), "Eraser", 0, 0, -2); + var eraserImagedButton = CreateImagedButton(layout, EraserObject.Instance.GetSprite(), "Eraser", toolOffset, toolOffset, -2); eraserImagedButton.Item1.WithProp(GridLayout.Column, 3).WithProp(GridLayout.Row, correctRow); - eraserImagedButton.Item2.WithProp(GridLayout.Column, 3).WithProp(GridLayout.Row, correctRow); + eraserImagedButton.Item2.WithProp(GridLayout.Column, 3).WithProp(GridLayout.Row, correctRow); extraSettings.Children.Add(eraserImagedButton.Item1); - extraSettings.Children.Add(eraserImagedButton.Item2); + extraSettings.Children.Add(eraserImagedButton.Item2); + + var lockImagedButton = CreateImagedButton(layout, LockObject.Instance.GetSprite(), "Lock", 0, 0, -6); + lockImagedButton.Item1.WithProp(GridLayout.Column, 4).WithProp(GridLayout.Row, correctRow); + lockImagedButton.Item2.WithProp(GridLayout.Column, 4).WithProp(GridLayout.Row, correctRow); + extraSettings.Children.Add(lockImagedButton.Item1); + extraSettings.Children.Add(lockImagedButton.Item2); + + var tileImagedButton = CreateImagedButton(layout, TilemapObject.Instance.GetSprite(), "Tilemap Editor", 0, 0, -7); + tileImagedButton.Item1.WithProp(GridLayout.Column, 5).WithProp(GridLayout.Row, correctRow); + tileImagedButton.Item2.WithProp(GridLayout.Column, 5).WithProp(GridLayout.Row, correctRow); + extraSettings.Children.Add(tileImagedButton.Item1); + extraSettings.Children.Add(tileImagedButton.Item2); PauseOptions.Add(extraSettings); } @@ -631,14 +884,15 @@ private static void SetupTools(LayoutRoot layout) private static (Button, Image) CreateImagedButton(LayoutRoot layout, Sprite sprite, string name, int horizontalPadding, int verticalPadding, int index) { + var ext = index < 0 ? 0 : 10; var img = new Image(layout, sprite, name + " Image") { HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Bottom, - Height = 40, - Width = 40, + Height = 40 + 2 * ext, + Width = 40 + 2 * ext, PreserveAspectRatio = true, - Padding = new Padding(horizontalPadding + 35, verticalPadding + 35) + Padding = new Padding(horizontalPadding + 35 - ext, verticalPadding + 35 - ext) }; var button = new Button(layout, name + " Button") @@ -646,9 +900,24 @@ private static (Button, Image) CreateImagedButton(LayoutRoot layout, Sprite spri Content = "", HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Bottom, - MinHeight = 80, - MinWidth = 80, - Padding = new Padding(horizontalPadding + 15, verticalPadding + 15) + MinHeight = buttonSize, + MinWidth = buttonSize, + Padding = new Padding( + (int)(horizontalPadding * _uiScaleFactor), + (int)(verticalPadding * _uiScaleFactor)) + }; + + var imageHorizontalPos = (int)(horizontalPadding * _uiScaleFactor) + (buttonSize - imageSize) / 2; + var imageVerticalPos = (int)(verticalPadding * _uiScaleFactor) + (buttonSize - imageSize) / 2; + + var img = new Image(layout, sprite, name + " Image") + { + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Bottom, + Height = imageSize, + Width = imageSize, + PreserveAspectRatio = true, + Padding = new Padding(imageHorizontalPos, imageVerticalPos) }; button.Click += _ => @@ -659,8 +928,8 @@ private static (Button, Image) CreateImagedButton(LayoutRoot layout, Sprite spri SelectedItem?.AfterSelect(); }; - PauseOptions.Add(img); PauseOptions.Add(button); + PauseOptions.Add(img); return (button, img); } @@ -1138,4 +1407,4 @@ private enum ConfigMode Broadcasters, Receivers } -} \ No newline at end of file +} diff --git a/WorldEditor/Architect/Util/EditorManager.cs b/WorldEditor/Architect/Util/EditorManager.cs index bd030b7..25a4510 100644 --- a/WorldEditor/Architect/Util/EditorManager.cs +++ b/WorldEditor/Architect/Util/EditorManager.cs @@ -66,7 +66,7 @@ public static void Initialize() On.GameManager.SaveGame += (orig, self) => { - if (IsEditing) SceneSaveLoader.SaveScene(self.sceneName, PlacementManager.GetCurrentPlacements()); + if (IsEditing) SceneSaveLoader.SaveScene(self.sceneName, PlacementManager.GetCurrentLevel()); orig(self); }; @@ -331,7 +331,7 @@ private static void CheckToggle(bool paused) if (HeroController.instance.controlReqlinquished) return; if (IsEditing) - SceneSaveLoader.SaveScene(GameManager.instance.sceneName, PlacementManager.GetCurrentPlacements()); + SceneSaveLoader.SaveScene(GameManager.instance.sceneName, PlacementManager.GetCurrentLevel()); else _freeMovePos = HeroController.instance.transform.position; IsEditing = !IsEditing; @@ -366,7 +366,11 @@ private static void TryPlace() var b1 = Input.GetMouseButtonDown(0); var b2 = Input.GetMouseButton(0); - if (!b2) ResetObject.RestartDelay(); + if (!b2) + { + ResetObject.RestartDelay(); + TilemapObject.Release(); + } if (!b1 && !b2) return; @@ -496,6 +500,7 @@ public static void DoPaste() pl.Name, wp - obj.Offset, pl.Flipped, + false, pl.Rotation, pl.Scale, converts[pl.GetId()], diff --git a/WorldEditor/Architect/Util/UndoManager.cs b/WorldEditor/Architect/Util/UndoManager.cs index d45c898..550f6be 100644 --- a/WorldEditor/Architect/Util/UndoManager.cs +++ b/WorldEditor/Architect/Util/UndoManager.cs @@ -105,6 +105,25 @@ public IUndoable Undo() } } +public class ToggleTile(List<(int, int)> tiles, bool empty) : IUndoable +{ + public IUndoable Undo() + { + var map = PlacementManager.GetTilemap(); + if (!map) return null; + foreach (var (x, y) in tiles) + { + if (empty) map.ClearTile(x, y, 0); + else map.SetTile(x, y, 0, 0); + + PlacementManager.GetCurrentLevel().ToggleTile((x, y)); + } + map.Build(); + + return new ToggleTile(tiles, !empty); + } +} + public class MoveObjects(List<(string, Vector3)> data) : IUndoable { public IUndoable Undo()