diff --git a/Assets/# Prototype/Shaders/ContactCollider_v2.mat b/Assets/# Prototype/Shaders/ObstacleCollider_v2.mat similarity index 94% rename from Assets/# Prototype/Shaders/ContactCollider_v2.mat rename to Assets/# Prototype/Shaders/ObstacleCollider_v2.mat index d2b685d..29beeb3 100644 --- a/Assets/# Prototype/Shaders/ContactCollider_v2.mat +++ b/Assets/# Prototype/Shaders/ObstacleCollider_v2.mat @@ -7,7 +7,7 @@ Material: m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_Name: ContactCollider_v2 + m_Name: ObstacleCollider_v2 m_Shader: {fileID: -6465566751694194690, guid: 0ca531f654dad4a6ca8be40f93612d9e, type: 3} m_Parent: {fileID: 0} m_ModifiedSerializedProperties: 0 @@ -47,7 +47,7 @@ Material: - _QueueOffset: 0 - _SideDarkness: 0.6 m_Colors: - - _BaseColor: {r: 1, g: 0.5529412, b: 0.5647059, a: 1} + - _BaseColor: {r: 0.3990299, g: 0.5432971, b: 0.5754717, a: 1} m_BuildTextureStacks: [] m_AllowLocking: 1 --- !u!114 &1069900037742449899 diff --git a/Assets/# Prototype/Shaders/ContactCollider_v2.mat.meta b/Assets/# Prototype/Shaders/ObstacleCollider_v2.mat.meta similarity index 100% rename from Assets/# Prototype/Shaders/ContactCollider_v2.mat.meta rename to Assets/# Prototype/Shaders/ObstacleCollider_v2.mat.meta diff --git a/Assets/# Prototype/Stages.meta b/Assets/# Prototype/Stages.meta new file mode 100644 index 0000000..cf3caa7 --- /dev/null +++ b/Assets/# Prototype/Stages.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 78ca007dda30545739e1fce83a853093 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/# Prototype/Stages/StageDefinition.asset b/Assets/# Prototype/Stages/StageDefinition.asset new file mode 100644 index 0000000..e6a828e --- /dev/null +++ b/Assets/# Prototype/Stages/StageDefinition.asset @@ -0,0 +1,22 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 778f9d79eb2c410482efdc25b248dd46, type: 3} + m_Name: StageDefinition + m_EditorClassIdentifier: Assembly-CSharp::Intrepid.Gameplay.Prototyping.StageDefinition + stageId: stagedefinition + cellSize: 1 + floorHeight: 6 + activeFloorIndex: 0 + floors: + - floorId: F0 + cells: [] + objects: [] diff --git a/Assets/# Prototype/Stages/StageDefinition.asset.meta b/Assets/# Prototype/Stages/StageDefinition.asset.meta new file mode 100644 index 0000000..5d9660a --- /dev/null +++ b/Assets/# Prototype/Stages/StageDefinition.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bb896366809bc439b8be4d0f1158dc6f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Prototypes/# Contact_Prototype_BASE.prefab b/Assets/Prefabs/Prototypes/# Obstacle_Prototype_BASE.prefab similarity index 99% rename from Assets/Prefabs/Prototypes/# Contact_Prototype_BASE.prefab rename to Assets/Prefabs/Prototypes/# Obstacle_Prototype_BASE.prefab index 37c402c..0011fbe 100644 --- a/Assets/Prefabs/Prototypes/# Contact_Prototype_BASE.prefab +++ b/Assets/Prefabs/Prototypes/# Obstacle_Prototype_BASE.prefab @@ -298,7 +298,7 @@ GameObject: m_Component: - component: {fileID: 7939123536182165016} m_Layer: 30 - m_Name: '# Contact_Prototype_BASE' + m_Name: '# Obstacle_Prototype_BASE' m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 diff --git a/Assets/Prefabs/Prototypes/# Contact_Prototype_BASE.prefab.meta b/Assets/Prefabs/Prototypes/# Obstacle_Prototype_BASE.prefab.meta similarity index 100% rename from Assets/Prefabs/Prototypes/# Contact_Prototype_BASE.prefab.meta rename to Assets/Prefabs/Prototypes/# Obstacle_Prototype_BASE.prefab.meta diff --git a/Assets/Prefabs/Prototypes/Contact_Cube_Prototype.prefab b/Assets/Prefabs/Prototypes/Obstacle_Cube_Prototype.prefab similarity index 99% rename from Assets/Prefabs/Prototypes/Contact_Cube_Prototype.prefab rename to Assets/Prefabs/Prototypes/Obstacle_Cube_Prototype.prefab index 7154fbe..a646d96 100644 --- a/Assets/Prefabs/Prototypes/Contact_Cube_Prototype.prefab +++ b/Assets/Prefabs/Prototypes/Obstacle_Cube_Prototype.prefab @@ -74,7 +74,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 8449749668273782584, guid: 64620cfd283d648b5b4d9c9a4fac7751, type: 3} propertyPath: m_Name - value: Contact_Cube_Prototype + value: Obstacle_Cube_Prototype objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] diff --git a/Assets/Prefabs/Prototypes/Contact_Cube_Prototype.prefab.meta b/Assets/Prefabs/Prototypes/Obstacle_Cube_Prototype.prefab.meta similarity index 100% rename from Assets/Prefabs/Prototypes/Contact_Cube_Prototype.prefab.meta rename to Assets/Prefabs/Prototypes/Obstacle_Cube_Prototype.prefab.meta diff --git a/Assets/Resources/Services/Player/Sentinel/Cadence Profiles/Dash - Cadence Profile.asset b/Assets/Resources/Services/Player/Sentinel/Cadence Profiles/Dash - Cadence Profile.asset index aa97297..cd420fa 100644 --- a/Assets/Resources/Services/Player/Sentinel/Cadence Profiles/Dash - Cadence Profile.asset +++ b/Assets/Resources/Services/Player/Sentinel/Cadence Profiles/Dash - Cadence Profile.asset @@ -12,10 +12,9 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f4d583fd594418e8ecea0731be75f23, type: 3} m_Name: Dash - Cadence Profile m_EditorClassIdentifier: Assembly-CSharp::Intrepid.Gameplay.Entities.Player.Sentinel.Cadence.CadenceProfile - totalTimeMS: 400 - strainedEndP: 0.25 - alignedEndP: 0.65 - resonantEndP: 1 + strained: 0.25 + aligned: 0.65 + resonant: 1 strainedSettlement: 1 alignedSettlement: 0.5 resonantSettlement: 0 diff --git a/Assets/Resources/Services/Player/Sentinel/Core Stability Service.asset b/Assets/Resources/Services/Player/Sentinel/Core Stability Service.asset index c9b44b6..3c9dffd 100644 --- a/Assets/Resources/Services/Player/Sentinel/Core Stability Service.asset +++ b/Assets/Resources/Services/Player/Sentinel/Core Stability Service.asset @@ -16,4 +16,4 @@ MonoBehaviour: unstableThresholdP: 0.7 criticalThresholdP: 0.35 collapsedRecoveryStability: 25 - naturalRecoveryPerSecond: 10 + naturalRecoveryPerSecond: 2 diff --git a/Assets/Scripts/Editor/GardensStageEditor.meta b/Assets/Scripts/Editor/GardensStageEditor.meta new file mode 100644 index 0000000..e50248f --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 93fdd19587ea435285157d73a25958f0 +timeCreated: 1781500522 \ No newline at end of file diff --git a/Assets/Scripts/Editor/GardensStageEditor/README.md b/Assets/Scripts/Editor/GardensStageEditor/README.md new file mode 100644 index 0000000..2e96e49 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/README.md @@ -0,0 +1,109 @@ +# Gardens Stage Editor Starter Kit + +Starter kit para um editor lógico de stages do **Gardens of Nil** dentro da Unity. + +Este pacote contém somente: + +- modelo de dados (`StageDefinition`, floors, células estruturais e objetos lógicos); +- janela de editor com UI Toolkit; +- grid 1x1 com pan/zoom; +- paleta por layer; +- criação/seleção/remoção de elementos lógicos; +- inspector simples; +- validação básica. + +Não contém bake/rebake de scene, geração de colliders ou integração com Tiled. + +## Instalação + +Descompacte a pasta `GardensStageEditor` dentro de: + +```txt +Assets/Editor/ +``` + +O resultado deve ficar parecido com: + +```txt +Assets/Editor/GardensStageEditor/ + Data/ + Editor/ + README.md +``` + +Depois espere a Unity recompilar. + +## Abrir o editor + +Na Unity: + +```txt +Gardens > Stage Editor +``` + +Na janela: + +1. Clique em `New` para criar um `StageDefinition.asset`. +2. Escolha a layer na paleta inferior. +3. Use `Paint`, `Select`, `Erase` ou `Pan`. +4. Pinte o mapa lógico no canvas central. +5. Edite o elemento selecionado no inspector à direita. +6. Use `Validate` para checagens básicas. + +## Controles + +```txt +LMB: pintar / selecionar +RMB ou MMB: pan +Mouse wheel: zoom +``` + +## Modelo mental + +O editor trabalha em um plano lógico 2D: + +```txt +Editor X/Y +``` + +Mais tarde, o baker pode mapear isso para o mundo Unity: + +```txt +Editor X -> Unity X +Editor Y -> Unity Z +Floor index -> Unity Y offset +``` + +## Layers iniciais + +```txt +Structural: Ground, Wall, Hole, Erase Structural +Design: Falling Floor, Door, Interactable, Portal +Camera: Camera Zone +Markers: Player Spawn, Elevator, Marker +``` + +## Filosofia + +A scene não é fonte da verdade. + +```txt +StageDefinition.asset = source of truth +Generated scene/prefab = output futuro do baker +``` + +Este pacote só cuida do primeiro lado: autoria visual e dados lógicos. + +## Próximos passos sugeridos + +1. Transformar `StagePaletteEntry` em assets configuráveis, se necessário. +2. Adicionar hotkeys e melhor seleção/move/resize visual. +3. Adicionar overlays de erro no canvas. +4. Implementar o baker separado consumindo `StageDefinition`. +5. Depois, se fizer sentido, criar import/export Tiled apenas para o mapa lógico. + +## Requisitos recomendados + +Unity 2022.3 LTS ou superior. Unity 6 também deve funcionar. + +O canvas usa UI Toolkit custom drawing (`Painter2D`). Em versões muito antigas da Unity, pode ser necessário trocar o canvas por IMGUI ou SceneView Handles. diff --git a/Assets/Scripts/Editor/GardensStageEditor/README.md.meta b/Assets/Scripts/Editor/GardensStageEditor/README.md.meta new file mode 100644 index 0000000..1f165fc --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/README.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 494fb84451f94e218111409283a7f795 +timeCreated: 1781500522 \ No newline at end of file diff --git a/Assets/Scripts/Editor/GardensStageEditor/STAGE_MODEL_NOTES.md b/Assets/Scripts/Editor/GardensStageEditor/STAGE_MODEL_NOTES.md new file mode 100644 index 0000000..f611682 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/STAGE_MODEL_NOTES.md @@ -0,0 +1,71 @@ +# Stage Model Notes + +## StageDefinition + +Fonte da verdade do stage lógico. + +Contém: + +- `stageId` +- `cellSize` +- `floorHeight` +- `floors` +- `objects` + +## StageFloorDefinition + +Um andar lógico do stage. + +Contém: + +- `floorId` +- `width` +- `height` +- células estruturais preenchidas + +Células vazias não são serializadas explicitamente. Apenas células com `Ground`, `Wall` ou `Hole` são guardadas. + +## Structural cells + +A layer estrutural é cell-based. + +```txt +Empty +Ground +Wall +Hole +``` + +## StageObjectDefinition + +Objetos lógicos são object-based. + +Exemplos: + +- traps +- doors +- interactables +- portals +- camera zones +- spawn points +- elevators +- markers + +Cada objeto tem: + +- `id` +- `layer` +- `kind` +- `typeId` +- `prefabId` +- `floorId` +- `x`, `y`, `width`, `height` +- flags como `cutsGround` e `blocksMovement` + +## Regra sugerida para bake futuro + +```txt +GroundCollision = Structural.Ground - Structural.Hole - Objects(cutsGround) +``` + +Depois o baker pode compactar células em BoxColliders maiores. diff --git a/Assets/Scripts/Editor/GardensStageEditor/STAGE_MODEL_NOTES.md.meta b/Assets/Scripts/Editor/GardensStageEditor/STAGE_MODEL_NOTES.md.meta new file mode 100644 index 0000000..f75e4da --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/STAGE_MODEL_NOTES.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ffb9c2df7a0f4b3ca08544979287cf20 +timeCreated: 1781500522 \ No newline at end of file diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageDefinition.cs b/Assets/Scripts/Editor/GardensStageEditor/StageDefinition.cs new file mode 100644 index 0000000..816d9f9 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageDefinition.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Editor.GardensStageEditor +{ + public enum StageLogicalLayer + { + Structural = 0, + Design = 1, + Camera = 2, + Markers = 3 + } + + public enum StructuralCellKind + { + Empty = 0, + Ground = 1, + Wall = 2, + Hole = 3 + } + + public enum StageObjectKind + { + Trap = 0, + Door = 1, + Interactable = 2, + Portal = 3, + CameraZone = 4, + Spawn = 5, + Elevator = 6, + Marker = 7, + Custom = 100 + } + + [CreateAssetMenu(menuName = "GON/Editor/Stage Definition", fileName = "StageDefinition")] + public sealed class StageDefinition : ScriptableObject + { + public string stageId = "stage_new"; + public float cellSize = 1f; + public float floorHeight = 6f; + public int activeFloorIndex; + + public List floors = new(); + public List objects = new(); + + public StageFloorDefinition ActiveFloor + { + get + { + EnsureDefaults(); + if (activeFloorIndex < 0) activeFloorIndex = 0; + if (activeFloorIndex >= floors.Count) activeFloorIndex = floors.Count - 1; + return floors[activeFloorIndex]; + } + } + + public void EnsureDefaults() + { + floors ??= new List(); + objects ??= new List(); + + if (floors.Count == 0) + { + floors.Add(new StageFloorDefinition + { + floorId = "F0" + }); + } + + if (cellSize <= 0f) + { + cellSize = 1f; + } + + if (floorHeight <= 0f) + { + floorHeight = 6f; + } + } + + public StageFloorDefinition GetFloor(string floorId) + { + EnsureDefaults(); + foreach (var stageFloorDefinition in floors) + { + if (stageFloorDefinition != null && stageFloorDefinition.floorId == floorId) + { + return stageFloorDefinition; + } + } + + return null; + } + + public bool ContainsFloor(string floorId) + { + return GetFloor(floorId) != null; + } + + public string CreateUniqueObjectId(StageObjectKind kind) + { + objects ??= new List(); + + var prefix = kind.ToString().ToLowerInvariant(); + var index = 1; + while (true) + { + var candidate = prefix + "_" + index.ToString("000"); + var exists = false; + foreach (var stageObjectDefinition in objects) + { + if (stageObjectDefinition != null && stageObjectDefinition.id == candidate) + { + exists = true; + break; + } + } + + if (!exists) + { + return candidate; + } + + index++; + } + } + } + + [Serializable] + public sealed class StageFloorDefinition + { + public string floorId = "F0"; + + [SerializeField] + private List cells = new(); + + public List Cells + { + get + { + cells ??= new List(); + return cells; + } + } + + // Infinite/sparse floor: every integer coordinate is valid. + public bool IsInside(int x, int y) + { + return true; + } + + public StructuralCellKind GetCell(int x, int y) + { + int index = FindCellIndex(x, y); + if (index < 0) + { + return StructuralCellKind.Empty; + } + + return Cells[index].structural; + } + + public void SetCell(int x, int y, StructuralCellKind kind) + { + var index = FindCellIndex(x, y); + if (kind == StructuralCellKind.Empty) + { + if (index >= 0) + { + Cells.RemoveAt(index); + } + + return; + } + + LogicalCell cell = new LogicalCell + { + x = x, + y = y, + structural = kind + }; + + if (index >= 0) + { + Cells[index] = cell; + } + else + { + Cells.Add(cell); + } + } + + public void FillRect(RectInt rect, StructuralCellKind kind) + { + for (var y = rect.yMin; y < rect.yMax; y++) + { + for (var x = rect.xMin; x < rect.xMax; x++) + { + SetCell(x, y, kind); + } + } + } + + public bool TryGetUsedBounds(out RectInt bounds) + { + if (Cells.Count == 0) + { + bounds = default; + return false; + } + + var minX = Cells[0].x; + var maxX = Cells[0].x; + var minY = Cells[0].y; + var maxY = Cells[0].y; + + for (var i = 1; i < Cells.Count; i++) + { + var cell = Cells[i]; + minX = Mathf.Min(minX, cell.x); + maxX = Mathf.Max(maxX, cell.x); + minY = Mathf.Min(minY, cell.y); + maxY = Mathf.Max(maxY, cell.y); + } + + bounds = new RectInt(minX, minY, maxX - minX + 1, maxY - minY + 1); + return true; + } + + private int FindCellIndex(int x, int y) + { + for (var i = 0; i < Cells.Count; i++) + { + var cell = Cells[i]; + if (cell.x == x && cell.y == y) + { + return i; + } + } + + return -1; + } + } + + [Serializable] + public struct LogicalCell + { + public int x; + public int y; + public StructuralCellKind structural; + } + + [Serializable] + public sealed class StageObjectDefinition + { + public string id; + public StageLogicalLayer layer; + public StageObjectKind kind; + public string typeId; + public string prefabId; + public string floorId; + + public int x; + public int y; + public int width = 1; + public int height = 1; + + public bool cutsGround; + public bool blocksMovement; + public string notes; + + public RectInt Bounds => new(x, y, Mathf.Max(1, width), Mathf.Max(1, height)); + + public bool ContainsCell(int cellX, int cellY) + { + var bounds = Bounds; + return cellX >= bounds.xMin && cellX < bounds.xMax && cellY >= bounds.yMin && cellY < bounds.yMax; + } + } +} diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageDefinition.cs.meta b/Assets/Scripts/Editor/GardensStageEditor/StageDefinition.cs.meta new file mode 100644 index 0000000..3d64664 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageDefinition.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 778f9d79eb2c410482efdc25b248dd46 \ No newline at end of file diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageEditorColors.cs b/Assets/Scripts/Editor/GardensStageEditor/StageEditorColors.cs new file mode 100644 index 0000000..674a087 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageEditorColors.cs @@ -0,0 +1,46 @@ +using UnityEngine; + +namespace Editor.GardensStageEditor +{ + internal static class StageEditorColors + { + public static readonly Color Background = new(0.13f, 0.13f, 0.13f, 1f); + public static readonly Color GridMinor = new(1f, 1f, 1f, 0.08f); + public static readonly Color GridMajor = new(1f, 1f, 1f, 0.18f); + public static readonly Color GridBorder = new(1f, 1f, 1f, 0.28f); + public static readonly Color PreviewFill = new(1f, 1f, 1f, 0.18f); + public static readonly Color PreviewStroke = new(1f, 1f, 1f, 0.9f); + public static readonly Color SelectionStroke = new(1f, 0.85f, 0.25f, 1f); + + public static Color Structural(StructuralCellKind kind) + { + switch (kind) + { + case StructuralCellKind.Ground: + return new Color(0.28f, 0.52f, 0.35f, 0.8f); + case StructuralCellKind.Wall: + return new Color(0.38f, 0.38f, 0.42f, 0.9f); + case StructuralCellKind.Hole: + return new Color(0.05f, 0.05f, 0.05f, 0.9f); + default: + return new Color(0f, 0f, 0f, 0f); + } + } + + public static Color Layer(StageLogicalLayer layer) + { + switch (layer) + { + case StageLogicalLayer.Design: + return new Color(0.85f, 0.35f, 0.25f, 0.75f); + case StageLogicalLayer.Camera: + return new Color(0.25f, 0.48f, 0.95f, 0.35f); + case StageLogicalLayer.Markers: + return new Color(0.85f, 0.75f, 0.25f, 0.75f); + case StageLogicalLayer.Structural: + default: + return new Color(0.35f, 0.7f, 0.45f, 0.75f); + } + } + } +} diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageEditorColors.cs.meta b/Assets/Scripts/Editor/GardensStageEditor/StageEditorColors.cs.meta new file mode 100644 index 0000000..c14e980 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageEditorColors.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fa76e08c40c94bf3a8e3aa57f849200d +timeCreated: 1781500522 \ No newline at end of file diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageEditorState.cs b/Assets/Scripts/Editor/GardensStageEditor/StageEditorState.cs new file mode 100644 index 0000000..e6e28c1 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageEditorState.cs @@ -0,0 +1,46 @@ +using System; +using Intrepid.Gameplay.Prototyping; + +namespace Editor.GardensStageEditor +{ + public sealed class StageEditorState + { + public StageLogicalLayer activeLayer = StageLogicalLayer.Structural; + public StagePaletteEntry activePaletteEntry; + public StageObjectDefinition selectedObject; + + public event Action Changed; + + public void ClearListeners() + { + Changed = null; + } + + public void SetActiveLayer(StageLogicalLayer layer) + { + activeLayer = layer; + NotifyChanged(); + } + + public void SetPaletteEntry(StagePaletteEntry entry) + { + activePaletteEntry = entry; + if (entry != null) + { + activeLayer = entry.layer; + } + NotifyChanged(); + } + + public void SelectObject(StageObjectDefinition obj) + { + selectedObject = obj; + NotifyChanged(); + } + + public void NotifyChanged() + { + Changed?.Invoke(); + } + } +} diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageEditorState.cs.meta b/Assets/Scripts/Editor/GardensStageEditor/StageEditorState.cs.meta new file mode 100644 index 0000000..5c1824f --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageEditorState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b999d27637ff4b45bb5fbdf1ef4de17b \ No newline at end of file diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageEditorWindow.cs b/Assets/Scripts/Editor/GardensStageEditor/StageEditorWindow.cs new file mode 100644 index 0000000..0fdeb07 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageEditorWindow.cs @@ -0,0 +1,295 @@ +using System.Collections.Generic; +using Intrepid.Gameplay.Prototyping; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Editor.GardensStageEditor +{ + public sealed class StageEditorWindow : EditorWindow + { + private StageDefinition stage; + private StageEditorState state; + private List palette; + + private VisualElement topBar; + private StageMapCanvas canvas; + private StageInspectorPanel inspector; + private StagePalettePanel palettePanel; + + [MenuItem("Tools/Gardens Of Nil/Stage Editor")] + public static void Open() + { + var window = GetWindow(); + window.titleContent = new GUIContent("Gardens Stage Editor"); + window.minSize = new Vector2(900, 600); + window.Show(); + } + + public void CreateGUI() + { + state ??= new StageEditorState(); + state.ClearListeners(); + + if (palette is null) + { + palette = StagePaletteEntry.CreateBuiltInPalette(); + if (state.activePaletteEntry is null && palette.Count > 0) + { + state.activePaletteEntry = palette[0]; + state.activeLayer = palette[0].layer; + } + } + + rootVisualElement.Clear(); + rootVisualElement.style.flexDirection = FlexDirection.Column; + + topBar = new VisualElement + { + style = + { + flexDirection = FlexDirection.Row, + alignItems = Align.Center, + paddingLeft = 8, + paddingRight = 8, + paddingTop = 6, + paddingBottom = 6, + borderBottomWidth = 1, + borderBottomColor = new Color(1f, 1f, 1f, 0.12f) + } + }; + rootVisualElement.Add(topBar); + + var main = new VisualElement + { + style = + { + flexGrow = 1f, + flexDirection = FlexDirection.Row + } + }; + rootVisualElement.Add(main); + + canvas = new StageMapCanvas(this, state); + canvas.SetStage(stage); + main.Add(canvas); + + inspector = new StageInspectorPanel(this, state); + inspector.SetStage(stage); + main.Add(inspector); + + palettePanel = new StagePalettePanel(state, palette); + rootVisualElement.Add(palettePanel); + + state.Changed += delegate + { + canvas?.RequestRepaint(); + inspector?.Rebuild(); + }; + + RebuildTopBar(); + } + + public void MutateStage(string undoName, System.Action mutation) + { + if (stage is null || mutation is null) + { + return; + } + + Undo.RecordObject(stage, undoName); + mutation(); + stage.EnsureDefaults(); + EditorUtility.SetDirty(stage); + + canvas?.RequestRepaint(); + inspector?.Rebuild(); + + RebuildTopBar(); + Repaint(); + } + + private void RebuildTopBar() + { + if (topBar is null) + { + return; + } + + topBar.Clear(); + + var stageField = new ObjectField("Stage") + { + objectType = typeof(StageDefinition), + value = stage, + style = + { + minWidth = 300 + } + }; + stageField.RegisterValueChangedCallback(evt => + { + SetStage(evt.newValue as StageDefinition); + }); + topBar.Add(stageField); + + var newButton = new Button(CreateNewStageAsset) + { + text = "New", + style = + { + marginLeft = 6 + } + }; + topBar.Add(newButton); + + if (stage is not null) + { + stage.EnsureDefaults(); + + var floorNames = new List(); + foreach (var stageFloorDefinition in stage.floors) + { + floorNames.Add(stageFloorDefinition.floorId); + } + + var floorPopup = new PopupField("Floor", floorNames, Mathf.Clamp(stage.activeFloorIndex, 0, floorNames.Count - 1)) + { + style = + { + marginLeft = 10, + minWidth = 170 + } + }; + floorPopup.RegisterValueChangedCallback(evt => + { + var index = floorNames.IndexOf(evt.newValue); + if (index >= 0) + { + MutateStage("Change active floor", delegate { stage.activeFloorIndex = index; }); + } + }); + topBar.Add(floorPopup); + + var addFloor = new Button(AddFloor) + { + text = "+ Floor", + style = + { + marginLeft = 4 + } + }; + topBar.Add(addFloor); + + var validate = new Button(ValidateStage) + { + text = "Validate", + style = + { + marginLeft = 12 + } + }; + topBar.Add(validate); + + var save = new Button(delegate + { + EditorUtility.SetDirty(stage); + AssetDatabase.SaveAssets(); + Debug.Log("Saved StageDefinition: " + AssetDatabase.GetAssetPath(stage)); + }) + { + text = "Save Asset", + style = + { + marginLeft = 4 + } + }; + topBar.Add(save); + } + + var spacer = new VisualElement + { + style = + { + flexGrow = 1f + } + }; + topBar.Add(spacer); + + var hint = new Label("Infinite grid · LMB select/place/paint · RMB/MMB pan · Wheel zoom") + { + style = + { + opacity = 0.75f + } + }; + topBar.Add(hint); + } + + private void SetStage(StageDefinition newStage) + { + stage = newStage; + stage?.EnsureDefaults(); + state.SelectObject(null); + canvas?.SetStage(stage); + inspector?.SetStage(stage); + + RebuildTopBar(); + } + + private void CreateNewStageAsset() + { + var path = EditorUtility.SaveFilePanelInProject( + "Create Stage Definition", + "StageDefinition", + "asset", + "Choose where to create the new StageDefinition asset." + ); + + if (string.IsNullOrEmpty(path)) + { + return; + } + + var asset = CreateInstance(); + asset.stageId = System.IO.Path.GetFileNameWithoutExtension(path).ToLowerInvariant(); + asset.EnsureDefaults(); + + AssetDatabase.CreateAsset(asset, path); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + SetStage(asset); + Selection.activeObject = asset; + } + + private void AddFloor() + { + if (stage is null) + { + return; + } + + MutateStage("Add floor", delegate + { + var index = stage.floors.Count; + var floor = new StageFloorDefinition + { + floorId = "F" + index + }; + + stage.floors.Add(floor); + stage.activeFloorIndex = stage.floors.Count - 1; + }); + } + + private void ValidateStage() + { + var messages = StageValidator.Validate(stage); + var report = StageValidator.Format(messages); + Debug.Log(report); + EditorUtility.DisplayDialog("Stage Validation", report, "OK"); + } + } +} diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageEditorWindow.cs.meta b/Assets/Scripts/Editor/GardensStageEditor/StageEditorWindow.cs.meta new file mode 100644 index 0000000..603b61c --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageEditorWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f7065bab1fc14f53909281036dbb6368 \ No newline at end of file diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageInspectorPanel.cs b/Assets/Scripts/Editor/GardensStageEditor/StageInspectorPanel.cs new file mode 100644 index 0000000..3338c65 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageInspectorPanel.cs @@ -0,0 +1,258 @@ +using UnityEngine; +using UnityEngine.UIElements; + +namespace Editor.GardensStageEditor +{ + public sealed class StageInspectorPanel : VisualElement + { + private readonly StageEditorWindow owner; + private readonly StageEditorState state; + private StageDefinition stage; + + public StageInspectorPanel(StageEditorWindow owner, StageEditorState state) + { + this.owner = owner; + this.state = state; + + style.width = 300; + style.minWidth = 260; + style.maxWidth = 380; + style.paddingLeft = 8; + style.paddingRight = 8; + style.paddingTop = 8; + style.paddingBottom = 8; + style.borderLeftWidth = 1; + style.borderLeftColor = new Color(1f, 1f, 1f, 0.12f); + style.flexShrink = 0; + + state.Changed += Rebuild; + } + + public void SetStage(StageDefinition newStage) + { + stage = newStage; + Rebuild(); + } + + public void Rebuild() + { + Clear(); + + var title = new Label("Inspector") + { + style = + { + unityFontStyleAndWeight = FontStyle.Bold, + marginBottom = 8 + } + }; + Add(title); + + if (stage is null) + { + Add(new HelpBox("Select or create a StageDefinition asset.", HelpBoxMessageType.Info)); + return; + } + + stage.EnsureDefaults(); + + if (state.selectedObject is not null && stage.objects.Contains(state.selectedObject)) + { + BuildObjectInspector(state.selectedObject); + } + else + { + state.selectedObject = null; + BuildStageInspector(); + } + } + + private void BuildStageInspector() + { + var section = new Label("Stage") + { + style = + { + unityFontStyleAndWeight = FontStyle.Bold + } + }; + Add(section); + + var stageId = new TextField("Stage Id") + { + value = stage.stageId + }; + stageId.RegisterValueChangedCallback(evt => owner.MutateStage("Change stage id", delegate { stage.stageId = evt.newValue; })); + Add(stageId); + + var cellSize = new FloatField("Cell Size") + { + value = stage.cellSize + }; + cellSize.RegisterValueChangedCallback(evt => owner.MutateStage("Change cell size", delegate { stage.cellSize = Mathf.Max(0.01f, evt.newValue); })); + Add(cellSize); + + var floorHeight = new FloatField("Floor Height") + { + value = stage.floorHeight + }; + floorHeight.RegisterValueChangedCallback(evt => owner.MutateStage("Change floor height", delegate { stage.floorHeight = Mathf.Max(0.01f, evt.newValue); })); + Add(floorHeight); + + var spacer = new VisualElement + { + style = + { + height = 10 + } + }; + Add(spacer); + + var floor = stage.ActiveFloor; + var floorSection = new Label("Active Floor") + { + style = + { + unityFontStyleAndWeight = FontStyle.Bold + } + }; + Add(floorSection); + + var floorId = new TextField("Floor Id") + { + value = floor.floorId + }; + floorId.RegisterValueChangedCallback(evt => owner.MutateStage("Change floor id", delegate + { + var oldId = floor.floorId; + floor.floorId = string.IsNullOrEmpty(evt.newValue) ? oldId : evt.newValue; + foreach (var stageObjectDefinition in stage.objects) + { + if (stageObjectDefinition is not null && stageObjectDefinition.floorId == oldId) + { + stageObjectDefinition.floorId = floor.floorId; + } + } + })); + Add(floorId); + + Add(new Label("Structural Cells: " + floor.Cells.Count)); + + Add(floor.TryGetUsedBounds(out var bounds) + ? new Label("Used Bounds: x " + bounds.xMin + ".." + bounds.xMax + ", y " + bounds.yMin + ".." + + bounds.yMax) + : new Label("Used Bounds: empty")); + + Add(new HelpBox("The grid is infinite/sparse. The stage size emerges from painted cells and logical objects.", HelpBoxMessageType.Info)); + Add(new HelpBox("Editor plane uses X/Y. Later, the stage baker can map editor Y to Unity Z and floor index to Unity Y.", HelpBoxMessageType.Info)); + } + + private void BuildObjectInspector(StageObjectDefinition obj) + { + var section = new Label("Selected Object") + { + style = + { + unityFontStyleAndWeight = FontStyle.Bold + } + }; + Add(section); + + var id = new TextField("Id") + { + value = obj.id + }; + id.RegisterValueChangedCallback(evt => owner.MutateStage("Change object id", delegate { obj.id = evt.newValue; })); + Add(id); + + var layer = new EnumField("Layer", obj.layer); + layer.RegisterValueChangedCallback(evt => owner.MutateStage("Change object layer", delegate { obj.layer = (StageLogicalLayer)evt.newValue; })); + Add(layer); + + var kind = new EnumField("Kind", obj.kind); + kind.RegisterValueChangedCallback(evt => owner.MutateStage("Change object kind", delegate { obj.kind = (StageObjectKind)evt.newValue; })); + Add(kind); + + var typeId = new TextField("Type Id") + { + value = obj.typeId + }; + typeId.RegisterValueChangedCallback(evt => owner.MutateStage("Change type id", delegate { obj.typeId = evt.newValue; })); + Add(typeId); + + var prefabId = new TextField("Prefab Id") + { + value = obj.prefabId + }; + prefabId.RegisterValueChangedCallback(evt => owner.MutateStage("Change prefab id", delegate { obj.prefabId = evt.newValue; })); + Add(prefabId); + + var x = new IntegerField("X") + { + value = obj.x + }; + x.RegisterValueChangedCallback(evt => owner.MutateStage("Move object x", delegate { obj.x = evt.newValue; })); + Add(x); + + var y = new IntegerField("Y") + { + value = obj.y + }; + y.RegisterValueChangedCallback(evt => owner.MutateStage("Move object y", delegate { obj.y = evt.newValue; })); + Add(y); + + var width = new IntegerField("Width") + { + value = obj.width + }; + width.RegisterValueChangedCallback(evt => owner.MutateStage("Resize object width", delegate { obj.width = Mathf.Max(1, evt.newValue); })); + Add(width); + + var height = new IntegerField("Height") + { + value = obj.height + }; + height.RegisterValueChangedCallback(evt => owner.MutateStage("Resize object height", delegate { obj.height = Mathf.Max(1, evt.newValue); })); + Add(height); + + var cutsGround = new Toggle("Cuts Ground") + { + value = obj.cutsGround + }; + cutsGround.RegisterValueChangedCallback(evt => owner.MutateStage("Toggle cuts ground", delegate { obj.cutsGround = evt.newValue; })); + Add(cutsGround); + + var blocksMovement = new Toggle("Blocks Movement") + { + value = obj.blocksMovement + }; + blocksMovement.RegisterValueChangedCallback(evt => owner.MutateStage("Toggle blocks movement", delegate { obj.blocksMovement = evt.newValue; })); + Add(blocksMovement); + + var notes = new TextField("Notes") + { + multiline = true, + value = obj.notes + }; + notes.RegisterValueChangedCallback(evt => owner.MutateStage("Change object notes", delegate { obj.notes = evt.newValue; })); + Add(notes); + + var delete = new Button(delegate + { + owner.MutateStage("Delete object", delegate + { + stage.objects.Remove(obj); + state.SelectObject(null); + }); + }) + { + text = "Delete Object", + style = + { + marginTop = 8 + } + }; + Add(delete); + } + } +} diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageInspectorPanel.cs.meta b/Assets/Scripts/Editor/GardensStageEditor/StageInspectorPanel.cs.meta new file mode 100644 index 0000000..606a73b --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageInspectorPanel.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a1b3226b742b4e5e94cab859de25494a \ No newline at end of file diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageMapCanvas.cs b/Assets/Scripts/Editor/GardensStageEditor/StageMapCanvas.cs new file mode 100644 index 0000000..981b304 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageMapCanvas.cs @@ -0,0 +1,466 @@ +using UnityEngine; +using UnityEngine.UIElements; + +namespace Editor.GardensStageEditor +{ + public sealed class StageMapCanvas : VisualElement + { + private const float MinCellPixels = 12f; + private const float MaxCellPixels = 96f; + + private readonly StageEditorWindow owner; + private readonly StageEditorState state; + + private StageDefinition stage; + private Vector2 pan = new(40f, 40f); + private float cellPixels = 32f; + + private bool isPanning; + private bool isPainting; + private Vector2 panStartMouse; + private Vector2 panStartValue; + private Vector2Int dragStartCell; + private Vector2Int dragCurrentCell; + + public StageMapCanvas(StageEditorWindow owner, StageEditorState state) + { + this.owner = owner; + this.state = state; + + focusable = true; + style.flexGrow = 1f; + style.backgroundColor = StageEditorColors.Background; + + generateVisualContent += OnGenerateVisualContent; + + RegisterCallback(OnMouseDown); + RegisterCallback(OnMouseMove); + RegisterCallback(OnMouseUp); + RegisterCallback(OnWheel); + } + + public void SetStage(StageDefinition newStage) + { + stage = newStage; + stage?.EnsureDefaults(); + MarkDirtyRepaint(); + } + + public void RequestRepaint() + { + MarkDirtyRepaint(); + } + + private void OnWheel(WheelEvent evt) + { + if (stage is null) + { + return; + } + + var mouse = evt.localMousePosition; + var before = ScreenToGridFloat(mouse); + + var delta = -evt.delta.y * 0.08f; + cellPixels = Mathf.Clamp(cellPixels * (1f + delta), MinCellPixels, MaxCellPixels); + + var after = ScreenToGridFloat(mouse); + pan += (after - before) * cellPixels; + + evt.StopPropagation(); + MarkDirtyRepaint(); + } + + private void OnMouseDown(MouseDownEvent evt) + { + Focus(); + + if (stage is null) + { + return; + } + + // Pan is no longer a palette tool. It is always available through RMB/MMB. + if (evt.button == 2 || evt.button == 1) + { + isPanning = true; + panStartMouse = evt.localMousePosition; + panStartValue = pan; + this.CaptureMouse(); + evt.StopPropagation(); + return; + } + + if (evt.button != 0) + { + return; + } + + if (!TryScreenToCell(evt.localMousePosition, out Vector2Int cell)) + { + return; + } + + // LMB always tries to select an existing logical object first. + // This removes the need for a Select tool. + var hit = FindObjectAt(cell, false); + if (hit != null) + { + state.SelectObject(hit); + MarkDirtyRepaint(); + evt.StopPropagation(); + return; + } + + state.SelectObject(null); + + // Empty space uses the active palette item to paint/place. + dragStartCell = cell; + dragCurrentCell = cell; + isPainting = true; + this.CaptureMouse(); + + evt.StopPropagation(); + MarkDirtyRepaint(); + } + + private void OnMouseMove(MouseMoveEvent evt) + { + if (isPanning) + { + pan = panStartValue + evt.localMousePosition - panStartMouse; + evt.StopPropagation(); + MarkDirtyRepaint(); + return; + } + + if (isPainting) + { + if (TryScreenToCell(evt.localMousePosition, out Vector2Int cell)) + { + dragCurrentCell = cell; + MarkDirtyRepaint(); + } + + evt.StopPropagation(); + } + } + + private void OnMouseUp(MouseUpEvent evt) + { + if (isPanning) + { + isPanning = false; + if (this.HasMouseCapture()) + { + this.ReleaseMouse(); + } + + evt.StopPropagation(); + return; + } + + if (!isPainting || evt.button != 0) + { + return; + } + + isPainting = false; + if (this.HasMouseCapture()) + { + this.ReleaseMouse(); + } + + var rect = GetDragRect(); + ApplyPalette(rect); + + evt.StopPropagation(); + MarkDirtyRepaint(); + } + + private void ApplyPalette(RectInt rect) + { + StageFloorDefinition floor = stage?.ActiveFloor; + if (floor is null) + { + return; + } + + var entry = state.activePaletteEntry; + if (entry is null) + { + return; + } + + if (entry.paintsStructural) + { + owner.MutateStage("Paint " + entry.displayName, delegate + { + floor.FillRect(rect, entry.structuralKind); + }); + return; + } + + var objectRect = rect; + if (rect is { width: 1, height: 1 }) + { + objectRect = new RectInt(rect.x, rect.y, Mathf.Max(1, entry.defaultSize.x), Mathf.Max(1, entry.defaultSize.y)); + } + + objectRect.width = Mathf.Clamp(objectRect.width, Mathf.Max(1, entry.minSize.x), Mathf.Max(1, entry.maxSize.x)); + objectRect.height = Mathf.Clamp(objectRect.height, Mathf.Max(1, entry.minSize.y), Mathf.Max(1, entry.maxSize.y)); + + owner.MutateStage("Place " + entry.displayName, delegate + { + var obj = new StageObjectDefinition + { + id = stage.CreateUniqueObjectId(entry.objectKind), + layer = entry.layer, + kind = entry.objectKind, + typeId = entry.typeId, + prefabId = entry.prefabId, + floorId = floor.floorId, + x = objectRect.x, + y = objectRect.y, + width = Mathf.Max(1, objectRect.width), + height = Mathf.Max(1, objectRect.height), + cutsGround = entry.cutsGround, + blocksMovement = entry.blocksMovement + }; + + stage.objects.Add(obj); + state.SelectObject(obj); + }); + } + + private StageObjectDefinition FindObjectAt(Vector2Int cell, bool activeLayerOnly) + { + if (stage is null) + { + return null; + } + + var floorId = stage.ActiveFloor.floorId; + for (var i = stage.objects.Count - 1; i >= 0; i--) + { + var obj = stage.objects[i]; + if (obj is null || obj.floorId != floorId) + { + continue; + } + + if (activeLayerOnly && obj.layer != state.activeLayer) + { + continue; + } + + if (obj.ContainsCell(cell.x, cell.y)) + { + return obj; + } + } + + return null; + } + + private void OnGenerateVisualContent(MeshGenerationContext mgc) + { + var p = mgc.painter2D; + FillRect(p, contentRect, StageEditorColors.Background); + + if (stage is null) + { + return; + } + + stage.EnsureDefaults(); + var floor = stage.ActiveFloor; + if (floor is null) + { + return; + } + + DrawInfiniteGrid(p); + DrawCells(p, floor); + DrawObjects(p, floor.floorId); + DrawOriginAxes(p); + DrawPreview(p); + } + + private void DrawCells(Painter2D p, StageFloorDefinition floor) + { + foreach (var cell in floor.Cells) + { + var color = StageEditorColors.Structural(cell.structural); + if (color.a <= 0f) + { + continue; + } + + var rect = CellRect(cell.x, cell.y); + FillRect(p, rect, color); + } + } + + private void DrawObjects(Painter2D p, string floorId) + { + foreach (var obj in stage.objects) + { + if (obj is null || obj.floorId != floorId) + { + continue; + } + + var rect = GridRect(obj.Bounds); + var color = StageEditorColors.Layer(obj.layer); + FillRect(p, rect, color); + StrokeRect(p, rect, color.WithAlpha(Mathf.Min(1f, color.a + 0.25f)), 2f); + + if (state.selectedObject == obj) + { + StrokeRect(p, rect.Expanded(2f), StageEditorColors.SelectionStroke, 3f); + } + } + } + + private void DrawInfiniteGrid(Painter2D p) + { + var r = contentRect; + var minX = Mathf.FloorToInt((r.xMin - pan.x) / cellPixels) - 1; + var maxX = Mathf.CeilToInt((r.xMax - pan.x) / cellPixels) + 1; + var minY = Mathf.FloorToInt((r.yMin - pan.y) / cellPixels) - 1; + var maxY = Mathf.CeilToInt((r.yMax - pan.y) / cellPixels) + 1; + + for (var x = minX; x <= maxX; x++) + { + var sx = pan.x + x * cellPixels; + var c = x % 4 == 0 ? StageEditorColors.GridMajor : StageEditorColors.GridMinor; + DrawLine(p, new Vector2(sx, r.yMin), new Vector2(sx, r.yMax), c, 1f); + } + + for (var y = minY; y <= maxY; y++) + { + var sy = pan.y + y * cellPixels; + var c = y % 4 == 0 ? StageEditorColors.GridMajor : StageEditorColors.GridMinor; + DrawLine(p, new Vector2(r.xMin, sy), new Vector2(r.xMax, sy), c, 1f); + } + } + + private void DrawOriginAxes(Painter2D p) + { + var r = contentRect; + var sx = pan.x; + var sy = pan.y; + + if (sx >= r.xMin && sx <= r.xMax) + { + DrawLine(p, new Vector2(sx, r.yMin), new Vector2(sx, r.yMax), StageEditorColors.GridBorder, 2f); + } + + if (sy >= r.yMin && sy <= r.yMax) + { + DrawLine(p, new Vector2(r.xMin, sy), new Vector2(r.xMax, sy), StageEditorColors.GridBorder, 2f); + } + } + + private void DrawPreview(Painter2D p) + { + if (!isPainting || state.activePaletteEntry is null) + { + return; + } + + var rect = GetDragRect(); + var entry = state.activePaletteEntry; + + if (!entry.paintsStructural && rect is { width: 1, height: 1 }) + { + rect = new RectInt(rect.x, rect.y, Mathf.Max(1, entry.defaultSize.x), Mathf.Max(1, entry.defaultSize.y)); + } + + var screenRect = GridRect(rect); + FillRect(p, screenRect, StageEditorColors.PreviewFill); + StrokeRect(p, screenRect, StageEditorColors.PreviewStroke, 2f); + } + + private RectInt GetDragRect() + { + var minX = Mathf.Min(dragStartCell.x, dragCurrentCell.x); + var maxX = Mathf.Max(dragStartCell.x, dragCurrentCell.x); + var minY = Mathf.Min(dragStartCell.y, dragCurrentCell.y); + var maxY = Mathf.Max(dragStartCell.y, dragCurrentCell.y); + return new RectInt(minX, minY, maxX - minX + 1, maxY - minY + 1); + } + + private bool TryScreenToCell(Vector2 screen, out Vector2Int cell) + { + var grid = ScreenToGridFloat(screen); + cell = new Vector2Int(Mathf.FloorToInt(grid.x), Mathf.FloorToInt(grid.y)); + return stage?.ActiveFloor is not null; + } + + private Vector2 ScreenToGridFloat(Vector2 screen) + { + return new Vector2((screen.x - pan.x) / cellPixels, (screen.y - pan.y) / cellPixels); + } + + private Rect CellRect(int x, int y) + { + return new Rect(pan.x + x * cellPixels, pan.y + y * cellPixels, cellPixels, cellPixels); + } + + private Rect GridRect(RectInt rect) + { + return new Rect(pan.x + rect.x * cellPixels, pan.y + rect.y * cellPixels, rect.width * cellPixels, rect.height * cellPixels); + } + + private void FillRect(Painter2D p, Rect rect, Color color) + { + p.fillColor = color; + p.BeginPath(); + p.MoveTo(new Vector2(rect.xMin, rect.yMin)); + p.LineTo(new Vector2(rect.xMax, rect.yMin)); + p.LineTo(new Vector2(rect.xMax, rect.yMax)); + p.LineTo(new Vector2(rect.xMin, rect.yMax)); + p.ClosePath(); + p.Fill(); + } + + private void StrokeRect(Painter2D p, Rect rect, Color color, float width) + { + p.strokeColor = color; + p.lineWidth = width; + p.BeginPath(); + p.MoveTo(new Vector2(rect.xMin, rect.yMin)); + p.LineTo(new Vector2(rect.xMax, rect.yMin)); + p.LineTo(new Vector2(rect.xMax, rect.yMax)); + p.LineTo(new Vector2(rect.xMin, rect.yMax)); + p.ClosePath(); + p.Stroke(); + } + + private void DrawLine(Painter2D p, Vector2 a, Vector2 b, Color color, float width) + { + p.strokeColor = color; + p.lineWidth = width; + p.BeginPath(); + p.MoveTo(a); + p.LineTo(b); + p.Stroke(); + } + } + + internal static class StageEditorRectExtensions + { + public static Rect Expanded(this Rect rect, float amount) + { + return new Rect(rect.x - amount, rect.y - amount, rect.width + amount * 2f, rect.height + amount * 2f); + } + + public static Color WithAlpha(this Color color, float alpha) + { + color.a = alpha; + return color; + } + } +} diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageMapCanvas.cs.meta b/Assets/Scripts/Editor/GardensStageEditor/StageMapCanvas.cs.meta new file mode 100644 index 0000000..1bcc39c --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageMapCanvas.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8c584058841343debb04c00199a79393 \ No newline at end of file diff --git a/Assets/Scripts/Editor/GardensStageEditor/StagePaletteEntry.cs b/Assets/Scripts/Editor/GardensStageEditor/StagePaletteEntry.cs new file mode 100644 index 0000000..7501810 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StagePaletteEntry.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Editor.GardensStageEditor +{ + public sealed class StagePaletteEntry + { + public string id; + public string displayName; + public StageLogicalLayer layer; + + public bool paintsStructural; + public StructuralCellKind structuralKind; + + public StageObjectKind objectKind; + public string typeId; + public string prefabId; + + public Vector2Int defaultSize = Vector2Int.one; + public Vector2Int minSize = Vector2Int.one; + public Vector2Int maxSize = new(32, 32); + public bool resizable = true; + public bool cutsGround; + public bool blocksMovement; + + public static StagePaletteEntry Structural(string id, string name, StructuralCellKind kind) + { + return new StagePaletteEntry + { + id = id, + displayName = name, + layer = StageLogicalLayer.Structural, + paintsStructural = true, + structuralKind = kind, + defaultSize = Vector2Int.one, + minSize = Vector2Int.one, + maxSize = new Vector2Int(64, 64), + resizable = true + }; + } + + public static StagePaletteEntry Object(string id, string name, StageLogicalLayer layer, StageObjectKind kind, string typeId, Vector2Int defaultSize) + { + return new StagePaletteEntry + { + id = id, + displayName = name, + layer = layer, + paintsStructural = false, + objectKind = kind, + typeId = typeId, + prefabId = string.Empty, + defaultSize = defaultSize, + minSize = Vector2Int.one, + maxSize = new Vector2Int(64, 64), + resizable = true + }; + } + + public static List CreateBuiltInPalette() + { + var entries = new List + { + Structural("struct_ground", "Ground", StructuralCellKind.Ground), + Structural("struct_wall", "Wall", StructuralCellKind.Wall), + Structural("struct_hole", "Hole", StructuralCellKind.Hole), + Structural("struct_empty", "Erase Structural", StructuralCellKind.Empty) + }; + + var trap = Object("trap_falling_floor", "Falling Floor", StageLogicalLayer.Design, StageObjectKind.Trap, "falling_floor", Vector2Int.one); + trap.cutsGround = true; + trap.blocksMovement = true; + entries.Add(trap); + + var door = Object("door_standard", "Door", StageLogicalLayer.Design, StageObjectKind.Door, "door_standard", new Vector2Int(1, 2)); + door.blocksMovement = true; + entries.Add(door); + + entries.Add(Object("interactable", "Interactable", StageLogicalLayer.Design, StageObjectKind.Interactable, "interactable", Vector2Int.one)); + entries.Add(Object("portal", "Portal", StageLogicalLayer.Design, StageObjectKind.Portal, "portal", new Vector2Int(2, 2))); + entries.Add(Object("camera_zone", "Camera Zone", StageLogicalLayer.Camera, StageObjectKind.CameraZone, "camera_zone", new Vector2Int(8, 5))); + entries.Add(Object("spawn_player", "Player Spawn", StageLogicalLayer.Markers, StageObjectKind.Spawn, "player_spawn", Vector2Int.one)); + entries.Add(Object("elevator", "Elevator", StageLogicalLayer.Markers, StageObjectKind.Elevator, "elevator", new Vector2Int(2, 2))); + entries.Add(Object("marker", "Marker", StageLogicalLayer.Markers, StageObjectKind.Marker, "marker", Vector2Int.one)); + + return entries; + } + } +} diff --git a/Assets/Scripts/Editor/GardensStageEditor/StagePaletteEntry.cs.meta b/Assets/Scripts/Editor/GardensStageEditor/StagePaletteEntry.cs.meta new file mode 100644 index 0000000..99a1e3c --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StagePaletteEntry.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0e7ed87816264200a12fcc4bf3584153 +timeCreated: 1781500522 \ No newline at end of file diff --git a/Assets/Scripts/Editor/GardensStageEditor/StagePalettePanel.cs b/Assets/Scripts/Editor/GardensStageEditor/StagePalettePanel.cs new file mode 100644 index 0000000..a939489 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StagePalettePanel.cs @@ -0,0 +1,163 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Editor.GardensStageEditor +{ + public sealed class StagePalettePanel : VisualElement + { + private readonly StageEditorState state; + private readonly List entries; + + public StagePalettePanel(StageEditorState state, List entries) + { + this.state = state; + this.entries = entries; + + style.flexDirection = FlexDirection.Column; + style.minHeight = 110; + style.maxHeight = 170; + style.paddingLeft = 8; + style.paddingRight = 8; + style.paddingTop = 6; + style.paddingBottom = 6; + style.borderTopWidth = 1; + style.borderTopColor = new Color(1f, 1f, 1f, 0.12f); + + state.Changed += Rebuild; + Rebuild(); + } + + private void Rebuild() + { + Clear(); + + var row = new VisualElement + { + style = + { + flexDirection = FlexDirection.Row, + alignItems = Align.Center + } + }; + Add(row); + + var title = new Label("Palette") + { + style = + { + unityFontStyleAndWeight = FontStyle.Bold, + marginRight = 12 + } + }; + row.Add(title); + + AddLayerButton(row, StageLogicalLayer.Structural); + AddLayerButton(row, StageLogicalLayer.Design); + AddLayerButton(row, StageLogicalLayer.Camera); + AddLayerButton(row, StageLogicalLayer.Markers); + + var hint = new Label("LMB selects objects. Empty space paints/places selected palette item. RMB/MMB pans.") + { + style = + { + opacity = 0.65f, + marginLeft = 12 + } + }; + row.Add(hint); + + var scroll = new ScrollView(ScrollViewMode.Horizontal) + { + style = + { + flexGrow = 1f, + marginTop = 8 + } + }; + Add(scroll); + + var itemRow = new VisualElement + { + style = + { + flexDirection = FlexDirection.Row + } + }; + scroll.Add(itemRow); + + foreach (var entry in entries) + { + if (entry.layer != state.activeLayer) + { + continue; + } + + var button = new Button(delegate { state.SetPaletteEntry(entry); }) + { + text = entry.displayName, + style = + { + minWidth = 120, + height = 48, + marginRight = 6, + unityTextAlign = TextAnchor.MiddleCenter + } + }; + + if (state.activePaletteEntry == entry) + { + button.style.borderBottomWidth = 3; + button.style.borderBottomColor = StageEditorColors.SelectionStroke; + } + + itemRow.Add(button); + } + } + + private void AddLayerButton(VisualElement row, StageLogicalLayer layer) + { + + var button = new Button(delegate + { + state.SetActiveLayer(layer); + + if (state.activePaletteEntry == null || state.activePaletteEntry.layer != layer) + { + var first = FindFirstEntryForLayer(layer); + if (first != null) + { + state.SetPaletteEntry(first); + } + } + }) + { + text = layer.ToString(), + style = + { + marginRight = 4 + } + }; + + if (state.activeLayer == layer) + { + button.style.borderBottomWidth = 3; + button.style.borderBottomColor = StageEditorColors.SelectionStroke; + } + row.Add(button); + } + + private StagePaletteEntry FindFirstEntryForLayer(StageLogicalLayer layer) + { + foreach (var stagePaletteEntry in entries) + { + if (stagePaletteEntry.layer == layer) + { + return stagePaletteEntry; + } + } + + return null; + } + } +} diff --git a/Assets/Scripts/Editor/GardensStageEditor/StagePalettePanel.cs.meta b/Assets/Scripts/Editor/GardensStageEditor/StagePalettePanel.cs.meta new file mode 100644 index 0000000..84c82bf --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StagePalettePanel.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e9ffd5aa3cf34c84be81a3c01679fc3e \ No newline at end of file diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageValidator.cs b/Assets/Scripts/Editor/GardensStageEditor/StageValidator.cs new file mode 100644 index 0000000..27219f7 --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageValidator.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.Text; + +namespace Editor.GardensStageEditor +{ + public enum StageValidationSeverity + { + Info, + Warning, + Error + } + + public readonly struct StageValidationMessage + { + public readonly StageValidationSeverity severity; + public readonly string message; + + public StageValidationMessage(StageValidationSeverity severity, string message) + { + this.severity = severity; + this.message = message; + } + + public override string ToString() + { + return severity + ": " + message; + } + } + + public static class StageValidator + { + public static List Validate(StageDefinition stage) + { + var messages = new List(); + + if (stage is null) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "No StageDefinition selected.")); + return messages; + } + + stage.EnsureDefaults(); + + if (string.IsNullOrEmpty(stage.stageId)) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Warning, "Stage id is empty.")); + } + + if (stage.floors.Count == 0) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Stage has no floors.")); + } + + for (int i = 0; i < stage.floors.Count; i++) + { + StageFloorDefinition floor = stage.floors[i]; + if (floor is null) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Floor at index " + i + " is null.")); + continue; + } + + if (string.IsNullOrEmpty(floor.floorId)) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Floor at index " + i + " has no floor id.")); + } + } + + var hasSpawn = false; + var ids = new HashSet(); + for (var i = 0; i < stage.objects.Count; i++) + { + var obj = stage.objects[i]; + if (obj is null) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Warning, "Object at index " + i + " is null.")); + continue; + } + + if (string.IsNullOrEmpty(obj.id)) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Warning, "Object at index " + i + " has no id.")); + } + else if (!ids.Add(obj.id)) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Duplicate object id: " + obj.id)); + } + + var floor = stage.GetFloor(obj.floorId); + if (floor is null) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Object " + obj.id + " references missing floor: " + obj.floorId)); + continue; + } + + if (obj.width <= 0 || obj.height <= 0) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Object " + obj.id + " has invalid size.")); + } + + if (obj.kind == StageObjectKind.Spawn) + { + hasSpawn = true; + } + } + + if (!hasSpawn) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Warning, "Stage has no player spawn marker.")); + } + + if (messages.Count == 0) + { + messages.Add(new StageValidationMessage(StageValidationSeverity.Info, "No validation issues found.")); + } + + return messages; + } + + public static string Format(List messages) + { + var sb = new StringBuilder(); + foreach (var stageValidationMessage in messages) + { + sb.AppendLine(stageValidationMessage.ToString()); + } + return sb.ToString(); + } + } +} diff --git a/Assets/Scripts/Editor/GardensStageEditor/StageValidator.cs.meta b/Assets/Scripts/Editor/GardensStageEditor/StageValidator.cs.meta new file mode 100644 index 0000000..0489f4d --- /dev/null +++ b/Assets/Scripts/Editor/GardensStageEditor/StageValidator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a32d1b16f27f4bfd90425ffa3fde2c89 \ No newline at end of file diff --git a/Packages/manifest.json b/Packages/manifest.json index ff374dd..a41d2b7 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -9,7 +9,7 @@ "com.unity.2d.tilemap": "1.0.0", "com.unity.2d.tilemap.extras": "6.0.2", "com.unity.2d.tooling": "1.0.3", - "com.unity.cinemachine": "3.1.6", + "com.unity.cinemachine": "3.1.7", "com.unity.collab-proxy": "2.12.4", "com.unity.ide.rider": "3.0.40", "com.unity.ide.visualstudio": "2.0.27", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index e39c9a0..f363de9 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -117,7 +117,7 @@ "url": "https://packages.unity.com" }, "com.unity.cinemachine": { - "version": "3.1.6", + "version": "3.1.7", "depth": 0, "source": "registry", "dependencies": {