using System; using System.Collections.Generic; using Cthangover.Core.Interactive; using Cthangover.Core.Mods; using Cthangover.Core.Mods.Resolvers; using Cthangover.Core.Factories.Impls; using Cthangover.Core.UI.Tool; using Cthangover.Core.Utils; using Cthangover.Tools.Services; using Godot; namespace Cthangover.Tools.InteractiveEditor { public partial class InteractiveEditorWindow : ToolWindow { private static readonly Color _rectColor = new(0f, 1f, 0f, 0.6f); private static readonly Color _circleColor = new(0f, 0.5f, 1f, 0.6f); private static readonly Color _vertexColor = new(1f, 0.8f, 0f, 0.8f); private static readonly Color _colliderOverlayColor = new(0f, 1f, 0f, 0.15f); private static readonly Color _colliderCircleOverlayColor = new(0f, 0.5f, 1f, 0.15f); private static readonly Color _colliderPolyOverlayColor = new(1f, 0.5f, 0f, 0.15f); private static readonly Color _colliderPolyOutlineColor = new(1f, 0.5f, 0f, 0.6f); private InteractiveEditorController _controller = new(); private Control _previewContainer; private TextureRect _preview; private Control _colliderOverlay; private string _currentBgId; private readonly List _handles = new(); private TextEdit _jsonEdit; private LineEdit _idEdit, _textureEdit; private OptionButton _layerSelect; private OptionButton _hitTypeSelect; private ItemList _bgList; private LineEdit _rectX, _rectY, _rectW, _rectH; private LineEdit _circleX, _circleY, _circleR; private VBoxContainer _rectParams, _circleParams, _polyParams; private ColorPickerButton _highlightColorBtn; private LineEdit _highlightScaleEdit; private LineEdit _clickScenario, _hoverEnter, _hoverLeave; private TextEdit _clickCommands; private VBoxContainer _vertexList; private Button _addVertexBtn; private bool _suppressingSidebarEvents; private int _draggingHandleIndex = -1; public InteractiveEditorWindow() : base("tools/interactive_editor/title") { Size = new Vector2I(1280, 820); var outer = CreateFillContainer(); AddChild(outer); var toolbar = CreateToolbar(); outer.AddChild(toolbar); var loadBtn = new Button { Text = Tr("tools/interactive_editor/btn_load_json") }; loadBtn.Pressed += OnLoadJson; toolbar.AddChild(loadBtn); var saveBtn = new Button { Text = Tr("tools/interactive_editor/btn_save") }; saveBtn.Pressed += OnSave; toolbar.AddChild(saveBtn); var importDefBtn = new Button { Text = Tr("tools/interactive_editor/btn_import_mod") }; importDefBtn.Pressed += OnImportFromMod; toolbar.AddChild(importDefBtn); var mainHBox = new HBoxContainer(); mainHBox.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; mainHBox.SizeFlagsVertical = Control.SizeFlags.ExpandFill; outer.AddChild(mainHBox); var sidebarVBox = new VBoxContainer(); var sidebar = CreateSidebar(sidebarVBox, 290); mainHBox.AddChild(sidebar); BuildSidebar(sidebarVBox); _previewContainer = new Control { MouseFilter = Control.MouseFilterEnum.Stop, SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, SizeFlagsVertical = Control.SizeFlags.ExpandFill }; mainHBox.AddChild(_previewContainer); _preview = new TextureRect { ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize, StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered, MouseFilter = Control.MouseFilterEnum.Ignore, AnchorRight = 1f, AnchorBottom = 1f }; _previewContainer.AddChild(_preview); _colliderOverlay = new Control { MouseFilter = Control.MouseFilterEnum.Ignore, Visible = false }; _colliderOverlay.Connect("draw", Callable.From((Action)DrawColliderShape)); _previewContainer.AddChild(_colliderOverlay); _previewContainer.Resized += () => { RebuildHandles(); UpdateColliderOverlay(); }; _jsonEdit = new TextEdit { CustomMinimumSize = new Vector2(0, 150) }; _jsonEdit.AddThemeFontOverride("font", GetMonospaceFont()); _jsonEdit.TextChanged += () => SetDirty(); outer.AddChild(_jsonEdit); RefreshJson(); } private void BuildSidebar(VBoxContainer sidebar) { sidebar.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; var tabs = new TabContainer(); tabs.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; sidebar.AddChild(tabs); var bgTab = new VBoxContainer(); tabs.AddChild(bgTab); tabs.SetTabTitle(0, Tr("tools/interactive_editor/tab_background")); BuildBackgroundTab(bgTab); var interactiveTab = new VBoxContainer(); tabs.AddChild(interactiveTab); tabs.SetTabTitle(1, Tr("tools/interactive_editor/tab_interactive")); BuildInteractiveTab(interactiveTab); var scenarioTab = new VBoxContainer(); tabs.AddChild(scenarioTab); tabs.SetTabTitle(2, Tr("tools/interactive_editor/tab_scenario")); BuildScenarioTab(scenarioTab); } private void BuildBackgroundTab(VBoxContainer parent) { AddLabel(parent, "tools/interactive_editor/label_id"); _idEdit = AddLineEdit(parent, "my_interactive"); _idEdit.TextChanged += OnParamChanged; AddLabel(parent, "tools/interactive_editor/label_texture_key"); _textureEdit = AddLineEdit(parent, "lamp"); _textureEdit.TextChanged += OnParamChanged; AddLabel(parent, "tools/interactive_editor/label_layer"); _layerSelect = new OptionButton(); _layerSelect.AddItem("foreground"); _layerSelect.AddItem("background"); _layerSelect.AddItem("ui"); _layerSelect.ItemSelected += _ => OnParamChanged(); parent.AddChild(_layerSelect); AddLabel(parent, "tools/interactive_editor/label_bg_preview"); _bgList = new ItemList(); _bgList.CustomMinimumSize = new Vector2(0, 100); _bgList.ItemSelected += OnBgSelected; parent.AddChild(_bgList); LoadBackgroundList(); } private void BuildInteractiveTab(VBoxContainer parent) { AddLabel(parent, "tools/interactive_editor/label_collider_type"); _hitTypeSelect = new OptionButton(); _hitTypeSelect.AddItem("rect"); _hitTypeSelect.AddItem("circle"); _hitTypeSelect.AddItem("polygon"); _hitTypeSelect.ItemSelected += OnHitTypeChanged; parent.AddChild(_hitTypeSelect); _rectParams = new VBoxContainer(); AddLabel(_rectParams, "tools/interactive_editor/label_rect_params"); _rectX = AddLabeledLine(_rectParams, "tools/interactive_editor/label_x"); _rectX.TextChanged += OnParamChanged; _rectY = AddLabeledLine(_rectParams, "tools/interactive_editor/label_y"); _rectY.TextChanged += OnParamChanged; _rectW = AddLabeledLine(_rectParams, "tools/interactive_editor/label_w"); _rectW.TextChanged += OnParamChanged; _rectH = AddLabeledLine(_rectParams, "tools/interactive_editor/label_h"); _rectH.TextChanged += OnParamChanged; parent.AddChild(_rectParams); _circleParams = new VBoxContainer(); AddLabel(_circleParams, "tools/interactive_editor/label_circle_params"); _circleX = AddLabeledLine(_circleParams, "tools/interactive_editor/label_cx"); _circleX.TextChanged += OnParamChanged; _circleY = AddLabeledLine(_circleParams, "tools/interactive_editor/label_cy"); _circleY.TextChanged += OnParamChanged; _circleR = AddLabeledLine(_circleParams, "tools/interactive_editor/label_r"); _circleR.TextChanged += OnParamChanged; _circleParams.Visible = false; parent.AddChild(_circleParams); _polyParams = new VBoxContainer(); AddLabel(_polyParams, "tools/interactive_editor/label_polygon_vertices"); _addVertexBtn = new Button { Text = Tr("tools/interactive_editor/btn_add_vertex") }; _addVertexBtn.Pressed += OnAddVertex; _polyParams.AddChild(_addVertexBtn); _vertexList = new VBoxContainer(); _polyParams.AddChild(_vertexList); _polyParams.Visible = false; parent.AddChild(_polyParams); AddLabel(parent, "tools/interactive_editor/label_highlight_color"); _highlightColorBtn = new ColorPickerButton { Color = new Color(1f, 1f, 0f, 0.2f), CustomMinimumSize = new Vector2(40, 30) }; _highlightColorBtn.ColorChanged += _ => OnParamChanged(); parent.AddChild(_highlightColorBtn); AddLabel(parent, "tools/interactive_editor/label_highlight_scale"); _highlightScaleEdit = AddLineEdit(parent, "1.02"); _highlightScaleEdit.TextChanged += OnParamChanged; } private void BuildScenarioTab(VBoxContainer parent) { AddLabel(parent, "tools/interactive_editor/label_cursor"); var cursorSelect = new OptionButton(); cursorSelect.AddItem(Tr("tools/interactive_editor/cursor_none")); cursorSelect.AddItem("PointingHand"); cursorSelect.ItemSelected += idx => { _controller.Cursor = idx == 0 ? "" : "PointingHand"; OnParamChanged(); }; parent.AddChild(cursorSelect); AddLabel(parent, "tools/interactive_editor/label_onclick_scenario"); _clickScenario = AddLineEdit(parent, "scenarios/my_click.scenario"); _clickScenario.TextChanged += OnParamChanged; AddLabel(parent, "tools/interactive_editor/label_onclick_commands"); _clickCommands = new TextEdit { CustomMinimumSize = new Vector2(0, 50) }; _clickCommands.TextChanged += OnParamChanged; parent.AddChild(_clickCommands); AddLabel(parent, "tools/interactive_editor/label_hover_enter"); _hoverEnter = AddLineEdit(parent, "set cursor_hint=click_me"); _hoverEnter.TextChanged += OnParamChanged; AddLabel(parent, "tools/interactive_editor/label_hover_leave"); _hoverLeave = AddLineEdit(parent, "set cursor_hint="); _hoverLeave.TextChanged += OnParamChanged; } private static string Tr(string key) => TranslationServer.Translate(key); private void AddLabel(Control parent, string locKey) { var label = new Label { Text = Tr(locKey) }; label.AddThemeColorOverride("font_color", new Color(0.7f, 0.7f, 0.7f)); parent.AddChild(label); } private LineEdit AddLineEdit(Control parent, string placeholder) { var edit = new LineEdit { PlaceholderText = placeholder }; parent.AddChild(edit); return edit; } private LineEdit AddLabeledLine(Control parent, string locKey) { var hbox = new HBoxContainer(); var label = new Label { Text = Tr(locKey) }; label.CustomMinimumSize = new Vector2(25, 0); hbox.AddChild(label); var edit = new LineEdit(); edit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; hbox.AddChild(edit); parent.AddChild(hbox); return edit; } private void LoadBackgroundList() { _bgList.Clear(); var ids = ModResourceService.GetBackgroundIds(); foreach (var id in ids) _bgList.AddItem(id); } private void OnBgSelected(long index) { var id = _bgList.GetItemText((int)index); _currentBgId = id; var tex = ModResourceService.LoadBackgroundTexture(id); if (tex != null) { _preview.Texture = tex; } OnParamChanged(); } private void OnHitTypeChanged(long index) { _controller.HitType = _hitTypeSelect.GetItemText((int)index); _rectParams.Visible = _controller.HitType == "rect"; _circleParams.Visible = _controller.HitType == "circle"; _polyParams.Visible = _controller.HitType == "polygon"; OnParamChanged(); } private void OnAddVertex() { _controller.AddPolygonVertex(); OnParamChanged(); } private void OnParamChanged() { if (_suppressingSidebarEvents) { GameLogger.Log("INTERACTIVE_EDITOR", "OnParamChanged suppressed"); return; } GameLogger.Log("INTERACTIVE_EDITOR", $"OnParamChanged hitType={_controller.HitType} bgSize=({_previewContainer.Size.X:F0}x{_previewContainer.Size.Y:F0})"); ReadSidebarToController(); RebuildHandles(); UpdateColliderOverlay(); RefreshJson(); SetDirty(); UpdateVertexList(); } private void OnDragUpdate() { UpdateNonDraggedHandles(); UpdateColliderOverlay(); RefreshJson(); SetDirty(); } private void ReadSidebarToController() { _controller.Id = string.IsNullOrWhiteSpace(_idEdit.Text) ? "new_interactive" : _idEdit.Text.Trim(); _controller.Texture = _textureEdit.Text.Trim(); _controller.Layer = _layerSelect.GetItemText(_layerSelect.Selected); if (_controller.HitType == "rect") { float.TryParse(_rectX.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var v); _controller.RectX = v; float.TryParse(_rectY.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v); _controller.RectY = v; float.TryParse(_rectW.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v); _controller.RectW = v; float.TryParse(_rectH.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v); _controller.RectH = v; } else if (_controller.HitType == "circle") { float.TryParse(_circleX.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var v); _controller.CircleX = v; float.TryParse(_circleY.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v); _controller.CircleY = v; float.TryParse(_circleR.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v); _controller.CircleRadius = v; } _controller.HighlightColorHex = "#" + _highlightColorBtn.Color.ToHtml(false); float.TryParse(_highlightScaleEdit.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var scale); _controller.HighlightScale = scale; _controller.OnClickScenario = _clickScenario.Text.Trim(); _controller.OnClickCommands = _clickCommands.Text.Trim(); _controller.OnHoverEnter = _hoverEnter.Text.Trim(); _controller.OnHoverLeave = _hoverLeave.Text.Trim(); } private Rect2 GetBackgroundRect() { var cw = _previewContainer.Size.X; var ch = _previewContainer.Size.Y; if (cw <= 0 || ch <= 0) return new Rect2(); var texSize = new Vector2(1920, 1024); var tex = _preview.Texture; if (tex != null) { var ts = tex.GetSize(); if (ts.X > 0 && ts.Y > 0) texSize = ts; } var scale = Mathf.Min(cw / texSize.X, ch / texSize.Y); var rw = texSize.X * scale; var rh = texSize.Y * scale; var ox = (cw - rw) / 2f; var oy = (ch - rh) / 2f; return new Rect2(ox, oy, rw, rh); } private Vector2 NormToPixel(Vector2 norm) { var bg = GetBackgroundRect(); return bg.Position + norm * bg.Size; } private Vector2 PixelToNorm(Vector2 pixel) { var bg = GetBackgroundRect(); if (bg.Size.X <= 0 || bg.Size.Y <= 0) return Vector2.Zero; return new Vector2( (pixel.X - bg.Position.X) / bg.Size.X, (pixel.Y - bg.Position.Y) / bg.Size.Y); } private void WriteControllerToSidebar() { _suppressingSidebarEvents = true; try { _idEdit.Text = _controller.Id; _textureEdit.Text = _controller.Texture; for (int i = 0; i < _layerSelect.ItemCount; i++) if (_layerSelect.GetItemText(i) == _controller.Layer) _layerSelect.Select(i); for (int i = 0; i < _hitTypeSelect.ItemCount; i++) if (_hitTypeSelect.GetItemText(i) == _controller.HitType) _hitTypeSelect.Select(i); _rectX.Text = _controller.RectX.ToString("F3", System.Globalization.CultureInfo.InvariantCulture); _rectY.Text = _controller.RectY.ToString("F3", System.Globalization.CultureInfo.InvariantCulture); _rectW.Text = _controller.RectW.ToString("F3", System.Globalization.CultureInfo.InvariantCulture); _rectH.Text = _controller.RectH.ToString("F3", System.Globalization.CultureInfo.InvariantCulture); _circleX.Text = _controller.CircleX.ToString("F3", System.Globalization.CultureInfo.InvariantCulture); _circleY.Text = _controller.CircleY.ToString("F3", System.Globalization.CultureInfo.InvariantCulture); _circleR.Text = _controller.CircleRadius.ToString("F3", System.Globalization.CultureInfo.InvariantCulture); _rectParams.Visible = _controller.HitType == "rect"; _circleParams.Visible = _controller.HitType == "circle"; _polyParams.Visible = _controller.HitType == "polygon"; _highlightColorBtn.Color = new Color(_controller.HighlightColorHex); _highlightScaleEdit.Text = _controller.HighlightScale.ToString("F2", System.Globalization.CultureInfo.InvariantCulture); _clickScenario.Text = _controller.OnClickScenario; _clickCommands.Text = _controller.OnClickCommands; _hoverEnter.Text = _controller.OnHoverEnter; _hoverLeave.Text = _controller.OnHoverLeave; } finally { _suppressingSidebarEvents = false; } } private void RebuildHandles() { foreach (var h in _handles) h.QueueFree(); _handles.Clear(); var bg = GetBackgroundRect(); if (bg.Size.X <= 0 || bg.Size.Y <= 0) { GameLogger.Log("INTERACTIVE_EDITOR", "RebuildHandles skipped: bg size <= 0"); return; } GameLogger.Log("INTERACTIVE_EDITOR", $"RebuildHandles hitType={_controller.HitType} bg=({bg.Position.X:F0},{bg.Position.Y:F0} {bg.Size.X:F0}x{bg.Size.Y:F0})"); var globalTopLeft = _previewContainer.GlobalPosition; switch (_controller.HitType) { case "rect": { var topLeft = NormToPixel(new Vector2(_controller.RectX, _controller.RectY)); var bottomRight = NormToPixel(new Vector2( _controller.RectX + _controller.RectW, _controller.RectY + _controller.RectH)); AddHandleAt(globalTopLeft + topLeft, _rectColor, 0, pos => { var norm = PixelToNorm(pos - globalTopLeft); _controller.RectX = Mathf.Clamp(norm.X, 0f, 1f); _controller.RectY = Mathf.Clamp(norm.Y, 0f, 1f); _draggingHandleIndex = 0; OnDragUpdate(); }); AddHandleAt(globalTopLeft + bottomRight, _rectColor, 1, pos => { var norm = PixelToNorm(pos - globalTopLeft); _controller.RectW = Mathf.Max(0.01f, norm.X - _controller.RectX); _controller.RectH = Mathf.Max(0.01f, norm.Y - _controller.RectY); _draggingHandleIndex = 1; OnDragUpdate(); }); break; } case "circle": { var refDim = Mathf.Min(bg.Size.X, bg.Size.Y); var center = NormToPixel(new Vector2(_controller.CircleX, _controller.CircleY)); var radiusPx = _controller.CircleRadius * refDim; AddHandleAt(globalTopLeft + center, _circleColor, 0, pos => { var norm = PixelToNorm(pos - globalTopLeft); _controller.CircleX = Mathf.Clamp(norm.X, 0f, 1f); _controller.CircleY = Mathf.Clamp(norm.Y, 0f, 1f); _draggingHandleIndex = 0; OnDragUpdate(); }); AddHandleAt(globalTopLeft + new Vector2(center.X + radiusPx, center.Y), _circleColor, 1, pos => { var local = pos - globalTopLeft; var centerPx = NormToPixel(new Vector2(_controller.CircleX, _controller.CircleY)); var dist = (local - centerPx).Length(); _controller.CircleRadius = Mathf.Max(0.005f, dist / refDim); _draggingHandleIndex = 1; OnDragUpdate(); }); break; } case "polygon": { for (int i = 0; i < _controller.PolygonVertices.Count; i++) { var idx = i; var v = _controller.PolygonVertices[i]; AddHandleAt(globalTopLeft + NormToPixel(v), _vertexColor, i, pos => { var norm = PixelToNorm(pos - globalTopLeft); _controller.SetPolygonVertex(idx, new Vector2( Mathf.Clamp(norm.X, 0f, 1f), Mathf.Clamp(norm.Y, 0f, 1f))); _draggingHandleIndex = idx; OnDragUpdate(); }); } break; } } } private void UpdateNonDraggedHandles() { var bg = GetBackgroundRect(); if (bg.Size.X <= 0 || bg.Size.Y <= 0) return; var globalTopLeft = _previewContainer.GlobalPosition; for (int i = 0; i < _handles.Count; i++) { if (i == _draggingHandleIndex) continue; var pos = GetHandleNormPosition(i); var pixelPos = globalTopLeft + NormToPixel(pos); var h = _handles[i]; if (h != null && !h.IsQueuedForDeletion()) h.GlobalPosition = pixelPos - h.Size / 2; } } private Vector2 GetHandleNormPosition(int index) { switch (_controller.HitType) { case "rect": if (index == 0) return new Vector2(_controller.RectX, _controller.RectY); return new Vector2(_controller.RectX + _controller.RectW, _controller.RectY + _controller.RectH); case "circle": if (index == 0) return new Vector2(_controller.CircleX, _controller.CircleY); return new Vector2(_controller.CircleX + _controller.CircleRadius, _controller.CircleY); case "polygon": if (index >= 0 && index < _controller.PolygonVertices.Count) return _controller.PolygonVertices[index]; return Vector2.Zero; default: return Vector2.Zero; } } private void UpdateColliderOverlay() { var bg = GetBackgroundRect(); if (bg.Size.X <= 0 || bg.Size.Y <= 0) { _colliderOverlay.Visible = false; return; } _colliderOverlay.Visible = true; _colliderOverlay.Position = bg.Position; _colliderOverlay.Size = bg.Size; _colliderOverlay.QueueRedraw(); } private void DrawColliderShape() { var size = _colliderOverlay.Size; if (size.X <= 0 || size.Y <= 0) return; switch (_controller.HitType) { case "rect": { var pos = NormToPixelLocal(new Vector2(_controller.RectX, _controller.RectY)); var rectSize = new Vector2(_controller.RectW * size.X, _controller.RectH * size.Y); _colliderOverlay.DrawRect(new Rect2(pos, rectSize), _colliderOverlayColor); break; } case "circle": { var rPx = _controller.CircleRadius * Mathf.Min(size.X, size.Y); var center = NormToPixelLocal(new Vector2(_controller.CircleX, _controller.CircleY)); _colliderOverlay.DrawCircle(center, rPx, _colliderCircleOverlayColor); break; } case "polygon": { if (_controller.PolygonVertices.Count >= 3) { var points = new Vector2[_controller.PolygonVertices.Count]; for (int i = 0; i < _controller.PolygonVertices.Count; i++) points[i] = NormToPixelLocal(_controller.PolygonVertices[i]); if (PolygonArea(points) > 0.0001f) _colliderOverlay.DrawColoredPolygon(points, _colliderPolyOverlayColor); _colliderOverlay.DrawPolyline(points, _colliderPolyOutlineColor); } break; } } } private Vector2 NormToPixelLocal(Vector2 norm) { return norm * _colliderOverlay.Size; } private static float PolygonArea(Vector2[] points) { float area = 0; for (int i = 0; i < points.Length; i++) { var j = (i + 1) % points.Length; area += points[i].X * points[j].Y; area -= points[j].X * points[i].Y; } return Mathf.Abs(area) * 0.5f; } private void AddHandleAt(Vector2 globalPos, Color color, int index, Action onDrag) { var handle = new InteractiveEditorHandle(color, index); handle.DragUpdate = center => { onDrag(center); }; handle.Clicked = () => { SelectHandle(index); }; handle.DragEnd = () => { GameLogger.Log("INTERACTIVE_EDITOR", $"DragEnd idx={index} syncing sidebar"); _draggingHandleIndex = -1; _suppressingSidebarEvents = true; WriteControllerToSidebar(); _suppressingSidebarEvents = false; UpdateVertexList(); }; handle.MouseDefaultCursorShape = Control.CursorShape.PointingHand; _previewContainer.AddChild(handle); handle.GlobalPosition = globalPos - handle.Size / 2; _handles.Add(handle); } private void SelectHandle(int index) { if (_controller.HitType == "polygon" && index >= 0 && index < _controller.PolygonVertices.Count) { var v = _controller.PolygonVertices[index]; GD.Print($"Selected vertex {index}: ({v.X:F3}, {v.Y:F3})"); } } private void UpdateVertexList() { for (int i = _vertexList.GetChildCount() - 1; i >= 0; i--) _vertexList.GetChild(i).QueueFree(); if (_controller.HitType != "polygon") return; for (int i = 0; i < _controller.PolygonVertices.Count; i++) { var idx = i; var v = _controller.PolygonVertices[i]; var row = new HBoxContainer(); var xEdit = new LineEdit(); xEdit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; xEdit.Text = v.X.ToString("F3", System.Globalization.CultureInfo.InvariantCulture); xEdit.TextChanged += txt => { if (float.TryParse(xEdit.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var val)) _controller.SetPolygonVertex(idx, new Vector2(val, _controller.PolygonVertices[idx].Y)); OnParamChanged(); }; row.AddChild(xEdit); var yEdit = new LineEdit(); yEdit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; yEdit.Text = v.Y.ToString("F3", System.Globalization.CultureInfo.InvariantCulture); yEdit.TextChanged += txt => { if (float.TryParse(yEdit.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var val)) _controller.SetPolygonVertex(idx, new Vector2(_controller.PolygonVertices[idx].X, val)); OnParamChanged(); }; row.AddChild(yEdit); var delBtn = new Button { Text = "X" }; delBtn.CustomMinimumSize = new Vector2(30, 0); delBtn.Pressed += () => { _controller.RemovePolygonVertex(idx); OnParamChanged(); }; row.AddChild(delBtn); _vertexList.AddChild(row); } } private void RefreshJson() { _jsonEdit.Text = _controller.ToJson(); } private void OnLoadJson() { var dialog = new FileDialog(); dialog.FileMode = FileDialog.FileModeEnum.OpenFile; dialog.AddFilter("*.json", "JSON Files"); dialog.AddFilter("*.interactive", "Interactive Files"); dialog.FileSelected += path => { var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); if (file != null) { var text = file.GetAsText(); file.Close(); if (_controller.FromJson(text)) { WriteControllerToSidebar(); UpdateVertexList(); RebuildHandles(); UpdateColliderOverlay(); RefreshJson(); MarkClean(); } else { GD.PrintErr("Failed to parse JSON"); } } dialog.QueueFree(); }; dialog.Canceled += () => dialog.QueueFree(); AddChild(dialog); dialog.PopupCentered(new Vector2I(800, 600)); } private void OnImportFromMod() { var ids = Interactives.Collect(); if (ids.Count == 0) { GD.Print("No interactive definitions found in mods"); return; } var dialog = new AcceptDialog { Title = Tr("tools/interactive_editor/dlg_select_def") }; dialog.Size = new Vector2I(400, 300); var list = new ItemList(); list.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; list.SizeFlagsVertical = Control.SizeFlags.ExpandFill; foreach (var kv in ids) list.AddItem(kv.Key); list.ItemActivated += idx => { var id = list.GetItemText((int)idx); var def = InteractiveFactory.Instance.Get(id); if (def != null) { _controller.FromDefinition(def); WriteControllerToSidebar(); UpdateVertexList(); RebuildHandles(); UpdateColliderOverlay(); RefreshJson(); MarkClean(); } dialog.QueueFree(); }; dialog.AddChild(list); AddChild(dialog); dialog.PopupCentered(); } private void OnSave() { var dialog = new FileDialog(); dialog.FileMode = FileDialog.FileModeEnum.SaveFile; dialog.AddFilter("*.json", "JSON Files"); dialog.FileSelected += path => { var json = _controller.ToJson(); var file = FileAccess.Open(path, FileAccess.ModeFlags.Write); if (file != null) { file.StoreString(json); file.Close(); MarkClean(); GD.Print($"Saved to {path}"); } dialog.QueueFree(); }; dialog.Canceled += () => dialog.QueueFree(); AddChild(dialog); dialog.PopupCentered(new Vector2I(800, 600)); } private void OnParamChanged(string _ = null) => OnParamChanged(); protected override void Cleanup() { foreach (var h in _handles) h.QueueFree(); _handles.Clear(); } } }