implements PLN-0003 world editor authoring

This commit is contained in:
Nilton Constantino 2026-06-19 13:40:55 +01:00
parent 7d9947e5f0
commit da4f1caaa7
No known key found for this signature in database
12 changed files with 916 additions and 2 deletions

View 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);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c934ed58cc61487aaf68600c93e0d8b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6782fb3fdb8f49d5939b0c92f86adad9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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");
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a24a25a37da44f078a55114d872d2f66
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8cbbff7dfcc24d2b9030f41f9f2ef718
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 817967f2b72043129f5018488de4c53d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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":[]}

View File

@ -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