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":"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
|
||||
ticket: world-editor-journey-spec
|
||||
title: World Editor Authoring UI and WorldLayoutSO
|
||||
status: review
|
||||
status: done
|
||||
created: 2026-06-19
|
||||
decisions:
|
||||
- DEC-0001
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user