implements PLN-0003 world editor authoring
This commit is contained in:
parent
7d9947e5f0
commit
da4f1caaa7
15
Assets/Scripts/Editor/GON/WorldEditor/WorldEditorColors.cs
Normal file
15
Assets/Scripts/Editor/GON/WorldEditor/WorldEditorColors.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Editor.GON.WorldEditor
|
||||||
|
{
|
||||||
|
public static class WorldEditorColors
|
||||||
|
{
|
||||||
|
public static readonly Color Background = new(0.08f, 0.09f, 0.1f, 1f);
|
||||||
|
public static readonly Color Grid = new(1f, 1f, 1f, 0.08f);
|
||||||
|
public static readonly Color GridAxis = new(1f, 1f, 1f, 0.18f);
|
||||||
|
public static readonly Color Selected = new(1f, 0.86f, 0.32f, 1f);
|
||||||
|
public static readonly Color Socket = new(0.95f, 0.95f, 0.95f, 1f);
|
||||||
|
public static readonly Color Anchor = new(0.35f, 0.9f, 0.75f, 1f);
|
||||||
|
public static readonly Color Lift = new(0.95f, 0.45f, 0.35f, 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c934ed58cc61487aaf68600c93e0d8b3
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
19
Assets/Scripts/Editor/GON/WorldEditor/WorldEditorState.cs
Normal file
19
Assets/Scripts/Editor/GON/WorldEditor/WorldEditorState.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace Editor.GON.WorldEditor
|
||||||
|
{
|
||||||
|
public enum WorldEditorTool
|
||||||
|
{
|
||||||
|
Select = 0,
|
||||||
|
PaintStage = 1,
|
||||||
|
EraseStage = 2,
|
||||||
|
Socket = 3,
|
||||||
|
Anchor = 4,
|
||||||
|
Lift = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WorldEditorState
|
||||||
|
{
|
||||||
|
public int activeLayer;
|
||||||
|
public string selectedStageId;
|
||||||
|
public WorldEditorTool activeTool = WorldEditorTool.PaintStage;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6782fb3fdb8f49d5939b0c92f86adad9
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
275
Assets/Scripts/Editor/GON/WorldEditor/WorldEditorWindow.cs
Normal file
275
Assets/Scripts/Editor/GON/WorldEditor/WorldEditorWindow.cs
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Intrepid.Gameplay.Journey;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.UIElements;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
|
namespace Editor.GON.WorldEditor
|
||||||
|
{
|
||||||
|
public sealed class WorldEditorWindow : EditorWindow
|
||||||
|
{
|
||||||
|
private WorldLayoutSO layout;
|
||||||
|
private WorldEditorState state;
|
||||||
|
private VisualElement topBar;
|
||||||
|
private WorldMapCanvas canvas;
|
||||||
|
private Label infoLabel;
|
||||||
|
|
||||||
|
[MenuItem("Tools/Gardens Of Nil/World Editor")]
|
||||||
|
public static void Open()
|
||||||
|
{
|
||||||
|
var window = GetWindow<WorldEditorWindow>();
|
||||||
|
window.titleContent = new GUIContent("Gardens World Editor");
|
||||||
|
window.minSize = new Vector2(900, 600);
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateGUI()
|
||||||
|
{
|
||||||
|
state ??= new WorldEditorState();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
canvas = new WorldMapCanvas(this, state);
|
||||||
|
canvas.SetLayout(layout);
|
||||||
|
rootVisualElement.Add(canvas);
|
||||||
|
|
||||||
|
infoLabel = new Label
|
||||||
|
{
|
||||||
|
style =
|
||||||
|
{
|
||||||
|
paddingLeft = 8,
|
||||||
|
paddingTop = 4,
|
||||||
|
paddingBottom = 4
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rootVisualElement.Add(infoLabel);
|
||||||
|
|
||||||
|
RebuildTopBar();
|
||||||
|
RebuildInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MutateLayout(string undoName, Action mutation)
|
||||||
|
{
|
||||||
|
if (layout is null || mutation is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Undo.RecordObject(layout, undoName);
|
||||||
|
mutation();
|
||||||
|
layout.EnsureDefaults();
|
||||||
|
EditorUtility.SetDirty(layout);
|
||||||
|
canvas?.RequestRepaint();
|
||||||
|
RebuildTopBar();
|
||||||
|
RebuildInfo();
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldLayoutSO Layout => layout;
|
||||||
|
|
||||||
|
private void RebuildTopBar()
|
||||||
|
{
|
||||||
|
if (topBar is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
topBar.Clear();
|
||||||
|
|
||||||
|
var layoutField = new ObjectField("World Layout")
|
||||||
|
{
|
||||||
|
objectType = typeof(WorldLayoutSO),
|
||||||
|
value = layout,
|
||||||
|
style =
|
||||||
|
{
|
||||||
|
minWidth = 240
|
||||||
|
}
|
||||||
|
};
|
||||||
|
layoutField.RegisterValueChangedCallback(evt => SetLayout(evt.newValue as WorldLayoutSO));
|
||||||
|
topBar.Add(layoutField);
|
||||||
|
|
||||||
|
topBar.Add(new Button(CreateNewLayoutAsset)
|
||||||
|
{
|
||||||
|
text = "New",
|
||||||
|
style = { marginLeft = 6 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (layout is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.EnsureDefaults();
|
||||||
|
|
||||||
|
var layerField = new IntegerField("Layer")
|
||||||
|
{
|
||||||
|
value = state.activeLayer,
|
||||||
|
style =
|
||||||
|
{
|
||||||
|
marginLeft = 10,
|
||||||
|
width = 90
|
||||||
|
}
|
||||||
|
};
|
||||||
|
layerField.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
state.activeLayer = evt.newValue;
|
||||||
|
canvas?.RequestRepaint();
|
||||||
|
RebuildInfo();
|
||||||
|
});
|
||||||
|
topBar.Add(layerField);
|
||||||
|
|
||||||
|
var toolField = new EnumField("Tool", state.activeTool)
|
||||||
|
{
|
||||||
|
style =
|
||||||
|
{
|
||||||
|
marginLeft = 8,
|
||||||
|
minWidth = 170
|
||||||
|
}
|
||||||
|
};
|
||||||
|
toolField.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
state.activeTool = (WorldEditorTool)evt.newValue;
|
||||||
|
canvas?.RequestRepaint();
|
||||||
|
RebuildInfo();
|
||||||
|
});
|
||||||
|
topBar.Add(toolField);
|
||||||
|
|
||||||
|
topBar.Add(new Button(CreateStage)
|
||||||
|
{
|
||||||
|
text = "+ Stage",
|
||||||
|
style = { marginLeft = 8 }
|
||||||
|
});
|
||||||
|
|
||||||
|
topBar.Add(new Button(ValidatePlaceholder)
|
||||||
|
{
|
||||||
|
text = "Validate",
|
||||||
|
style = { marginLeft = 8 }
|
||||||
|
});
|
||||||
|
|
||||||
|
var commit = new Button(CommitPlaceholder)
|
||||||
|
{
|
||||||
|
text = "Commit Journey",
|
||||||
|
style = { marginLeft = 4 }
|
||||||
|
};
|
||||||
|
commit.SetEnabled(false);
|
||||||
|
topBar.Add(commit);
|
||||||
|
|
||||||
|
topBar.Add(new Button(SaveLayout)
|
||||||
|
{
|
||||||
|
text = "Save",
|
||||||
|
style = { marginLeft = 8 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildInfo()
|
||||||
|
{
|
||||||
|
if (infoLabel is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout is null)
|
||||||
|
{
|
||||||
|
infoLabel.text = "Select or create a WorldLayoutSO.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected = layout.FindStage(state.selectedStageId);
|
||||||
|
infoLabel.text = selected is null
|
||||||
|
? "No stage selected."
|
||||||
|
: "Selected: " + selected.displayName + " (" + selected.id + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetLayout(WorldLayoutSO newLayout)
|
||||||
|
{
|
||||||
|
layout = newLayout;
|
||||||
|
layout?.EnsureDefaults();
|
||||||
|
if (layout?.FindStage(state.selectedStageId) is null)
|
||||||
|
{
|
||||||
|
state.selectedStageId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas?.SetLayout(layout);
|
||||||
|
RebuildTopBar();
|
||||||
|
RebuildInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateNewLayoutAsset()
|
||||||
|
{
|
||||||
|
var path = EditorUtility.SaveFilePanelInProject(
|
||||||
|
"Create WorldLayoutSO",
|
||||||
|
"WorldLayout",
|
||||||
|
"asset",
|
||||||
|
"Choose where to create the new WorldLayoutSO asset.");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var asset = CreateInstance<WorldLayoutSO>();
|
||||||
|
asset.EnsureDefaults();
|
||||||
|
AssetDatabase.CreateAsset(asset, path);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
SetLayout(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateStage()
|
||||||
|
{
|
||||||
|
MutateLayout("Create world stage", delegate
|
||||||
|
{
|
||||||
|
var stage = new WorldStageAuthoring
|
||||||
|
{
|
||||||
|
id = layout.CreateId("stage"),
|
||||||
|
displayName = "Stage " + (layout.stages.Count + 1),
|
||||||
|
worldLayer = state.activeLayer,
|
||||||
|
color = UnityEngine.Random.ColorHSV(0f, 1f, 0.35f, 0.75f, 0.55f, 0.85f, 0.75f, 0.9f)
|
||||||
|
};
|
||||||
|
stage.AddTile(new JWorldTile(0, 0));
|
||||||
|
layout.stages.Add(stage);
|
||||||
|
state.selectedStageId = stage.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveLayout()
|
||||||
|
{
|
||||||
|
if (layout is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorUtility.SetDirty(layout);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
Debug.Log("Saved WorldLayoutSO: " + AssetDatabase.GetAssetPath(layout));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidatePlaceholder()
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("World Editor", "Validation is implemented by PLN-0004.", "OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CommitPlaceholder()
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("World Editor", "Journey commit is implemented by PLN-0004.", "OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a24a25a37da44f078a55114d872d2f66
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
175
Assets/Scripts/Editor/GON/WorldEditor/WorldLayoutSO.cs
Normal file
175
Assets/Scripts/Editor/GON/WorldEditor/WorldLayoutSO.cs
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Editor.GON.StageEditor;
|
||||||
|
using Intrepid.Gameplay.Journey;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Editor.GON.WorldEditor
|
||||||
|
{
|
||||||
|
[CreateAssetMenu(menuName = "GON/Editor/World Layout", fileName = "WorldLayout")]
|
||||||
|
public sealed class WorldLayoutSO : ScriptableObject
|
||||||
|
{
|
||||||
|
public string id;
|
||||||
|
public float worldTileSize = 24f;
|
||||||
|
public float worldLayerHeight = 24f;
|
||||||
|
public List<WorldStageAuthoring> stages = new();
|
||||||
|
public List<WorldEndpointAuthoring> endpoints = new();
|
||||||
|
public List<WorldConnectionAuthoring> connections = new();
|
||||||
|
|
||||||
|
public void EnsureDefaults()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
id = Guid.NewGuid().ToString("N");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (worldTileSize <= 0f)
|
||||||
|
{
|
||||||
|
worldTileSize = 24f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (worldLayerHeight <= 0f)
|
||||||
|
{
|
||||||
|
worldLayerHeight = 24f;
|
||||||
|
}
|
||||||
|
|
||||||
|
stages ??= new List<WorldStageAuthoring>();
|
||||||
|
endpoints ??= new List<WorldEndpointAuthoring>();
|
||||||
|
connections ??= new List<WorldConnectionAuthoring>();
|
||||||
|
|
||||||
|
foreach (var stage in stages)
|
||||||
|
{
|
||||||
|
stage?.EnsureDefaults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreateId(string prefix)
|
||||||
|
{
|
||||||
|
return prefix + "_" + Guid.NewGuid().ToString("N");
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldStageAuthoring FindStage(string stageId)
|
||||||
|
{
|
||||||
|
EnsureDefaults();
|
||||||
|
foreach (var stage in stages)
|
||||||
|
{
|
||||||
|
if (stage != null && stage.id == stageId)
|
||||||
|
{
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldStageAuthoring FindStageAt(int worldLayer, JWorldTile tile)
|
||||||
|
{
|
||||||
|
EnsureDefaults();
|
||||||
|
foreach (var stage in stages)
|
||||||
|
{
|
||||||
|
if (stage != null && stage.worldLayer == worldLayer && stage.ContainsTile(tile))
|
||||||
|
{
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsTileOccupiedByOther(string stageId, int worldLayer, JWorldTile tile)
|
||||||
|
{
|
||||||
|
var occupyingStage = FindStageAt(worldLayer, tile);
|
||||||
|
return occupyingStage != null && occupyingStage.id != stageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldEndpointAuthoring FindEndpointAt(string stageId, JWorldTile tile, JEndpointKind kind)
|
||||||
|
{
|
||||||
|
EnsureDefaults();
|
||||||
|
foreach (var endpoint in endpoints)
|
||||||
|
{
|
||||||
|
if (endpoint != null && endpoint.stageId == stageId && endpoint.kind == kind &&
|
||||||
|
endpoint.worldTile.x == tile.x && endpoint.worldTile.y == tile.y)
|
||||||
|
{
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public sealed class WorldStageAuthoring
|
||||||
|
{
|
||||||
|
public string id;
|
||||||
|
public string displayName;
|
||||||
|
public int worldLayer;
|
||||||
|
public Color color = new(0.25f, 0.62f, 0.95f, 0.7f);
|
||||||
|
public StageDefinition stageDefinition;
|
||||||
|
public List<JWorldTile> occupiedTiles = new();
|
||||||
|
|
||||||
|
public void EnsureDefaults()
|
||||||
|
{
|
||||||
|
occupiedTiles ??= new List<JWorldTile>();
|
||||||
|
if (string.IsNullOrWhiteSpace(displayName))
|
||||||
|
{
|
||||||
|
displayName = "Stage";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsTile(JWorldTile tile)
|
||||||
|
{
|
||||||
|
EnsureDefaults();
|
||||||
|
foreach (var occupiedTile in occupiedTiles)
|
||||||
|
{
|
||||||
|
if (occupiedTile.x == tile.x && occupiedTile.y == tile.y)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTile(JWorldTile tile)
|
||||||
|
{
|
||||||
|
EnsureDefaults();
|
||||||
|
if (!ContainsTile(tile))
|
||||||
|
{
|
||||||
|
occupiedTiles.Add(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveTile(JWorldTile tile)
|
||||||
|
{
|
||||||
|
EnsureDefaults();
|
||||||
|
for (var i = occupiedTiles.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (occupiedTiles[i].x == tile.x && occupiedTiles[i].y == tile.y)
|
||||||
|
{
|
||||||
|
occupiedTiles.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public sealed class WorldEndpointAuthoring
|
||||||
|
{
|
||||||
|
public string id;
|
||||||
|
public string stageId;
|
||||||
|
public JWorldTile worldTile;
|
||||||
|
public JEndpointKind kind;
|
||||||
|
public bool required = true;
|
||||||
|
public JSocketFace socketFace;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public sealed class WorldConnectionAuthoring
|
||||||
|
{
|
||||||
|
public string id;
|
||||||
|
public string endpointAId;
|
||||||
|
public string endpointBId;
|
||||||
|
public JConnectionDirectionality directionality = JConnectionDirectionality.Bidirectional;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Assets/Scripts/Editor/GON/WorldEditor/WorldLayoutSO.cs.meta
Normal file
11
Assets/Scripts/Editor/GON/WorldEditor/WorldLayoutSO.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8cbbff7dfcc24d2b9030f41f9f2ef718
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
375
Assets/Scripts/Editor/GON/WorldEditor/WorldMapCanvas.cs
Normal file
375
Assets/Scripts/Editor/GON/WorldEditor/WorldMapCanvas.cs
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
using Intrepid.Gameplay.Journey;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
|
namespace Editor.GON.WorldEditor
|
||||||
|
{
|
||||||
|
public sealed class WorldMapCanvas : VisualElement
|
||||||
|
{
|
||||||
|
private const float MinCellPixels = 16f;
|
||||||
|
private const float MaxCellPixels = 96f;
|
||||||
|
|
||||||
|
private readonly WorldEditorWindow owner;
|
||||||
|
private readonly WorldEditorState state;
|
||||||
|
|
||||||
|
private WorldLayoutSO layout;
|
||||||
|
private Vector2 pan = new(60f, 60f);
|
||||||
|
private float cellPixels = 36f;
|
||||||
|
private bool isPanning;
|
||||||
|
private Vector2 panStartMouse;
|
||||||
|
private Vector2 panStartValue;
|
||||||
|
|
||||||
|
public WorldMapCanvas(WorldEditorWindow owner, WorldEditorState state)
|
||||||
|
{
|
||||||
|
this.owner = owner;
|
||||||
|
this.state = state;
|
||||||
|
|
||||||
|
focusable = true;
|
||||||
|
style.flexGrow = 1f;
|
||||||
|
style.overflow = Overflow.Hidden;
|
||||||
|
style.backgroundColor = WorldEditorColors.Background;
|
||||||
|
|
||||||
|
generateVisualContent += OnGenerateVisualContent;
|
||||||
|
RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||||
|
RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||||
|
RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||||
|
RegisterCallback<WheelEvent>(OnWheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLayout(WorldLayoutSO newLayout)
|
||||||
|
{
|
||||||
|
layout = newLayout;
|
||||||
|
layout?.EnsureDefaults();
|
||||||
|
MarkDirtyRepaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestRepaint()
|
||||||
|
{
|
||||||
|
MarkDirtyRepaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMouseDown(MouseDownEvent evt)
|
||||||
|
{
|
||||||
|
Focus();
|
||||||
|
if (evt.button == 1 || evt.button == 2)
|
||||||
|
{
|
||||||
|
isPanning = true;
|
||||||
|
panStartMouse = evt.localMousePosition;
|
||||||
|
panStartValue = pan;
|
||||||
|
this.CaptureMouse();
|
||||||
|
evt.StopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.button != 0 || layout is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryScreenToTile(evt.localMousePosition, out var tile))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyTool(tile, evt.localMousePosition);
|
||||||
|
evt.StopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMouseMove(MouseMoveEvent evt)
|
||||||
|
{
|
||||||
|
if (!isPanning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pan = panStartValue + evt.localMousePosition - panStartMouse;
|
||||||
|
MarkDirtyRepaint();
|
||||||
|
evt.StopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMouseUp(MouseUpEvent evt)
|
||||||
|
{
|
||||||
|
if (!isPanning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPanning = false;
|
||||||
|
if (this.HasMouseCapture())
|
||||||
|
{
|
||||||
|
this.ReleaseMouse();
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.StopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWheel(WheelEvent evt)
|
||||||
|
{
|
||||||
|
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 ApplyTool(JWorldTile tile, Vector2 mouse)
|
||||||
|
{
|
||||||
|
var stageAtTile = layout.FindStageAt(state.activeLayer, tile);
|
||||||
|
if (state.activeTool == WorldEditorTool.Select)
|
||||||
|
{
|
||||||
|
state.selectedStageId = stageAtTile?.id;
|
||||||
|
owner.MutateLayout("Select world stage", delegate { });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedStage = layout.FindStage(state.selectedStageId);
|
||||||
|
if (selectedStage is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedStage.worldLayer != state.activeLayer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state.activeTool)
|
||||||
|
{
|
||||||
|
case WorldEditorTool.PaintStage:
|
||||||
|
if (!layout.IsTileOccupiedByOther(selectedStage.id, state.activeLayer, tile))
|
||||||
|
{
|
||||||
|
owner.MutateLayout("Paint world tile", delegate { selectedStage.AddTile(tile); });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WorldEditorTool.EraseStage:
|
||||||
|
owner.MutateLayout("Erase world tile", delegate { selectedStage.RemoveTile(tile); });
|
||||||
|
break;
|
||||||
|
case WorldEditorTool.Anchor:
|
||||||
|
AddEndpoint(selectedStage, tile, JEndpointKind.Anchor, JSocketFace.North);
|
||||||
|
break;
|
||||||
|
case WorldEditorTool.Lift:
|
||||||
|
AddEndpoint(selectedStage, tile, JEndpointKind.Lift, JSocketFace.North);
|
||||||
|
break;
|
||||||
|
case WorldEditorTool.Socket:
|
||||||
|
AddEndpoint(selectedStage, tile, JEndpointKind.Socket, GetNearestFace(mouse, tile));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddEndpoint(WorldStageAuthoring stage, JWorldTile tile, JEndpointKind kind, JSocketFace face)
|
||||||
|
{
|
||||||
|
if (!stage.ContainsTile(tile))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((kind == JEndpointKind.Anchor && layout.FindEndpointAt(stage.id, tile, JEndpointKind.Lift) != null) ||
|
||||||
|
(kind == JEndpointKind.Lift && layout.FindEndpointAt(stage.id, tile, JEndpointKind.Anchor) != null))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout.FindEndpointAt(stage.id, tile, kind) != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
owner.MutateLayout("Add journey endpoint", delegate
|
||||||
|
{
|
||||||
|
layout.endpoints.Add(new WorldEndpointAuthoring
|
||||||
|
{
|
||||||
|
id = layout.CreateId("endpoint"),
|
||||||
|
stageId = stage.id,
|
||||||
|
worldTile = tile,
|
||||||
|
kind = kind,
|
||||||
|
socketFace = face,
|
||||||
|
required = true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGenerateVisualContent(MeshGenerationContext mgc)
|
||||||
|
{
|
||||||
|
var p = mgc.painter2D;
|
||||||
|
DrawGrid(p);
|
||||||
|
DrawStages(p);
|
||||||
|
DrawEndpoints(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawGrid(Painter2D p)
|
||||||
|
{
|
||||||
|
var rect = contentRect;
|
||||||
|
var min = ScreenToGridFloat(new Vector2(rect.xMin, rect.yMin));
|
||||||
|
var max = ScreenToGridFloat(new Vector2(rect.xMax, rect.yMax));
|
||||||
|
var minX = Mathf.FloorToInt(min.x) - 1;
|
||||||
|
var maxX = Mathf.CeilToInt(max.x) + 1;
|
||||||
|
var minY = Mathf.FloorToInt(min.y) - 1;
|
||||||
|
var maxY = Mathf.CeilToInt(max.y) + 1;
|
||||||
|
|
||||||
|
for (var x = minX; x <= maxX; x++)
|
||||||
|
{
|
||||||
|
DrawLine(p, GridToScreen(new Vector2(x, minY)), GridToScreen(new Vector2(x, maxY)),
|
||||||
|
x == 0 ? WorldEditorColors.GridAxis : WorldEditorColors.Grid, x == 0 ? 2f : 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var y = minY; y <= maxY; y++)
|
||||||
|
{
|
||||||
|
DrawLine(p, GridToScreen(new Vector2(minX, y)), GridToScreen(new Vector2(maxX, y)),
|
||||||
|
y == 0 ? WorldEditorColors.GridAxis : WorldEditorColors.Grid, y == 0 ? 2f : 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawStages(Painter2D p)
|
||||||
|
{
|
||||||
|
if (layout is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.EnsureDefaults();
|
||||||
|
foreach (var stage in layout.stages)
|
||||||
|
{
|
||||||
|
if (stage == null || stage.worldLayer != state.activeLayer)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var tile in stage.occupiedTiles)
|
||||||
|
{
|
||||||
|
var rect = TileRect(tile);
|
||||||
|
FillRect(p, rect, stage.color);
|
||||||
|
StrokeRect(p, rect, stage.id == state.selectedStageId ? WorldEditorColors.Selected : Color.black, 2f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawEndpoints(Painter2D p)
|
||||||
|
{
|
||||||
|
if (layout is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var endpoint in layout.endpoints)
|
||||||
|
{
|
||||||
|
var stage = layout.FindStage(endpoint.stageId);
|
||||||
|
if (stage is null || stage.worldLayer != state.activeLayer)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rect = TileRect(endpoint.worldTile);
|
||||||
|
var center = rect.center;
|
||||||
|
switch (endpoint.kind)
|
||||||
|
{
|
||||||
|
case JEndpointKind.Anchor:
|
||||||
|
FillMarker(p, center, Mathf.Max(8f, cellPixels * 0.28f), WorldEditorColors.Anchor);
|
||||||
|
break;
|
||||||
|
case JEndpointKind.Lift:
|
||||||
|
FillMarker(p, center, Mathf.Max(8f, cellPixels * 0.28f), WorldEditorColors.Lift);
|
||||||
|
DrawLine(p, center + Vector2.down * 7f, center + Vector2.up * 7f, Color.white, 2f);
|
||||||
|
break;
|
||||||
|
case JEndpointKind.Socket:
|
||||||
|
DrawSocket(p, rect, endpoint.socketFace);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSocket(Painter2D p, Rect rect, JSocketFace face)
|
||||||
|
{
|
||||||
|
var center = rect.center;
|
||||||
|
var half = cellPixels * 0.5f;
|
||||||
|
var size = Mathf.Max(5f, cellPixels * 0.16f);
|
||||||
|
var point = face switch
|
||||||
|
{
|
||||||
|
JSocketFace.North => new Vector2(center.x, center.y - half),
|
||||||
|
JSocketFace.South => new Vector2(center.x, center.y + half),
|
||||||
|
JSocketFace.East => new Vector2(center.x + half, center.y),
|
||||||
|
_ => new Vector2(center.x - half, center.y)
|
||||||
|
};
|
||||||
|
FillMarker(p, point, size * 2f, WorldEditorColors.Socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSocketFace GetNearestFace(Vector2 mouse, JWorldTile tile)
|
||||||
|
{
|
||||||
|
var rect = TileRect(tile);
|
||||||
|
var left = Mathf.Abs(mouse.x - rect.xMin);
|
||||||
|
var right = Mathf.Abs(mouse.x - rect.xMax);
|
||||||
|
var top = Mathf.Abs(mouse.y - rect.yMin);
|
||||||
|
var bottom = Mathf.Abs(mouse.y - rect.yMax);
|
||||||
|
var min = Mathf.Min(left, right, top, bottom);
|
||||||
|
if (Mathf.Approximately(min, top)) return JSocketFace.North;
|
||||||
|
if (Mathf.Approximately(min, bottom)) return JSocketFace.South;
|
||||||
|
if (Mathf.Approximately(min, right)) return JSocketFace.East;
|
||||||
|
return JSocketFace.West;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Rect TileRect(JWorldTile tile)
|
||||||
|
{
|
||||||
|
var min = GridToScreen(new Vector2(tile.x, tile.y));
|
||||||
|
return new Rect(min.x, min.y, cellPixels, cellPixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryScreenToTile(Vector2 screen, out JWorldTile tile)
|
||||||
|
{
|
||||||
|
var grid = ScreenToGridFloat(screen);
|
||||||
|
tile = new JWorldTile(Mathf.FloorToInt(grid.x), Mathf.FloorToInt(grid.y));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 ScreenToGridFloat(Vector2 screen)
|
||||||
|
{
|
||||||
|
return (screen - pan) / cellPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 GridToScreen(Vector2 grid)
|
||||||
|
{
|
||||||
|
return pan + grid * 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 FillMarker(Painter2D p, Vector2 center, float size, Color color)
|
||||||
|
{
|
||||||
|
var half = size * 0.5f;
|
||||||
|
FillRect(p, new Rect(center.x - half, center.y - half, size, size), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Assets/Scripts/Editor/GON/WorldEditor/WorldMapCanvas.cs.meta
Normal file
11
Assets/Scripts/Editor/GON/WorldEditor/WorldMapCanvas.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 817967f2b72043129f5018488de4c53d
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -1,2 +1,2 @@
|
|||||||
{"type":"meta","next_id":{"DSC":2,"AGD":2,"DEC":2,"PLN":5,"LSN":1,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":2,"AGD":2,"DEC":2,"PLN":5,"LSN":1,"CLSN":1}}
|
||||||
{"type":"discussion","id":"DSC-0001","status":"accepted","ticket":"world-editor-journey-spec","title":"World Editor / Journey Spec","created_at":"2026-06-19","updated_at":"2026-06-19","tags":["world-editor","journey","stage-contract","architecture"],"agendas":[{"id":"AGD-0001","file":"AGD-0001-world-editor-journey-spec.md","status":"accepted","created_at":"2026-06-19","updated_at":"2026-06-19"}],"decisions":[{"id":"DEC-0001","file":"DEC-0001-world-editor-journey-v1.md","status":"accepted","created_at":"2026-06-19","updated_at":"2026-06-19","ref_agenda":"AGD-0001"}],"plans":[{"id":"PLN-0001","file":"PLN-0001-world-journey-spec.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0002","file":"PLN-0002-journey-runtime-model.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0003","file":"PLN-0003-world-editor-authoring.md","status":"review","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0004","file":"PLN-0004-journey-commit-validation.md","status":"review","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]}],"lessons":[]}
|
{"type":"discussion","id":"DSC-0001","status":"accepted","ticket":"world-editor-journey-spec","title":"World Editor / Journey Spec","created_at":"2026-06-19","updated_at":"2026-06-19","tags":["world-editor","journey","stage-contract","architecture"],"agendas":[{"id":"AGD-0001","file":"AGD-0001-world-editor-journey-spec.md","status":"accepted","created_at":"2026-06-19","updated_at":"2026-06-19"}],"decisions":[{"id":"DEC-0001","file":"DEC-0001-world-editor-journey-v1.md","status":"accepted","created_at":"2026-06-19","updated_at":"2026-06-19","ref_agenda":"AGD-0001"}],"plans":[{"id":"PLN-0001","file":"PLN-0001-world-journey-spec.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0002","file":"PLN-0002-journey-runtime-model.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0003","file":"PLN-0003-world-editor-authoring.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0004","file":"PLN-0004-journey-commit-validation.md","status":"review","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]}],"lessons":[]}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0003
|
id: PLN-0003
|
||||||
ticket: world-editor-journey-spec
|
ticket: world-editor-journey-spec
|
||||||
title: World Editor Authoring UI and WorldLayoutSO
|
title: World Editor Authoring UI and WorldLayoutSO
|
||||||
status: review
|
status: done
|
||||||
created: 2026-06-19
|
created: 2026-06-19
|
||||||
decisions:
|
decisions:
|
||||||
- DEC-0001
|
- DEC-0001
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user