From f5cf990d72ac00402ad40e73addfb3299ddf273a Mon Sep 17 00:00:00 2001 From: iisaev Date: Thu, 29 Oct 2026 13:22:26 +0100 Subject: [PATCH] Turn editor_tools into a package of the split tool mods The code moved to tools_base, scenario_tools_base and the seven editor mods; editor_tools depends on them and keeps the tool windows suite. Docs list all seven tool box entries with a new tool box screenshot. --- docs/assets/toolbox.jpg | Bin 12692 -> 17653 bytes docs/en.md | 9 +- docs/es.md | 9 +- docs/ja.md | 9 +- docs/ko.md | 9 +- docs/ru.md | 9 +- docs/vi.md | 9 +- docs/zh.md | 9 +- locale/catalog_export_en.properties | 5 - locale/catalog_export_es.properties | 5 - locale/catalog_export_ja.properties | 5 - locale/catalog_export_ko.properties | 5 - locale/catalog_export_ru.properties | 5 - locale/catalog_export_vi.properties | 5 - locale/catalog_export_zh.properties | 5 - locale/schema_en.properties | 6 - locale/schema_es.properties | 6 - locale/schema_ja.properties | 6 - locale/schema_ko.properties | 6 - locale/schema_ru.properties | 6 - locale/schema_vi.properties | 6 - locale/schema_zh.properties | 6 - locale/ui_en.properties | 337 +----- locale/ui_es.properties | 337 +----- locale/ui_ja.properties | 337 +----- locale/ui_ko.properties | 337 +----- locale/ui_ru.properties | 337 +----- locale/ui_vi.properties | 337 +----- locale/ui_zh.properties | 337 +----- manifest.json | 34 +- scenes/scenario_file_tree_panel.tscn | 16 - scenes/scenario_info_panel.tscn | 48 - scenes/scenario_thumbnail_panel.tscn | 10 - .../CatalogExportToolProvider.cs | 27 - src/CatalogExport/CatalogExportWindow.cs | 367 ------ src/Common/BackgroundCanvas.cs | 137 --- src/Common/CollapsibleSection.cs | 62 - src/Common/DragHandle.cs | 102 -- src/Common/FilteredList.cs | 159 --- src/Common/ModFileSelector.cs | 180 --- src/Common/ToolUi.cs | 146 --- src/EntityEditor/EntityDocument.cs | 252 ---- src/EntityEditor/EntityEditorWindow.cs | 740 ------------ src/EntityEditor/EntityField.cs | 35 - src/EntityEditor/EntityFieldKind.cs | 21 - src/EntityEditor/EntityRecord.cs | 24 - src/EntityEditor/PicturePickerDialog.cs | 57 - .../InteractiveEditorCanvas.cs | 260 ---- .../InteractiveEditorController.cs | 206 ---- .../InteractiveEditorToolProvider.cs | 39 - .../InteractiveEditorWindow.cs | 644 ---------- src/LightEditor/LightEditorCanvas.cs | 129 -- src/LightEditor/LightEditorController.cs | 202 ---- src/LightEditor/LightEditorToolProvider.cs | 32 - src/LightEditor/LightEditorWindow.cs | 387 ------ src/MapEditor/MapEditorCanvas.cs | 397 ------- src/MapEditor/MapEditorController.cs | 241 ---- src/MapEditor/MapEditorToolBoxButton.cs | 24 - src/MapEditor/MapEditorToolProvider.cs | 28 - src/MapEditor/MapEditorWindow.cs | 1044 ----------------- src/ScenarioEditor/ScenarioEditTabs.cs | 315 ----- .../ScenarioEditorToolProvider.cs | 19 - src/ScenarioEditor/ScenarioEditorWindow.cs | 411 ------- src/ScenarioEditor/ScenarioFileTreePanel.cs | 156 --- src/ScenarioEditor/ScenarioGraphLayout.cs | 74 -- src/ScenarioEditor/ScenarioGraphView.cs | 205 ---- src/ScenarioEditor/ScenarioInfoPanel.cs | 101 -- src/ScenarioEditor/ScenarioThumbnailPanel.cs | 70 -- src/ScenarioEditor/TabData.cs | 17 - src/SceneBuilder/InspectableProperty.cs | 24 - src/SceneBuilder/InspectorBoolField.cs | 26 - src/SceneBuilder/InspectorColorField.cs | 28 - src/SceneBuilder/InspectorEnumField.cs | 49 - src/SceneBuilder/InspectorNumericField.cs | 38 - src/SceneBuilder/InspectorPanel.cs | 407 ------- src/SceneBuilder/InspectorPropertyCatalog.cs | 155 --- src/SceneBuilder/InspectorPropertyKind.cs | 9 - src/SceneBuilder/InspectorTextField.cs | 27 - src/SceneBuilder/InspectorVector2Field.cs | 46 - src/SceneBuilder/SceneBuilderController.cs | 438 ------- src/SceneBuilder/SceneBuilderToolProvider.cs | 19 - src/SceneBuilder/SceneBuilderWindow.cs | 785 ------------- src/SceneBuilder/SelectionOverlay.cs | 532 --------- src/SceneBuilder/TscnFileProps.cs | 58 - src/SceneBuilder/TscnLiteralReader.cs | 109 -- src/SceneBuilder/TscnNodeAddressing.cs | 25 - src/SceneBuilder/TscnTextSurgery.cs | 283 ----- src/SceneBuilder/TscnValueFormatter.cs | 82 -- src/SceneEditor/SceneBackgroundTile.cs | 69 -- src/SceneEditor/SceneEditorToolBoxButton.cs | 17 - src/SceneEditor/SceneEditorToolProvider.cs | 19 - src/SceneEditor/SceneEditorWindow.cs | 743 ------------ src/Schemas/ISchemaFieldEditor.cs | 20 - src/Schemas/ListFieldEditor.cs | 175 --- src/Schemas/LocaleFieldEditor.cs | 112 -- src/Schemas/MapFieldEditor.cs | 130 -- src/Schemas/ScalarFieldEditor.cs | 30 - src/Schemas/SchemaFieldControl.cs | 221 ---- src/Schemas/SchemaFieldEditors.cs | 34 - src/Services/LocaleFileService.cs | 72 -- src/Services/MapFileService.cs | 45 - src/Services/ModFileService.cs | 115 -- src/Services/ModResourceService.cs | 177 --- src/Services/PropertiesText.cs | 78 -- src/Services/ScenarioFileService.cs | 375 ------ src/Services/ScenarioGraphData.cs | 38 - src/Services/ScenarioSyntaxService.cs | 105 -- src/Services/SceneBackgroundService.cs | 80 -- src/Services/SceneFileEntry.cs | 89 -- src/Services/SceneFileService.cs | 81 -- test_suites/ingame_play/manifest.json | 10 - .../src/InGamePlayTestDriverNode.cs | 163 --- .../src/InGamePlayTestInitializer.cs | 31 - .../novel_flow/src/NovelFlowTestDriver.cs | 2 +- test_suites/windows/manifest.json | 5 +- .../windows/src/ToolWindowsAtlasExercise.cs | 77 ++ .../windows/src/ToolWindowsExercises.cs | 3 + .../windows/src/ToolWindowsTestDriverNode.cs | 7 +- tests/EntityDocumentTests.cs | 119 -- tests/PropertiesTextTests.cs | 32 - tests/SchemaFieldControlTests.cs | 42 - tests/ThemeToggleStatesTests.cs | 44 - 122 files changed, 149 insertions(+), 16268 deletions(-) delete mode 100644 locale/catalog_export_en.properties delete mode 100644 locale/catalog_export_es.properties delete mode 100644 locale/catalog_export_ja.properties delete mode 100644 locale/catalog_export_ko.properties delete mode 100644 locale/catalog_export_ru.properties delete mode 100644 locale/catalog_export_vi.properties delete mode 100644 locale/catalog_export_zh.properties delete mode 100644 locale/schema_en.properties delete mode 100644 locale/schema_es.properties delete mode 100644 locale/schema_ja.properties delete mode 100644 locale/schema_ko.properties delete mode 100644 locale/schema_ru.properties delete mode 100644 locale/schema_vi.properties delete mode 100644 locale/schema_zh.properties delete mode 100644 scenes/scenario_file_tree_panel.tscn delete mode 100644 scenes/scenario_info_panel.tscn delete mode 100644 scenes/scenario_thumbnail_panel.tscn delete mode 100644 src/CatalogExport/CatalogExportToolProvider.cs delete mode 100644 src/CatalogExport/CatalogExportWindow.cs delete mode 100644 src/Common/BackgroundCanvas.cs delete mode 100644 src/Common/CollapsibleSection.cs delete mode 100644 src/Common/DragHandle.cs delete mode 100644 src/Common/FilteredList.cs delete mode 100644 src/Common/ModFileSelector.cs delete mode 100644 src/Common/ToolUi.cs delete mode 100644 src/EntityEditor/EntityDocument.cs delete mode 100644 src/EntityEditor/EntityEditorWindow.cs delete mode 100644 src/EntityEditor/EntityField.cs delete mode 100644 src/EntityEditor/EntityFieldKind.cs delete mode 100644 src/EntityEditor/EntityRecord.cs delete mode 100644 src/EntityEditor/PicturePickerDialog.cs delete mode 100644 src/InteractiveEditor/InteractiveEditorCanvas.cs delete mode 100644 src/InteractiveEditor/InteractiveEditorController.cs delete mode 100644 src/InteractiveEditor/InteractiveEditorToolProvider.cs delete mode 100644 src/InteractiveEditor/InteractiveEditorWindow.cs delete mode 100644 src/LightEditor/LightEditorCanvas.cs delete mode 100644 src/LightEditor/LightEditorController.cs delete mode 100644 src/LightEditor/LightEditorToolProvider.cs delete mode 100644 src/LightEditor/LightEditorWindow.cs delete mode 100644 src/MapEditor/MapEditorCanvas.cs delete mode 100644 src/MapEditor/MapEditorController.cs delete mode 100644 src/MapEditor/MapEditorToolBoxButton.cs delete mode 100644 src/MapEditor/MapEditorToolProvider.cs delete mode 100644 src/MapEditor/MapEditorWindow.cs delete mode 100644 src/ScenarioEditor/ScenarioEditTabs.cs delete mode 100644 src/ScenarioEditor/ScenarioEditorToolProvider.cs delete mode 100644 src/ScenarioEditor/ScenarioEditorWindow.cs delete mode 100644 src/ScenarioEditor/ScenarioFileTreePanel.cs delete mode 100644 src/ScenarioEditor/ScenarioGraphLayout.cs delete mode 100644 src/ScenarioEditor/ScenarioGraphView.cs delete mode 100644 src/ScenarioEditor/ScenarioInfoPanel.cs delete mode 100644 src/ScenarioEditor/ScenarioThumbnailPanel.cs delete mode 100644 src/ScenarioEditor/TabData.cs delete mode 100644 src/SceneBuilder/InspectableProperty.cs delete mode 100644 src/SceneBuilder/InspectorBoolField.cs delete mode 100644 src/SceneBuilder/InspectorColorField.cs delete mode 100644 src/SceneBuilder/InspectorEnumField.cs delete mode 100644 src/SceneBuilder/InspectorNumericField.cs delete mode 100644 src/SceneBuilder/InspectorPanel.cs delete mode 100644 src/SceneBuilder/InspectorPropertyCatalog.cs delete mode 100644 src/SceneBuilder/InspectorPropertyKind.cs delete mode 100644 src/SceneBuilder/InspectorTextField.cs delete mode 100644 src/SceneBuilder/InspectorVector2Field.cs delete mode 100644 src/SceneBuilder/SceneBuilderController.cs delete mode 100644 src/SceneBuilder/SceneBuilderToolProvider.cs delete mode 100644 src/SceneBuilder/SceneBuilderWindow.cs delete mode 100644 src/SceneBuilder/SelectionOverlay.cs delete mode 100644 src/SceneBuilder/TscnFileProps.cs delete mode 100644 src/SceneBuilder/TscnLiteralReader.cs delete mode 100644 src/SceneBuilder/TscnNodeAddressing.cs delete mode 100644 src/SceneBuilder/TscnTextSurgery.cs delete mode 100644 src/SceneBuilder/TscnValueFormatter.cs delete mode 100644 src/SceneEditor/SceneBackgroundTile.cs delete mode 100644 src/SceneEditor/SceneEditorToolBoxButton.cs delete mode 100644 src/SceneEditor/SceneEditorToolProvider.cs delete mode 100644 src/SceneEditor/SceneEditorWindow.cs delete mode 100644 src/Schemas/ISchemaFieldEditor.cs delete mode 100644 src/Schemas/ListFieldEditor.cs delete mode 100644 src/Schemas/LocaleFieldEditor.cs delete mode 100644 src/Schemas/MapFieldEditor.cs delete mode 100644 src/Schemas/ScalarFieldEditor.cs delete mode 100644 src/Schemas/SchemaFieldControl.cs delete mode 100644 src/Schemas/SchemaFieldEditors.cs delete mode 100644 src/Services/LocaleFileService.cs delete mode 100644 src/Services/MapFileService.cs delete mode 100644 src/Services/ModFileService.cs delete mode 100644 src/Services/ModResourceService.cs delete mode 100644 src/Services/PropertiesText.cs delete mode 100644 src/Services/ScenarioFileService.cs delete mode 100644 src/Services/ScenarioGraphData.cs delete mode 100644 src/Services/ScenarioSyntaxService.cs delete mode 100644 src/Services/SceneBackgroundService.cs delete mode 100644 src/Services/SceneFileEntry.cs delete mode 100644 src/Services/SceneFileService.cs delete mode 100644 test_suites/ingame_play/manifest.json delete mode 100644 test_suites/ingame_play/src/InGamePlayTestDriverNode.cs delete mode 100644 test_suites/ingame_play/src/InGamePlayTestInitializer.cs create mode 100644 test_suites/windows/src/ToolWindowsAtlasExercise.cs delete mode 100644 tests/EntityDocumentTests.cs delete mode 100644 tests/PropertiesTextTests.cs delete mode 100644 tests/SchemaFieldControlTests.cs delete mode 100644 tests/ThemeToggleStatesTests.cs diff --git a/docs/assets/toolbox.jpg b/docs/assets/toolbox.jpg index 301b6ad4ec7d9c853e1820c104da1d07ca1c7f3f..e9cbc60b2c33ac49c7b916c7133a6a8878bcc104 100644 GIT binary patch literal 17653 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 lsbjURZrr~?(W_Y+$GRh@V=dy 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 Ozm{nJo8KV#!hZnLSXMg# diff --git a/docs/en.md b/docs/en.md index 53d1ec9..db2f630 100644 --- a/docs/en.md +++ b/docs/en.md @@ -2,17 +2,16 @@ Editor Tools are the author's windows that open over the running game: the scene editor, the scenario editor, editors for the world map, interactive objects and lights, a builder of Godot scenes and the catalog export of a mod. Everything they save is ordinary mod files, and the game picks the change up at once, so an author writes a place, walks into it and plays it without restarting. -The mod also holds the base of the item and character editors (they are separate mods: Items Editor, Characters Editor). A game gets these tools through the Developer Tools toolset or its own collection. +Compatibility package: the seven windows now live in their own mods (`scenario_editor`, `scene_editor`, `scene_builder`, `map_editor`, `interactive_editor`, `light_editor`, `catalog_export`). Shared forms and files are in `tools_base`, scenario services in `scenario_tools_base`. All windows open from the in-game toolbar; no title-screen buttons are added. ## Opening the tools In a game, the hammer button on the toolbar opens the tool box: -![The toolbar of the novel template with the hammer pressed and the tool box open: Interactive Editor, Light Editor, Map Editor, Scene editor.](assets/toolbox.jpg) +![The toolbar of the novel template with the hammer pressed and the tool box open: Export Catalog Entry, Interactive Editor, Light Editor, Map Editor, Scenario Editor, Scene Builder, Scene editor.](assets/toolbox.jpg) -- **Scene editor**, **Map Editor**, **Interactive Editor** and **Light Editor** are in the tool box. Other tool mods add their own entries there (the item, character and RPG map editors, the Data Editor). -- The **scenario editor** opens from the scene editor: select a scene, pick one of its scenarios and press **Open**, or **+ New scenario**. -- When the tools are loaded at the main menu (for example with `--all-mods`), every tool also has a button in the menu's tool block, including the scene builder and the catalog export. +- All seven windows are in the tool box: **Scene editor**, **Scenario Editor**, **Map Editor**, **Interactive Editor**, **Light Editor**, **Scene Builder** and **Export Catalog Entry**. Other tool mods add their own entries there (the item, character, RPG map and tileset editors, the Data Editor). +- The **scenario editor** also opens from the scene editor: select a scene, pick one of its scenarios and press **Open**, or **+ New scenario**. The windows stay open across scene changes, can be moved and resized, and close with the cross. diff --git a/docs/es.md b/docs/es.md index ed76b6f..e6d9de7 100644 --- a/docs/es.md +++ b/docs/es.md @@ -2,17 +2,16 @@ Editor Tools son las ventanas de autor que se abren sobre el juego en marcha: el editor de escenas, el de escenarios, los editores del mapa del mundo, de objetos interactivos y de luces, un constructor de escenas de Godot y la exportación al catálogo de un mod. Todo lo que guardan son archivos de mod normales, y el juego toma el cambio al instante, así que puedes escribir un lugar, entrar en él y jugarlo sin reiniciar. -El mod también contiene la base de los editores de objetos y de personajes (que son mods aparte: Items Editor y Characters Editor). Un juego obtiene estas herramientas mediante el conjunto Developer Tools o con su propia colección. +Paquete de compatibilidad: las siete ventanas viven en mods independientes. `tools_base` contiene los componentes comunes y `scenario_tools_base` los servicios de escenarios. Todas se abren desde la barra del juego, sin botones en el menú principal. ## Cómo abrir las herramientas Dentro de un juego, el botón del martillo de la barra de herramientas abre la caja de herramientas: -![La barra de herramientas de la plantilla de novela con el martillo pulsado y la caja de herramientas abierta: Interactive Editor, Light Editor, Map Editor, Scene editor.](assets/toolbox.jpg) +![La barra de herramientas de la plantilla de novela con el martillo pulsado y la caja de herramientas abierta: Export Catalog Entry, Interactive Editor, Light Editor, Map Editor, Scenario Editor, Scene Builder, Scene editor.](assets/toolbox.jpg) -- **Editor de escenas**, **Editor de mapas**, **Editor de interactivos** y **Editor de luces** están en la caja. Otros mods de herramientas añaden ahí sus propias entradas (los editores de objetos, de personajes y de mapas RPG, el editor de datos). -- El **editor de escenarios** se abre desde el editor de escenas: elige una escena, selecciona uno de sus escenarios y pulsa **Abrir**, o **+ Nuevo escenario**. -- Si las herramientas se cargan en el menú principal (por ejemplo con `--all-mods`), cada una tiene también un botón en el bloque de herramientas del menú, incluidos el constructor de escenas y la exportación al catálogo. +- Las siete ventanas están en la caja: **Editor de escenas**, **Editor de escenarios**, **Editor de mapas**, **Editor de interactivos**, **Editor de luces**, **Constructor de escenas** y **Exportar entrada del catálogo**. Otros mods de herramientas añaden ahí sus propias entradas (los editores de objetos, de personajes, de mapas RPG y de tilesets, el editor de datos). +- El **editor de escenarios** también se abre desde el editor de escenas: selecciona una escena, elige uno de sus escenarios y pulsa **Abrir**, o **+ Nuevo escenario**. Las ventanas siguen abiertas al cambiar de escena, se pueden mover y redimensionar, y se cierran con la cruz. diff --git a/docs/ja.md b/docs/ja.md index d659096..e885351 100644 --- a/docs/ja.md +++ b/docs/ja.md @@ -2,17 +2,16 @@ Editor Tools は、実行中のゲームの上に開く作者向けのウィンドウ群です。シーンエディター、シナリオエディター、ワールドマップ・インタラクティブ・ライトの各エディター、Godot シーンのビルダー、MOD カタログの書き出しが含まれます。保存されるのはどれも普通の MOD ファイルで、ゲームは変更をすぐに取り込みます。そのため、場所を書いて、その場に入って遊ぶまでを、再起動なしで行えます。 -この MOD には、アイテムエディターとキャラクターエディターの土台も入っています(本体は別の MOD、Items Editor と Characters Editor です)。ゲームにこれらのツールを入れるには、Developer Tools ツールセットか、そのゲーム独自のコレクションを使います。 +互換パッケージです。7つのウィンドウは独立したMODになり、共通機能は `tools_base`、シナリオ機能は `scenario_tools_base` にあります。すべてゲーム内ツールバーから開き、メインメニューにはボタンを追加しません。 ## ツールを開く ゲーム内では、ツールバーのハンマーのボタンでツールボックスが開きます: -![ノベルのテンプレートのツールバー。ハンマーが押され、ツールボックスが開いている: Interactive Editor、Light Editor、Map Editor、Scene editor。](assets/toolbox.jpg) +![ノベルのテンプレートのツールバー。ハンマーが押され、ツールボックスが開いている: Export Catalog Entry、Interactive Editor、Light Editor、Map Editor、Scenario Editor、Scene Builder、Scene editor。](assets/toolbox.jpg) -- ツールボックスには**シーンエディター**、**マップエディター**、**インタラクティブエディター**、**ライトエディター**が入っています。他のツール MOD もここに項目を追加します(アイテム、キャラクター、RPG マップの各エディター、データエディター)。 -- **シナリオエディター**はシーンエディターから開きます。シーンを選び、そのシナリオのひとつを選んで**開く**を押すか、**+ 新しいシナリオ**を押します。 -- メインメニューでツールを読み込んでいる場合(たとえば `--all-mods`)、メニューのツール欄にも各ツールのボタンが並びます。シーンビルダーとカタログの書き出しも含まれます。 +- 7 つのウィンドウはすべてツールボックスにあります: **シーンエディター**、**シナリオエディター**、**マップエディター**、**インタラクティブエディター**、**ライトエディター**、**シーンビルダー**、**カタログ項目を書き出す**。ほかのツール MOD もそこに独自の項目を追加します(アイテム、キャラクター、RPG マップ、タイルセットのエディター、データエディター)。 +- **シナリオエディター**はシーンエディターからも開けます。シーンを選び、そのシナリオのひとつを選んで**開く**を押すか、**+ 新しいシナリオ**を押します。 ウィンドウはシーンを切り替えても開いたまま残り、移動やサイズ変更ができ、×で閉じます。 diff --git a/docs/ko.md b/docs/ko.md index 2ae9960..67d49fb 100644 --- a/docs/ko.md +++ b/docs/ko.md @@ -2,17 +2,16 @@ Editor Tools는 실행 중인 게임 위에 열리는 창작용 창 모음입니다. 장면 에디터, 시나리오 에디터, 월드 맵·상호작용·조명 에디터, Godot 장면 빌더, 모드 카탈로그 내보내기가 들어 있습니다. 저장되는 것은 모두 평범한 모드 파일이고 게임은 변경을 곧바로 받아들입니다. 그래서 장소를 쓰고, 바로 들어가 플레이하는 일을 재시작 없이 할 수 있습니다. -이 모드에는 아이템·캐릭터 에디터의 기반도 들어 있습니다(본체는 별도 모드인 Items Editor와 Characters Editor입니다). 게임에 이 도구들을 넣으려면 Developer Tools 도구 모음이나 그 게임의 컬렉션을 사용합니다. +호환 패키지입니다. 일곱 도구 창은 독립 모드로 분리되었으며 공통 기능은 `tools_base`, 시나리오 기능은 `scenario_tools_base`에 있습니다. 모든 창은 게임 도구 모음에서 열며 주 메뉴에는 버튼을 추가하지 않습니다. ## 도구 열기 게임 안에서 도구 모음의 망치 버튼을 누르면 도구 상자가 열립니다: -![소설 템플릿의 도구 모음. 망치가 눌려 있고 도구 상자가 열려 있습니다: Interactive Editor, Light Editor, Map Editor, Scene editor.](assets/toolbox.jpg) +![소설 템플릿의 도구 모음. 망치가 눌려 있고 도구 상자가 열려 있습니다: Export Catalog Entry, Interactive Editor, Light Editor, Map Editor, Scenario Editor, Scene Builder, Scene editor.](assets/toolbox.jpg) -- 도구 상자에는 **장면 에디터**, **맵 에디터**, **상호작용 에디터**, **조명 에디터**가 있습니다. 다른 도구 모드도 여기에 항목을 추가합니다(아이템, 캐릭터, RPG 맵 에디터, 데이터 에디터). -- **시나리오 에디터**는 장면 에디터에서 엽니다. 장면을 고르고 그 시나리오 중 하나를 선택한 뒤 **열기**를 누르거나 **+ 새 시나리오**를 누릅니다. -- 메인 메뉴에서 도구를 불러오는 경우(예: `--all-mods`), 메뉴의 도구 영역에도 각 도구 버튼이 생깁니다. 장면 빌더와 카탈로그 내보내기도 포함됩니다. +- 일곱 창 모두 도구 상자에 있습니다: **장면 에디터**, **시나리오 에디터**, **맵 에디터**, **상호작용 에디터**, **조명 에디터**, **장면 빌더**, **카탈로그 항목 내보내기**. 다른 도구 모드도 그곳에 자기 항목을 추가합니다(아이템, 캐릭터, RPG 맵, 타일셋 에디터, 데이터 에디터). +- **시나리오 에디터**는 장면 에디터에서도 엽니다. 장면을 고르고 그 시나리오 중 하나를 선택한 뒤 **열기**를 누르거나 **+ 새 시나리오**를 누릅니다. 창은 장면을 바꿔도 열린 채로 남고, 옮기고 크기를 바꿀 수 있으며 ×로 닫습니다. diff --git a/docs/ru.md b/docs/ru.md index 66cfa33..207808b 100644 --- a/docs/ru.md +++ b/docs/ru.md @@ -2,17 +2,16 @@ Editor Tools — это авторские окна поверх работающей игры: редактор сцен, редактор сценариев, редакторы карты мира, интерактивных объектов и света, конструктор сцен Godot и экспорт каталога мода. Всё, что они сохраняют, — обычные файлы модов, и игра подхватывает правки сразу: можно описать место, тут же войти в него и сыграть, ничего не перезапуская. -Здесь же лежит основа редакторов предметов и персонажей (сами они — отдельные моды: Items Editor и Characters Editor). В игру эти инструменты попадают через набор Developer Tools или через её собственную коллекцию. +Пакет совместимости: семь окон выделены в отдельные моды (`scenario_editor`, `scene_editor`, `scene_builder`, `map_editor`, `interactive_editor`, `light_editor`, `catalog_export`). Общие формы и файлы находятся в `tools_base`, сценарные сервисы — в `scenario_tools_base`. Все окна открываются с игрового тулбара; кнопок редакторов в главном меню нет. ## Как открыть инструменты Молоток на панели инструментов в игре открывает набор инструментов: -![Панель шаблона новеллы: нажат молоток, открыт набор инструментов — Interactive Editor, Light Editor, Map Editor, Scene editor.](assets/toolbox.jpg) +![Панель шаблона новеллы: нажат молоток, открыт набор инструментов — Export Catalog Entry, Interactive Editor, Light Editor, Map Editor, Scenario Editor, Scene Builder, Scene editor.](assets/toolbox.jpg) -- **Редактор сцен**, **Редактор карты**, **Редактор интерактивов** и **Редактор освещения** входят в набор. Другие инструментальные моды добавляют туда свои пункты (редакторы предметов, персонажей и RPG-карт, редактор данных). -- **Редактор сценариев** открывается из редактора сцен: выберите сцену, укажите один из её сценариев и нажмите **Открыть** — или **+ Новый сценарий**. -- Если инструменты загружены в главном меню (например, с `--all-mods`), у каждого есть кнопка и в блоке инструментов меню, включая конструктор сцен и экспорт каталога. +- Все семь окон есть в наборе: **Редактор сцен**, **Редактор сценариев**, **Редактор карты**, **Редактор интерактивов**, **Редактор освещения**, **Конструктор сцен** и **Экспорт записи каталога**. Другие инструментальные моды добавляют туда свои пункты (редакторы предметов, персонажей, RPG-карт и тайлсетов, редактор данных). +- **Редактор сценариев** открывается и из редактора сцен: выберите сцену, укажите один из её сценариев и нажмите **Открыть** — или **+ Новый сценарий**. Окна остаются открытыми при смене сцен, их можно двигать и растягивать, а закрываются они крестиком. diff --git a/docs/vi.md b/docs/vi.md index b5fd81b..482ceb7 100644 --- a/docs/vi.md +++ b/docs/vi.md @@ -2,17 +2,16 @@ Editor Tools là những cửa sổ dành cho tác giả mở trên trò chơi đang chạy: trình sửa cảnh, trình sửa kịch bản, trình sửa bản đồ thế giới, trình sửa vật tương tác và ánh sáng, trình dựng cảnh Godot và phần xuất danh mục của mod. Mọi thứ chúng lưu đều là tệp mod bình thường, và trò chơi nhận thay đổi ngay lập tức, nên bạn viết ra một địa điểm, bước vào đó và chơi luôn mà không phải khởi động lại. -Mod này cũng chứa phần nền của trình sửa vật phẩm và nhân vật (bản thân chúng là mod riêng: Items Editor và Characters Editor). Một trò chơi nhận các công cụ này qua bộ Developer Tools hoặc qua bộ sưu tập riêng của nó. +Gói tương thích: bảy cửa sổ đã tách thành các mod riêng. `tools_base` chứa thành phần chung và `scenario_tools_base` chứa dịch vụ kịch bản. Mọi cửa sổ mở từ thanh công cụ trong trò chơi, không thêm nút ở menu chính. ## Mở các công cụ Trong game, nút chiếc búa trên thanh công cụ mở hộp công cụ: -![Thanh công cụ của mẫu tiểu thuyết với chiếc búa đang được nhấn và hộp công cụ mở ra: Interactive Editor, Light Editor, Map Editor, Scene editor.](assets/toolbox.jpg) +![Thanh công cụ của mẫu tiểu thuyết với chiếc búa đang được nhấn và hộp công cụ mở ra: Export Catalog Entry, Interactive Editor, Light Editor, Map Editor, Scenario Editor, Scene Builder, Scene editor.](assets/toolbox.jpg) -- **Trình sửa cảnh**, **trình sửa bản đồ**, **trình sửa tương tác** và **trình sửa ánh sáng** nằm trong hộp công cụ. Các mod công cụ khác thêm mục của riêng chúng vào đó (trình sửa vật phẩm, nhân vật và bản đồ RPG, trình sửa dữ liệu). -- **Trình sửa kịch bản** mở từ trình sửa cảnh: chọn một cảnh, chọn một kịch bản của cảnh rồi nhấn **Mở**, hoặc **+ Kịch bản mới**. -- Nếu các công cụ được tải ở menu chính (ví dụ với `--all-mods`), mỗi công cụ cũng có nút trong khối công cụ của menu, kể cả trình dựng cảnh và phần xuất danh mục. +- Cả bảy cửa sổ đều có trong hộp công cụ: **Trình sửa cảnh**, **Trình sửa kịch bản**, **Trình sửa bản đồ**, **Trình sửa tương tác**, **Trình sửa ánh sáng**, **Trình dựng cảnh** và **Xuất mục danh mục**. Các mod công cụ khác thêm mục của riêng chúng vào đó (trình sửa vật phẩm, nhân vật, bản đồ RPG và tileset, trình sửa dữ liệu). +- **Trình sửa kịch bản** cũng mở được từ trình sửa cảnh: chọn một cảnh, chọn một kịch bản của cảnh rồi nhấn **Mở**, hoặc **+ Kịch bản mới**. Các cửa sổ vẫn mở khi chuyển cảnh, có thể di chuyển và đổi kích thước, và đóng bằng dấu ×. diff --git a/docs/zh.md b/docs/zh.md index ac118a8..78e37d6 100644 --- a/docs/zh.md +++ b/docs/zh.md @@ -2,17 +2,16 @@ Editor Tools 是叠在运行中游戏之上的作者工具窗口:场景编辑器、剧本编辑器、世界地图编辑器、交互物编辑器和光照编辑器,还有一个 Godot 场景构建器与模组目录导出。它们保存的全是普通的模组文件,游戏会立刻读到改动,所以作者可以写下一个地点,马上走进去试玩,无需重启。 -本模组还包含物品与角色编辑器的底层部分(它们是两个独立的模组:Items Editor 和 Characters Editor)。游戏通过 Developer Tools 工具集或自己的集合获得这些工具。 +这是兼容工具包:七个工具窗口已拆分为独立模组。公共表单和文件服务位于 `tools_base`,剧本服务位于 `scenario_tools_base`。所有窗口从游戏工具栏打开,不添加主菜单按钮。 ## 打开工具 在游戏中,工具栏上的锤子按钮会打开工具箱: -![小说模板的工具栏:锤子按钮已按下,工具箱已打开:Interactive Editor、Light Editor、Map Editor、Scene editor。](assets/toolbox.jpg) +![小说模板的工具栏:锤子按钮已按下,工具箱已打开:Export Catalog Entry、Interactive Editor、Light Editor、Map Editor、Scenario Editor、Scene Builder、Scene editor。](assets/toolbox.jpg) -- 工具箱里有**场景编辑器**、**地图编辑器**、**交互编辑器**和**光照编辑器**。其他工具模组会往里面添加自己的条目(物品、角色和 RPG 地图编辑器,以及数据编辑器)。 -- **剧本编辑器**从场景编辑器打开:选中一个场景,挑一个它的剧本,按**打开**,或者按 **+ 新剧本**。 -- 如果在主菜单就加载了工具(比如用 `--all-mods`),菜单的工具区里每个工具也都有按钮,包括场景构建器和目录导出。 +- 七个窗口都在工具箱里:**场景编辑器**、**剧本编辑器**、**地图编辑器**、**交互编辑器**、**光照编辑器**、**场景构建器**和**导出目录条目**。其他工具模组会往里面添加自己的条目(物品、角色、RPG 地图和图块集编辑器,以及数据编辑器)。 +- **剧本编辑器**也可以从场景编辑器打开:选中一个场景,挑一个它的剧本,按**打开**,或者按 **+ 新剧本**。 这些窗口在切换场景后依然开着,可以拖动和调整大小,点叉号关闭。 diff --git a/locale/catalog_export_en.properties b/locale/catalog_export_en.properties deleted file mode 100644 index 8e9c6c3..0000000 --- a/locale/catalog_export_en.properties +++ /dev/null @@ -1,5 +0,0 @@ -# Catalog export tool — extra keys -mods/catalog_select_mod = Select a mod: -mods/catalog_add_locale = Add language -mods/catalog_locale_name = Name in this language -mods/catalog_locale_desc = Description in this language diff --git a/locale/catalog_export_es.properties b/locale/catalog_export_es.properties deleted file mode 100644 index 87000da..0000000 --- a/locale/catalog_export_es.properties +++ /dev/null @@ -1,5 +0,0 @@ -# Catalog export tool — extra keys -mods/catalog_select_mod = Selecciona un mod: -mods/catalog_add_locale = Añadir idioma -mods/catalog_locale_name = Nombre en este idioma -mods/catalog_locale_desc = Descripción en este idioma diff --git a/locale/catalog_export_ja.properties b/locale/catalog_export_ja.properties deleted file mode 100644 index 29c3e2e..0000000 --- a/locale/catalog_export_ja.properties +++ /dev/null @@ -1,5 +0,0 @@ -# Catalog export tool — extra keys -mods/catalog_select_mod = MOD を選択: -mods/catalog_add_locale = 言語を追加 -mods/catalog_locale_name = この言語での名前 -mods/catalog_locale_desc = この言語での説明 diff --git a/locale/catalog_export_ko.properties b/locale/catalog_export_ko.properties deleted file mode 100644 index 72f8ebf..0000000 --- a/locale/catalog_export_ko.properties +++ /dev/null @@ -1,5 +0,0 @@ -# Catalog export tool — extra keys -mods/catalog_select_mod = 모드 선택: -mods/catalog_add_locale = 언어 추가 -mods/catalog_locale_name = 이 언어의 이름 -mods/catalog_locale_desc = 이 언어의 설명 diff --git a/locale/catalog_export_ru.properties b/locale/catalog_export_ru.properties deleted file mode 100644 index 9de9806..0000000 --- a/locale/catalog_export_ru.properties +++ /dev/null @@ -1,5 +0,0 @@ -# Catalog export tool — дополнительные ключи -mods/catalog_select_mod = Выберите мод: -mods/catalog_add_locale = Добавить язык -mods/catalog_locale_name = Название на этом языке -mods/catalog_locale_desc = Описание на этом языке diff --git a/locale/catalog_export_vi.properties b/locale/catalog_export_vi.properties deleted file mode 100644 index cce0a86..0000000 --- a/locale/catalog_export_vi.properties +++ /dev/null @@ -1,5 +0,0 @@ -# Catalog export tool — extra keys -mods/catalog_select_mod = Chọn một mod: -mods/catalog_add_locale = Thêm ngôn ngữ -mods/catalog_locale_name = Tên bằng ngôn ngữ này -mods/catalog_locale_desc = Mô tả bằng ngôn ngữ này diff --git a/locale/catalog_export_zh.properties b/locale/catalog_export_zh.properties deleted file mode 100644 index c1b19c2..0000000 --- a/locale/catalog_export_zh.properties +++ /dev/null @@ -1,5 +0,0 @@ -# Catalog export tool — extra keys -mods/catalog_select_mod = 选择模组: -mods/catalog_add_locale = 添加语言 -mods/catalog_locale_name = 此语言的名称 -mods/catalog_locale_desc = 此语言的描述 diff --git a/locale/schema_en.properties b/locale/schema_en.properties deleted file mode 100644 index 0e31d44..0000000 --- a/locale/schema_en.properties +++ /dev/null @@ -1,6 +0,0 @@ -tools/entity_editor/section_schema = Fields of other mods -tools/entity_editor/schema_hint = These fields are described in the schemas of the mods: {0}. The game reads them from the same JSON. -tools/schema/add_ref = + add… -tools/schema/list_add = + Add -tools/schema/list_remove = ✕ -tools/schema/map_add = + Stat diff --git a/locale/schema_es.properties b/locale/schema_es.properties deleted file mode 100644 index 4a2f377..0000000 --- a/locale/schema_es.properties +++ /dev/null @@ -1,6 +0,0 @@ -tools/entity_editor/section_schema = Campos de otros mods -tools/entity_editor/schema_hint = Estos campos los describen los esquemas de los mods: {0}. El juego los lee del mismo JSON. -tools/schema/add_ref = + añadir… -tools/schema/list_add = + Añadir -tools/schema/list_remove = ✕ -tools/schema/map_add = + Estadística diff --git a/locale/schema_ja.properties b/locale/schema_ja.properties deleted file mode 100644 index b2d080d..0000000 --- a/locale/schema_ja.properties +++ /dev/null @@ -1,6 +0,0 @@ -tools/entity_editor/section_schema = 他の MOD のフィールド -tools/entity_editor/schema_hint = これらのフィールドは次の MOD のスキーマが記述している:{0}。ゲームは同じ JSON から読む。 -tools/schema/add_ref = + 追加… -tools/schema/list_add = + 追加 -tools/schema/list_remove = ✕ -tools/schema/map_add = + ステータス diff --git a/locale/schema_ko.properties b/locale/schema_ko.properties deleted file mode 100644 index 5d6597e..0000000 --- a/locale/schema_ko.properties +++ /dev/null @@ -1,6 +0,0 @@ -tools/entity_editor/section_schema = 다른 모드의 필드 -tools/entity_editor/schema_hint = 이 필드는 다음 모드들의 스키마가 설명합니다: {0}. 게임은 같은 JSON에서 읽습니다. -tools/schema/add_ref = + 추가… -tools/schema/list_add = + 추가 -tools/schema/list_remove = ✕ -tools/schema/map_add = + 능력치 diff --git a/locale/schema_ru.properties b/locale/schema_ru.properties deleted file mode 100644 index 8dfdc13..0000000 --- a/locale/schema_ru.properties +++ /dev/null @@ -1,6 +0,0 @@ -tools/entity_editor/section_schema = Поля других модов -tools/entity_editor/schema_hint = Эти поля описали в своих схемах моды: {0}. Игра читает их из того же JSON. -tools/schema/add_ref = + добавить… -tools/schema/list_add = + Добавить -tools/schema/list_remove = ✕ -tools/schema/map_add = + Стат diff --git a/locale/schema_vi.properties b/locale/schema_vi.properties deleted file mode 100644 index 9fa7df1..0000000 --- a/locale/schema_vi.properties +++ /dev/null @@ -1,6 +0,0 @@ -tools/entity_editor/section_schema = Trường của mod khác -tools/entity_editor/schema_hint = Các trường này do lược đồ của các mod mô tả: {0}. Game đọc chúng từ cùng tệp JSON. -tools/schema/add_ref = + thêm… -tools/schema/list_add = + Thêm -tools/schema/list_remove = ✕ -tools/schema/map_add = + Chỉ số diff --git a/locale/schema_zh.properties b/locale/schema_zh.properties deleted file mode 100644 index d3b5d74..0000000 --- a/locale/schema_zh.properties +++ /dev/null @@ -1,6 +0,0 @@ -tools/entity_editor/section_schema = 其他模组的字段 -tools/entity_editor/schema_hint = 这些字段由以下模组的模式描述:{0}。游戏从同一个 JSON 读取它们。 -tools/schema/add_ref = + 添加… -tools/schema/list_add = + 添加 -tools/schema/list_remove = ✕ -tools/schema/map_add = + 属性 diff --git a/locale/ui_en.properties b/locale/ui_en.properties index ac63174..56e45de 100644 --- a/locale/ui_en.properties +++ b/locale/ui_en.properties @@ -1,337 +1,2 @@ mod_editor_tools_name = Editor Tools -mod_editor_tools_desc = The author's windows over a running game: scene, scenario, map, interactive and light editors and a Godot scene builder. What they save are ordinary mod files the game applies at once. - -tools/common/save = Save -tools/common/save_tip = Save the file (Ctrl+S) -tools/common/saved = Saved: {0} -tools/common/loaded = Opened: {0} -tools/common/load_error = Load error -tools/common/readonly = Cannot save: the mod is read-only (zip) or the path is invalid. -tools/common/no_writable_mods = No writable mods (only folder mods can be edited). -tools/common/no_files = Mod "{0}" has no such files yet — create one with "+". -tools/common/no_file_to_save = Pick or create a file first ("+" next to the file list). -tools/common/new_file = New file -tools/common/new_file_hint = The file is created in {0}/{1}/ with the {2} extension on first save. -tools/common/new_file_status = New file {0} — created on save. -tools/common/mod_tip = Mod (mods that already have such files first; · — none yet) -tools/common/file_tip = File inside the mod -tools/common/search = Search… -tools/common/json = JSON -tools/common/json_tip = Show the resulting file JSON -tools/common/background_missing = Background "{0}" of scene "{1}" is not provided by any mod — pick one by hand. - -tools/map_editor/title = Map Editor -tools/map_editor/hint = LMB — select / drag · RMB — delete vertex · double-click, Enter or Esc — finish region · Del — delete zone · Ctrl+S — save -tools/map_editor/reload_map = Refresh in-game map -tools/map_editor/reload_map_tip = Re-read saved map files in the running game -tools/map_editor/reloaded = In-game map refreshed. -tools/map_editor/select_mode = Select -tools/map_editor/draw_region = Draw -tools/map_editor/draw_started = Click the map to place vertices. Double-click, Enter or Esc finishes, RMB removes the last one. -tools/map_editor/background = Preview background -tools/map_editor/zones = Zones -tools/map_editor/add_region = + Region -tools/map_editor/add_region_tip = New region: draw the polygon by clicking the map -tools/map_editor/add_marker = + Marker -tools/map_editor/add_label = + Label -tools/map_editor/delete = Delete zone -tools/map_editor/delete_tip = Delete the selected zone (Del) -tools/map_editor/delete_confirm = Delete the selected zone? The change reaches the file on save. -tools/map_editor/deleted = Zone "{0}" deleted. -tools/map_editor/props = Zone properties -tools/map_editor/zone_missing = Pick a zone in the list or on the map. -tools/map_editor/id = ID -tools/map_editor/id_empty = Zone ID cannot be empty. -tools/map_editor/duplicate_id = Zone ID already used: -tools/map_editor/title_key = Title -tools/map_editor/description = Description -tools/map_editor/no_translation = key not found in localization -tools/map_editor/scene = Scene -tools/map_editor/scene_none = (none) -tools/map_editor/availability = Availability -tools/map_editor/locked = Locked for travel -tools/map_editor/hidden = Hidden from the map -tools/map_editor/effective = In game: -tools/map_editor/effective_open = open -tools/map_editor/effective_locked = locked (Locked flag) -tools/map_editor/effective_no_scene = locked (no scene) -tools/map_editor/effective_unknown_scene = locked (scene not found) -tools/map_editor/effective_hidden = hidden from the map -tools/map_editor/marker = Marker -tools/map_editor/image = Icon -tools/map_editor/image_placeholder = texture ID -tools/map_editor/icon_search = Find icon… -tools/map_editor/label_section = Label -tools/map_editor/label = Text label on the map -tools/map_editor/x = X (0–1) -tools/map_editor/y = Y (0–1) -tools/map_editor/vertices = Polygon vertices -tools/map_editor/vertices_hint = Drag vertices on the map; RMB on a vertex deletes it. -tools/map_editor/add_vertex = + Vertex -tools/map_editor/delete_vertex = Delete vertex -tools/map_editor/preview = ▶ As in game -tools/map_editor/preview_tip = View the map as in game: hover, zone card, markers (unsaved edits included) -tools/map_editor/preview_on = Preview: Esc or ✕ returns to the editor. -tools/map_editor/preview_off = Editing mode. -tools/map_editor/preview_missing = Cannot load the map scene (map_widget.tscn). - -tools/scenario_editor/title = Scenario Editor -tools/scenario_editor/hint = Ctrl+S — save · F5 — play the scenario · click a scene in the graph — its scenarios -tools/scenario_editor/play = ▶ Play -tools/scenario_editor/play_tooltip = Save and play the scenario in game (F5) -tools/scenario_editor/play_stopped = Test play stopped. -tools/scenario_editor/stop = ■ Stop -tools/scenario_editor/open_file = Open in external editor -tools/scenario_editor/open_file_tip = Open the file with the system default program -tools/scenario_editor/open_file_error = Cannot open the file -tools/scenario_editor/rescan = Rescan -tools/scenario_editor/rescan_tip = Re-read the scenario list and graph (after adding files) -tools/scenario_editor/rescanned = Scenario list refreshed. -tools/scenario_editor/search_files = Find file… -tools/scenario_editor/tab_files = Files -tools/scenario_editor/tab_info = Summary -tools/scenario_editor/tab_graph = Scene graph -tools/scenario_editor/graph_select_node = Select a scene in the graph to see its scenarios and transitions. -tools/scenario_editor/graph_node = mod {0} · scenarios {1} -tools/scenario_editor/graph_node_bg = background: {0} -tools/scenario_editor/graph_detail_mod = mod: {0} -tools/scenario_editor/graph_detail_scenarios = Scenarios ({0}), by priority: -tools/scenario_editor/info_scene = Scene -tools/scenario_editor/info_priority = Priority -tools/scenario_editor/info_condition = Condition -tools/scenario_editor/info_switch_targets = Scene switches -tools/scenario_editor/info_locale_keys = Locale keys -tools/scenario_editor/info_quest_refs = Quests -tools/scenario_editor/bg_previews = Scenario backgrounds -tools/scenario_editor/bg_missing = Background not found: {0} -tools/scenario_editor/no_backgrounds = The scenario does not change the background -tools/scenario_editor/save_error = Save error: {0} -tools/scenario_editor/readonly_zip = Files of zip mods are read-only -tools/scenario_editor/no_scene_error = The scenario header has no scene (scene:) — nothing to play. -tools/scenario_editor/unsaved_title = Unsaved changes -tools/scenario_editor/unsaved_switch_text = This tab has unsaved changes. Close it without saving? - -tools/scene_builder/title = Scene Builder -tools/scene_builder/hint = Drag a node by its body or handles · click — pick the node under the cursor · Del — delete node · Ctrl+S — save and rebuild -tools/scene_builder/base_scene = Base scene the mod scene is shown over -tools/scene_builder/mod = Mod -tools/scene_builder/scene = Mod scene (.tscn) -tools/scene_builder/empty_scene = (empty base scene) -tools/scene_builder/select_mod = — pick a mod — -tools/scene_builder/select_scene = — pick a scene — -tools/scene_builder/save_tip = Save the .tscn and rebuild the preview (Ctrl+S) -tools/scene_builder/reload = Reload from disk -tools/scene_builder/reload_tip = Drop unsaved edits and re-read the scene file -tools/scene_builder/reloaded = Scene reloaded from disk. -tools/scene_builder/saved_ok = Saved. -tools/scene_builder/saved_rebuilt = Saved, preview rebuilt. -tools/scene_builder/autosaved = Autosaved. -tools/scene_builder/error_save = Save error: the mod is read-only. -tools/scene_builder/error_no_scene = Pick a mod scene first. -tools/scene_builder/hierarchy = Hierarchy -tools/scene_builder/hierarchy_hint = Gold — mod scene root, bright — editable nodes, faint — base scene (read-only). -tools/scene_builder/node_type_tip = Type of the new node -tools/scene_builder/add_node_tip = Add a node of the chosen type into the selected node (or the scene root) -tools/scene_builder/delete_node_tip = Delete the selected mod scene node -tools/scene_builder/delete_node_title = Delete node -tools/scene_builder/delete_node_confirm = Delete the selected node with its children? The file is saved automatically. -tools/scene_builder/refresh_tip = Refresh the node tree -tools/scene_builder/tab_inspector = Inspector -tools/scene_builder/tab_tscn = .tscn source -tools/scene_builder/tscn_hint = Inspector and handle edits save themselves; hand edits of the text save with Ctrl+S, which also rebuilds the preview. -tools/scene_builder/inspector_cat_container = In container -tools/scene_builder/inspector_select = Select a node of the mod scene to edit its properties -tools/scene_builder/inspector_readonly = Read-only: node belongs to the base scene -tools/scene_builder/inspector_missing_section = Node section not found in tscn (nodes of instanced sub-scenes are not editable yet) -tools/scene_builder/inspector_preset = Anchors preset -tools/scene_builder/inspector_cat_layout = Layout -tools/scene_builder/inspector_cat_rect = Rect -tools/scene_builder/inspector_cat_visibility = Visibility -tools/scene_builder/inspector_cat_misc = Misc -tools/scene_builder/inspector_bad_value = Value cannot be written to tscn. -tools/scene_builder/inspector_in_file_tooltip = Stored in tscn file -tools/scene_builder/inspector_container_banner = This node is managed by a container: anchors and offsets are recomputed by the parent and will have no effect here. -tools/scene_builder/err_add_failed = Error: cannot create node of this type. -tools/scene_builder/err_add_parent_missing = Error: parent section not found in tscn (instanced sub-scene parents are not supported). -tools/scene_builder/err_del_root = Error: the scene root cannot be deleted. -tools/scene_builder/err_del_none = Error: select a mod node to delete. - -tools/interactive_editor/title = Interactive Editor -tools/interactive_editor/hint = Click a collider — select · drag handles · in polygon mode a click on the background adds a vertex, RMB on a vertex deletes it · Del — delete object · Ctrl+S — save -tools/interactive_editor/items = Objects in file -tools/interactive_editor/add = + New -tools/interactive_editor/duplicate = Duplicate -tools/interactive_editor/delete = Delete object -tools/interactive_editor/delete_confirm = Remove the selected interactive from the file? The change reaches the file on save. -tools/interactive_editor/deleted = Object "{0}" removed. -tools/interactive_editor/background = Preview background -tools/interactive_editor/props = Properties -tools/interactive_editor/none_selected = Pick an object in the list or on the preview. -tools/interactive_editor/label_id = ID -tools/interactive_editor/label_texture_key = Texture -tools/interactive_editor/texture_placeholder = no sprite -tools/interactive_editor/texture_tip = Sprite texture ID; empty — an invisible clickable area -tools/interactive_editor/label_layer = Layer -tools/interactive_editor/label_z = Z order -tools/interactive_editor/label_cursor = Cursor -tools/interactive_editor/cursor_none = (default) -tools/interactive_editor/enabled = Enabled -tools/interactive_editor/visible = Visible -tools/interactive_editor/collider = Collider -tools/interactive_editor/label_collider_type = Shape -tools/interactive_editor/hit_rect = rectangle -tools/interactive_editor/hit_circle = circle -tools/interactive_editor/hit_polygon = polygon -tools/interactive_editor/label_x = X -tools/interactive_editor/label_y = Y -tools/interactive_editor/label_w = Width -tools/interactive_editor/label_h = Height -tools/interactive_editor/label_cx = Centre X -tools/interactive_editor/label_cy = Centre Y -tools/interactive_editor/label_r = Radius -tools/interactive_editor/polygon_hint = A click on the background adds a vertex, RMB on a vertex deletes it. At least 3 vertices. -tools/interactive_editor/polygon_too_small = Polygon "{0}" has fewer than 3 vertices. -tools/interactive_editor/highlight = Highlight -tools/interactive_editor/label_highlight_color = Colour -tools/interactive_editor/label_highlight_scale = Scale -tools/interactive_editor/actions = Actions -tools/interactive_editor/label_onclick_scenario = Scenario -tools/interactive_editor/label_onclick_commands = Commands on click (one per line) -tools/interactive_editor/label_hover_enter = Hover in -tools/interactive_editor/label_hover_leave = Hover out -tools/interactive_editor/id_empty = An object has an empty ID. -tools/interactive_editor/duplicate_id = Duplicate object ID: - -tools/light_editor/title = Light Editor -tools/light_editor/hint = Double-click the background — new light · drag lights · Del — delete · Ctrl+S — write light_set into the scenario -tools/light_editor/scenario_tip = Scenario whose light_set line is edited (● — already sets lights) -tools/light_editor/pick_scenario = — pick a scenario — -tools/light_editor/save_tip = Write the light_set line into the scenario (Ctrl+S) -tools/light_editor/copy_line = Copy line -tools/light_editor/copy_line_tip = Copy the light_set command to the clipboard -tools/light_editor/copied = light_set line copied. -tools/light_editor/show_line = light_set -tools/light_editor/loaded = Lights of {0} loaded. -tools/light_editor/no_lights_yet = {0} has no light_set yet — add lights, the line is inserted after the header. -tools/light_editor/static_lights = Lights -tools/light_editor/add_light = + Light -tools/light_editor/delete = Delete -tools/light_editor/limit = At most {0} static lights. -tools/light_editor/selected_props = Selected light -tools/light_editor/none_selected = Pick a light in the list or on the preview. -tools/light_editor/x = X (0–1) -tools/light_editor/y = Y (0–1) -tools/light_editor/radius = Radius -tools/light_editor/radius_tip = Radius in pixels of the 1920 px game screen -tools/light_editor/influence = Strength -tools/light_editor/influence_tip = Light influence (0–1) -tools/light_editor/color = Colour -tools/light_editor/background = Preview background - -tools/catalog_export/hint = The archive and hashes are computed on export; the JSON is written next to the mod archive -tools/catalog_export/mod_info = Mod -tools/catalog_export/id = ID -tools/catalog_export/name = Name -tools/catalog_export/metadata = Catalog -tools/catalog_export/locales = Localizations -tools/catalog_export/locales_hint = With no rows the English name and description of the manifest are used. -tools/catalog_export/remove_locale = Remove language -tools/catalog_export/preview = Resulting JSON -tools/catalog_export/copied = JSON copied to the clipboard. -tools/catalog_export/export_tip = Build the archive, compute hashes and write the catalog entry JSON -tools/catalog_export/exported = Exported: {0} -tools/catalog_export/export_error = Export error -tools/catalog_export/url_required = Set the download URL — a catalog entry is useless without it. - -tools/scene_editor/title = Scene editor -tools/scene_editor/hint = A scene is a place: backgrounds, ambient sound and scenarios. Ctrl+S saves · double-click a background in the list to add it -tools/scene_editor/save_tip = Save the scene file into the chosen mod (Ctrl+S); the game sees the change at once -tools/scene_editor/new_scene = + New scene -tools/scene_editor/new_scene_tip = Create a new place: name, mod, backgrounds. Scenarios can be added later; a scene may also have none -tools/scene_editor/delete = Delete -tools/scene_editor/delete_tip = Delete the chosen mod's scene file -tools/scene_editor/delete_confirm = Delete this scene's file from the chosen mod? The scene's scenarios stay. -tools/scene_editor/go = ▶ Go to scene -tools/scene_editor/go_tip = Switch the game to this scene -tools/scene_editor/rescan_tip = Re-read scenes, backgrounds and sounds (after adding files to mods) -tools/scene_editor/scenes = Scenes -tools/scene_editor/backgrounds = Scene backgrounds (one is picked at random on entry) -tools/scene_editor/all_backgrounds = All backgrounds -tools/scene_editor/add_background = + Add background to scene -tools/scene_editor/add_background_tip = Add the shown background to the scene (or double-click it in the list) -tools/scene_editor/remove_background = Remove the background from the scene -tools/scene_editor/properties = Properties -tools/scene_editor/name = Name -tools/scene_editor/name_tip = Scene id: latin letters, digits and _; switch_scene and scenario headers refer to it. Set only when creating -tools/scene_editor/mod = Save to -tools/scene_editor/mod_tip = Mod the scene file is written to (folder mods only) -tools/scene_editor/ambient = Sound -tools/scene_editor/ambient_tip = Ambient sound playing in the scene -tools/scene_editor/listen = ▶ -tools/scene_editor/listen_tip = Listen to the ambient sound -tools/scene_editor/stop = ■ -tools/scene_editor/default_scenario = Default scenario -tools/scene_editor/default_scenario_tip = Plays when no other scenario of the scene is left -tools/scene_editor/scenarios = Scenarios of the scene -tools/scene_editor/open_scenario = Open -tools/scene_editor/open_scenario_tip = Open the selected scenario in the scenario editor (or double-click) -tools/scene_editor/new_scenario = + New scenario -tools/scene_editor/new_scenario_tip = Create a scenario of this scene in the chosen mod and open it -tools/scene_editor/none = — none — -tools/scene_editor/effective = In game: {0} (from mod {1}) -tools/scene_editor/adds_to = The scene is defined in: {0}. Saving creates a file in the chosen mod that adds to it. -tools/scene_editor/also_in = Also added to by: {0} -tools/scene_editor/no_writable_mod = No folder mod to save the scene to. -tools/scene_editor/bad_name = Set the scene name: latin letters, digits and _ (starting with a letter). -tools/scene_editor/name_taken = Scene "{0}" already exists; pick it in the list. -tools/scene_editor/need_background = Add at least one background to the scene. -tools/scene_editor/created = Scene "{0}" created. -tools/scene_editor/deleted = Deleted: {0} -tools/scene_editor/opened = Scene: {0} -tools/scene_editor/new_started = New scene: set the name and the mod, add backgrounds. -tools/scene_editor/already_there = Background {0} is already in the scene. -tools/scene_editor/go_failed = The game does not know scene "{0}"; save it first. -tools/scene_editor/went = Switched to scene {0}. -tools/scene_editor/rescanned = Scenes, backgrounds and sounds re-read. -tools/scene_editor/scenario_created = Scenario {0} created. -tools/scene_editor/no_scenarios = The scene has no scenarios yet -tools/scene_editor/no_backgrounds = The scene has no backgrounds: pick one in the list below and add it. -tools/scene_editor/pick_scene = Pick a scene on the left or create a new one. -tools/scene_editor/preview_hint = Pick a background to see it. -tools/scene_editor/not_in_scene = (not in the scene) -tools/scene_editor/new_line_text = A new scene. Replace this line with your text. -tools/entity_editor/hint = The list with a search on the left, the fields of the selected entry on the right · Ctrl+N new, Ctrl+D copy · Ctrl+Z/Y undo/redo · Ctrl+S save, the game sees the change at once -tools/entity_editor/section_list = Entries of the mod -tools/entity_editor/search = Search by name or id -tools/entity_editor/mod_tip = The mod whose entries are edited; new entries go into its file -tools/entity_editor/new = New -tools/entity_editor/new_tip = Add an entry to the chosen mod (Ctrl+N) -tools/entity_editor/duplicate = Copy -tools/entity_editor/duplicate_tip = Copy the selected entry under a new id (Ctrl+D) -tools/entity_editor/delete = Delete -tools/entity_editor/delete_tip = Remove the selected entry from the mod's file -tools/entity_editor/delete_title = Delete the entry? -tools/entity_editor/delete_text = The entry leaves the mod's file on save. References to it in scenarios and maps stay — check them. -tools/entity_editor/undo = Undo -tools/entity_editor/undo_tip = Undo the last change (Ctrl+Z) -tools/entity_editor/redo = Redo -tools/entity_editor/redo_tip = Redo what was undone (Ctrl+Y) -tools/entity_editor/no_entity = Pick an entry on the left or make one with "New". -tools/entity_editor/file = Mod {0} · file {1} -tools/entity_editor/id = Id -tools/entity_editor/id_tip = Unique id: scenarios, maps and other mods find the entry by it. A mod that depends on the owner and ships the same id replaces the entry -tools/entity_editor/locale_key_tip = Locale key; the texts below go into the mod's locale/*.properties -tools/entity_editor/text_in = text ({0}) -tools/entity_editor/picture_pick_tip = Pick a picture from the mod folders -tools/entity_editor/picture_pick_title = Pictures of the “{0}” group -tools/entity_editor/section_other = Other fields -tools/entity_editor/other_hint = Fields the form does not show (other mods' data, loot…) as JSON. Edits apply as soon as the JSON is valid. -tools/entity_editor/other_invalid = Other fields: invalid JSON — {0} -tools/entity_editor/other_applied = Other fields applied -tools/entity_editor/opened = {0} entries in mod {1} -tools/entity_editor/added = Added entry {0} to {1} -tools/entity_editor/deleted = Entry {0} deleted (save to write it) -tools/entity_editor/saved = Saved: {0} file(s), {1} text(s) in mod {2}; the game re-read its data -tools/entity_editor/broken_files = Files with broken JSON were not read: {0} -tools/entity_editor/problem_id = An entry in file {0} has an empty id -tools/entity_editor/problem_duplicate = Id {0} occurs more than once +mod_editor_tools_desc = Compatibility toolset: installs the individual scene, scenario, map, interactive, light, Godot scene and catalog tools. diff --git a/locale/ui_es.properties b/locale/ui_es.properties index f828b8c..431be63 100644 --- a/locale/ui_es.properties +++ b/locale/ui_es.properties @@ -1,337 +1,2 @@ mod_editor_tools_name = Herramientas de edición -mod_editor_tools_desc = Ventanas de autor sobre el juego en marcha: editores de escenas, escenarios, mapa, interactivos y luces, y un constructor de escenas de Godot. Todo lo que guardan son archivos de mod normales que el juego aplica al instante. - -tools/common/save = Guardar -tools/common/save_tip = Guardar el archivo (Ctrl+S) -tools/common/saved = Guardado: {0} -tools/common/loaded = Abierto: {0} -tools/common/load_error = Error al cargar -tools/common/readonly = No se puede guardar: el mod es de solo lectura (zip) o la ruta no es válida. -tools/common/no_writable_mods = No hay mods editables (solo se pueden editar los mods en carpeta). -tools/common/no_files = El mod «{0}» todavía no tiene archivos de este tipo: crea uno con «+». -tools/common/no_file_to_save = Primero elige o crea un archivo («+» junto a la lista de archivos). -tools/common/new_file = Nuevo archivo -tools/common/new_file_hint = El archivo se crea en {0}/{1}/ con la extensión {2} al guardarlo por primera vez. -tools/common/new_file_status = Nuevo archivo {0}: se crea al guardar. -tools/common/mod_tip = Mod (primero los mods que ya tienen archivos así; · — todavía ninguno) -tools/common/file_tip = Archivo dentro del mod -tools/common/search = Buscar… -tools/common/json = JSON -tools/common/json_tip = Mostrar el JSON del archivo resultante -tools/common/background_missing = Ningún mod aporta el fondo «{0}» de la escena «{1}»: elige uno a mano. - -tools/map_editor/title = Editor de mapas -tools/map_editor/hint = Clic izq.: seleccionar / arrastrar · clic der.: borrar vértice · doble clic, Intro o Esc: terminar la región · Supr: borrar zona · Ctrl+S: guardar -tools/map_editor/reload_map = Actualizar el mapa del juego -tools/map_editor/reload_map_tip = Volver a leer los archivos del mapa guardados en el juego en marcha -tools/map_editor/reloaded = Mapa del juego actualizado. -tools/map_editor/select_mode = Seleccionar -tools/map_editor/draw_region = Dibujar -tools/map_editor/draw_started = Haz clic en el mapa para poner vértices. Doble clic, Intro o Esc terminan; el clic derecho quita el último. -tools/map_editor/background = Fondo de vista previa -tools/map_editor/zones = Zonas -tools/map_editor/add_region = + Región -tools/map_editor/add_region_tip = Nueva región: dibuja el polígono haciendo clic en el mapa -tools/map_editor/add_marker = + Marcador -tools/map_editor/add_label = + Rótulo -tools/map_editor/delete = Borrar zona -tools/map_editor/delete_tip = Borrar la zona seleccionada (Supr) -tools/map_editor/delete_confirm = ¿Borrar la zona seleccionada? El cambio llega al archivo al guardar. -tools/map_editor/deleted = Zona «{0}» borrada. -tools/map_editor/props = Propiedades de la zona -tools/map_editor/zone_missing = Elige una zona en la lista o en el mapa. -tools/map_editor/id = ID -tools/map_editor/id_empty = El ID de la zona no puede estar vacío. -tools/map_editor/duplicate_id = ID de zona ya usado: -tools/map_editor/title_key = Título -tools/map_editor/description = Descripción -tools/map_editor/no_translation = clave no encontrada en la localización -tools/map_editor/scene = Escena -tools/map_editor/scene_none = (ninguna) -tools/map_editor/availability = Disponibilidad -tools/map_editor/locked = Bloqueada para viajar -tools/map_editor/hidden = Oculta en el mapa -tools/map_editor/effective = En el juego: -tools/map_editor/effective_open = abierta -tools/map_editor/effective_locked = bloqueada (marca Locked) -tools/map_editor/effective_no_scene = bloqueada (sin escena) -tools/map_editor/effective_unknown_scene = bloqueada (escena no encontrada) -tools/map_editor/effective_hidden = oculta en el mapa -tools/map_editor/marker = Marcador -tools/map_editor/image = Icono -tools/map_editor/image_placeholder = ID de textura -tools/map_editor/icon_search = Buscar icono… -tools/map_editor/label_section = Rótulo -tools/map_editor/label = Rótulo de texto en el mapa -tools/map_editor/x = X (0–1) -tools/map_editor/y = Y (0–1) -tools/map_editor/vertices = Vértices del polígono -tools/map_editor/vertices_hint = Arrastra los vértices en el mapa; clic derecho en un vértice lo borra. -tools/map_editor/add_vertex = + Vértice -tools/map_editor/delete_vertex = Borrar vértice -tools/map_editor/preview = ▶ Como en el juego -tools/map_editor/preview_tip = Ver el mapa como en el juego: paso del ratón, tarjeta de zona, marcadores (incluidos los cambios sin guardar) -tools/map_editor/preview_on = Vista previa: Esc o ✕ vuelve al editor. -tools/map_editor/preview_off = Modo de edición. -tools/map_editor/preview_missing = No se puede cargar la escena del mapa (map_widget.tscn). - -tools/scenario_editor/title = Editor de escenarios -tools/scenario_editor/hint = Ctrl+S: guardar · F5: jugar el escenario · clic en una escena del grafo: sus escenarios -tools/scenario_editor/play = ▶ Jugar -tools/scenario_editor/play_tooltip = Guardar y jugar el escenario en el juego (F5) -tools/scenario_editor/play_stopped = Prueba detenida. -tools/scenario_editor/stop = ■ Detener -tools/scenario_editor/open_file = Abrir en un editor externo -tools/scenario_editor/open_file_tip = Abrir el archivo con el programa predeterminado del sistema -tools/scenario_editor/open_file_error = No se puede abrir el archivo -tools/scenario_editor/rescan = Volver a escanear -tools/scenario_editor/rescan_tip = Volver a leer la lista de escenarios y el grafo (tras añadir archivos) -tools/scenario_editor/rescanned = Lista de escenarios actualizada. -tools/scenario_editor/search_files = Buscar archivo… -tools/scenario_editor/tab_files = Archivos -tools/scenario_editor/tab_info = Resumen -tools/scenario_editor/tab_graph = Grafo de escenas -tools/scenario_editor/graph_select_node = Selecciona una escena en el grafo para ver sus escenarios y transiciones. -tools/scenario_editor/graph_node = mod {0} · escenarios {1} -tools/scenario_editor/graph_node_bg = fondo: {0} -tools/scenario_editor/graph_detail_mod = mod: {0} -tools/scenario_editor/graph_detail_scenarios = Escenarios ({0}), por prioridad: -tools/scenario_editor/info_scene = Escena -tools/scenario_editor/info_priority = Prioridad -tools/scenario_editor/info_condition = Condición -tools/scenario_editor/info_switch_targets = Cambios de escena -tools/scenario_editor/info_locale_keys = Claves de localización -tools/scenario_editor/info_quest_refs = Misiones -tools/scenario_editor/bg_previews = Fondos del escenario -tools/scenario_editor/bg_missing = Fondo no encontrado: {0} -tools/scenario_editor/no_backgrounds = El escenario no cambia el fondo -tools/scenario_editor/save_error = Error al guardar: {0} -tools/scenario_editor/readonly_zip = Los archivos de los mods zip son de solo lectura -tools/scenario_editor/no_scene_error = La cabecera del escenario no tiene escena (scene:): no hay nada que jugar. -tools/scenario_editor/unsaved_title = Cambios sin guardar -tools/scenario_editor/unsaved_switch_text = Esta pestaña tiene cambios sin guardar. ¿Cerrarla sin guardar? - -tools/scene_builder/title = Constructor de escenas -tools/scene_builder/hint = Arrastra un nodo por su cuerpo o sus tiradores · clic: elegir el nodo bajo el cursor · Supr: borrar nodo · Ctrl+S: guardar y reconstruir -tools/scene_builder/base_scene = Escena base sobre la que se muestra la escena del mod -tools/scene_builder/mod = Mod -tools/scene_builder/scene = Escena del mod (.tscn) -tools/scene_builder/empty_scene = (escena base vacía) -tools/scene_builder/select_mod = — elige un mod — -tools/scene_builder/select_scene = — elige una escena — -tools/scene_builder/save_tip = Guardar el .tscn y reconstruir la vista previa (Ctrl+S) -tools/scene_builder/reload = Recargar desde el disco -tools/scene_builder/reload_tip = Descartar los cambios sin guardar y volver a leer el archivo de la escena -tools/scene_builder/reloaded = Escena recargada desde el disco. -tools/scene_builder/saved_ok = Guardado. -tools/scene_builder/saved_rebuilt = Guardado, vista previa reconstruida. -tools/scene_builder/autosaved = Guardado automático. -tools/scene_builder/error_save = Error al guardar: el mod es de solo lectura. -tools/scene_builder/error_no_scene = Primero elige una escena del mod. -tools/scene_builder/hierarchy = Jerarquía -tools/scene_builder/hierarchy_hint = Dorado: raíz de la escena del mod; brillante: nodos editables; tenue: escena base (solo lectura). -tools/scene_builder/node_type_tip = Tipo del nuevo nodo -tools/scene_builder/add_node_tip = Añadir un nodo del tipo elegido dentro del nodo seleccionado (o en la raíz de la escena) -tools/scene_builder/delete_node_tip = Borrar el nodo seleccionado de la escena del mod -tools/scene_builder/delete_node_title = Borrar nodo -tools/scene_builder/delete_node_confirm = ¿Borrar el nodo seleccionado con sus hijos? El archivo se guarda automáticamente. -tools/scene_builder/refresh_tip = Actualizar el árbol de nodos -tools/scene_builder/tab_inspector = Inspector -tools/scene_builder/tab_tscn = Código .tscn -tools/scene_builder/tscn_hint = Los cambios del inspector y de los tiradores se guardan solos; los cambios a mano en el texto se guardan con Ctrl+S, que también reconstruye la vista previa. -tools/scene_builder/inspector_cat_container = En contenedor -tools/scene_builder/inspector_select = Selecciona un nodo de la escena del mod para editar sus propiedades -tools/scene_builder/inspector_readonly = Solo lectura: el nodo pertenece a la escena base -tools/scene_builder/inspector_missing_section = No se encontró la sección del nodo en el tscn (los nodos de subescenas instanciadas todavía no se pueden editar) -tools/scene_builder/inspector_preset = Preajuste de anclas -tools/scene_builder/inspector_cat_layout = Disposición -tools/scene_builder/inspector_cat_rect = Rectángulo -tools/scene_builder/inspector_cat_visibility = Visibilidad -tools/scene_builder/inspector_cat_misc = Varios -tools/scene_builder/inspector_bad_value = El valor no se puede escribir en el tscn. -tools/scene_builder/inspector_in_file_tooltip = Guardado en el archivo tscn -tools/scene_builder/inspector_container_banner = Este nodo lo gestiona un contenedor: el padre recalcula las anclas y los desplazamientos, así que aquí no tendrán efecto. -tools/scene_builder/err_add_failed = Error: no se puede crear un nodo de este tipo. -tools/scene_builder/err_add_parent_missing = Error: no se encontró la sección del padre en el tscn (no se admiten padres de subescenas instanciadas). -tools/scene_builder/err_del_root = Error: no se puede borrar la raíz de la escena. -tools/scene_builder/err_del_none = Error: elige un nodo del mod para borrarlo. - -tools/interactive_editor/title = Editor de interactivos -tools/interactive_editor/hint = Clic en un colisionador: seleccionar · arrastra los tiradores · en modo polígono, un clic en el fondo añade un vértice y el clic derecho en un vértice lo borra · Supr: borrar objeto · Ctrl+S: guardar -tools/interactive_editor/items = Objetos del archivo -tools/interactive_editor/add = + Nuevo -tools/interactive_editor/duplicate = Duplicar -tools/interactive_editor/delete = Borrar objeto -tools/interactive_editor/delete_confirm = ¿Quitar del archivo el interactivo seleccionado? El cambio llega al archivo al guardar. -tools/interactive_editor/deleted = Objeto «{0}» quitado. -tools/interactive_editor/background = Fondo de vista previa -tools/interactive_editor/props = Propiedades -tools/interactive_editor/none_selected = Elige un objeto en la lista o en la vista previa. -tools/interactive_editor/label_id = ID -tools/interactive_editor/label_texture_key = Textura -tools/interactive_editor/texture_placeholder = sin sprite -tools/interactive_editor/texture_tip = ID de la textura del sprite; vacío: un área clicable invisible -tools/interactive_editor/label_layer = Capa -tools/interactive_editor/label_z = Orden Z -tools/interactive_editor/label_cursor = Cursor -tools/interactive_editor/cursor_none = (por defecto) -tools/interactive_editor/enabled = Activado -tools/interactive_editor/visible = Visible -tools/interactive_editor/collider = Colisionador -tools/interactive_editor/label_collider_type = Forma -tools/interactive_editor/hit_rect = rectángulo -tools/interactive_editor/hit_circle = círculo -tools/interactive_editor/hit_polygon = polígono -tools/interactive_editor/label_x = X -tools/interactive_editor/label_y = Y -tools/interactive_editor/label_w = Ancho -tools/interactive_editor/label_h = Alto -tools/interactive_editor/label_cx = Centro X -tools/interactive_editor/label_cy = Centro Y -tools/interactive_editor/label_r = Radio -tools/interactive_editor/polygon_hint = Un clic en el fondo añade un vértice; el clic derecho en un vértice lo borra. Al menos 3 vértices. -tools/interactive_editor/polygon_too_small = El polígono «{0}» tiene menos de 3 vértices. -tools/interactive_editor/highlight = Resaltado -tools/interactive_editor/label_highlight_color = Color -tools/interactive_editor/label_highlight_scale = Escala -tools/interactive_editor/actions = Acciones -tools/interactive_editor/label_onclick_scenario = Escenario -tools/interactive_editor/label_onclick_commands = Comandos al hacer clic (uno por línea) -tools/interactive_editor/label_hover_enter = Al entrar el ratón -tools/interactive_editor/label_hover_leave = Al salir el ratón -tools/interactive_editor/id_empty = Un objeto tiene el ID vacío. -tools/interactive_editor/duplicate_id = ID de objeto duplicado: - -tools/light_editor/title = Editor de luces -tools/light_editor/hint = Doble clic en el fondo: nueva luz · arrastra las luces · Supr: borrar · Ctrl+S: escribir light_set en el escenario -tools/light_editor/scenario_tip = Escenario cuya línea light_set se edita (●: ya define luces) -tools/light_editor/pick_scenario = — elige un escenario — -tools/light_editor/save_tip = Escribir la línea light_set en el escenario (Ctrl+S) -tools/light_editor/copy_line = Copiar línea -tools/light_editor/copy_line_tip = Copiar el comando light_set al portapapeles -tools/light_editor/copied = Línea light_set copiada. -tools/light_editor/show_line = light_set -tools/light_editor/loaded = Luces de {0} cargadas. -tools/light_editor/no_lights_yet = {0} todavía no tiene light_set: añade luces y la línea se insertará tras la cabecera. -tools/light_editor/static_lights = Luces -tools/light_editor/add_light = + Luz -tools/light_editor/delete = Borrar -tools/light_editor/limit = Como máximo {0} luces estáticas. -tools/light_editor/selected_props = Luz seleccionada -tools/light_editor/none_selected = Elige una luz en la lista o en la vista previa. -tools/light_editor/x = X (0–1) -tools/light_editor/y = Y (0–1) -tools/light_editor/radius = Radio -tools/light_editor/radius_tip = Radio en píxeles de la pantalla de juego de 1920 px -tools/light_editor/influence = Intensidad -tools/light_editor/influence_tip = Influencia de la luz (0–1) -tools/light_editor/color = Color -tools/light_editor/background = Fondo de vista previa - -tools/catalog_export/hint = El archivo y los hashes se calculan al exportar; el JSON se escribe junto al archivo del mod -tools/catalog_export/mod_info = Mod -tools/catalog_export/id = ID -tools/catalog_export/name = Nombre -tools/catalog_export/metadata = Catálogo -tools/catalog_export/locales = Localizaciones -tools/catalog_export/locales_hint = Sin filas se usan el nombre y la descripción en inglés del manifiesto. -tools/catalog_export/remove_locale = Quitar idioma -tools/catalog_export/preview = JSON resultante -tools/catalog_export/copied = JSON copiado al portapapeles. -tools/catalog_export/export_tip = Crear el archivo, calcular los hashes y escribir el JSON de la entrada del catálogo -tools/catalog_export/exported = Exportado: {0} -tools/catalog_export/export_error = Error al exportar -tools/catalog_export/url_required = Indica la URL de descarga: sin ella la entrada del catálogo no sirve. - -tools/scene_editor/title = Editor de escenas -tools/scene_editor/hint = Una escena es un lugar: fondos, sonido ambiente y escenarios. Ctrl+S guarda · doble clic en un fondo de la lista para añadirlo -tools/scene_editor/save_tip = Guardar el archivo de la escena en el mod elegido (Ctrl+S); el juego ve el cambio al momento -tools/scene_editor/new_scene = + Nueva escena -tools/scene_editor/new_scene_tip = Crear un lugar nuevo: nombre, mod, fondos. Los escenarios se pueden añadir después; una escena también puede no tener ninguno -tools/scene_editor/delete = Borrar -tools/scene_editor/delete_tip = Borrar el archivo de la escena del mod elegido -tools/scene_editor/delete_confirm = ¿Borrar el archivo de esta escena del mod elegido? Los escenarios de la escena se conservan. -tools/scene_editor/go = ▶ Ir a la escena -tools/scene_editor/go_tip = Cambiar el juego a esta escena -tools/scene_editor/rescan_tip = Volver a leer escenas, fondos y sonidos (tras añadir archivos a los mods) -tools/scene_editor/scenes = Escenas -tools/scene_editor/backgrounds = Fondos de la escena (al entrar se elige uno al azar) -tools/scene_editor/all_backgrounds = Todos los fondos -tools/scene_editor/add_background = + Añadir fondo a la escena -tools/scene_editor/add_background_tip = Añadir a la escena el fondo mostrado (o haz doble clic en él en la lista) -tools/scene_editor/remove_background = Quitar el fondo de la escena -tools/scene_editor/properties = Propiedades -tools/scene_editor/name = Nombre -tools/scene_editor/name_tip = Id de la escena: letras latinas, dígitos y _; lo usan switch_scene y las cabeceras de los escenarios. Solo se indica al crearla -tools/scene_editor/mod = Guardar en -tools/scene_editor/mod_tip = Mod en el que se escribe el archivo de la escena (solo mods en carpeta) -tools/scene_editor/ambient = Sonido -tools/scene_editor/ambient_tip = Sonido ambiente que suena en la escena -tools/scene_editor/listen = ▶ -tools/scene_editor/listen_tip = Escuchar el sonido ambiente -tools/scene_editor/stop = ■ -tools/scene_editor/default_scenario = Escenario por defecto -tools/scene_editor/default_scenario_tip = Se reproduce cuando no queda ningún otro escenario de la escena -tools/scene_editor/scenarios = Escenarios de la escena -tools/scene_editor/open_scenario = Abrir -tools/scene_editor/open_scenario_tip = Abrir el escenario seleccionado en el editor de escenarios (o doble clic) -tools/scene_editor/new_scenario = + Nuevo escenario -tools/scene_editor/new_scenario_tip = Crear un escenario de esta escena en el mod elegido y abrirlo -tools/scene_editor/none = — ninguno — -tools/scene_editor/effective = En el juego: {0} (del mod {1}) -tools/scene_editor/adds_to = La escena está definida en: {0}. Al guardar se crea un archivo en el mod elegido que la amplía. -tools/scene_editor/also_in = También la amplían: {0} -tools/scene_editor/no_writable_mod = No hay ningún mod en carpeta donde guardar la escena. -tools/scene_editor/bad_name = Indica el nombre de la escena: letras latinas, dígitos y _ (empezando por una letra). -tools/scene_editor/name_taken = La escena «{0}» ya existe; elígela en la lista. -tools/scene_editor/need_background = Añade al menos un fondo a la escena. -tools/scene_editor/created = Escena «{0}» creada. -tools/scene_editor/deleted = Borrado: {0} -tools/scene_editor/opened = Escena: {0} -tools/scene_editor/new_started = Nueva escena: indica el nombre y el mod, añade fondos. -tools/scene_editor/already_there = El fondo {0} ya está en la escena. -tools/scene_editor/go_failed = El juego no conoce la escena «{0}»; guárdala primero. -tools/scene_editor/went = Cambiado a la escena {0}. -tools/scene_editor/rescanned = Escenas, fondos y sonidos releídos. -tools/scene_editor/scenario_created = Escenario {0} creado. -tools/scene_editor/no_scenarios = La escena todavía no tiene escenarios -tools/scene_editor/no_backgrounds = La escena no tiene fondos: elige uno en la lista de abajo y añádelo. -tools/scene_editor/pick_scene = Elige una escena a la izquierda o crea una nueva. -tools/scene_editor/preview_hint = Elige un fondo para verlo. -tools/scene_editor/not_in_scene = (no está en la escena) -tools/scene_editor/new_line_text = Una escena nueva. Sustituye esta línea por tu texto. -tools/entity_editor/hint = A la izquierda, la lista con búsqueda; a la derecha, los campos de la entrada elegida · Ctrl+N: nueva, Ctrl+D: copia · Ctrl+Z/Y: deshacer/rehacer · Ctrl+S: guardar, el juego ve el cambio al momento -tools/entity_editor/section_list = Entradas del mod -tools/entity_editor/search = Buscar por nombre o id -tools/entity_editor/mod_tip = El mod cuyas entradas se editan; las nuevas entradas van a su archivo -tools/entity_editor/new = Nueva -tools/entity_editor/new_tip = Añadir una entrada al mod elegido (Ctrl+N) -tools/entity_editor/duplicate = Copia -tools/entity_editor/duplicate_tip = Copiar la entrada seleccionada con un id nuevo (Ctrl+D) -tools/entity_editor/delete = Borrar -tools/entity_editor/delete_tip = Quitar la entrada seleccionada del archivo del mod -tools/entity_editor/delete_title = ¿Borrar la entrada? -tools/entity_editor/delete_text = La entrada sale del archivo del mod al guardar. Las referencias a ella en escenarios y mapas se quedan: revísalas. -tools/entity_editor/undo = Deshacer -tools/entity_editor/undo_tip = Deshacer el último cambio (Ctrl+Z) -tools/entity_editor/redo = Rehacer -tools/entity_editor/redo_tip = Rehacer lo deshecho (Ctrl+Y) -tools/entity_editor/no_entity = Elige una entrada a la izquierda o crea una con «Nueva». -tools/entity_editor/file = Mod {0} · archivo {1} -tools/entity_editor/id = Id -tools/entity_editor/id_tip = Id único: los escenarios, los mapas y otros mods encuentran la entrada por él. Un mod que depende del propietario y trae el mismo id sustituye la entrada -tools/entity_editor/locale_key_tip = Clave de localización; los textos de abajo van a locale/*.properties del mod -tools/entity_editor/text_in = texto ({0}) -tools/entity_editor/picture_pick_tip = Elegir una imagen de las carpetas del mod -tools/entity_editor/picture_pick_title = Imágenes del grupo «{0}» -tools/entity_editor/section_other = Otros campos -tools/entity_editor/other_hint = Campos que el formulario no muestra (datos de otros mods, botín…) como JSON. Los cambios se aplican en cuanto el JSON es válido. -tools/entity_editor/other_invalid = Otros campos: JSON no válido — {0} -tools/entity_editor/other_applied = Otros campos aplicados -tools/entity_editor/opened = {0} entradas en el mod {1} -tools/entity_editor/added = Entrada {0} añadida a {1} -tools/entity_editor/deleted = Entrada {0} borrada (guarda para escribirlo) -tools/entity_editor/saved = Guardado: {0} archivo(s), {1} texto(s) en el mod {2}; el juego ha releído sus datos -tools/entity_editor/broken_files = No se leyeron los archivos con JSON roto: {0} -tools/entity_editor/problem_id = Una entrada del archivo {0} tiene el id vacío -tools/entity_editor/problem_duplicate = El id {0} aparece más de una vez +mod_editor_tools_desc = Paquete compatible de editores de escenas, escenarios, mapas, interactivos, luces, escenas Godot y exportación. diff --git a/locale/ui_ja.properties b/locale/ui_ja.properties index b6c476a..bcc91ce 100644 --- a/locale/ui_ja.properties +++ b/locale/ui_ja.properties @@ -1,337 +1,2 @@ mod_editor_tools_name = エディターツール -mod_editor_tools_desc = 実行中のゲームの上に開くオーサリング用ウィンドウ:シーン、シナリオ、マップ、インタラクティブ、ライトの各エディターと Godot シーンビルダー。保存内容は普通の MOD ファイルで、ゲームはすぐに反映します。 - -tools/common/save = 保存 -tools/common/save_tip = ファイルを保存(Ctrl+S) -tools/common/saved = 保存しました:{0} -tools/common/loaded = 開きました:{0} -tools/common/load_error = 読み込みエラー -tools/common/readonly = 保存できません:MOD が読み取り専用(zip)か、パスが不正です。 -tools/common/no_writable_mods = 書き込める MOD がありません(編集できるのはフォルダー形式の MOD だけです)。 -tools/common/no_files = MOD「{0}」にはまだこの種類のファイルがありません——「+」で作成してください。 -tools/common/no_file_to_save = 先にファイルを選ぶか作成してください(ファイル一覧の横の「+」)。 -tools/common/new_file = 新しいファイル -tools/common/new_file_hint = 最初に保存したとき、{0}/{1}/ に拡張子 {2} のファイルが作られます。 -tools/common/new_file_status = 新しいファイル {0}——保存時に作成されます。 -tools/common/mod_tip = MOD(この種類のファイルがある MOD が先頭;·——まだなし) -tools/common/file_tip = MOD 内のファイル -tools/common/search = 検索… -tools/common/json = JSON -tools/common/json_tip = できあがるファイルの JSON を表示 -tools/common/background_missing = シーン「{1}」の背景「{0}」はどの MOD にもありません——手で選んでください。 - -tools/map_editor/title = マップエディター -tools/map_editor/hint = 左クリック——選択/ドラッグ · 右クリック——頂点を削除 · ダブルクリック、Enter、Esc——領域を確定 · Del——ゾーンを削除 · Ctrl+S——保存 -tools/map_editor/reload_map = ゲーム内のマップを更新 -tools/map_editor/reload_map_tip = 実行中のゲームで、保存済みのマップファイルを読み直す -tools/map_editor/reloaded = ゲーム内のマップを更新しました。 -tools/map_editor/select_mode = 選択 -tools/map_editor/draw_region = 描画 -tools/map_editor/draw_started = マップをクリックして頂点を置きます。ダブルクリック、Enter、Esc で確定、右クリックで最後の頂点を取り消します。 -tools/map_editor/background = プレビューの背景 -tools/map_editor/zones = ゾーン -tools/map_editor/add_region = + 領域 -tools/map_editor/add_region_tip = 新しい領域:マップをクリックして多角形を描く -tools/map_editor/add_marker = + マーカー -tools/map_editor/add_label = + ラベル -tools/map_editor/delete = ゾーンを削除 -tools/map_editor/delete_tip = 選択したゾーンを削除(Del) -tools/map_editor/delete_confirm = 選択したゾーンを削除しますか? 変更は保存時にファイルへ反映されます。 -tools/map_editor/deleted = ゾーン「{0}」を削除しました。 -tools/map_editor/props = ゾーンのプロパティ -tools/map_editor/zone_missing = 一覧かマップでゾーンを選んでください。 -tools/map_editor/id = ID -tools/map_editor/id_empty = ゾーンの ID は空にできません。 -tools/map_editor/duplicate_id = ゾーンの ID はすでに使われています: -tools/map_editor/title_key = タイトル -tools/map_editor/description = 説明 -tools/map_editor/no_translation = ローカライズにキーがありません -tools/map_editor/scene = シーン -tools/map_editor/scene_none = (なし) -tools/map_editor/availability = 利用可否 -tools/map_editor/locked = 移動不可 -tools/map_editor/hidden = マップに表示しない -tools/map_editor/effective = ゲーム内: -tools/map_editor/effective_open = 開いている -tools/map_editor/effective_locked = 閉鎖(Locked フラグ) -tools/map_editor/effective_no_scene = 閉鎖(シーンなし) -tools/map_editor/effective_unknown_scene = 閉鎖(シーンが見つからない) -tools/map_editor/effective_hidden = マップに表示しない -tools/map_editor/marker = マーカー -tools/map_editor/image = アイコン -tools/map_editor/image_placeholder = テクスチャ ID -tools/map_editor/icon_search = アイコンを探す… -tools/map_editor/label_section = ラベル -tools/map_editor/label = マップ上のテキストラベル -tools/map_editor/x = X(0–1) -tools/map_editor/y = Y(0–1) -tools/map_editor/vertices = 多角形の頂点 -tools/map_editor/vertices_hint = マップ上で頂点をドラッグ。頂点を右クリックすると削除。 -tools/map_editor/add_vertex = + 頂点 -tools/map_editor/delete_vertex = 頂点を削除 -tools/map_editor/preview = ▶ ゲームと同じ表示 -tools/map_editor/preview_tip = ゲームと同じようにマップを見る:ホバー、ゾーンカード、マーカー(未保存の編集も含む) -tools/map_editor/preview_on = プレビュー:Esc か ✕ でエディターに戻ります。 -tools/map_editor/preview_off = 編集モード。 -tools/map_editor/preview_missing = マップのシーン(map_widget.tscn)を読み込めません。 - -tools/scenario_editor/title = シナリオエディター -tools/scenario_editor/hint = Ctrl+S——保存 · F5——シナリオをプレイ · グラフのシーンをクリック——そのシナリオ -tools/scenario_editor/play = ▶ プレイ -tools/scenario_editor/play_tooltip = 保存してゲーム内でシナリオをプレイ(F5) -tools/scenario_editor/play_stopped = テストプレイを止めました。 -tools/scenario_editor/stop = ■ 停止 -tools/scenario_editor/open_file = 外部エディターで開く -tools/scenario_editor/open_file_tip = システムの既定のプログラムでファイルを開く -tools/scenario_editor/open_file_error = ファイルを開けません -tools/scenario_editor/rescan = 再スキャン -tools/scenario_editor/rescan_tip = シナリオ一覧とグラフを読み直す(ファイルを追加した後) -tools/scenario_editor/rescanned = シナリオ一覧を更新しました。 -tools/scenario_editor/search_files = ファイルを探す… -tools/scenario_editor/tab_files = ファイル -tools/scenario_editor/tab_info = 概要 -tools/scenario_editor/tab_graph = シーングラフ -tools/scenario_editor/graph_select_node = グラフでシーンを選ぶと、そのシナリオと遷移が表示されます。 -tools/scenario_editor/graph_node = MOD {0} · シナリオ {1} -tools/scenario_editor/graph_node_bg = 背景:{0} -tools/scenario_editor/graph_detail_mod = MOD:{0} -tools/scenario_editor/graph_detail_scenarios = シナリオ({0})、優先度順: -tools/scenario_editor/info_scene = シーン -tools/scenario_editor/info_priority = 優先度 -tools/scenario_editor/info_condition = 条件 -tools/scenario_editor/info_switch_targets = シーン切り替え -tools/scenario_editor/info_locale_keys = ローカライズキー -tools/scenario_editor/info_quest_refs = クエスト -tools/scenario_editor/bg_previews = シナリオの背景 -tools/scenario_editor/bg_missing = 背景が見つかりません:{0} -tools/scenario_editor/no_backgrounds = このシナリオは背景を変えません -tools/scenario_editor/save_error = 保存エラー:{0} -tools/scenario_editor/readonly_zip = zip の MOD のファイルは読み取り専用です -tools/scenario_editor/no_scene_error = シナリオのヘッダーにシーン(scene:)がありません——プレイするものがありません。 -tools/scenario_editor/unsaved_title = 保存していない変更 -tools/scenario_editor/unsaved_switch_text = このタブには保存していない変更があります。保存せずに閉じますか? - -tools/scene_builder/title = シーンビルダー -tools/scene_builder/hint = ノードは本体かハンドルでドラッグ · クリック——カーソル下のノードを選択 · Del——ノードを削除 · Ctrl+S——保存して再構築 -tools/scene_builder/base_scene = MOD のシーンを重ねる基本シーン -tools/scene_builder/mod = MOD -tools/scene_builder/scene = MOD のシーン(.tscn) -tools/scene_builder/empty_scene = (空の基本シーン) -tools/scene_builder/select_mod = — MOD を選択 — -tools/scene_builder/select_scene = — シーンを選択 — -tools/scene_builder/save_tip = .tscn を保存してプレビューを再構築(Ctrl+S) -tools/scene_builder/reload = ディスクから読み直す -tools/scene_builder/reload_tip = 保存していない編集を捨てて、シーンファイルを読み直す -tools/scene_builder/reloaded = ディスクからシーンを読み直しました。 -tools/scene_builder/saved_ok = 保存しました。 -tools/scene_builder/saved_rebuilt = 保存し、プレビューを再構築しました。 -tools/scene_builder/autosaved = 自動保存しました。 -tools/scene_builder/error_save = 保存エラー:MOD は読み取り専用です。 -tools/scene_builder/error_no_scene = 先に MOD のシーンを選んでください。 -tools/scene_builder/hierarchy = 階層 -tools/scene_builder/hierarchy_hint = 金——MOD シーンのルート、明るい——編集できるノード、薄い——基本シーン(読み取り専用)。 -tools/scene_builder/node_type_tip = 新しいノードの型 -tools/scene_builder/add_node_tip = 選択したノード(またはシーンのルート)の中に、選んだ型のノードを追加 -tools/scene_builder/delete_node_tip = 選択した MOD シーンのノードを削除 -tools/scene_builder/delete_node_title = ノードを削除 -tools/scene_builder/delete_node_confirm = 選択したノードを子ごと削除しますか? ファイルは自動で保存されます。 -tools/scene_builder/refresh_tip = ノードツリーを更新 -tools/scene_builder/tab_inspector = インスペクター -tools/scene_builder/tab_tscn = .tscn ソース -tools/scene_builder/tscn_hint = インスペクターとハンドルの編集は自動で保存されます。テキストを手で編集したときは Ctrl+S で保存し、同時にプレビューも再構築されます。 -tools/scene_builder/inspector_cat_container = コンテナー内 -tools/scene_builder/inspector_select = MOD シーンのノードを選ぶとプロパティを編集できます -tools/scene_builder/inspector_readonly = 読み取り専用:基本シーンのノードです -tools/scene_builder/inspector_missing_section = tscn にノードのセクションがありません(インスタンス化したサブシーンのノードはまだ編集できません) -tools/scene_builder/inspector_preset = アンカーのプリセット -tools/scene_builder/inspector_cat_layout = レイアウト -tools/scene_builder/inspector_cat_rect = 矩形 -tools/scene_builder/inspector_cat_visibility = 表示 -tools/scene_builder/inspector_cat_misc = その他 -tools/scene_builder/inspector_bad_value = この値は tscn に書き込めません。 -tools/scene_builder/inspector_in_file_tooltip = tscn ファイルに保存されています -tools/scene_builder/inspector_container_banner = このノードはコンテナーが管理しています:アンカーとオフセットは親が計算し直すので、ここでは効果がありません。 -tools/scene_builder/err_add_failed = エラー:この型のノードを作成できません。 -tools/scene_builder/err_add_parent_missing = エラー:tscn に親のセクションがありません(インスタンス化したサブシーンの親には対応していません)。 -tools/scene_builder/err_del_root = エラー:シーンのルートは削除できません。 -tools/scene_builder/err_del_none = エラー:削除する MOD のノードを選んでください。 - -tools/interactive_editor/title = インタラクティブエディター -tools/interactive_editor/hint = コライダーをクリック——選択 · ハンドルをドラッグ · 多角形モードでは背景のクリックで頂点を追加、頂点の右クリックで削除 · Del——オブジェクトを削除 · Ctrl+S——保存 -tools/interactive_editor/items = ファイル内のオブジェクト -tools/interactive_editor/add = + 新規 -tools/interactive_editor/duplicate = 複製 -tools/interactive_editor/delete = オブジェクトを削除 -tools/interactive_editor/delete_confirm = 選択したインタラクティブをファイルから取り除きますか? 変更は保存時にファイルへ反映されます。 -tools/interactive_editor/deleted = オブジェクト「{0}」を取り除きました。 -tools/interactive_editor/background = プレビューの背景 -tools/interactive_editor/props = プロパティ -tools/interactive_editor/none_selected = 一覧かプレビューでオブジェクトを選んでください。 -tools/interactive_editor/label_id = ID -tools/interactive_editor/label_texture_key = テクスチャ -tools/interactive_editor/texture_placeholder = スプライトなし -tools/interactive_editor/texture_tip = スプライトのテクスチャ ID。空——見えないクリック領域 -tools/interactive_editor/label_layer = レイヤー -tools/interactive_editor/label_z = Z 順 -tools/interactive_editor/label_cursor = カーソル -tools/interactive_editor/cursor_none = (既定) -tools/interactive_editor/enabled = 有効 -tools/interactive_editor/visible = 表示 -tools/interactive_editor/collider = コライダー -tools/interactive_editor/label_collider_type = 形 -tools/interactive_editor/hit_rect = 矩形 -tools/interactive_editor/hit_circle = 円 -tools/interactive_editor/hit_polygon = 多角形 -tools/interactive_editor/label_x = X -tools/interactive_editor/label_y = Y -tools/interactive_editor/label_w = 幅 -tools/interactive_editor/label_h = 高さ -tools/interactive_editor/label_cx = 中心 X -tools/interactive_editor/label_cy = 中心 Y -tools/interactive_editor/label_r = 半径 -tools/interactive_editor/polygon_hint = 背景をクリックすると頂点を追加、頂点を右クリックすると削除。頂点は 3 つ以上。 -tools/interactive_editor/polygon_too_small = 多角形「{0}」の頂点が 3 つ未満です。 -tools/interactive_editor/highlight = ハイライト -tools/interactive_editor/label_highlight_color = 色 -tools/interactive_editor/label_highlight_scale = 拡大率 -tools/interactive_editor/actions = アクション -tools/interactive_editor/label_onclick_scenario = シナリオ -tools/interactive_editor/label_onclick_commands = クリック時のコマンド(1 行に 1 つ) -tools/interactive_editor/label_hover_enter = カーソルが入ったとき -tools/interactive_editor/label_hover_leave = カーソルが出たとき -tools/interactive_editor/id_empty = ID が空のオブジェクトがあります。 -tools/interactive_editor/duplicate_id = オブジェクト ID が重複しています: - -tools/light_editor/title = ライトエディター -tools/light_editor/hint = 背景をダブルクリック——新しいライト · ライトをドラッグ · Del——削除 · Ctrl+S——シナリオに light_set を書き込む -tools/light_editor/scenario_tip = light_set の行を編集するシナリオ(●——すでにライトを設定済み) -tools/light_editor/pick_scenario = — シナリオを選択 — -tools/light_editor/save_tip = シナリオに light_set の行を書き込む(Ctrl+S) -tools/light_editor/copy_line = 行をコピー -tools/light_editor/copy_line_tip = light_set コマンドをクリップボードにコピー -tools/light_editor/copied = light_set の行をコピーしました。 -tools/light_editor/show_line = light_set -tools/light_editor/loaded = {0} のライトを読み込みました。 -tools/light_editor/no_lights_yet = {0} にはまだ light_set がありません——ライトを追加すると、ヘッダーの後に行が入ります。 -tools/light_editor/static_lights = ライト -tools/light_editor/add_light = + ライト -tools/light_editor/delete = 削除 -tools/light_editor/limit = 静的ライトは最大 {0} 個です。 -tools/light_editor/selected_props = 選択したライト -tools/light_editor/none_selected = 一覧かプレビューでライトを選んでください。 -tools/light_editor/x = X(0–1) -tools/light_editor/y = Y(0–1) -tools/light_editor/radius = 半径 -tools/light_editor/radius_tip = 幅 1920 px のゲーム画面でのピクセル半径 -tools/light_editor/influence = 強さ -tools/light_editor/influence_tip = ライトの影響(0–1) -tools/light_editor/color = 色 -tools/light_editor/background = プレビューの背景 - -tools/catalog_export/hint = アーカイブとハッシュは書き出し時に計算され、JSON は MOD のアーカイブの隣に書かれます -tools/catalog_export/mod_info = MOD -tools/catalog_export/id = ID -tools/catalog_export/name = 名前 -tools/catalog_export/metadata = カタログ -tools/catalog_export/locales = ローカライズ -tools/catalog_export/locales_hint = 行がない場合は、マニフェストの英語の名前と説明が使われます。 -tools/catalog_export/remove_locale = 言語を削除 -tools/catalog_export/preview = できあがる JSON -tools/catalog_export/copied = JSON をクリップボードにコピーしました。 -tools/catalog_export/export_tip = アーカイブを作り、ハッシュを計算し、カタログ項目の JSON を書き出す -tools/catalog_export/exported = 書き出しました:{0} -tools/catalog_export/export_error = 書き出しエラー -tools/catalog_export/url_required = ダウンロード URL を設定してください——それがないカタログ項目は役に立ちません。 - -tools/scene_editor/title = シーンエディター -tools/scene_editor/hint = シーンは場所です:背景、環境音、シナリオ。Ctrl+S で保存 · 一覧の背景をダブルクリックで追加 -tools/scene_editor/save_tip = 選んだ MOD にシーンファイルを保存(Ctrl+S)。ゲームはすぐに変更を反映します -tools/scene_editor/new_scene = + 新しいシーン -tools/scene_editor/new_scene_tip = 新しい場所を作る:名前、MOD、背景。シナリオは後から追加でき、シナリオのないシーンでも構いません -tools/scene_editor/delete = 削除 -tools/scene_editor/delete_tip = 選んだ MOD のシーンファイルを削除 -tools/scene_editor/delete_confirm = 選んだ MOD からこのシーンのファイルを削除しますか? シーンのシナリオは残ります。 -tools/scene_editor/go = ▶ シーンへ移動 -tools/scene_editor/go_tip = ゲームをこのシーンに切り替える -tools/scene_editor/rescan_tip = シーン、背景、音を読み直す(MOD にファイルを追加した後) -tools/scene_editor/scenes = シーン -tools/scene_editor/backgrounds = シーンの背景(入るたびにランダムに 1 つ) -tools/scene_editor/all_backgrounds = すべての背景 -tools/scene_editor/add_background = + シーンに背景を追加 -tools/scene_editor/add_background_tip = 表示中の背景をシーンに追加(または一覧でダブルクリック) -tools/scene_editor/remove_background = シーンから背景を外す -tools/scene_editor/properties = プロパティ -tools/scene_editor/name = 名前 -tools/scene_editor/name_tip = シーンの id:ラテン文字、数字、_。switch_scene とシナリオのヘッダーが参照します。作成時にだけ設定 -tools/scene_editor/mod = 保存先 -tools/scene_editor/mod_tip = シーンファイルを書き込む MOD(フォルダー形式の MOD のみ) -tools/scene_editor/ambient = 音 -tools/scene_editor/ambient_tip = シーンで流れる環境音 -tools/scene_editor/listen = ▶ -tools/scene_editor/listen_tip = 環境音を聴く -tools/scene_editor/stop = ■ -tools/scene_editor/default_scenario = 既定のシナリオ -tools/scene_editor/default_scenario_tip = シーンの他のシナリオがなくなったときに流れる -tools/scene_editor/scenarios = シーンのシナリオ -tools/scene_editor/open_scenario = 開く -tools/scene_editor/open_scenario_tip = 選択したシナリオをシナリオエディターで開く(またはダブルクリック) -tools/scene_editor/new_scenario = + 新しいシナリオ -tools/scene_editor/new_scenario_tip = 選んだ MOD にこのシーンのシナリオを作って開く -tools/scene_editor/none = — なし — -tools/scene_editor/effective = ゲーム内:{0}(MOD {1} から) -tools/scene_editor/adds_to = シーンの定義元:{0}。保存すると、選んだ MOD にそれを補うファイルが作られます。 -tools/scene_editor/also_in = ほかに補っている MOD:{0} -tools/scene_editor/no_writable_mod = シーンを保存できるフォルダー形式の MOD がありません。 -tools/scene_editor/bad_name = シーンの名前を設定してください:ラテン文字、数字、_(先頭は文字)。 -tools/scene_editor/name_taken = シーン「{0}」はすでにあります。一覧から選んでください。 -tools/scene_editor/need_background = シーンに背景を 1 つ以上追加してください。 -tools/scene_editor/created = シーン「{0}」を作成しました。 -tools/scene_editor/deleted = 削除しました:{0} -tools/scene_editor/opened = シーン:{0} -tools/scene_editor/new_started = 新しいシーン:名前と MOD を設定し、背景を追加してください。 -tools/scene_editor/already_there = 背景 {0} はすでにシーンにあります。 -tools/scene_editor/go_failed = ゲームはシーン「{0}」を知りません。先に保存してください。 -tools/scene_editor/went = シーン {0} に切り替えました。 -tools/scene_editor/rescanned = シーン、背景、音を読み直しました。 -tools/scene_editor/scenario_created = シナリオ {0} を作成しました。 -tools/scene_editor/no_scenarios = このシーンにはまだシナリオがありません -tools/scene_editor/no_backgrounds = このシーンには背景がありません:下の一覧から選んで追加してください。 -tools/scene_editor/pick_scene = 左でシーンを選ぶか、新しく作ってください。 -tools/scene_editor/preview_hint = 背景を選ぶと表示されます。 -tools/scene_editor/not_in_scene = (シーンにない) -tools/scene_editor/new_line_text = 新しいシーン。この行をあなたの文章に置き換えてください。 -tools/entity_editor/hint = 左は検索付きの一覧、右は選んだ項目のフィールド · Ctrl+N 新規、Ctrl+D コピー · Ctrl+Z/Y 元に戻す/やり直す · Ctrl+S 保存、ゲームはすぐに変更を反映します -tools/entity_editor/section_list = MOD の項目 -tools/entity_editor/search = 名前か id で検索 -tools/entity_editor/mod_tip = 項目を編集する MOD。新しい項目はこの MOD のファイルに入ります -tools/entity_editor/new = 新規 -tools/entity_editor/new_tip = 選んだ MOD に項目を追加(Ctrl+N) -tools/entity_editor/duplicate = コピー -tools/entity_editor/duplicate_tip = 選択した項目を新しい id でコピー(Ctrl+D) -tools/entity_editor/delete = 削除 -tools/entity_editor/delete_tip = 選択した項目を MOD のファイルから取り除く -tools/entity_editor/delete_title = 項目を削除しますか? -tools/entity_editor/delete_text = 保存すると項目は MOD のファイルから消えます。シナリオやマップからの参照は残るので確認してください。 -tools/entity_editor/undo = 元に戻す -tools/entity_editor/undo_tip = 最後の変更を元に戻す(Ctrl+Z) -tools/entity_editor/redo = やり直す -tools/entity_editor/redo_tip = 元に戻した操作をやり直す(Ctrl+Y) -tools/entity_editor/no_entity = 左で項目を選ぶか、「新規」で作ってください。 -tools/entity_editor/file = MOD {0} · ファイル {1} -tools/entity_editor/id = Id -tools/entity_editor/id_tip = 一意の id:シナリオ、マップ、他の MOD はこれで項目を見つけます。所有者に依存して同じ id を持つ MOD は、その項目を置き換えます -tools/entity_editor/locale_key_tip = ローカライズキー。下のテキストは MOD の locale/*.properties に書かれます -tools/entity_editor/text_in = テキスト({0}) -tools/entity_editor/picture_pick_tip = MOD のフォルダーから画像を選ぶ -tools/entity_editor/picture_pick_title = 「{0}」グループの画像 -tools/entity_editor/section_other = その他のフィールド -tools/entity_editor/other_hint = フォームに表示されないフィールド(他の MOD のデータ、戦利品…)を JSON で。JSON が正しくなった時点で編集が適用されます。 -tools/entity_editor/other_invalid = その他のフィールド:不正な JSON——{0} -tools/entity_editor/other_applied = その他のフィールドを適用しました -tools/entity_editor/opened = MOD {1} の項目 {0} 件 -tools/entity_editor/added = 項目 {0} を {1} に追加しました -tools/entity_editor/deleted = 項目 {0} を削除しました(書き込むには保存) -tools/entity_editor/saved = 保存しました:ファイル {0}、テキスト {1}(MOD {2})。ゲームはデータを読み直しました -tools/entity_editor/broken_files = JSON が壊れているファイルは読み込みませんでした:{0} -tools/entity_editor/problem_id = ファイル {0} に id が空の項目があります -tools/entity_editor/problem_duplicate = id {0} が複数回出てきます +mod_editor_tools_desc = シーン、シナリオ、マップ、操作対象、照明、Godotシーン、カタログ出力の互換ツールセット。 diff --git a/locale/ui_ko.properties b/locale/ui_ko.properties index a445eb8..c3a278f 100644 --- a/locale/ui_ko.properties +++ b/locale/ui_ko.properties @@ -1,337 +1,2 @@ mod_editor_tools_name = 에디터 도구 -mod_editor_tools_desc = 실행 중인 게임 위에 열리는 창작용 창: 장면, 시나리오, 맵, 상호작용, 조명 에디터와 Godot 장면 빌더. 저장하는 것은 모두 일반 모드 파일이며 게임이 즉시 적용합니다. - -tools/common/save = 저장 -tools/common/save_tip = 파일 저장 (Ctrl+S) -tools/common/saved = 저장됨: {0} -tools/common/loaded = 열림: {0} -tools/common/load_error = 불러오기 오류 -tools/common/readonly = 저장할 수 없습니다: 모드가 읽기 전용(zip)이거나 경로가 잘못되었습니다. -tools/common/no_writable_mods = 쓸 수 있는 모드가 없습니다(폴더형 모드만 편집할 수 있습니다). -tools/common/no_files = 모드 "{0}"에는 아직 이런 파일이 없습니다 — "+"로 만드세요. -tools/common/no_file_to_save = 먼저 파일을 고르거나 만드세요(파일 목록 옆의 "+"). -tools/common/new_file = 새 파일 -tools/common/new_file_hint = 처음 저장할 때 {0}/{1}/에 확장자 {2}인 파일이 만들어집니다. -tools/common/new_file_status = 새 파일 {0} — 저장할 때 만들어집니다. -tools/common/mod_tip = 모드(이런 파일이 이미 있는 모드가 먼저; · — 아직 없음) -tools/common/file_tip = 모드 안의 파일 -tools/common/search = 검색… -tools/common/json = JSON -tools/common/json_tip = 결과 파일의 JSON 보기 -tools/common/background_missing = 장면 "{1}"의 배경 "{0}"을(를) 제공하는 모드가 없습니다 — 직접 고르세요. - -tools/map_editor/title = 맵 에디터 -tools/map_editor/hint = 좌클릭 — 선택 / 드래그 · 우클릭 — 꼭짓점 삭제 · 더블클릭, Enter 또는 Esc — 영역 완성 · Del — 구역 삭제 · Ctrl+S — 저장 -tools/map_editor/reload_map = 게임 내 지도 새로 고침 -tools/map_editor/reload_map_tip = 실행 중인 게임에서 저장된 지도 파일을 다시 읽습니다 -tools/map_editor/reloaded = 게임 내 지도를 새로 고쳤습니다. -tools/map_editor/select_mode = 선택 -tools/map_editor/draw_region = 그리기 -tools/map_editor/draw_started = 지도를 클릭해 꼭짓점을 놓으세요. 더블클릭, Enter 또는 Esc로 완성, 우클릭으로 마지막 꼭짓점을 지웁니다. -tools/map_editor/background = 미리 보기 배경 -tools/map_editor/zones = 구역 -tools/map_editor/add_region = + 영역 -tools/map_editor/add_region_tip = 새 영역: 지도를 클릭해 다각형을 그립니다 -tools/map_editor/add_marker = + 표식 -tools/map_editor/add_label = + 라벨 -tools/map_editor/delete = 구역 삭제 -tools/map_editor/delete_tip = 선택한 구역 삭제 (Del) -tools/map_editor/delete_confirm = 선택한 구역을 삭제할까요? 변경은 저장할 때 파일에 반영됩니다. -tools/map_editor/deleted = 구역 "{0}"을(를) 삭제했습니다. -tools/map_editor/props = 구역 속성 -tools/map_editor/zone_missing = 목록이나 지도에서 구역을 고르세요. -tools/map_editor/id = ID -tools/map_editor/id_empty = 구역 ID는 비워 둘 수 없습니다. -tools/map_editor/duplicate_id = 이미 쓰인 구역 ID: -tools/map_editor/title_key = 제목 -tools/map_editor/description = 설명 -tools/map_editor/no_translation = 로컬라이즈에 키가 없음 -tools/map_editor/scene = 장면 -tools/map_editor/scene_none = (없음) -tools/map_editor/availability = 이용 여부 -tools/map_editor/locked = 이동 불가 -tools/map_editor/hidden = 지도에서 숨김 -tools/map_editor/effective = 게임에서: -tools/map_editor/effective_open = 열림 -tools/map_editor/effective_locked = 잠김 (Locked 플래그) -tools/map_editor/effective_no_scene = 잠김 (장면 없음) -tools/map_editor/effective_unknown_scene = 잠김 (장면을 찾을 수 없음) -tools/map_editor/effective_hidden = 지도에서 숨김 -tools/map_editor/marker = 표식 -tools/map_editor/image = 아이콘 -tools/map_editor/image_placeholder = 텍스처 ID -tools/map_editor/icon_search = 아이콘 찾기… -tools/map_editor/label_section = 라벨 -tools/map_editor/label = 지도 위의 텍스트 라벨 -tools/map_editor/x = X (0–1) -tools/map_editor/y = Y (0–1) -tools/map_editor/vertices = 다각형 꼭짓점 -tools/map_editor/vertices_hint = 지도에서 꼭짓점을 끌어 옮기세요. 꼭짓점을 우클릭하면 삭제됩니다. -tools/map_editor/add_vertex = + 꼭짓점 -tools/map_editor/delete_vertex = 꼭짓점 삭제 -tools/map_editor/preview = ▶ 게임처럼 보기 -tools/map_editor/preview_tip = 게임에서처럼 지도 보기: 호버, 구역 카드, 표식(저장하지 않은 편집 포함) -tools/map_editor/preview_on = 미리 보기: Esc 또는 ✕로 에디터로 돌아갑니다. -tools/map_editor/preview_off = 편집 모드. -tools/map_editor/preview_missing = 지도 장면(map_widget.tscn)을 불러올 수 없습니다. - -tools/scenario_editor/title = 시나리오 에디터 -tools/scenario_editor/hint = Ctrl+S — 저장 · F5 — 시나리오 플레이 · 그래프의 장면 클릭 — 그 장면의 시나리오 -tools/scenario_editor/play = ▶ 플레이 -tools/scenario_editor/play_tooltip = 저장하고 게임에서 시나리오 플레이 (F5) -tools/scenario_editor/play_stopped = 테스트 플레이를 멈췄습니다. -tools/scenario_editor/stop = ■ 정지 -tools/scenario_editor/open_file = 외부 에디터로 열기 -tools/scenario_editor/open_file_tip = 시스템 기본 프로그램으로 파일 열기 -tools/scenario_editor/open_file_error = 파일을 열 수 없습니다 -tools/scenario_editor/rescan = 다시 검색 -tools/scenario_editor/rescan_tip = 시나리오 목록과 그래프를 다시 읽습니다(파일을 추가한 뒤) -tools/scenario_editor/rescanned = 시나리오 목록을 새로 고쳤습니다. -tools/scenario_editor/search_files = 파일 찾기… -tools/scenario_editor/tab_files = 파일 -tools/scenario_editor/tab_info = 요약 -tools/scenario_editor/tab_graph = 장면 그래프 -tools/scenario_editor/graph_select_node = 그래프에서 장면을 고르면 그 시나리오와 전환이 보입니다. -tools/scenario_editor/graph_node = 모드 {0} · 시나리오 {1} -tools/scenario_editor/graph_node_bg = 배경: {0} -tools/scenario_editor/graph_detail_mod = 모드: {0} -tools/scenario_editor/graph_detail_scenarios = 시나리오 ({0}), 우선순위순: -tools/scenario_editor/info_scene = 장면 -tools/scenario_editor/info_priority = 우선순위 -tools/scenario_editor/info_condition = 조건 -tools/scenario_editor/info_switch_targets = 장면 전환 -tools/scenario_editor/info_locale_keys = 로컬라이즈 키 -tools/scenario_editor/info_quest_refs = 퀘스트 -tools/scenario_editor/bg_previews = 시나리오 배경 -tools/scenario_editor/bg_missing = 배경을 찾을 수 없음: {0} -tools/scenario_editor/no_backgrounds = 이 시나리오는 배경을 바꾸지 않습니다 -tools/scenario_editor/save_error = 저장 오류: {0} -tools/scenario_editor/readonly_zip = zip 모드의 파일은 읽기 전용입니다 -tools/scenario_editor/no_scene_error = 시나리오 헤더에 장면(scene:)이 없습니다 — 플레이할 것이 없습니다. -tools/scenario_editor/unsaved_title = 저장하지 않은 변경 -tools/scenario_editor/unsaved_switch_text = 이 탭에 저장하지 않은 변경이 있습니다. 저장하지 않고 닫을까요? - -tools/scene_builder/title = 장면 빌더 -tools/scene_builder/hint = 노드는 본체나 핸들로 끌어 옮기기 · 클릭 — 커서 아래 노드 선택 · Del — 노드 삭제 · Ctrl+S — 저장하고 다시 빌드 -tools/scene_builder/base_scene = 모드 장면을 겹쳐 보여 줄 기본 장면 -tools/scene_builder/mod = 모드 -tools/scene_builder/scene = 모드 장면 (.tscn) -tools/scene_builder/empty_scene = (빈 기본 장면) -tools/scene_builder/select_mod = — 모드 선택 — -tools/scene_builder/select_scene = — 장면 선택 — -tools/scene_builder/save_tip = .tscn을 저장하고 미리 보기를 다시 빌드 (Ctrl+S) -tools/scene_builder/reload = 디스크에서 다시 읽기 -tools/scene_builder/reload_tip = 저장하지 않은 편집을 버리고 장면 파일을 다시 읽습니다 -tools/scene_builder/reloaded = 디스크에서 장면을 다시 읽었습니다. -tools/scene_builder/saved_ok = 저장했습니다. -tools/scene_builder/saved_rebuilt = 저장했고, 미리 보기를 다시 빌드했습니다. -tools/scene_builder/autosaved = 자동 저장했습니다. -tools/scene_builder/error_save = 저장 오류: 모드가 읽기 전용입니다. -tools/scene_builder/error_no_scene = 먼저 모드 장면을 고르세요. -tools/scene_builder/hierarchy = 계층 -tools/scene_builder/hierarchy_hint = 금색 — 모드 장면의 루트, 밝은색 — 편집 가능한 노드, 흐린색 — 기본 장면(읽기 전용). -tools/scene_builder/node_type_tip = 새 노드의 유형 -tools/scene_builder/add_node_tip = 선택한 노드(또는 장면 루트) 안에 고른 유형의 노드를 추가합니다 -tools/scene_builder/delete_node_tip = 선택한 모드 장면 노드 삭제 -tools/scene_builder/delete_node_title = 노드 삭제 -tools/scene_builder/delete_node_confirm = 선택한 노드를 자식과 함께 삭제할까요? 파일은 자동으로 저장됩니다. -tools/scene_builder/refresh_tip = 노드 트리 새로 고침 -tools/scene_builder/tab_inspector = 인스펙터 -tools/scene_builder/tab_tscn = .tscn 소스 -tools/scene_builder/tscn_hint = 인스펙터와 핸들로 한 편집은 저절로 저장됩니다. 텍스트를 직접 고친 내용은 Ctrl+S로 저장하며, 그때 미리 보기도 다시 빌드됩니다. -tools/scene_builder/inspector_cat_container = 컨테이너 안 -tools/scene_builder/inspector_select = 속성을 편집하려면 모드 장면의 노드를 고르세요 -tools/scene_builder/inspector_readonly = 읽기 전용: 기본 장면에 속한 노드입니다 -tools/scene_builder/inspector_missing_section = tscn에서 노드 섹션을 찾을 수 없습니다(인스턴스화한 하위 장면의 노드는 아직 편집할 수 없습니다) -tools/scene_builder/inspector_preset = 앵커 프리셋 -tools/scene_builder/inspector_cat_layout = 레이아웃 -tools/scene_builder/inspector_cat_rect = 사각형 -tools/scene_builder/inspector_cat_visibility = 보이기 -tools/scene_builder/inspector_cat_misc = 기타 -tools/scene_builder/inspector_bad_value = 이 값은 tscn에 쓸 수 없습니다. -tools/scene_builder/inspector_in_file_tooltip = tscn 파일에 저장됨 -tools/scene_builder/inspector_container_banner = 이 노드는 컨테이너가 관리합니다: 앵커와 오프셋은 부모가 다시 계산하므로 여기서는 효과가 없습니다. -tools/scene_builder/err_add_failed = 오류: 이 유형의 노드를 만들 수 없습니다. -tools/scene_builder/err_add_parent_missing = 오류: tscn에서 부모 섹션을 찾을 수 없습니다(인스턴스화한 하위 장면의 부모는 지원하지 않습니다). -tools/scene_builder/err_del_root = 오류: 장면 루트는 삭제할 수 없습니다. -tools/scene_builder/err_del_none = 오류: 삭제할 모드 노드를 고르세요. - -tools/interactive_editor/title = 상호작용 에디터 -tools/interactive_editor/hint = 충돌체 클릭 — 선택 · 핸들 드래그 · 다각형 모드에서는 배경 클릭으로 꼭짓점 추가, 꼭짓점 우클릭으로 삭제 · Del — 오브젝트 삭제 · Ctrl+S — 저장 -tools/interactive_editor/items = 파일 안의 오브젝트 -tools/interactive_editor/add = + 새로 만들기 -tools/interactive_editor/duplicate = 복제 -tools/interactive_editor/delete = 오브젝트 삭제 -tools/interactive_editor/delete_confirm = 선택한 상호작용을 파일에서 지울까요? 변경은 저장할 때 파일에 반영됩니다. -tools/interactive_editor/deleted = 오브젝트 "{0}"을(를) 지웠습니다. -tools/interactive_editor/background = 미리 보기 배경 -tools/interactive_editor/props = 속성 -tools/interactive_editor/none_selected = 목록이나 미리 보기에서 오브젝트를 고르세요. -tools/interactive_editor/label_id = ID -tools/interactive_editor/label_texture_key = 텍스처 -tools/interactive_editor/texture_placeholder = 스프라이트 없음 -tools/interactive_editor/texture_tip = 스프라이트 텍스처 ID. 비우면 보이지 않는 클릭 영역 -tools/interactive_editor/label_layer = 레이어 -tools/interactive_editor/label_z = Z 순서 -tools/interactive_editor/label_cursor = 커서 -tools/interactive_editor/cursor_none = (기본) -tools/interactive_editor/enabled = 켜짐 -tools/interactive_editor/visible = 보임 -tools/interactive_editor/collider = 충돌체 -tools/interactive_editor/label_collider_type = 모양 -tools/interactive_editor/hit_rect = 사각형 -tools/interactive_editor/hit_circle = 원 -tools/interactive_editor/hit_polygon = 다각형 -tools/interactive_editor/label_x = X -tools/interactive_editor/label_y = Y -tools/interactive_editor/label_w = 너비 -tools/interactive_editor/label_h = 높이 -tools/interactive_editor/label_cx = 중심 X -tools/interactive_editor/label_cy = 중심 Y -tools/interactive_editor/label_r = 반지름 -tools/interactive_editor/polygon_hint = 배경을 클릭하면 꼭짓점이 추가되고, 꼭짓점을 우클릭하면 삭제됩니다. 꼭짓점은 3개 이상. -tools/interactive_editor/polygon_too_small = 다각형 "{0}"의 꼭짓점이 3개보다 적습니다. -tools/interactive_editor/highlight = 강조 -tools/interactive_editor/label_highlight_color = 색 -tools/interactive_editor/label_highlight_scale = 크기 -tools/interactive_editor/actions = 액션 -tools/interactive_editor/label_onclick_scenario = 시나리오 -tools/interactive_editor/label_onclick_commands = 클릭할 때의 명령(한 줄에 하나) -tools/interactive_editor/label_hover_enter = 마우스가 들어올 때 -tools/interactive_editor/label_hover_leave = 마우스가 나갈 때 -tools/interactive_editor/id_empty = ID가 비어 있는 오브젝트가 있습니다. -tools/interactive_editor/duplicate_id = 중복된 오브젝트 ID: - -tools/light_editor/title = 조명 에디터 -tools/light_editor/hint = 배경 더블클릭 — 새 조명 · 조명 드래그 · Del — 삭제 · Ctrl+S — 시나리오에 light_set 쓰기 -tools/light_editor/scenario_tip = light_set 줄을 편집할 시나리오 (● — 이미 조명을 설정함) -tools/light_editor/pick_scenario = — 시나리오 선택 — -tools/light_editor/save_tip = 시나리오에 light_set 줄 쓰기 (Ctrl+S) -tools/light_editor/copy_line = 줄 복사 -tools/light_editor/copy_line_tip = light_set 명령을 클립보드에 복사 -tools/light_editor/copied = light_set 줄을 복사했습니다. -tools/light_editor/show_line = light_set -tools/light_editor/loaded = {0}의 조명을 불러왔습니다. -tools/light_editor/no_lights_yet = {0}에는 아직 light_set이 없습니다 — 조명을 추가하면 헤더 뒤에 줄이 들어갑니다. -tools/light_editor/static_lights = 조명 -tools/light_editor/add_light = + 조명 -tools/light_editor/delete = 삭제 -tools/light_editor/limit = 정적 조명은 최대 {0}개입니다. -tools/light_editor/selected_props = 선택한 조명 -tools/light_editor/none_selected = 목록이나 미리 보기에서 조명을 고르세요. -tools/light_editor/x = X (0–1) -tools/light_editor/y = Y (0–1) -tools/light_editor/radius = 반지름 -tools/light_editor/radius_tip = 너비 1920 px 게임 화면 기준 픽셀 반지름 -tools/light_editor/influence = 세기 -tools/light_editor/influence_tip = 조명 영향 (0–1) -tools/light_editor/color = 색 -tools/light_editor/background = 미리 보기 배경 - -tools/catalog_export/hint = 압축 파일과 해시는 내보낼 때 계산되며, JSON은 모드 압축 파일 옆에 쓰입니다 -tools/catalog_export/mod_info = 모드 -tools/catalog_export/id = ID -tools/catalog_export/name = 이름 -tools/catalog_export/metadata = 카탈로그 -tools/catalog_export/locales = 로컬라이즈 -tools/catalog_export/locales_hint = 줄이 없으면 매니페스트의 영어 이름과 설명이 쓰입니다. -tools/catalog_export/remove_locale = 언어 제거 -tools/catalog_export/preview = 결과 JSON -tools/catalog_export/copied = JSON을 클립보드에 복사했습니다. -tools/catalog_export/export_tip = 압축 파일을 만들고 해시를 계산해 카탈로그 항목 JSON을 씁니다 -tools/catalog_export/exported = 내보냄: {0} -tools/catalog_export/export_error = 내보내기 오류 -tools/catalog_export/url_required = 다운로드 URL을 설정하세요 — 그것 없는 카탈로그 항목은 쓸모가 없습니다. - -tools/scene_editor/title = 장면 에디터 -tools/scene_editor/hint = 장면은 장소입니다: 배경, 환경음, 시나리오. Ctrl+S로 저장 · 목록의 배경을 더블클릭하면 추가 -tools/scene_editor/save_tip = 선택한 모드에 장면 파일 저장 (Ctrl+S). 게임에 바로 반영됩니다 -tools/scene_editor/new_scene = + 새 장면 -tools/scene_editor/new_scene_tip = 새 장소 만들기: 이름, 모드, 배경. 시나리오는 나중에 추가할 수 있고, 시나리오가 없는 장면도 괜찮습니다 -tools/scene_editor/delete = 삭제 -tools/scene_editor/delete_tip = 선택한 모드의 장면 파일 삭제 -tools/scene_editor/delete_confirm = 선택한 모드에서 이 장면의 파일을 삭제할까요? 장면의 시나리오는 남습니다. -tools/scene_editor/go = ▶ 장면으로 이동 -tools/scene_editor/go_tip = 게임을 이 장면으로 전환 -tools/scene_editor/rescan_tip = 장면, 배경, 소리를 다시 읽습니다(모드에 파일을 추가한 뒤) -tools/scene_editor/scenes = 장면 -tools/scene_editor/backgrounds = 장면 배경 (들어갈 때 하나가 무작위로 선택됨) -tools/scene_editor/all_backgrounds = 모든 배경 -tools/scene_editor/add_background = + 장면에 배경 추가 -tools/scene_editor/add_background_tip = 보이는 배경을 장면에 추가(또는 목록에서 더블클릭) -tools/scene_editor/remove_background = 장면에서 배경 빼기 -tools/scene_editor/properties = 속성 -tools/scene_editor/name = 이름 -tools/scene_editor/name_tip = 장면 id: 라틴 문자, 숫자, _. switch_scene과 시나리오 헤더가 이를 참조합니다. 만들 때만 정합니다 -tools/scene_editor/mod = 저장할 곳 -tools/scene_editor/mod_tip = 장면 파일을 쓸 모드(폴더형 모드만) -tools/scene_editor/ambient = 소리 -tools/scene_editor/ambient_tip = 장면에서 재생되는 환경음 -tools/scene_editor/listen = ▶ -tools/scene_editor/listen_tip = 환경음 듣기 -tools/scene_editor/stop = ■ -tools/scene_editor/default_scenario = 기본 시나리오 -tools/scene_editor/default_scenario_tip = 장면에 다른 시나리오가 남지 않았을 때 재생됩니다 -tools/scene_editor/scenarios = 장면의 시나리오 -tools/scene_editor/open_scenario = 열기 -tools/scene_editor/open_scenario_tip = 선택한 시나리오를 시나리오 에디터에서 열기(또는 더블클릭) -tools/scene_editor/new_scenario = + 새 시나리오 -tools/scene_editor/new_scenario_tip = 선택한 모드에 이 장면의 시나리오를 만들고 엽니다 -tools/scene_editor/none = — 없음 — -tools/scene_editor/effective = 게임에서: {0} (모드 {1}) -tools/scene_editor/adds_to = 장면이 정의된 곳: {0}. 저장하면 선택한 모드에 이를 보충하는 파일이 만들어집니다. -tools/scene_editor/also_in = 이 장면을 보충하는 모드: {0} -tools/scene_editor/no_writable_mod = 장면을 저장할 폴더형 모드가 없습니다. -tools/scene_editor/bad_name = 장면 이름을 정하세요: 라틴 문자, 숫자, _ (문자로 시작). -tools/scene_editor/name_taken = 장면 "{0}"이(가) 이미 있습니다. 목록에서 고르세요. -tools/scene_editor/need_background = 장면에 배경을 하나 이상 추가하세요. -tools/scene_editor/created = 장면 "{0}"을(를) 만들었습니다. -tools/scene_editor/deleted = 삭제함: {0} -tools/scene_editor/opened = 장면: {0} -tools/scene_editor/new_started = 새 장면: 이름과 모드를 정하고 배경을 추가하세요. -tools/scene_editor/already_there = 배경 {0}은(는) 이미 장면에 있습니다. -tools/scene_editor/go_failed = 게임이 장면 "{0}"을(를) 모릅니다. 먼저 저장하세요. -tools/scene_editor/went = 장면 {0}(으)로 전환했습니다. -tools/scene_editor/rescanned = 장면, 배경, 소리를 다시 읽었습니다. -tools/scene_editor/scenario_created = 시나리오 {0}을(를) 만들었습니다. -tools/scene_editor/no_scenarios = 이 장면에는 아직 시나리오가 없습니다 -tools/scene_editor/no_backgrounds = 이 장면에는 배경이 없습니다: 아래 목록에서 하나 골라 추가하세요. -tools/scene_editor/pick_scene = 왼쪽에서 장면을 고르거나 새로 만드세요. -tools/scene_editor/preview_hint = 배경을 고르면 보입니다. -tools/scene_editor/not_in_scene = (장면에 없음) -tools/scene_editor/new_line_text = 새 장면입니다. 이 줄을 당신의 글로 바꾸세요. -tools/entity_editor/hint = 왼쪽은 검색이 있는 목록, 오른쪽은 선택한 항목의 필드 · Ctrl+N 새로 만들기, Ctrl+D 복사 · Ctrl+Z/Y 되돌리기/다시 실행 · Ctrl+S 저장, 게임에 바로 반영 -tools/entity_editor/section_list = 모드의 항목 -tools/entity_editor/search = 이름 또는 id로 검색 -tools/entity_editor/mod_tip = 항목을 편집할 모드. 새 항목은 이 모드의 파일에 들어갑니다 -tools/entity_editor/new = 새로 만들기 -tools/entity_editor/new_tip = 선택한 모드에 항목 추가 (Ctrl+N) -tools/entity_editor/duplicate = 복사 -tools/entity_editor/duplicate_tip = 선택한 항목을 새 id로 복사 (Ctrl+D) -tools/entity_editor/delete = 삭제 -tools/entity_editor/delete_tip = 선택한 항목을 모드 파일에서 제거 -tools/entity_editor/delete_title = 항목을 삭제할까요? -tools/entity_editor/delete_text = 저장하면 항목이 모드 파일에서 사라집니다. 시나리오와 맵의 참조는 남으니 확인하세요. -tools/entity_editor/undo = 되돌리기 -tools/entity_editor/undo_tip = 마지막 변경 되돌리기 (Ctrl+Z) -tools/entity_editor/redo = 다시 실행 -tools/entity_editor/redo_tip = 되돌린 것을 다시 실행 (Ctrl+Y) -tools/entity_editor/no_entity = 왼쪽에서 항목을 고르거나 "새로 만들기"로 만드세요. -tools/entity_editor/file = 모드 {0} · 파일 {1} -tools/entity_editor/id = Id -tools/entity_editor/id_tip = 고유 id: 시나리오, 맵, 다른 모드가 이것으로 항목을 찾습니다. 소유자에 의존하고 같은 id를 가진 모드가 그 항목을 대체합니다 -tools/entity_editor/locale_key_tip = 로컬라이즈 키. 아래 텍스트는 모드의 locale/*.properties에 쓰입니다 -tools/entity_editor/text_in = 텍스트 ({0}) -tools/entity_editor/picture_pick_tip = 모드 폴더에서 그림 고르기 -tools/entity_editor/picture_pick_title = "{0}" 그룹의 그림 -tools/entity_editor/section_other = 기타 필드 -tools/entity_editor/other_hint = 폼에 보이지 않는 필드(다른 모드의 데이터, 전리품…)를 JSON으로. JSON이 올바르면 바로 적용됩니다. -tools/entity_editor/other_invalid = 기타 필드: 잘못된 JSON — {0} -tools/entity_editor/other_applied = 기타 필드를 적용했습니다 -tools/entity_editor/opened = 모드 {1}의 항목 {0}개 -tools/entity_editor/added = 항목 {0}을(를) {1}에 추가했습니다 -tools/entity_editor/deleted = 항목 {0}을(를) 삭제했습니다(기록하려면 저장) -tools/entity_editor/saved = 저장함: 파일 {0}개, 텍스트 {1}개 (모드 {2}). 게임이 데이터를 다시 읽었습니다 -tools/entity_editor/broken_files = JSON이 깨진 파일은 읽지 않았습니다: {0} -tools/entity_editor/problem_id = 파일 {0}에 id가 비어 있는 항목이 있습니다 -tools/entity_editor/problem_duplicate = id {0}이(가) 두 번 이상 나옵니다 +mod_editor_tools_desc = 장면, 시나리오, 지도, 상호작용, 조명, Godot 장면 및 카탈로그 내보내기 도구의 호환 모음. diff --git a/locale/ui_ru.properties b/locale/ui_ru.properties index 1d43801..17f45eb 100644 --- a/locale/ui_ru.properties +++ b/locale/ui_ru.properties @@ -1,337 +1,2 @@ mod_editor_tools_name = Редакторы -mod_editor_tools_desc = Авторские окна поверх запущенной игры: редакторы сцен, сценариев, карты, интерактива и света, а также конструктор сцен Godot. Всё, что они сохраняют, — обычные файлы модов, которые игра подхватывает сразу. - -tools/common/save = Сохранить -tools/common/save_tip = Сохранить файл (Ctrl+S) -tools/common/saved = Сохранено: {0} -tools/common/loaded = Открыт: {0} -tools/common/load_error = Ошибка загрузки -tools/common/readonly = Не удалось сохранить: мод доступен только для чтения (zip) или путь неверен. -tools/common/no_writable_mods = Нет модов, доступных для записи (только папочные моды можно редактировать). -tools/common/no_files = В моде «{0}» таких файлов пока нет — создайте новый кнопкой «+». -tools/common/no_file_to_save = Сначала выберите или создайте файл («+» рядом со списком файлов). -tools/common/new_file = Новый файл -tools/common/new_file_hint = Файл будет создан в {0}/{1}/ с расширением {2} при первом сохранении. -tools/common/new_file_status = Новый файл {0} — будет создан при сохранении. -tools/common/mod_tip = Мод (сначала — моды, где такие файлы уже есть; · — пока нет) -tools/common/file_tip = Файл внутри мода -tools/common/search = Поиск… -tools/common/json = JSON -tools/common/json_tip = Показать итоговый JSON файла -tools/common/background_missing = Фон «{0}» сцены «{1}» не найден ни в одном моде — выберите фон вручную. - -tools/map_editor/title = Редактор карты -tools/map_editor/hint = ЛКМ — выбрать / тянуть · ПКМ — удалить вершину · двойной клик, Enter или Esc — закончить регион · Del — удалить зону · Ctrl+S — сохранить -tools/map_editor/reload_map = Обновить карту в игре -tools/map_editor/reload_map_tip = Перечитать сохранённые файлы карты в запущенной игре -tools/map_editor/reloaded = Карта в игре обновлена. -tools/map_editor/select_mode = Выбор -tools/map_editor/draw_region = Рисование -tools/map_editor/draw_started = Кликайте по карте, чтобы ставить вершины. Двойной клик, Enter или Esc — закончить, ПКМ — убрать последнюю. -tools/map_editor/background = Фон превью -tools/map_editor/zones = Зоны -tools/map_editor/add_region = + Регион -tools/map_editor/add_region_tip = Новый регион: нарисуйте полигон кликами по карте -tools/map_editor/add_marker = + Метка -tools/map_editor/add_label = + Надпись -tools/map_editor/delete = Удалить зону -tools/map_editor/delete_tip = Удалить выбранную зону (Del) -tools/map_editor/delete_confirm = Удалить выбранную зону? Изменение попадёт в файл при сохранении. -tools/map_editor/deleted = Зона «{0}» удалена. -tools/map_editor/props = Свойства зоны -tools/map_editor/zone_missing = Выберите зону в списке или на карте. -tools/map_editor/id = ID -tools/map_editor/id_empty = ID зоны не может быть пустым. -tools/map_editor/duplicate_id = ID зоны уже занят: -tools/map_editor/title_key = Название -tools/map_editor/description = Описание -tools/map_editor/no_translation = ключ не найден в локализации -tools/map_editor/scene = Сцена -tools/map_editor/scene_none = (нет) -tools/map_editor/availability = Доступность -tools/map_editor/locked = Закрыта для посещения -tools/map_editor/hidden = Скрыта с карты -tools/map_editor/effective = В игре: -tools/map_editor/effective_open = доступна -tools/map_editor/effective_locked = закрыта (флаг «Закрыта») -tools/map_editor/effective_no_scene = закрыта (сцена не задана) -tools/map_editor/effective_unknown_scene = закрыта (сцена не найдена) -tools/map_editor/effective_hidden = скрыта с карты -tools/map_editor/marker = Метка -tools/map_editor/image = Иконка -tools/map_editor/image_placeholder = ID текстуры -tools/map_editor/icon_search = Поиск иконки… -tools/map_editor/label_section = Надпись -tools/map_editor/label = Текстовая надпись на карте -tools/map_editor/x = X (0–1) -tools/map_editor/y = Y (0–1) -tools/map_editor/vertices = Вершины полигона -tools/map_editor/vertices_hint = Тяните вершины на карте; ПКМ по вершине — удалить. -tools/map_editor/add_vertex = + Вершина -tools/map_editor/delete_vertex = Удалить вершину -tools/map_editor/preview = ▶ Как в игре -tools/map_editor/preview_tip = Посмотреть карту как в игре: наведение, карточка зоны, метки (несохранённые правки тоже) -tools/map_editor/preview_on = Просмотр: Esc или ✕ — вернуться к редактору. -tools/map_editor/preview_off = Режим редактирования. -tools/map_editor/preview_missing = Не удалось загрузить сцену карты (map_widget.tscn). - -tools/scenario_editor/title = Редактор сценариев -tools/scenario_editor/hint = Ctrl+S — сохранить · F5 — запустить сценарий · клик по сцене в графе — список её сценариев -tools/scenario_editor/play = ▶ Запустить -tools/scenario_editor/play_tooltip = Сохранить и запустить сценарий в игре (F5) -tools/scenario_editor/play_stopped = Тестовый запуск остановлен. -tools/scenario_editor/stop = ■ Стоп -tools/scenario_editor/open_file = Открыть во внешнем редакторе -tools/scenario_editor/open_file_tip = Открыть файл программой, назначенной в системе -tools/scenario_editor/open_file_error = Не удалось открыть файл -tools/scenario_editor/rescan = Пересканировать -tools/scenario_editor/rescan_tip = Перечитать список сценариев и граф (после добавления файлов) -tools/scenario_editor/rescanned = Список сценариев обновлён. -tools/scenario_editor/search_files = Поиск файла… -tools/scenario_editor/tab_files = Файлы -tools/scenario_editor/tab_info = Сводка -tools/scenario_editor/tab_graph = Граф сцен -tools/scenario_editor/graph_select_node = Выберите сцену в графе, чтобы увидеть её сценарии и переходы. -tools/scenario_editor/graph_node = мод {0} · сценариев {1} -tools/scenario_editor/graph_node_bg = фон: {0} -tools/scenario_editor/graph_detail_mod = мод: {0} -tools/scenario_editor/graph_detail_scenarios = Сценарии ({0}), по приоритету: -tools/scenario_editor/info_scene = Сцена -tools/scenario_editor/info_priority = Приоритет -tools/scenario_editor/info_condition = Условие -tools/scenario_editor/info_switch_targets = Переходы на сцены -tools/scenario_editor/info_locale_keys = Ключи локализации -tools/scenario_editor/info_quest_refs = Квесты -tools/scenario_editor/bg_previews = Фоны сценария -tools/scenario_editor/bg_missing = Фон не найден: {0} -tools/scenario_editor/no_backgrounds = Сценарий не меняет фон -tools/scenario_editor/save_error = Ошибка сохранения: {0} -tools/scenario_editor/readonly_zip = Файл из zip-мода доступен только для чтения -tools/scenario_editor/no_scene_error = В заголовке сценария не указана сцена (scene:) — запускать некуда. -tools/scenario_editor/unsaved_title = Несохранённые изменения -tools/scenario_editor/unsaved_switch_text = В этой вкладке есть несохранённые изменения. Закрыть её без сохранения? - -tools/scene_builder/title = Конструктор сцен -tools/scene_builder/hint = Тяните узел за тело или маркеры · клик — выбрать узел под курсором · Del — удалить узел · Ctrl+S — сохранить и пересобрать -tools/scene_builder/base_scene = Базовая сцена, поверх которой показывается мод-сцена -tools/scene_builder/mod = Мод -tools/scene_builder/scene = Сцена мода (.tscn) -tools/scene_builder/empty_scene = (пустая базовая сцена) -tools/scene_builder/select_mod = — выберите мод — -tools/scene_builder/select_scene = — выберите сцену — -tools/scene_builder/save_tip = Сохранить .tscn и пересобрать превью (Ctrl+S) -tools/scene_builder/reload = Перечитать с диска -tools/scene_builder/reload_tip = Отбросить несохранённое и перечитать сцену из файла -tools/scene_builder/reloaded = Сцена перечитана с диска. -tools/scene_builder/saved_ok = Сохранено. -tools/scene_builder/saved_rebuilt = Сохранено, превью пересобрано. -tools/scene_builder/autosaved = Автосохранено. -tools/scene_builder/error_save = Ошибка сохранения: мод доступен только для чтения. -tools/scene_builder/error_no_scene = Сначала выберите сцену мода. -tools/scene_builder/hierarchy = Иерархия -tools/scene_builder/hierarchy_hint = Золотой — корень мод-сцены, светлые — редактируемые узлы, тусклые — базовая сцена (только чтение). -tools/scene_builder/node_type_tip = Тип нового узла -tools/scene_builder/add_node_tip = Добавить узел выбранного типа в выделенный узел (или в корень сцены) -tools/scene_builder/delete_node_tip = Удалить выбранный узел мод-сцены -tools/scene_builder/delete_node_title = Удалить узел -tools/scene_builder/delete_node_confirm = Удалить выбранный узел вместе с дочерними? Файл сохранится автоматически. -tools/scene_builder/refresh_tip = Обновить дерево узлов -tools/scene_builder/tab_inspector = Инспектор -tools/scene_builder/tab_tscn = Исходник .tscn -tools/scene_builder/tscn_hint = Правки инспектора и маркеров сохраняются сами; ручные правки текста — по Ctrl+S, после чего превью пересобирается. -tools/scene_builder/inspector_cat_container = В контейнере -tools/scene_builder/inspector_select = Выберите узел мод-сцены для правки свойств -tools/scene_builder/inspector_readonly = Только чтение: узел принадлежит базовой сцене -tools/scene_builder/inspector_missing_section = Секция узла не найдена в tscn (узлы вложенных сцен пока не редактируются) -tools/scene_builder/inspector_preset = Пресет якорей -tools/scene_builder/inspector_cat_layout = Расположение -tools/scene_builder/inspector_cat_rect = Прямоугольник -tools/scene_builder/inspector_cat_visibility = Видимость -tools/scene_builder/inspector_cat_misc = Прочее -tools/scene_builder/inspector_bad_value = Значение не может быть записано в tscn. -tools/scene_builder/inspector_in_file_tooltip = Записано в tscn-файле -tools/scene_builder/inspector_container_banner = Узлом управляет контейнер: якоря и отступы пересчитываются родителем и здесь не действуют. -tools/scene_builder/err_add_failed = Ошибка: не удалось создать узел этого типа. -tools/scene_builder/err_add_parent_missing = Ошибка: родительская секция не найдена в tscn (родители из вложенных сцен не поддерживаются). -tools/scene_builder/err_del_root = Ошибка: корень сцены удалить нельзя. -tools/scene_builder/err_del_none = Ошибка: выберите узел мод-сцены для удаления. - -tools/interactive_editor/title = Редактор интерактивов -tools/interactive_editor/hint = Клик по коллайдеру — выбрать · тяните маркеры · в режиме полигона клик по фону добавляет вершину, ПКМ по вершине — удаляет · Del — удалить объект · Ctrl+S — сохранить -tools/interactive_editor/items = Объекты файла -tools/interactive_editor/add = + Новый -tools/interactive_editor/duplicate = Дублировать -tools/interactive_editor/delete = Удалить объект -tools/interactive_editor/delete_confirm = Удалить выбранный интерактив из файла? Изменение попадёт в файл при сохранении. -tools/interactive_editor/deleted = Объект «{0}» удалён. -tools/interactive_editor/background = Фон превью -tools/interactive_editor/props = Свойства -tools/interactive_editor/none_selected = Выберите объект в списке или на превью. -tools/interactive_editor/label_id = ID -tools/interactive_editor/label_texture_key = Текстура -tools/interactive_editor/texture_placeholder = без спрайта -tools/interactive_editor/texture_tip = ID текстуры спрайта объекта; пусто — невидимая кликабельная зона -tools/interactive_editor/label_layer = Слой -tools/interactive_editor/label_z = Z-порядок -tools/interactive_editor/label_cursor = Курсор -tools/interactive_editor/cursor_none = (обычный) -tools/interactive_editor/enabled = Активен -tools/interactive_editor/visible = Видим -tools/interactive_editor/collider = Коллайдер -tools/interactive_editor/label_collider_type = Форма -tools/interactive_editor/hit_rect = прямоугольник -tools/interactive_editor/hit_circle = круг -tools/interactive_editor/hit_polygon = полигон -tools/interactive_editor/label_x = X -tools/interactive_editor/label_y = Y -tools/interactive_editor/label_w = Ширина -tools/interactive_editor/label_h = Высота -tools/interactive_editor/label_cx = Центр X -tools/interactive_editor/label_cy = Центр Y -tools/interactive_editor/label_r = Радиус -tools/interactive_editor/polygon_hint = Клик по фону добавляет вершину, ПКМ по вершине удаляет её. Нужно минимум 3 вершины. -tools/interactive_editor/polygon_too_small = У полигона «{0}» меньше 3 вершин. -tools/interactive_editor/highlight = Подсветка -tools/interactive_editor/label_highlight_color = Цвет -tools/interactive_editor/label_highlight_scale = Масштаб -tools/interactive_editor/actions = Действия -tools/interactive_editor/label_onclick_scenario = Сценарий -tools/interactive_editor/label_onclick_commands = Команды по клику (по одной в строке) -tools/interactive_editor/label_hover_enter = Наведение -tools/interactive_editor/label_hover_leave = Уход -tools/interactive_editor/id_empty = У объекта пустой ID. -tools/interactive_editor/duplicate_id = ID объекта повторяется: - -tools/light_editor/title = Редактор освещения -tools/light_editor/hint = Двойной клик по фону — новый источник · тяните источники · Del — удалить · Ctrl+S — записать light_set в сценарий -tools/light_editor/scenario_tip = Сценарий, чья строка light_set редактируется (● — уже задаёт свет) -tools/light_editor/pick_scenario = — выберите сценарий — -tools/light_editor/save_tip = Записать строку light_set в сценарий (Ctrl+S) -tools/light_editor/copy_line = Копировать строку -tools/light_editor/copy_line_tip = Скопировать команду light_set в буфер обмена -tools/light_editor/copied = Строка light_set скопирована. -tools/light_editor/show_line = light_set -tools/light_editor/loaded = Свет сценария {0} загружен. -tools/light_editor/no_lights_yet = В {0} ещё нет light_set — добавьте источники, строка будет вставлена после заголовка. -tools/light_editor/static_lights = Источники света -tools/light_editor/add_light = + Источник -tools/light_editor/delete = Удалить -tools/light_editor/limit = Не больше {0} статичных источников. -tools/light_editor/selected_props = Выбранный источник -tools/light_editor/none_selected = Выберите источник в списке или на превью. -tools/light_editor/x = X (0–1) -tools/light_editor/y = Y (0–1) -tools/light_editor/radius = Радиус -tools/light_editor/radius_tip = Радиус в пикселях игрового экрана 1920 px -tools/light_editor/influence = Сила -tools/light_editor/influence_tip = Влияние источника (0–1) -tools/light_editor/color = Цвет -tools/light_editor/background = Фон превью - -tools/catalog_export/hint = Архив и хэши считаются при экспорте; JSON кладётся рядом с архивом мода -tools/catalog_export/mod_info = Мод -tools/catalog_export/id = ID -tools/catalog_export/name = Название -tools/catalog_export/metadata = Каталог -tools/catalog_export/locales = Локализации -tools/catalog_export/locales_hint = Без строк будет использовано английское название и описание из манифеста. -tools/catalog_export/remove_locale = Убрать язык -tools/catalog_export/preview = Итоговый JSON -tools/catalog_export/copied = JSON скопирован в буфер обмена. -tools/catalog_export/export_tip = Собрать архив, посчитать хэши и записать JSON записи каталога -tools/catalog_export/exported = Экспортировано: {0} -tools/catalog_export/export_error = Ошибка экспорта -tools/catalog_export/url_required = Укажите URL скачивания — без него запись каталога бесполезна. - -tools/scene_editor/title = Редактор сцен -tools/scene_editor/hint = Сцена — это место: фоны, звук окружения и сценарии. Ctrl+S — сохранить · двойной клик по фону в списке — добавить в сцену -tools/scene_editor/save_tip = Сохранить файл сцены в выбранный мод (Ctrl+S); игра увидит изменения сразу -tools/scene_editor/new_scene = + Новая сцена -tools/scene_editor/new_scene_tip = Создать новое место: имя, мод, фоны. Сценарии можно добавить потом — сцена может обходиться и без них -tools/scene_editor/delete = Удалить -tools/scene_editor/delete_tip = Удалить файл сцены выбранного мода -tools/scene_editor/delete_confirm = Удалить файл этой сцены из выбранного мода? Сценарии сцены останутся на месте. -tools/scene_editor/go = ▶ Перейти в сцену -tools/scene_editor/go_tip = Переключить игру на эту сцену -tools/scene_editor/rescan_tip = Перечитать сцены, фоны и звуки (после добавления файлов в моды) -tools/scene_editor/scenes = Сцены -tools/scene_editor/backgrounds = Фоны сцены (при входе выбирается один случайно) -tools/scene_editor/all_backgrounds = Все фоны -tools/scene_editor/add_background = + Добавить фон в сцену -tools/scene_editor/add_background_tip = Добавить показанный фон в сцену (или двойной клик по нему в списке) -tools/scene_editor/remove_background = Убрать фон из сцены -tools/scene_editor/properties = Свойства -tools/scene_editor/name = Имя -tools/scene_editor/name_tip = Идентификатор сцены: латиница, цифры и _; на него ссылаются switch_scene и заголовки сценариев. Задаётся только при создании -tools/scene_editor/mod = Сохранять в -tools/scene_editor/mod_tip = Мод, в который пишется файл сцены (только моды-папки) -tools/scene_editor/ambient = Звук -tools/scene_editor/ambient_tip = Звук окружения, который играет в сцене -tools/scene_editor/listen = ▶ -tools/scene_editor/listen_tip = Прослушать звук окружения -tools/scene_editor/stop = ■ -tools/scene_editor/default_scenario = Сценарий по умолч. -tools/scene_editor/default_scenario_tip = Играет, когда в сцене не осталось других сценариев -tools/scene_editor/scenarios = Сценарии сцены -tools/scene_editor/open_scenario = Открыть -tools/scene_editor/open_scenario_tip = Открыть выбранный сценарий в редакторе сценариев (или двойной клик) -tools/scene_editor/new_scenario = + Новый сценарий -tools/scene_editor/new_scenario_tip = Создать сценарий этой сцены в выбранном моде и открыть его -tools/scene_editor/none = — нет — -tools/scene_editor/effective = В игре: {0} (из мода {1}) -tools/scene_editor/adds_to = Сцена определена в: {0}. Сохранение создаст в выбранном моде файл, который дополнит её. -tools/scene_editor/also_in = Сцену дополняют также: {0} -tools/scene_editor/no_writable_mod = Нет мода-папки, куда можно сохранить сцену. -tools/scene_editor/bad_name = Задайте имя сцены: латиница, цифры и _ (начинается с буквы). -tools/scene_editor/name_taken = Сцена «{0}» уже есть — выберите её в списке. -tools/scene_editor/need_background = Добавьте в сцену хотя бы один фон. -tools/scene_editor/created = Сцена «{0}» создана. -tools/scene_editor/deleted = Удалено: {0} -tools/scene_editor/opened = Сцена: {0} -tools/scene_editor/new_started = Новая сцена: задайте имя, мод и добавьте фоны. -tools/scene_editor/already_there = Фон {0} уже есть в сцене. -tools/scene_editor/go_failed = Игра не знает сцену «{0}» — сохраните её. -tools/scene_editor/went = Переход в сцену {0}. -tools/scene_editor/rescanned = Сцены, фоны и звуки перечитаны. -tools/scene_editor/scenario_created = Создан сценарий {0}. -tools/scene_editor/no_scenarios = У сцены пока нет сценариев -tools/scene_editor/no_backgrounds = В сцене нет фонов — выберите фон в списке ниже и добавьте его. -tools/scene_editor/pick_scene = Выберите сцену слева или создайте новую. -tools/scene_editor/preview_hint = Выберите фон, чтобы посмотреть его. -tools/scene_editor/not_in_scene = (не в сцене) -tools/scene_editor/new_line_text = Новая сцена. Замените эту строку своим текстом. -tools/entity_editor/hint = Слева — список с поиском, справа — поля выбранной записи · Ctrl+N — новая, Ctrl+D — копия · Ctrl+Z/Y — отмена/повтор · Ctrl+S — сохранить, игра сразу видит изменения -tools/entity_editor/section_list = Записи мода -tools/entity_editor/search = Поиск по названию или id -tools/entity_editor/mod_tip = Мод, записи которого правятся; новые записи попадают в его файл -tools/entity_editor/new = Новый -tools/entity_editor/new_tip = Добавить запись в выбранный мод (Ctrl+N) -tools/entity_editor/duplicate = Копия -tools/entity_editor/duplicate_tip = Скопировать выбранную запись под новым id (Ctrl+D) -tools/entity_editor/delete = Удалить -tools/entity_editor/delete_tip = Удалить выбранную запись из файла мода -tools/entity_editor/delete_title = Удалить запись? -tools/entity_editor/delete_text = Запись исчезнет из файла мода после сохранения. Ссылки на неё в сценариях и картах останутся — проверьте их. -tools/entity_editor/undo = Отменить -tools/entity_editor/undo_tip = Отменить последнее изменение (Ctrl+Z) -tools/entity_editor/redo = Повторить -tools/entity_editor/redo_tip = Повторить отменённое (Ctrl+Y) -tools/entity_editor/no_entity = Выберите запись слева или создайте новую кнопкой «Новый». -tools/entity_editor/file = Мод {0} · файл {1} -tools/entity_editor/id = Id -tools/entity_editor/id_tip = Уникальный id: по нему запись находят сценарии, карты и другие моды. Мод, который зависит от владельца и даёт тот же id, заменяет запись -tools/entity_editor/locale_key_tip = Ключ локализации; тексты ниже пишутся в locale/*.properties мода -tools/entity_editor/text_in = текст ({0}) -tools/entity_editor/picture_pick_tip = Выбрать картинку из папки мода -tools/entity_editor/picture_pick_title = Картинки группы «{0}» -tools/entity_editor/section_other = Прочие поля -tools/entity_editor/other_hint = Поля, которых нет в форме (данные других модов, лут…), как JSON. Правки применяются, как только JSON корректен. -tools/entity_editor/other_invalid = Прочие поля: неверный JSON — {0} -tools/entity_editor/other_applied = Прочие поля применены -tools/entity_editor/opened = {0} записей в моде {1} -tools/entity_editor/added = Добавлена запись {0} в {1} -tools/entity_editor/deleted = Запись {0} удалена (сохраните, чтобы записать) -tools/entity_editor/saved = Сохранено: файлов {0}, текстов {1} в моде {2}; игра перечитала данные -tools/entity_editor/broken_files = Не прочитаны файлы с ошибкой JSON: {0} -tools/entity_editor/problem_id = У записи в файле {0} пустой id -tools/entity_editor/problem_duplicate = Id {0} встречается несколько раз +mod_editor_tools_desc = Совместимый набор отдельных редакторов сцен, сценариев, карт, интерактивов, света, Godot-сцен и экспорта каталога. diff --git a/locale/ui_vi.properties b/locale/ui_vi.properties index 6f89952..01df7a8 100644 --- a/locale/ui_vi.properties +++ b/locale/ui_vi.properties @@ -1,337 +1,2 @@ mod_editor_tools_name = Công cụ chỉnh sửa -mod_editor_tools_desc = Các cửa sổ tác giả mở trên trò chơi đang chạy: trình sửa cảnh, kịch bản, bản đồ, tương tác và ánh sáng, cùng trình dựng cảnh Godot. Mọi thứ chúng lưu là tệp mod bình thường và game áp dụng ngay lập tức. - -tools/common/save = Lưu -tools/common/save_tip = Lưu tệp (Ctrl+S) -tools/common/saved = Đã lưu: {0} -tools/common/loaded = Đã mở: {0} -tools/common/load_error = Lỗi tải -tools/common/readonly = Không thể lưu: mod chỉ đọc (zip) hoặc đường dẫn không hợp lệ. -tools/common/no_writable_mods = Không có mod nào ghi được (chỉ sửa được mod dạng thư mục). -tools/common/no_files = Mod "{0}" chưa có tệp loại này — hãy tạo bằng "+". -tools/common/no_file_to_save = Hãy chọn hoặc tạo một tệp trước ("+" cạnh danh sách tệp). -tools/common/new_file = Tệp mới -tools/common/new_file_hint = Tệp được tạo trong {0}/{1}/ với phần mở rộng {2} khi lưu lần đầu. -tools/common/new_file_status = Tệp mới {0} — được tạo khi lưu. -tools/common/mod_tip = Mod (mod đã có tệp loại này đứng trước; · — chưa có) -tools/common/file_tip = Tệp trong mod -tools/common/search = Tìm… -tools/common/json = JSON -tools/common/json_tip = Hiển thị JSON của tệp kết quả -tools/common/background_missing = Không mod nào cung cấp phông nền "{0}" của cảnh "{1}" — hãy chọn thủ công. - -tools/map_editor/title = Trình sửa bản đồ -tools/map_editor/hint = Chuột trái — chọn / kéo · chuột phải — xóa đỉnh · nhấp đúp, Enter hoặc Esc — hoàn tất vùng · Del — xóa vùng · Ctrl+S — lưu -tools/map_editor/reload_map = Làm mới bản đồ trong game -tools/map_editor/reload_map_tip = Đọc lại các tệp bản đồ đã lưu trong game đang chạy -tools/map_editor/reloaded = Đã làm mới bản đồ trong game. -tools/map_editor/select_mode = Chọn -tools/map_editor/draw_region = Vẽ -tools/map_editor/draw_started = Nhấp vào bản đồ để đặt đỉnh. Nhấp đúp, Enter hoặc Esc để hoàn tất, chuột phải bỏ đỉnh cuối. -tools/map_editor/background = Phông nền xem trước -tools/map_editor/zones = Vùng -tools/map_editor/add_region = + Khu vực -tools/map_editor/add_region_tip = Khu vực mới: nhấp vào bản đồ để vẽ đa giác -tools/map_editor/add_marker = + Điểm đánh dấu -tools/map_editor/add_label = + Nhãn -tools/map_editor/delete = Xóa vùng -tools/map_editor/delete_tip = Xóa vùng đã chọn (Del) -tools/map_editor/delete_confirm = Xóa vùng đã chọn? Thay đổi sẽ vào tệp khi lưu. -tools/map_editor/deleted = Đã xóa vùng "{0}". -tools/map_editor/props = Thuộc tính vùng -tools/map_editor/zone_missing = Chọn một vùng trong danh sách hoặc trên bản đồ. -tools/map_editor/id = ID -tools/map_editor/id_empty = ID vùng không được để trống. -tools/map_editor/duplicate_id = ID vùng đã được dùng: -tools/map_editor/title_key = Tiêu đề -tools/map_editor/description = Mô tả -tools/map_editor/no_translation = không tìm thấy khóa trong bản địa hóa -tools/map_editor/scene = Cảnh -tools/map_editor/scene_none = (không có) -tools/map_editor/availability = Khả dụng -tools/map_editor/locked = Không cho di chuyển tới -tools/map_editor/hidden = Ẩn khỏi bản đồ -tools/map_editor/effective = Trong game: -tools/map_editor/effective_open = mở -tools/map_editor/effective_locked = bị khóa (cờ Locked) -tools/map_editor/effective_no_scene = bị khóa (không có cảnh) -tools/map_editor/effective_unknown_scene = bị khóa (không tìm thấy cảnh) -tools/map_editor/effective_hidden = ẩn khỏi bản đồ -tools/map_editor/marker = Điểm đánh dấu -tools/map_editor/image = Biểu tượng -tools/map_editor/image_placeholder = ID texture -tools/map_editor/icon_search = Tìm biểu tượng… -tools/map_editor/label_section = Nhãn -tools/map_editor/label = Nhãn chữ trên bản đồ -tools/map_editor/x = X (0–1) -tools/map_editor/y = Y (0–1) -tools/map_editor/vertices = Các đỉnh đa giác -tools/map_editor/vertices_hint = Kéo các đỉnh trên bản đồ; chuột phải vào một đỉnh để xóa. -tools/map_editor/add_vertex = + Đỉnh -tools/map_editor/delete_vertex = Xóa đỉnh -tools/map_editor/preview = ▶ Như trong game -tools/map_editor/preview_tip = Xem bản đồ như trong game: rê chuột, thẻ vùng, điểm đánh dấu (kể cả thay đổi chưa lưu) -tools/map_editor/preview_on = Xem trước: Esc hoặc ✕ để quay lại trình sửa. -tools/map_editor/preview_off = Chế độ chỉnh sửa. -tools/map_editor/preview_missing = Không tải được cảnh bản đồ (map_widget.tscn). - -tools/scenario_editor/title = Trình sửa kịch bản -tools/scenario_editor/hint = Ctrl+S — lưu · F5 — chơi thử kịch bản · nhấp vào một cảnh trên đồ thị — các kịch bản của nó -tools/scenario_editor/play = ▶ Chơi -tools/scenario_editor/play_tooltip = Lưu và chơi thử kịch bản trong game (F5) -tools/scenario_editor/play_stopped = Đã dừng chơi thử. -tools/scenario_editor/stop = ■ Dừng -tools/scenario_editor/open_file = Mở bằng trình sửa ngoài -tools/scenario_editor/open_file_tip = Mở tệp bằng chương trình mặc định của hệ thống -tools/scenario_editor/open_file_error = Không mở được tệp -tools/scenario_editor/rescan = Quét lại -tools/scenario_editor/rescan_tip = Đọc lại danh sách kịch bản và đồ thị (sau khi thêm tệp) -tools/scenario_editor/rescanned = Đã làm mới danh sách kịch bản. -tools/scenario_editor/search_files = Tìm tệp… -tools/scenario_editor/tab_files = Tệp -tools/scenario_editor/tab_info = Tóm tắt -tools/scenario_editor/tab_graph = Đồ thị cảnh -tools/scenario_editor/graph_select_node = Chọn một cảnh trên đồ thị để xem các kịch bản và chuyển cảnh của nó. -tools/scenario_editor/graph_node = mod {0} · kịch bản {1} -tools/scenario_editor/graph_node_bg = phông nền: {0} -tools/scenario_editor/graph_detail_mod = mod: {0} -tools/scenario_editor/graph_detail_scenarios = Kịch bản ({0}), theo ưu tiên: -tools/scenario_editor/info_scene = Cảnh -tools/scenario_editor/info_priority = Ưu tiên -tools/scenario_editor/info_condition = Điều kiện -tools/scenario_editor/info_switch_targets = Chuyển cảnh -tools/scenario_editor/info_locale_keys = Khóa bản địa hóa -tools/scenario_editor/info_quest_refs = Nhiệm vụ -tools/scenario_editor/bg_previews = Phông nền của kịch bản -tools/scenario_editor/bg_missing = Không tìm thấy phông nền: {0} -tools/scenario_editor/no_backgrounds = Kịch bản không đổi phông nền -tools/scenario_editor/save_error = Lỗi lưu: {0} -tools/scenario_editor/readonly_zip = Tệp của mod zip là chỉ đọc -tools/scenario_editor/no_scene_error = Phần đầu kịch bản không có cảnh (scene:) — không có gì để chơi. -tools/scenario_editor/unsaved_title = Thay đổi chưa lưu -tools/scenario_editor/unsaved_switch_text = Thẻ này có thay đổi chưa lưu. Đóng mà không lưu? - -tools/scene_builder/title = Trình dựng cảnh -tools/scene_builder/hint = Kéo nút bằng thân hoặc tay cầm · nhấp — chọn nút dưới con trỏ · Del — xóa nút · Ctrl+S — lưu và dựng lại -tools/scene_builder/base_scene = Cảnh nền mà cảnh của mod hiển thị lên trên -tools/scene_builder/mod = Mod -tools/scene_builder/scene = Cảnh của mod (.tscn) -tools/scene_builder/empty_scene = (cảnh nền trống) -tools/scene_builder/select_mod = — chọn mod — -tools/scene_builder/select_scene = — chọn cảnh — -tools/scene_builder/save_tip = Lưu .tscn và dựng lại bản xem trước (Ctrl+S) -tools/scene_builder/reload = Tải lại từ đĩa -tools/scene_builder/reload_tip = Bỏ các chỉnh sửa chưa lưu và đọc lại tệp cảnh -tools/scene_builder/reloaded = Đã tải lại cảnh từ đĩa. -tools/scene_builder/saved_ok = Đã lưu. -tools/scene_builder/saved_rebuilt = Đã lưu, bản xem trước đã được dựng lại. -tools/scene_builder/autosaved = Đã tự động lưu. -tools/scene_builder/error_save = Lỗi lưu: mod chỉ đọc. -tools/scene_builder/error_no_scene = Hãy chọn một cảnh của mod trước. -tools/scene_builder/hierarchy = Phân cấp -tools/scene_builder/hierarchy_hint = Vàng — gốc cảnh của mod, sáng — nút sửa được, mờ — cảnh nền (chỉ đọc). -tools/scene_builder/node_type_tip = Loại của nút mới -tools/scene_builder/add_node_tip = Thêm một nút thuộc loại đã chọn vào nút đang chọn (hoặc gốc cảnh) -tools/scene_builder/delete_node_tip = Xóa nút đang chọn của cảnh mod -tools/scene_builder/delete_node_title = Xóa nút -tools/scene_builder/delete_node_confirm = Xóa nút đang chọn cùng các nút con? Tệp được lưu tự động. -tools/scene_builder/refresh_tip = Làm mới cây nút -tools/scene_builder/tab_inspector = Thuộc tính -tools/scene_builder/tab_tscn = Mã nguồn .tscn -tools/scene_builder/tscn_hint = Chỉnh sửa ở thanh thuộc tính và tay cầm tự lưu; sửa tay văn bản thì lưu bằng Ctrl+S, đồng thời dựng lại bản xem trước. -tools/scene_builder/inspector_cat_container = Trong vùng chứa -tools/scene_builder/inspector_select = Chọn một nút của cảnh mod để sửa thuộc tính -tools/scene_builder/inspector_readonly = Chỉ đọc: nút thuộc cảnh nền -tools/scene_builder/inspector_missing_section = Không tìm thấy phần của nút trong tscn (nút của cảnh con được thể hiện chưa sửa được) -tools/scene_builder/inspector_preset = Mẫu neo -tools/scene_builder/inspector_cat_layout = Bố cục -tools/scene_builder/inspector_cat_rect = Hình chữ nhật -tools/scene_builder/inspector_cat_visibility = Hiển thị -tools/scene_builder/inspector_cat_misc = Khác -tools/scene_builder/inspector_bad_value = Không thể ghi giá trị vào tscn. -tools/scene_builder/inspector_in_file_tooltip = Được lưu trong tệp tscn -tools/scene_builder/inspector_container_banner = Nút này do vùng chứa quản lý: neo và độ lệch được nút cha tính lại nên chỉnh ở đây sẽ không có tác dụng. -tools/scene_builder/err_add_failed = Lỗi: không tạo được nút loại này. -tools/scene_builder/err_add_parent_missing = Lỗi: không tìm thấy phần của nút cha trong tscn (không hỗ trợ nút cha của cảnh con được thể hiện). -tools/scene_builder/err_del_root = Lỗi: không thể xóa gốc cảnh. -tools/scene_builder/err_del_none = Lỗi: hãy chọn một nút của mod để xóa. - -tools/interactive_editor/title = Trình sửa tương tác -tools/interactive_editor/hint = Nhấp vào vùng va chạm — chọn · kéo tay cầm · ở chế độ đa giác, nhấp vào nền để thêm đỉnh, chuột phải vào đỉnh để xóa · Del — xóa đối tượng · Ctrl+S — lưu -tools/interactive_editor/items = Đối tượng trong tệp -tools/interactive_editor/add = + Mới -tools/interactive_editor/duplicate = Nhân bản -tools/interactive_editor/delete = Xóa đối tượng -tools/interactive_editor/delete_confirm = Gỡ đối tượng tương tác đã chọn khỏi tệp? Thay đổi sẽ vào tệp khi lưu. -tools/interactive_editor/deleted = Đã gỡ đối tượng "{0}". -tools/interactive_editor/background = Phông nền xem trước -tools/interactive_editor/props = Thuộc tính -tools/interactive_editor/none_selected = Chọn một đối tượng trong danh sách hoặc trên bản xem trước. -tools/interactive_editor/label_id = ID -tools/interactive_editor/label_texture_key = Texture -tools/interactive_editor/texture_placeholder = không có sprite -tools/interactive_editor/texture_tip = ID texture của sprite; để trống — vùng nhấp vô hình -tools/interactive_editor/label_layer = Lớp -tools/interactive_editor/label_z = Thứ tự Z -tools/interactive_editor/label_cursor = Con trỏ -tools/interactive_editor/cursor_none = (mặc định) -tools/interactive_editor/enabled = Bật -tools/interactive_editor/visible = Hiển thị -tools/interactive_editor/collider = Vùng va chạm -tools/interactive_editor/label_collider_type = Hình dạng -tools/interactive_editor/hit_rect = hình chữ nhật -tools/interactive_editor/hit_circle = hình tròn -tools/interactive_editor/hit_polygon = đa giác -tools/interactive_editor/label_x = X -tools/interactive_editor/label_y = Y -tools/interactive_editor/label_w = Chiều rộng -tools/interactive_editor/label_h = Chiều cao -tools/interactive_editor/label_cx = Tâm X -tools/interactive_editor/label_cy = Tâm Y -tools/interactive_editor/label_r = Bán kính -tools/interactive_editor/polygon_hint = Nhấp vào nền để thêm đỉnh, chuột phải vào đỉnh để xóa. Ít nhất 3 đỉnh. -tools/interactive_editor/polygon_too_small = Đa giác "{0}" có ít hơn 3 đỉnh. -tools/interactive_editor/highlight = Làm nổi bật -tools/interactive_editor/label_highlight_color = Màu -tools/interactive_editor/label_highlight_scale = Tỉ lệ -tools/interactive_editor/actions = Hành động -tools/interactive_editor/label_onclick_scenario = Kịch bản -tools/interactive_editor/label_onclick_commands = Lệnh khi nhấp (mỗi dòng một lệnh) -tools/interactive_editor/label_hover_enter = Khi rê chuột vào -tools/interactive_editor/label_hover_leave = Khi rê chuột ra -tools/interactive_editor/id_empty = Có đối tượng để trống ID. -tools/interactive_editor/duplicate_id = Trùng ID đối tượng: - -tools/light_editor/title = Trình sửa ánh sáng -tools/light_editor/hint = Nhấp đúp vào nền — đèn mới · kéo các đèn · Del — xóa · Ctrl+S — ghi light_set vào kịch bản -tools/light_editor/scenario_tip = Kịch bản có dòng light_set được sửa (● — đã đặt đèn) -tools/light_editor/pick_scenario = — chọn kịch bản — -tools/light_editor/save_tip = Ghi dòng light_set vào kịch bản (Ctrl+S) -tools/light_editor/copy_line = Sao chép dòng -tools/light_editor/copy_line_tip = Sao chép lệnh light_set vào bộ nhớ tạm -tools/light_editor/copied = Đã sao chép dòng light_set. -tools/light_editor/show_line = light_set -tools/light_editor/loaded = Đã tải đèn của {0}. -tools/light_editor/no_lights_yet = {0} chưa có light_set — hãy thêm đèn, dòng sẽ được chèn sau phần đầu. -tools/light_editor/static_lights = Đèn -tools/light_editor/add_light = + Đèn -tools/light_editor/delete = Xóa -tools/light_editor/limit = Tối đa {0} đèn tĩnh. -tools/light_editor/selected_props = Đèn đã chọn -tools/light_editor/none_selected = Chọn một đèn trong danh sách hoặc trên bản xem trước. -tools/light_editor/x = X (0–1) -tools/light_editor/y = Y (0–1) -tools/light_editor/radius = Bán kính -tools/light_editor/radius_tip = Bán kính tính bằng điểm ảnh của màn hình game rộng 1920 px -tools/light_editor/influence = Cường độ -tools/light_editor/influence_tip = Mức ảnh hưởng của đèn (0–1) -tools/light_editor/color = Màu -tools/light_editor/background = Phông nền xem trước - -tools/catalog_export/hint = Tệp nén và mã băm được tính khi xuất; JSON được ghi cạnh tệp nén của mod -tools/catalog_export/mod_info = Mod -tools/catalog_export/id = ID -tools/catalog_export/name = Tên -tools/catalog_export/metadata = Danh mục -tools/catalog_export/locales = Bản địa hóa -tools/catalog_export/locales_hint = Nếu không có dòng nào thì dùng tên và mô tả tiếng Anh trong manifest. -tools/catalog_export/remove_locale = Bỏ ngôn ngữ -tools/catalog_export/preview = JSON kết quả -tools/catalog_export/copied = Đã sao chép JSON vào bộ nhớ tạm. -tools/catalog_export/export_tip = Tạo tệp nén, tính mã băm và ghi JSON của mục danh mục -tools/catalog_export/exported = Đã xuất: {0} -tools/catalog_export/export_error = Lỗi xuất -tools/catalog_export/url_required = Hãy đặt URL tải xuống — thiếu nó thì mục danh mục vô dụng. - -tools/scene_editor/title = Trình sửa cảnh -tools/scene_editor/hint = Cảnh là một địa điểm: phông nền, âm thanh nền và kịch bản. Ctrl+S để lưu · nhấp đúp vào một phông nền trong danh sách để thêm -tools/scene_editor/save_tip = Lưu tệp cảnh vào mod đã chọn (Ctrl+S); game thấy thay đổi ngay -tools/scene_editor/new_scene = + Cảnh mới -tools/scene_editor/new_scene_tip = Tạo một địa điểm mới: tên, mod, phông nền. Có thể thêm kịch bản sau; cảnh cũng có thể không có kịch bản nào -tools/scene_editor/delete = Xóa -tools/scene_editor/delete_tip = Xóa tệp cảnh của mod đã chọn -tools/scene_editor/delete_confirm = Xóa tệp của cảnh này khỏi mod đã chọn? Các kịch bản của cảnh vẫn được giữ. -tools/scene_editor/go = ▶ Đến cảnh -tools/scene_editor/go_tip = Chuyển game sang cảnh này -tools/scene_editor/rescan_tip = Đọc lại cảnh, phông nền và âm thanh (sau khi thêm tệp vào mod) -tools/scene_editor/scenes = Cảnh -tools/scene_editor/backgrounds = Phông nền của cảnh (mỗi lần vào chọn ngẫu nhiên một cái) -tools/scene_editor/all_backgrounds = Tất cả phông nền -tools/scene_editor/add_background = + Thêm phông nền vào cảnh -tools/scene_editor/add_background_tip = Thêm phông nền đang hiển thị vào cảnh (hoặc nhấp đúp vào nó trong danh sách) -tools/scene_editor/remove_background = Bỏ phông nền khỏi cảnh -tools/scene_editor/properties = Thuộc tính -tools/scene_editor/name = Tên -tools/scene_editor/name_tip = Id của cảnh: chữ Latin, chữ số và _; switch_scene và phần đầu kịch bản tham chiếu tới nó. Chỉ đặt khi tạo -tools/scene_editor/mod = Lưu vào -tools/scene_editor/mod_tip = Mod mà tệp cảnh được ghi vào (chỉ mod dạng thư mục) -tools/scene_editor/ambient = Âm thanh -tools/scene_editor/ambient_tip = Âm thanh nền phát trong cảnh -tools/scene_editor/listen = ▶ -tools/scene_editor/listen_tip = Nghe âm thanh nền -tools/scene_editor/stop = ■ -tools/scene_editor/default_scenario = Kịch bản mặc định -tools/scene_editor/default_scenario_tip = Phát khi cảnh không còn kịch bản nào khác -tools/scene_editor/scenarios = Kịch bản của cảnh -tools/scene_editor/open_scenario = Mở -tools/scene_editor/open_scenario_tip = Mở kịch bản đã chọn trong trình sửa kịch bản (hoặc nhấp đúp) -tools/scene_editor/new_scenario = + Kịch bản mới -tools/scene_editor/new_scenario_tip = Tạo một kịch bản của cảnh này trong mod đã chọn và mở nó -tools/scene_editor/none = — không có — -tools/scene_editor/effective = Trong game: {0} (từ mod {1}) -tools/scene_editor/adds_to = Cảnh được định nghĩa trong: {0}. Khi lưu sẽ tạo trong mod đã chọn một tệp bổ sung cho nó. -tools/scene_editor/also_in = Cũng được bổ sung bởi: {0} -tools/scene_editor/no_writable_mod = Không có mod dạng thư mục nào để lưu cảnh. -tools/scene_editor/bad_name = Hãy đặt tên cảnh: chữ Latin, chữ số và _ (bắt đầu bằng chữ cái). -tools/scene_editor/name_taken = Cảnh "{0}" đã tồn tại; hãy chọn nó trong danh sách. -tools/scene_editor/need_background = Hãy thêm ít nhất một phông nền cho cảnh. -tools/scene_editor/created = Đã tạo cảnh "{0}". -tools/scene_editor/deleted = Đã xóa: {0} -tools/scene_editor/opened = Cảnh: {0} -tools/scene_editor/new_started = Cảnh mới: đặt tên và mod, thêm phông nền. -tools/scene_editor/already_there = Phông nền {0} đã có trong cảnh. -tools/scene_editor/go_failed = Game không biết cảnh "{0}"; hãy lưu nó trước. -tools/scene_editor/went = Đã chuyển sang cảnh {0}. -tools/scene_editor/rescanned = Đã đọc lại cảnh, phông nền và âm thanh. -tools/scene_editor/scenario_created = Đã tạo kịch bản {0}. -tools/scene_editor/no_scenarios = Cảnh chưa có kịch bản nào -tools/scene_editor/no_backgrounds = Cảnh chưa có phông nền: hãy chọn một cái trong danh sách bên dưới và thêm vào. -tools/scene_editor/pick_scene = Chọn một cảnh bên trái hoặc tạo cảnh mới. -tools/scene_editor/preview_hint = Chọn một phông nền để xem. -tools/scene_editor/not_in_scene = (không có trong cảnh) -tools/scene_editor/new_line_text = Một cảnh mới. Hãy thay dòng này bằng văn bản của bạn. -tools/entity_editor/hint = Bên trái là danh sách có tìm kiếm, bên phải là các trường của mục đã chọn · Ctrl+N mới, Ctrl+D sao chép · Ctrl+Z/Y hoàn tác/làm lại · Ctrl+S lưu, game thấy thay đổi ngay -tools/entity_editor/section_list = Các mục của mod -tools/entity_editor/search = Tìm theo tên hoặc id -tools/entity_editor/mod_tip = Mod có các mục đang được sửa; mục mới được ghi vào tệp của nó -tools/entity_editor/new = Mới -tools/entity_editor/new_tip = Thêm một mục vào mod đã chọn (Ctrl+N) -tools/entity_editor/duplicate = Sao chép -tools/entity_editor/duplicate_tip = Sao chép mục đã chọn với id mới (Ctrl+D) -tools/entity_editor/delete = Xóa -tools/entity_editor/delete_tip = Gỡ mục đã chọn khỏi tệp của mod -tools/entity_editor/delete_title = Xóa mục này? -tools/entity_editor/delete_text = Mục sẽ rời khỏi tệp của mod khi lưu. Các tham chiếu tới nó trong kịch bản và bản đồ vẫn còn — hãy kiểm tra chúng. -tools/entity_editor/undo = Hoàn tác -tools/entity_editor/undo_tip = Hoàn tác thay đổi cuối (Ctrl+Z) -tools/entity_editor/redo = Làm lại -tools/entity_editor/redo_tip = Làm lại thao tác đã hoàn tác (Ctrl+Y) -tools/entity_editor/no_entity = Chọn một mục bên trái hoặc tạo mới bằng "Mới". -tools/entity_editor/file = Mod {0} · tệp {1} -tools/entity_editor/id = Id -tools/entity_editor/id_tip = Id duy nhất: kịch bản, bản đồ và các mod khác tìm mục qua nó. Một mod phụ thuộc vào chủ sở hữu và có cùng id sẽ thay thế mục đó -tools/entity_editor/locale_key_tip = Khóa bản địa hóa; văn bản bên dưới được ghi vào locale/*.properties của mod -tools/entity_editor/text_in = văn bản ({0}) -tools/entity_editor/picture_pick_tip = Chọn một hình từ các thư mục của mod -tools/entity_editor/picture_pick_title = Hình của nhóm "{0}" -tools/entity_editor/section_other = Các trường khác -tools/entity_editor/other_hint = Các trường mà biểu mẫu không hiển thị (dữ liệu của mod khác, chiến lợi phẩm…) dưới dạng JSON. Chỉnh sửa có hiệu lực ngay khi JSON hợp lệ. -tools/entity_editor/other_invalid = Các trường khác: JSON không hợp lệ — {0} -tools/entity_editor/other_applied = Đã áp dụng các trường khác -tools/entity_editor/opened = {0} mục trong mod {1} -tools/entity_editor/added = Đã thêm mục {0} vào {1} -tools/entity_editor/deleted = Đã xóa mục {0} (lưu để ghi lại) -tools/entity_editor/saved = Đã lưu: {0} tệp, {1} văn bản trong mod {2}; game đã đọc lại dữ liệu -tools/entity_editor/broken_files = Không đọc các tệp có JSON hỏng: {0} -tools/entity_editor/problem_id = Một mục trong tệp {0} để trống id -tools/entity_editor/problem_duplicate = Id {0} xuất hiện nhiều lần +mod_editor_tools_desc = Bộ công cụ tương thích gồm trình biên tập cảnh, kịch bản, bản đồ, tương tác, ánh sáng, cảnh Godot và xuất danh mục. diff --git a/locale/ui_zh.properties b/locale/ui_zh.properties index 930bfd8..b7a3ab0 100644 --- a/locale/ui_zh.properties +++ b/locale/ui_zh.properties @@ -1,337 +1,2 @@ mod_editor_tools_name = 编辑工具 -mod_editor_tools_desc = 运行中的游戏之上的创作窗口:场景、剧本、地图、交互物和光照编辑器,以及 Godot 场景构建器。保存的都是普通模组文件,游戏会立即生效。 - -tools/common/save = 保存 -tools/common/save_tip = 保存文件(Ctrl+S) -tools/common/saved = 已保存:{0} -tools/common/loaded = 已打开:{0} -tools/common/load_error = 加载错误 -tools/common/readonly = 无法保存:模组是只读的(zip)或路径无效。 -tools/common/no_writable_mods = 没有可写的模组(只能编辑文件夹形式的模组)。 -tools/common/no_files = 模组"{0}"还没有这类文件——用"+"创建一个。 -tools/common/no_file_to_save = 请先选择或创建一个文件(文件列表旁的"+")。 -tools/common/new_file = 新文件 -tools/common/new_file_hint = 首次保存时会在 {0}/{1}/ 中创建扩展名为 {2} 的文件。 -tools/common/new_file_status = 新文件 {0}——保存时创建。 -tools/common/mod_tip = 模组(已有此类文件的模组排在前面;· —— 还没有) -tools/common/file_tip = 模组内的文件 -tools/common/search = 搜索… -tools/common/json = JSON -tools/common/json_tip = 显示生成文件的 JSON -tools/common/background_missing = 场景"{1}"的背景"{0}"没有任何模组提供——请手动选择一个。 - -tools/map_editor/title = 地图编辑器 -tools/map_editor/hint = 左键——选择 / 拖动 · 右键——删除顶点 · 双击、Enter 或 Esc——完成区域 · Del——删除区域 · Ctrl+S——保存 -tools/map_editor/reload_map = 刷新游戏内地图 -tools/map_editor/reload_map_tip = 在运行中的游戏里重新读取已保存的地图文件 -tools/map_editor/reloaded = 游戏内地图已刷新。 -tools/map_editor/select_mode = 选择 -tools/map_editor/draw_region = 绘制 -tools/map_editor/draw_started = 点击地图放置顶点。双击、Enter 或 Esc 完成,右键删除最后一个。 -tools/map_editor/background = 预览背景 -tools/map_editor/zones = 区域 -tools/map_editor/add_region = + 区域 -tools/map_editor/add_region_tip = 新区域:点击地图绘制多边形 -tools/map_editor/add_marker = + 标记 -tools/map_editor/add_label = + 文字标签 -tools/map_editor/delete = 删除区域 -tools/map_editor/delete_tip = 删除选中的区域(Del) -tools/map_editor/delete_confirm = 删除选中的区域?保存时更改会写入文件。 -tools/map_editor/deleted = 区域"{0}"已删除。 -tools/map_editor/props = 区域属性 -tools/map_editor/zone_missing = 在列表或地图上选择一个区域。 -tools/map_editor/id = ID -tools/map_editor/id_empty = 区域 ID 不能为空。 -tools/map_editor/duplicate_id = 区域 ID 已被使用: -tools/map_editor/title_key = 标题 -tools/map_editor/description = 描述 -tools/map_editor/no_translation = 在本地化中找不到该键 -tools/map_editor/scene = 场景 -tools/map_editor/scene_none = (无) -tools/map_editor/availability = 可用性 -tools/map_editor/locked = 禁止前往 -tools/map_editor/hidden = 在地图上隐藏 -tools/map_editor/effective = 游戏中: -tools/map_editor/effective_open = 开放 -tools/map_editor/effective_locked = 锁定(Locked 标志) -tools/map_editor/effective_no_scene = 锁定(没有场景) -tools/map_editor/effective_unknown_scene = 锁定(找不到场景) -tools/map_editor/effective_hidden = 在地图上隐藏 -tools/map_editor/marker = 标记 -tools/map_editor/image = 图标 -tools/map_editor/image_placeholder = 纹理 ID -tools/map_editor/icon_search = 查找图标… -tools/map_editor/label_section = 文字标签 -tools/map_editor/label = 地图上的文字标签 -tools/map_editor/x = X(0–1) -tools/map_editor/y = Y(0–1) -tools/map_editor/vertices = 多边形顶点 -tools/map_editor/vertices_hint = 在地图上拖动顶点;右键点击顶点将其删除。 -tools/map_editor/add_vertex = + 顶点 -tools/map_editor/delete_vertex = 删除顶点 -tools/map_editor/preview = ▶ 游戏中效果 -tools/map_editor/preview_tip = 以游戏中的样子查看地图:悬停、区域卡片、标记(包括未保存的修改) -tools/map_editor/preview_on = 预览:Esc 或 ✕ 返回编辑器。 -tools/map_editor/preview_off = 编辑模式。 -tools/map_editor/preview_missing = 无法加载地图场景(map_widget.tscn)。 - -tools/scenario_editor/title = 剧本编辑器 -tools/scenario_editor/hint = Ctrl+S——保存 · F5——试玩剧本 · 点击图中的场景——查看其剧本 -tools/scenario_editor/play = ▶ 试玩 -tools/scenario_editor/play_tooltip = 保存并在游戏中试玩剧本(F5) -tools/scenario_editor/play_stopped = 试玩已停止。 -tools/scenario_editor/stop = ■ 停止 -tools/scenario_editor/open_file = 在外部编辑器中打开 -tools/scenario_editor/open_file_tip = 用系统默认程序打开文件 -tools/scenario_editor/open_file_error = 无法打开文件 -tools/scenario_editor/rescan = 重新扫描 -tools/scenario_editor/rescan_tip = 重新读取剧本列表和图(添加文件后) -tools/scenario_editor/rescanned = 剧本列表已刷新。 -tools/scenario_editor/search_files = 查找文件… -tools/scenario_editor/tab_files = 文件 -tools/scenario_editor/tab_info = 概要 -tools/scenario_editor/tab_graph = 场景图 -tools/scenario_editor/graph_select_node = 在图中选择一个场景,查看它的剧本和转场。 -tools/scenario_editor/graph_node = 模组 {0} · 剧本 {1} -tools/scenario_editor/graph_node_bg = 背景:{0} -tools/scenario_editor/graph_detail_mod = 模组:{0} -tools/scenario_editor/graph_detail_scenarios = 剧本({0}),按优先级: -tools/scenario_editor/info_scene = 场景 -tools/scenario_editor/info_priority = 优先级 -tools/scenario_editor/info_condition = 条件 -tools/scenario_editor/info_switch_targets = 场景切换 -tools/scenario_editor/info_locale_keys = 本地化键 -tools/scenario_editor/info_quest_refs = 任务 -tools/scenario_editor/bg_previews = 剧本的背景 -tools/scenario_editor/bg_missing = 找不到背景:{0} -tools/scenario_editor/no_backgrounds = 这个剧本不更换背景 -tools/scenario_editor/save_error = 保存错误:{0} -tools/scenario_editor/readonly_zip = zip 模组中的文件是只读的 -tools/scenario_editor/no_scene_error = 剧本头部没有场景(scene:)——没有可试玩的内容。 -tools/scenario_editor/unsaved_title = 未保存的更改 -tools/scenario_editor/unsaved_switch_text = 这个标签页有未保存的更改。不保存就关闭吗? - -tools/scene_builder/title = 场景构建器 -tools/scene_builder/hint = 拖动节点本体或手柄 · 点击——选择光标下的节点 · Del——删除节点 · Ctrl+S——保存并重建 -tools/scene_builder/base_scene = 显示模组场景的基础场景 -tools/scene_builder/mod = 模组 -tools/scene_builder/scene = 模组场景(.tscn) -tools/scene_builder/empty_scene = (空的基础场景) -tools/scene_builder/select_mod = — 选择模组 — -tools/scene_builder/select_scene = — 选择场景 — -tools/scene_builder/save_tip = 保存 .tscn 并重建预览(Ctrl+S) -tools/scene_builder/reload = 从磁盘重新加载 -tools/scene_builder/reload_tip = 放弃未保存的修改并重新读取场景文件 -tools/scene_builder/reloaded = 已从磁盘重新加载场景。 -tools/scene_builder/saved_ok = 已保存。 -tools/scene_builder/saved_rebuilt = 已保存,预览已重建。 -tools/scene_builder/autosaved = 已自动保存。 -tools/scene_builder/error_save = 保存错误:模组是只读的。 -tools/scene_builder/error_no_scene = 请先选择一个模组场景。 -tools/scene_builder/hierarchy = 层级 -tools/scene_builder/hierarchy_hint = 金色——模组场景根节点,亮色——可编辑的节点,暗色——基础场景(只读)。 -tools/scene_builder/node_type_tip = 新节点的类型 -tools/scene_builder/add_node_tip = 在选中的节点(或场景根节点)中添加所选类型的节点 -tools/scene_builder/delete_node_tip = 删除选中的模组场景节点 -tools/scene_builder/delete_node_title = 删除节点 -tools/scene_builder/delete_node_confirm = 删除选中的节点及其子节点?文件会自动保存。 -tools/scene_builder/refresh_tip = 刷新节点树 -tools/scene_builder/tab_inspector = 检查器 -tools/scene_builder/tab_tscn = .tscn 源码 -tools/scene_builder/tscn_hint = 检查器和手柄的修改会自动保存;手动修改文本需用 Ctrl+S 保存,同时会重建预览。 -tools/scene_builder/inspector_cat_container = 在容器中 -tools/scene_builder/inspector_select = 选择模组场景中的一个节点来编辑其属性 -tools/scene_builder/inspector_readonly = 只读:该节点属于基础场景 -tools/scene_builder/inspector_missing_section = 在 tscn 中找不到节点段(实例化子场景中的节点暂不能编辑) -tools/scene_builder/inspector_preset = 锚点预设 -tools/scene_builder/inspector_cat_layout = 布局 -tools/scene_builder/inspector_cat_rect = 矩形 -tools/scene_builder/inspector_cat_visibility = 可见性 -tools/scene_builder/inspector_cat_misc = 其他 -tools/scene_builder/inspector_bad_value = 该值无法写入 tscn。 -tools/scene_builder/inspector_in_file_tooltip = 存储在 tscn 文件中 -tools/scene_builder/inspector_container_banner = 这个节点由容器管理:锚点和偏移由父节点重新计算,在这里修改不会生效。 -tools/scene_builder/err_add_failed = 错误:无法创建这种类型的节点。 -tools/scene_builder/err_add_parent_missing = 错误:在 tscn 中找不到父节点段(不支持实例化子场景的父节点)。 -tools/scene_builder/err_del_root = 错误:不能删除场景根节点。 -tools/scene_builder/err_del_none = 错误:请选择一个要删除的模组节点。 - -tools/interactive_editor/title = 交互编辑器 -tools/interactive_editor/hint = 点击碰撞体——选择 · 拖动手柄 · 多边形模式下点击背景添加顶点,右键点击顶点将其删除 · Del——删除对象 · Ctrl+S——保存 -tools/interactive_editor/items = 文件中的对象 -tools/interactive_editor/add = + 新建 -tools/interactive_editor/duplicate = 复制 -tools/interactive_editor/delete = 删除对象 -tools/interactive_editor/delete_confirm = 从文件中移除选中的交互物?保存时更改会写入文件。 -tools/interactive_editor/deleted = 已移除对象"{0}"。 -tools/interactive_editor/background = 预览背景 -tools/interactive_editor/props = 属性 -tools/interactive_editor/none_selected = 在列表或预览中选择一个对象。 -tools/interactive_editor/label_id = ID -tools/interactive_editor/label_texture_key = 纹理 -tools/interactive_editor/texture_placeholder = 无精灵图 -tools/interactive_editor/texture_tip = 精灵图的纹理 ID;为空——不可见的可点击区域 -tools/interactive_editor/label_layer = 图层 -tools/interactive_editor/label_z = Z 顺序 -tools/interactive_editor/label_cursor = 光标 -tools/interactive_editor/cursor_none = (默认) -tools/interactive_editor/enabled = 启用 -tools/interactive_editor/visible = 可见 -tools/interactive_editor/collider = 碰撞体 -tools/interactive_editor/label_collider_type = 形状 -tools/interactive_editor/hit_rect = 矩形 -tools/interactive_editor/hit_circle = 圆形 -tools/interactive_editor/hit_polygon = 多边形 -tools/interactive_editor/label_x = X -tools/interactive_editor/label_y = Y -tools/interactive_editor/label_w = 宽度 -tools/interactive_editor/label_h = 高度 -tools/interactive_editor/label_cx = 中心 X -tools/interactive_editor/label_cy = 中心 Y -tools/interactive_editor/label_r = 半径 -tools/interactive_editor/polygon_hint = 点击背景添加顶点,右键点击顶点将其删除。至少 3 个顶点。 -tools/interactive_editor/polygon_too_small = 多边形"{0}"的顶点少于 3 个。 -tools/interactive_editor/highlight = 高亮 -tools/interactive_editor/label_highlight_color = 颜色 -tools/interactive_editor/label_highlight_scale = 缩放 -tools/interactive_editor/actions = 动作 -tools/interactive_editor/label_onclick_scenario = 剧本 -tools/interactive_editor/label_onclick_commands = 点击时的命令(每行一条) -tools/interactive_editor/label_hover_enter = 鼠标移入 -tools/interactive_editor/label_hover_leave = 鼠标移出 -tools/interactive_editor/id_empty = 有对象的 ID 为空。 -tools/interactive_editor/duplicate_id = 对象 ID 重复: - -tools/light_editor/title = 光照编辑器 -tools/light_editor/hint = 双击背景——新建灯光 · 拖动灯光 · Del——删除 · Ctrl+S——把 light_set 写入剧本 -tools/light_editor/scenario_tip = 要编辑其 light_set 行的剧本(●——已设置灯光) -tools/light_editor/pick_scenario = — 选择剧本 — -tools/light_editor/save_tip = 把 light_set 行写入剧本(Ctrl+S) -tools/light_editor/copy_line = 复制行 -tools/light_editor/copy_line_tip = 把 light_set 命令复制到剪贴板 -tools/light_editor/copied = 已复制 light_set 行。 -tools/light_editor/show_line = light_set -tools/light_editor/loaded = 已加载 {0} 的灯光。 -tools/light_editor/no_lights_yet = {0} 还没有 light_set——添加灯光后,这一行会插在头部之后。 -tools/light_editor/static_lights = 灯光 -tools/light_editor/add_light = + 灯光 -tools/light_editor/delete = 删除 -tools/light_editor/limit = 最多 {0} 个静态灯光。 -tools/light_editor/selected_props = 选中的灯光 -tools/light_editor/none_selected = 在列表或预览中选择一个灯光。 -tools/light_editor/x = X(0–1) -tools/light_editor/y = Y(0–1) -tools/light_editor/radius = 半径 -tools/light_editor/radius_tip = 以 1920 像素宽的游戏画面计的半径(像素) -tools/light_editor/influence = 强度 -tools/light_editor/influence_tip = 灯光影响(0–1) -tools/light_editor/color = 颜色 -tools/light_editor/background = 预览背景 - -tools/catalog_export/hint = 归档和哈希在导出时计算;JSON 写在模组归档旁边 -tools/catalog_export/mod_info = 模组 -tools/catalog_export/id = ID -tools/catalog_export/name = 名称 -tools/catalog_export/metadata = 目录 -tools/catalog_export/locales = 本地化 -tools/catalog_export/locales_hint = 没有任何行时,使用清单中的英文名称和描述。 -tools/catalog_export/remove_locale = 移除语言 -tools/catalog_export/preview = 生成的 JSON -tools/catalog_export/copied = JSON 已复制到剪贴板。 -tools/catalog_export/export_tip = 打包归档、计算哈希并写入目录条目的 JSON -tools/catalog_export/exported = 已导出:{0} -tools/catalog_export/export_error = 导出错误 -tools/catalog_export/url_required = 请设置下载 URL——没有它的目录条目毫无用处。 - -tools/scene_editor/title = 场景编辑器 -tools/scene_editor/hint = 场景就是一个地点:背景、环境音和剧本。Ctrl+S 保存 · 双击列表中的背景将其添加 -tools/scene_editor/save_tip = 把场景文件保存到所选模组(Ctrl+S);游戏会立即看到更改 -tools/scene_editor/new_scene = + 新场景 -tools/scene_editor/new_scene_tip = 创建一个新地点:名称、模组、背景。剧本可以以后再加;场景也可以没有剧本 -tools/scene_editor/delete = 删除 -tools/scene_editor/delete_tip = 删除所选模组中的场景文件 -tools/scene_editor/delete_confirm = 从所选模组中删除这个场景的文件?场景的剧本会保留。 -tools/scene_editor/go = ▶ 前往场景 -tools/scene_editor/go_tip = 把游戏切换到这个场景 -tools/scene_editor/rescan_tip = 重新读取场景、背景和声音(向模组添加文件后) -tools/scene_editor/scenes = 场景 -tools/scene_editor/backgrounds = 场景背景(进入时随机选一个) -tools/scene_editor/all_backgrounds = 所有背景 -tools/scene_editor/add_background = + 把背景加入场景 -tools/scene_editor/add_background_tip = 把显示的背景加入场景(或在列表中双击它) -tools/scene_editor/remove_background = 从场景中移除背景 -tools/scene_editor/properties = 属性 -tools/scene_editor/name = 名称 -tools/scene_editor/name_tip = 场景 id:拉丁字母、数字和 _;switch_scene 和剧本头部会引用它。只能在创建时设置 -tools/scene_editor/mod = 保存到 -tools/scene_editor/mod_tip = 写入场景文件的模组(仅限文件夹模组) -tools/scene_editor/ambient = 声音 -tools/scene_editor/ambient_tip = 场景中播放的环境音 -tools/scene_editor/listen = ▶ -tools/scene_editor/listen_tip = 试听环境音 -tools/scene_editor/stop = ■ -tools/scene_editor/default_scenario = 默认剧本 -tools/scene_editor/default_scenario_tip = 当场景没有其他剧本剩下时播放 -tools/scene_editor/scenarios = 场景的剧本 -tools/scene_editor/open_scenario = 打开 -tools/scene_editor/open_scenario_tip = 在剧本编辑器中打开选中的剧本(或双击) -tools/scene_editor/new_scenario = + 新剧本 -tools/scene_editor/new_scenario_tip = 在所选模组中创建本场景的剧本并打开 -tools/scene_editor/none = — 无 — -tools/scene_editor/effective = 游戏中:{0}(来自模组 {1}) -tools/scene_editor/adds_to = 场景定义在:{0}。保存时会在所选模组中创建一个补充它的文件。 -tools/scene_editor/also_in = 也补充它的有:{0} -tools/scene_editor/no_writable_mod = 没有可以保存场景的文件夹模组。 -tools/scene_editor/bad_name = 请设置场景名称:拉丁字母、数字和 _(以字母开头)。 -tools/scene_editor/name_taken = 场景"{0}"已存在;请在列表中选择它。 -tools/scene_editor/need_background = 请至少给场景添加一个背景。 -tools/scene_editor/created = 已创建场景"{0}"。 -tools/scene_editor/deleted = 已删除:{0} -tools/scene_editor/opened = 场景:{0} -tools/scene_editor/new_started = 新场景:设置名称和模组,添加背景。 -tools/scene_editor/already_there = 背景 {0} 已在场景中。 -tools/scene_editor/go_failed = 游戏不认识场景"{0}";请先保存它。 -tools/scene_editor/went = 已切换到场景 {0}。 -tools/scene_editor/rescanned = 已重新读取场景、背景和声音。 -tools/scene_editor/scenario_created = 已创建剧本 {0}。 -tools/scene_editor/no_scenarios = 这个场景还没有剧本 -tools/scene_editor/no_backgrounds = 这个场景没有背景:在下方列表中选一个加入。 -tools/scene_editor/pick_scene = 在左边选择一个场景,或创建一个新场景。 -tools/scene_editor/preview_hint = 选择一个背景来查看。 -tools/scene_editor/not_in_scene = (不在场景中) -tools/scene_editor/new_line_text = 一个新场景。用你的文字替换这一行。 -tools/entity_editor/hint = 左边是带搜索的列表,右边是所选条目的字段 · Ctrl+N 新建,Ctrl+D 复制 · Ctrl+Z/Y 撤销/重做 · Ctrl+S 保存,游戏会立即看到更改 -tools/entity_editor/section_list = 模组的条目 -tools/entity_editor/search = 按名称或 id 搜索 -tools/entity_editor/mod_tip = 要编辑其条目的模组;新条目会写入它的文件 -tools/entity_editor/new = 新建 -tools/entity_editor/new_tip = 向所选模组添加一个条目(Ctrl+N) -tools/entity_editor/duplicate = 复制 -tools/entity_editor/duplicate_tip = 用新 id 复制选中的条目(Ctrl+D) -tools/entity_editor/delete = 删除 -tools/entity_editor/delete_tip = 从模组文件中移除选中的条目 -tools/entity_editor/delete_title = 删除该条目? -tools/entity_editor/delete_text = 保存时该条目会从模组文件中移除。剧本和地图中对它的引用会保留——请检查它们。 -tools/entity_editor/undo = 撤销 -tools/entity_editor/undo_tip = 撤销上一次更改(Ctrl+Z) -tools/entity_editor/redo = 重做 -tools/entity_editor/redo_tip = 重做已撤销的操作(Ctrl+Y) -tools/entity_editor/no_entity = 在左边选择一个条目,或用"新建"创建一个。 -tools/entity_editor/file = 模组 {0} · 文件 {1} -tools/entity_editor/id = Id -tools/entity_editor/id_tip = 唯一 id:剧本、地图和其他模组通过它找到这个条目。依赖所有者并提供相同 id 的模组会替换该条目 -tools/entity_editor/locale_key_tip = 本地化键;下面的文本会写入模组的 locale/*.properties -tools/entity_editor/text_in = 文本({0}) -tools/entity_editor/picture_pick_tip = 从模组文件夹中选择图片 -tools/entity_editor/picture_pick_title = "{0}"组的图片 -tools/entity_editor/section_other = 其他字段 -tools/entity_editor/other_hint = 表单中没有显示的字段(其他模组的数据、战利品…),以 JSON 形式显示。JSON 一旦有效,修改就会生效。 -tools/entity_editor/other_invalid = 其他字段:JSON 无效——{0} -tools/entity_editor/other_applied = 已应用其他字段 -tools/entity_editor/opened = 模组 {1} 中有 {0} 个条目 -tools/entity_editor/added = 已把条目 {0} 加入 {1} -tools/entity_editor/deleted = 已删除条目 {0}(保存后写入) -tools/entity_editor/saved = 已保存:{0} 个文件、{1} 条文本,模组 {2};游戏已重新读取数据 -tools/entity_editor/broken_files = 没有读取 JSON 损坏的文件:{0} -tools/entity_editor/problem_id = 文件 {0} 中有条目的 id 为空 -tools/entity_editor/problem_duplicate = id {0} 出现了不止一次 +mod_editor_tools_desc = 兼容工具包,安装场景、剧本、地图、交互、灯光、Godot 场景和目录导出工具。 diff --git a/manifest.json b/manifest.json index ce4f7ea..092b93a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,20 +1,32 @@ { "id": "editor_tools", "name": "Editor Tools", - "version": "1.0.0", + "version": "1.1.0", "author": "ct", - "description": "The author's windows over a running game: scene, scenario, map, interactive and light editors and a Godot scene builder. What they save are ordinary mod files the game applies at once.", + "description": "Compatibility toolset: installs the individual scene, scenario, map, interactive, light, Godot scene and catalog tools.", "category": "tools, editor", - "sources": [ - "src/**/*.cs" + "sources": [], + "depends": [ + "scenario_editor>=1", + "scene_editor>=1", + "scene_builder>=1", + "map_editor>=1", + "interactive_editor>=1", + "light_editor>=1", + "catalog_export>=1" ], - "test_sources": [ - "tests/*.cs" - ], - "depends": ["dialog_base>=1", "interactive_base>=1", "time_base>=1", "map_base>=1", "ui_base>=1", "schema_base>=1"], "test_suites": [ - {"id": "windows", "activate": ["--itest=tools_windows"]}, - {"id": "ingame_play", "activate": ["--itest=ingame_play"]}, - {"id": "novel_flow", "activate": ["--itest=novel_flow"]} + { + "id": "windows", + "activate": [ + "--itest=tools_windows" + ] + }, + { + "id": "novel_flow", + "activate": [ + "--itest=novel_flow" + ] + } ] } diff --git a/scenes/scenario_file_tree_panel.tscn b/scenes/scenario_file_tree_panel.tscn deleted file mode 100644 index 5bf0905..0000000 --- a/scenes/scenario_file_tree_panel.tscn +++ /dev/null @@ -1,16 +0,0 @@ -[gd_scene format=3] - -[ext_resource type="Script" path="src/ScenarioEditor/ScenarioFileTreePanel.cs" id="1_script"] - -[node name="ScenarioFileTreePanel" type="VBoxContainer" script=ExtResource("1_script")] -size_flags_horizontal = 3 -size_flags_vertical = 3 -theme_override_constants/separation = 4 - -[node name="Search" type="LineEdit" parent="."] -clear_button_enabled = true - -[node name="FileTree" type="Tree" parent="."] -size_flags_horizontal = 3 -size_flags_vertical = 3 -hide_root = true diff --git a/scenes/scenario_info_panel.tscn b/scenes/scenario_info_panel.tscn deleted file mode 100644 index 60892eb..0000000 --- a/scenes/scenario_info_panel.tscn +++ /dev/null @@ -1,48 +0,0 @@ -[gd_scene format=3] - -[ext_resource type="Script" path="src/ScenarioEditor/ScenarioInfoPanel.cs" id="1_script"] - -[node name="ScenarioInfoPanel" type="VBoxContainer" script=ExtResource("1_script")] -size_flags_horizontal = 3 -size_flags_vertical = 3 -theme_override_constants/separation = 6 - -[node name="Scroll" type="ScrollContainer" parent="."] -size_flags_vertical = 3 -horizontal_scroll_mode = 0 - -[node name="VBox" type="VBoxContainer" parent="Scroll"] -size_flags_horizontal = 3 -theme_override_constants/separation = 4 - -[node name="HintScene" type="Label" parent="Scroll/VBox"] - -[node name="InfoScene" type="Label" parent="Scroll/VBox"] -text = "-" - -[node name="HintPriority" type="Label" parent="Scroll/VBox"] - -[node name="InfoPriority" type="Label" parent="Scroll/VBox"] -text = "-" - -[node name="HintCondition" type="Label" parent="Scroll/VBox"] - -[node name="InfoCondition" type="Label" parent="Scroll/VBox"] -text = "-" - -[node name="HSep" type="HSeparator" parent="Scroll/VBox"] - -[node name="HintSwitchTargets" type="Label" parent="Scroll/VBox"] - -[node name="InfoSwitchTargets" type="Label" parent="Scroll/VBox"] -text = "-" - -[node name="HintLocaleKeys" type="Label" parent="Scroll/VBox"] - -[node name="InfoLocaleKeys" type="Label" parent="Scroll/VBox"] -text = "-" - -[node name="HintQuestRefs" type="Label" parent="Scroll/VBox"] - -[node name="InfoQuestRefs" type="Label" parent="Scroll/VBox"] -text = "-" diff --git a/scenes/scenario_thumbnail_panel.tscn b/scenes/scenario_thumbnail_panel.tscn deleted file mode 100644 index e8a580f..0000000 --- a/scenes/scenario_thumbnail_panel.tscn +++ /dev/null @@ -1,10 +0,0 @@ -[gd_scene format=3] - -[ext_resource type="Script" path="src/ScenarioEditor/ScenarioThumbnailPanel.cs" id="1_script"] - -[node name="ScenarioThumbnailPanel" type="ScrollContainer" script=ExtResource("1_script")] -custom_minimum_size = Vector2(0, 110) -horizontal_scroll_mode = 1 -vertical_scroll_mode = 3 - -[node name="ThumbContainer" type="HBoxContainer" parent="."] diff --git a/src/CatalogExport/CatalogExportToolProvider.cs b/src/CatalogExport/CatalogExportToolProvider.cs deleted file mode 100644 index fa45343..0000000 --- a/src/CatalogExport/CatalogExportToolProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Cthangover.Core.UI.Tool; -using Godot; - -using Cthangover.Mods.Interface.Tool; -namespace Cthangover.Core.UI.Tool.CatalogExport -{ - /// - /// Registers the catalog export window as a developer tool. Moved here from - /// the old mods menu when mod management UI was delegated to the - /// mod_manager mod — exporting a catalog entry is an authoring task, - /// not part of day-to-day mod management. - /// - public class CatalogExportToolProvider : IToolProvider - { - /// Unique tool identifier. - public string Id => "catalog_export"; - - /// Translation key for the tool's display name. - public string LocaleKey => "mods/catalog_title"; - - /// Creates the export window instance. - public Window CreateWindow() => new CatalogExportWindow(); - - /// Adds the catalog export launcher button to the main menu tool block. - public void AddMenuButton(Container container) => ToolMenuButton.Attach(container, this); - } -} diff --git a/src/CatalogExport/CatalogExportWindow.cs b/src/CatalogExport/CatalogExportWindow.cs deleted file mode 100644 index cb0b21a..0000000 --- a/src/CatalogExport/CatalogExportWindow.cs +++ /dev/null @@ -1,367 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Cthangover.Core.Mods; -using Cthangover.Core.UI.Theming; -using Cthangover.Core.Utils; -using Cthangover.Mods.Interface.Theming; -using Cthangover.Mods.Interface.Tool; -using Cthangover.Tools.Common; -using Godot; - -namespace Cthangover.Core.UI.Tool.CatalogExport -{ - /// - /// Developer tool that builds a catalog entry (JSON for the online mod - /// catalog) from a locally installed mod's manifest. Ported out of the old - /// mods menu so that mod-management UI can live in the mod_manager - /// mod while this authoring utility stays a tool. - /// - /// Left: the form — read-only manifest data, catalog-only metadata - /// (download URL, categories, tags, changelog, status, minimum game - /// version, icon URL) and localized names. Right: the live JSON preview. - /// Actions: copy the JSON or export it next to the mod's zip via - /// (archive and hashes are computed there). - /// - public partial class CatalogExportWindow : ToolWindow - { - private static readonly string[] Statuses = { "stable", "beta", "deprecated", "archived" }; - private static readonly CategoryType[] Categories = Enum.GetValues(); - - private OptionButton _modSelect; - private VBoxContainer _formHost; - private CodeEdit _previewEdit; - - private ModInfo _modInfo; - private ModManifest _manifest; - private LineEdit _urlEdit; - private readonly List _categoryChecks = new(); - private LineEdit _tagsEdit; - private TextEdit _changelogEdit; - private OptionButton _statusOpt; - private LineEdit _minGameVerEdit; - private LineEdit _iconUrlEdit; - private VBoxContainer _localeContainer; - - public CatalogExportWindow() : base("mods/catalog_title") - { - Size = new Vector2I(1200, 820); - CthangoverTheme.Apply(this); - - var outer = CreateFillContainer(); - AddChild(outer); - - var toolbar = CreateToolbar(); - outer.AddChild(toolbar); - _modSelect = new OptionButton { CustomMinimumSize = new Vector2(320, 0), TooltipText = ToolUi.Tr("mods/catalog_select_mod") }; - _modSelect.ItemSelected += OnModSelected; - toolbar.AddChild(_modSelect); - toolbar.AddChild(UiKit.Spacer()); - toolbar.AddChild(ToolUi.Button("mods/catalog_copy", OnCopyPressed)); - toolbar.AddChild(ToolUi.Button("mods/catalog_export_file", OnExportPressed, ButtonKind.Primary, "tools/catalog_export/export_tip")); - - var split = new HSplitContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - outer.AddChild(split); - - _formHost = new VBoxContainer(); - var form = CreateSidebar(_formHost, 520); - form.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - split.AddChild(form); - - var previewColumn = new VBoxContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - previewColumn.AddChild(ToolUi.Section("tools/catalog_export/preview")); - _previewEdit = ToolUi.Code(editable: false); - previewColumn.AddChild(_previewEdit); - split.AddChild(previewColumn); - - SetHint(ToolUi.Tr("tools/catalog_export/hint")); - PopulateModList(); - } - - private void PopulateModList() - { - var ordered = ModRegistry.Instance.GetOrderedModIds(filterDisabled: false); - foreach (var id in ordered) - { - var info = ModRegistry.Instance.GetMod(id); - if (info == null) - continue; - - _modSelect.AddItem($"{info.DisplayTitle} ({id})"); - _modSelect.SetItemMetadata(_modSelect.ItemCount - 1, id); - } - - if (_modSelect.ItemCount > 0) - { - _modSelect.Selected = 0; - BuildForm(_modSelect.GetItemMetadata(0).AsString()); - } - } - - private void OnModSelected(long index) - { - BuildForm(_modSelect.GetItemMetadata((int)index).AsString()); - } - - private void BuildForm(string modId) - { - foreach (var child in _formHost.GetChildren()) - child.QueueFree(); - _categoryChecks.Clear(); - - _modInfo = ModRegistry.Instance.GetMod(modId) as ModInfo; - _manifest = _modInfo?.Manifest; - if (_modInfo == null || _manifest == null) - return; - - _formHost.AddChild(ToolUi.Section("tools/catalog_export/mod_info")); - AddReadOnly("tools/catalog_export/id", _modInfo.Id); - AddReadOnly("tools/catalog_export/name", _modInfo.DisplayTitle); - AddReadOnly("mods/version", _modInfo.Version ?? "—"); - AddReadOnly("mods/author", _modInfo.Author ?? "—"); - - _formHost.AddChild(ToolUi.Section("tools/catalog_export/metadata")); - _urlEdit = ToolUi.Field(_formHost, "mods/catalog_download_url", ToolUi.Text("https://…/" + modId + ".zip", _ => UpdatePreview())); - _statusOpt = ToolUi.Field(_formHost, "mods/catalog_status", new OptionButton()); - foreach (var status in Statuses) - _statusOpt.AddItem(status); - _statusOpt.ItemSelected += _ => UpdatePreview(); - _minGameVerEdit = ToolUi.Field(_formHost, "mods/catalog_min_game_version", ToolUi.Text("1.0.0", _ => UpdatePreview())); - _iconUrlEdit = ToolUi.Field(_formHost, "mods/catalog_icon_url", ToolUi.Text("https://…/icon.png", _ => UpdatePreview())); - _tagsEdit = ToolUi.Field(_formHost, "mods/catalog_tags", ToolUi.Text("story, ru", _ => UpdatePreview())); - - _formHost.AddChild(ToolUi.Section("mods/catalog_category")); - var grid = new GridContainer { Columns = 3 }; - grid.AddThemeConstantOverride("h_separation", UiTokens.Space5); - var preselected = _manifest.Category ?? CategoryType.Other; - foreach (var category in Categories) - { - var check = new CheckBox { Text = GetCategoryDisplayName(category), ButtonPressed = (preselected & category) != 0 }; - check.Toggled += _ => UpdatePreview(); - _categoryChecks.Add(check); - grid.AddChild(check); - } - _formHost.AddChild(grid); - - _formHost.AddChild(ToolUi.Section("mods/catalog_changelog")); - _changelogEdit = new TextEdit - { - CustomMinimumSize = new Vector2(0, 80), - WrapMode = TextEdit.LineWrappingMode.Boundary, - }; - _changelogEdit.TextChanged += UpdatePreview; - _formHost.AddChild(_changelogEdit); - - _formHost.AddChild(ToolUi.Section("tools/catalog_export/locales")); - _formHost.AddChild(ToolUi.Hint(ToolUi.Tr("tools/catalog_export/locales_hint"))); - _localeContainer = new VBoxContainer(); - _localeContainer.AddThemeConstantOverride("separation", UiTokens.Space1); - _formHost.AddChild(_localeContainer); - _formHost.AddChild(ToolUi.Button("mods/catalog_add_locale", () => AddLocaleRow(string.Empty, string.Empty, string.Empty))); - - UpdatePreview(); - SetStatus(string.Empty); - } - - private void AddReadOnly(string labelKey, string value) - { - var label = UiKit.Label(value, UiTokens.FontBody, UiTokens.Body); - label.AddThemeFontOverride("font", ToolUi.Mono()); - ToolUi.Field(_formHost, labelKey, label); - } - - private static string GetCategoryDisplayName(CategoryType cat) => cat switch - { - CategoryType.Story => ToolUi.Tr("mods/category_story"), - CategoryType.Characters => ToolUi.Tr("mods/category_characters"), - CategoryType.Gameplay => ToolUi.Tr("mods/category_gameplay"), - CategoryType.Visual => ToolUi.Tr("mods/category_visual"), - CategoryType.Audio => ToolUi.Tr("mods/category_audio"), - CategoryType.UI => ToolUi.Tr("mods/category_ui"), - CategoryType.Locale => ToolUi.Tr("mods/category_locale"), - CategoryType.Library => ToolUi.Tr("mods/category_library"), - CategoryType.Engine => ToolUi.Tr("mods/category_engine"), - CategoryType.Overhaul => ToolUi.Tr("mods/category_overhaul"), - CategoryType.Tools => ToolUi.Tr("mods/category_tools"), - CategoryType.Editor => ToolUi.Tr("mods/category_editor"), - CategoryType.Debug => ToolUi.Tr("mods/category_debug"), - CategoryType.Other => ToolUi.Tr("mods/category_other"), - _ => cat.ToString(), - }; - - private void AddLocaleRow(string lang, string name, string description) - { - var row = new HBoxContainer(); - row.AddThemeConstantOverride("separation", UiTokens.Space1); - - var langEdit = new LineEdit { CustomMinimumSize = new Vector2(52, 0), PlaceholderText = "en", MaxLength = 5, Text = lang }; - row.AddChild(langEdit); - var nameEdit = new LineEdit { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, PlaceholderText = ToolUi.Tr("mods/catalog_locale_name"), Text = name }; - row.AddChild(nameEdit); - var descEdit = new LineEdit { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, PlaceholderText = ToolUi.Tr("mods/catalog_locale_desc"), Text = description }; - row.AddChild(descEdit); - - var removeBtn = UiKit.IconButton("✕", ToolUi.Tr("tools/catalog_export/remove_locale")); - removeBtn.Pressed += () => RemoveLocaleRow(row); - row.AddChild(removeBtn); - - langEdit.TextChanged += _ => UpdatePreview(); - nameEdit.TextChanged += _ => UpdatePreview(); - descEdit.TextChanged += _ => UpdatePreview(); - - _localeContainer.AddChild(row); - UpdatePreview(); - } - - private void RemoveLocaleRow(HBoxContainer row) - { - _localeContainer.RemoveChild(row); - row.QueueFree(); - UpdatePreview(); - } - - private void UpdatePreview() - { - if (_previewEdit == null || _modInfo == null || _manifest == null || _changelogEdit == null) - return; - - var entry = BuildExportEntry(); - // Hashes are computed by the exporter from the real archive. - entry.Downloads[0].ArchiveSha256 = "computed-on-export"; - entry.Downloads[0].InstallSha256 = "computed-on-export"; - _previewEdit.Text = JsonSerializer.Serialize(entry, JsonOptionsProvider.WriteIndentedIgnoreNullOptions); - } - - private List CollectLocales() - { - var list = new List(); - foreach (var child in _localeContainer.GetChildren()) - { - if (child is not HBoxContainer row || row.IsQueuedForDeletion() || row.GetChildCount() < 3) - continue; - - var lang = ((LineEdit)row.GetChild(0)).Text.Trim(); - if (lang.Length == 0) - continue; - - list.Add(new CatalogLocaleEntry - { - Lang = lang, - Name = NullIfEmpty(((LineEdit)row.GetChild(1)).Text), - Description = NullIfEmpty(((LineEdit)row.GetChild(2)).Text), - }); - } - - if (list.Count == 0) - list.Add(new CatalogLocaleEntry { Lang = "en", Name = _manifest.Name, Description = _manifest.Description }); - - return list; - } - - private CatalogExportEntry BuildExportEntry() - { - var entry = BuildEntryFromManifest(_manifest); - entry.Downloads = new List - { - new() - { - Version = entry.Version, - Urls = new List { _urlEdit.Text.Trim() }, - SizeBytes = 0, - Changelog = NullIfEmpty(_changelogEdit.Text), - }, - }; - entry.Category = CollectSelectedCategories(); - entry.Tags = ParseTags(_tagsEdit.Text); - entry.Status = Statuses[Math.Max(0, _statusOpt.Selected)]; - entry.MinGameVersion = NullIfEmpty(_minGameVerEdit.Text); - entry.IconUrl = NullIfEmpty(_iconUrlEdit.Text); - entry.Locale = CollectLocales(); - return entry; - } - - private void OnCopyPressed() - { - DisplayServer.ClipboardSet(_previewEdit.Text); - SetStatus(ToolUi.Tr("tools/catalog_export/copied"), ToolStatusKind.Ok); - } - - private void OnExportPressed() - { - if (_modInfo == null) - return; - - if (string.IsNullOrWhiteSpace(_urlEdit.Text)) - { - SetStatus(ToolUi.Tr("tools/catalog_export/url_required"), ToolStatusKind.Error); - return; - } - - try - { - var entry = BuildExportEntry(); - var modsRoot = ModRegistry.ModsPath; - var isZip = _modInfo.FileProvider.RootPath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase); - var path = isZip - ? ModCatalogExporter.ExportFromZip(_modInfo, entry, modsRoot) - : ModCatalogExporter.ExportFromFolder(_modInfo, entry, modsRoot); - SetStatus(string.Format(ToolUi.Tr("tools/catalog_export/exported"), path), ToolStatusKind.Ok); - } - catch (Exception ex) - { - GameLogger.Log("CATALOG_EXPORT", $"export of '{_modInfo.Id}' failed: {ex.Message}", LogLevel.Error); - SetStatus($"{ToolUi.Tr("tools/catalog_export/export_error")}: {ex.Message}", ToolStatusKind.Error); - } - } - - private static List ParseTags(string text) - { - if (string.IsNullOrWhiteSpace(text)) - return null; - return text.Split(',').Select(t => t.Trim()).Where(t => t.Length > 0).ToList(); - } - - private static string NullIfEmpty(string text) - { - var trimmed = text?.Trim(); - return string.IsNullOrEmpty(trimmed) ? null : trimmed; - } - - private static CatalogExportEntry BuildEntryFromManifest(ModManifest manifest) - { - var depends = manifest.Depends?.Select(d => d.ToString()).Where(s => s.Length > 0).ToList(); - - return new CatalogExportEntry - { - Id = manifest.Id, - Name = manifest.Name, - Author = manifest.Author, - Version = manifest.Version, - Description = manifest.Description, - Category = manifest.Category ?? CategoryType.Other, - Depends = depends, - ConflictsWith = manifest.ConflictsWith, - }; - } - - private CategoryType CollectSelectedCategories() - { - var selected = (CategoryType)0; - for (var i = 0; i < Categories.Length && i < _categoryChecks.Count; i++) - { - if (_categoryChecks[i].ButtonPressed) - selected |= Categories[i]; - } - return selected != 0 ? selected : CategoryType.Other; - } - } -} diff --git a/src/Common/BackgroundCanvas.cs b/src/Common/BackgroundCanvas.cs deleted file mode 100644 index c4a1650..0000000 --- a/src/Common/BackgroundCanvas.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; -using Cthangover.Core.UI.Theming; -using Godot; - -namespace Cthangover.Tools.Common -{ - /// - /// Base of the editors that place things over a scene background (map - /// zones, interactive colliders, lights). Draws the background with the - /// same aspect-preserving "fit" layout the game uses and converts between - /// canvas pixels and the normalised [0..1] coordinates stored in the data. - /// Getting this rectangle wrong is what made saved positions drift: the - /// data means "fraction of the background", not "fraction of the panel". - /// - /// Subclasses paint their overlay in and rebuild - /// their s in , which is - /// also called whenever the canvas is resized or the background changes. - /// - public abstract partial class BackgroundCanvas : Control - { - private static readonly Vector2 FallbackSize = new(1920, 1024); - - /// Live handles of the current selection. - protected readonly List _handles = new(); - - /// Reference background; null draws a flat placeholder. - public Texture2D Background { get; private set; } - - protected BackgroundCanvas() - { - MouseFilter = MouseFilterEnum.Stop; - ClipContents = true; - Resized += OnResized; - Connect("draw", Callable.From((Action)DrawAll)); - } - - /// Assigns the background texture and relayouts handles. - public void SetBackground(Texture2D texture) - { - Background = texture; - RebuildHandles(); - QueueRedraw(); - } - - /// Recreates the handles from the current selection. - public abstract void RebuildHandles(); - - /// Paints the overlay above the background inside . - protected abstract void DrawContent(Rect2 rect); - - /// - /// Aspect-preserving rectangle the background occupies inside the - /// canvas — identical to the runtime "fit" layout. - /// - public Rect2 GetBackgroundRect() - { - var controlSize = Size; - if (controlSize.X <= 0f || controlSize.Y <= 0f) - return new Rect2(); - - var textureSize = Background?.GetSize() ?? FallbackSize; - if (textureSize.X <= 0f || textureSize.Y <= 0f) - textureSize = FallbackSize; - - var scale = Mathf.Min(controlSize.X / textureSize.X, controlSize.Y / textureSize.Y); - var size = textureSize * scale; - return new Rect2((controlSize - size) / 2f, size); - } - - /// Canvas-local pixel of a normalised point. - public Vector2 NormToLocal(Vector2 normalized) - { - var rect = GetBackgroundRect(); - return rect.Position + normalized * rect.Size; - } - - /// Normalised point of a canvas-local pixel (not clamped). - public Vector2 LocalToNorm(Vector2 local) - { - var rect = GetBackgroundRect(); - if (rect.Size.X <= 0f || rect.Size.Y <= 0f) - return Vector2.Zero; - return (local - rect.Position) / rect.Size; - } - - /// Clamps both axes into [0..1]. - public static Vector2 Clamp01(Vector2 value) - { - return new Vector2(Mathf.Clamp(value.X, 0f, 1f), Mathf.Clamp(value.Y, 0f, 1f)); - } - - /// Frees every handle. - protected void ClearHandles() - { - foreach (var handle in _handles) - handle.QueueFree(); - _handles.Clear(); - } - - /// - /// Adds a handle centred on a normalised point. The handle enters the - /// tree before it is positioned (setting a global position on a - /// detached Control lands it in the wrong place). - /// receives the clamped normalised point while dragging. - /// - protected DragHandle AddHandle(Vector2 normalized, Color color, int index, Action drag, float diameter = 14f) - { - var handle = new DragHandle(color, index, diameter); - handle.DragUpdate = globalCenter => drag(Clamp01(LocalToNorm(globalCenter - GlobalPosition))); - AddChild(handle); - handle.CenterAt(GlobalPosition + NormToLocal(Clamp01(normalized))); - _handles.Add(handle); - return handle; - } - - private void OnResized() - { - RebuildHandles(); - QueueRedraw(); - } - - private void DrawAll() - { - var rect = GetBackgroundRect(); - if (rect.Size.X <= 0f || rect.Size.Y <= 0f) - return; - - if (Background != null) - DrawTextureRect(Background, rect, false); - else - DrawRect(rect, UiTokens.Surface2); - - DrawContent(rect); - } - } -} diff --git a/src/Common/CollapsibleSection.cs b/src/Common/CollapsibleSection.cs deleted file mode 100644 index 0d73174..0000000 --- a/src/Common/CollapsibleSection.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Cthangover.Core.UI.Theming; -using Godot; - -namespace Cthangover.Tools.Common -{ - /// - /// Sidebar section with a clickable uppercase caption (style guide - /// .ct-section-title plus a ▸/▾ marker) that shows or hides its - /// . Editors expand the sections relevant to the selected - /// object and collapse the rest, so a long property panel stays readable - /// without losing access to rarely used fields. - /// - public partial class CollapsibleSection : VBoxContainer - { - private readonly Button _header; - private readonly string _title; - - /// Container for the section's controls. - public VBoxContainer Body { get; } - - /// Whether the body is shown. - public bool Expanded - { - get => Body.Visible; - set - { - Body.Visible = value; - _header.Text = (value ? "▾ " : "▸ ") + _title; - } - } - - /// Creates a section titled by . - public CollapsibleSection(string titleKey, bool expanded = true) - { - _title = ToolUi.Tr(titleKey).ToUpperInvariant(); - AddThemeConstantOverride("separation", UiTokens.Space2); - - _header = new Button - { - Flat = true, - Alignment = HorizontalAlignment.Left, - FocusMode = FocusModeEnum.None, - MouseDefaultCursorShape = CursorShape.PointingHand - }; - _header.AddThemeFontSizeOverride("font_size", UiTokens.FontSmall); - _header.AddThemeColorOverride("font_color", UiTokens.Muted); - _header.AddThemeColorOverride("font_hover_color", UiTokens.AccentBright); - _header.AddThemeColorOverride("font_pressed_color", UiTokens.Muted); - _header.AddThemeStyleboxOverride("normal", new StyleBoxEmpty()); - _header.AddThemeStyleboxOverride("hover", new StyleBoxEmpty()); - _header.AddThemeStyleboxOverride("pressed", new StyleBoxEmpty()); - _header.Pressed += () => Expanded = !Expanded; - AddChild(_header); - - Body = new VBoxContainer(); - Body.AddThemeConstantOverride("separation", UiTokens.Space2); - AddChild(Body); - - Expanded = expanded; - } - } -} diff --git a/src/Common/DragHandle.cs b/src/Common/DragHandle.cs deleted file mode 100644 index d9372bb..0000000 --- a/src/Common/DragHandle.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using Cthangover.Core.UI.Theming; -using Godot; - -namespace Cthangover.Tools.Common -{ - /// - /// Round draggable point used by every canvas editor (polygon vertices, - /// rect corners, markers, lights). It is a small Control placed over its - /// canvas: the left button drags it and reports the new centre in global - /// coordinates, the right button reports a delete request. Input arrives - /// through the GuiInput event and drawing through the draw - /// signal — the forms that dispatch reliably for mod assemblies. - /// - /// is free for the owner: a vertex index or a negative - /// role constant (marker, label, rect corner...). - /// - public partial class DragHandle : Control - { - /// Owner-defined identity of the handle. - public int Index; - - /// Called while dragging with the handle centre in global coordinates. - public Action DragUpdate; - - /// Called on left press. - public Action Clicked; - - /// Called on left release after a press. - public Action DragEnd; - - /// Called on right press. - public Action RightClicked; - - private readonly Color _fill; - private bool _dragging; - private bool _hovered; - private Vector2 _dragOffset; - - /// Creates a handle of the given fill colour, role index and diameter. - public DragHandle(Color fill, int index, float diameter = 14f) - { - _fill = fill; - Index = index; - Size = new Vector2(diameter, diameter); - MouseFilter = MouseFilterEnum.Stop; - MouseDefaultCursorShape = CursorShape.Drag; - GuiInput += OnGuiInput; - MouseEntered += () => { _hovered = true; QueueRedraw(); }; - MouseExited += () => { _hovered = false; QueueRedraw(); }; - Connect("draw", Callable.From((Action)Paint)); - } - - /// Places the handle so that its centre sits at ; call after it is in the tree. - public void CenterAt(Vector2 globalCenter) - { - GlobalPosition = globalCenter - Size / 2f; - } - - private void Paint() - { - var center = Size / 2f; - var radius = Size.X / 2f - 1f; - DrawCircle(center, radius, UiTokens.Surface0); - DrawCircle(center, radius - 2f, _fill); - DrawArc(center, radius, 0f, Mathf.Tau, 24, _hovered || _dragging ? UiTokens.AccentHover : UiTokens.Ink, 1.5f, true); - } - - private void OnGuiInput(InputEvent @event) - { - if (@event is InputEventMouseButton mb) - { - if (mb.ButtonIndex == MouseButton.Left) - { - if (mb.Pressed) - { - _dragging = true; - _dragOffset = GetGlobalMousePosition() - GlobalPosition; - Clicked?.Invoke(); - } - else if (_dragging) - { - _dragging = false; - DragEnd?.Invoke(); - } - AcceptEvent(); - } - else if (mb.ButtonIndex == MouseButton.Right && mb.Pressed) - { - RightClicked?.Invoke(); - AcceptEvent(); - } - } - else if (@event is InputEventMouseMotion && _dragging) - { - GlobalPosition = GetGlobalMousePosition() - _dragOffset; - DragUpdate?.Invoke(GlobalPosition + Size / 2f); - AcceptEvent(); - } - } - } -} diff --git a/src/Common/FilteredList.cs b/src/Common/FilteredList.cs deleted file mode 100644 index a45f0f4..0000000 --- a/src/Common/FilteredList.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using Cthangover.Core.UI.Theming; -using Godot; - -namespace Cthangover.Tools.Common -{ - /// - /// Search field + list used wherever an editor picks from a long catalogue - /// (backgrounds, icons, files, definitions). Items are identified by a - /// stable id, not by row index: filtering reorders and hides rows, so the - /// list keeps the id↔row mapping itself and raises / - /// with the id. is silent so - /// windows can mirror their model into the list without feedback loops. - /// - public partial class FilteredList : VBoxContainer - { - // Scroll bar, item padding and a little text room on the right. - private const int IconBoxMargin = 40; - - private readonly LineEdit _search; - private readonly ItemList _list; - private readonly List<(string Id, string Text, Texture2D Icon)> _items = new(); - private readonly List _visibleIds = new(); - private string _selectedId; - private bool _silent; - private int _fitSide; - - /// Raised when the user selects a row (click or keyboard). - public event Action Chosen; - - /// Raised on double-click / Enter. - public event Action Activated; - - /// Currently selected id, or null. - public string SelectedId => _selectedId; - - /// Creates the list; is the list part's minimum height. - public FilteredList(float minHeight = 140f, string placeholderKey = "tools/common/search") - { - AddThemeConstantOverride("separation", UiTokens.Space1); - - _search = new LineEdit - { - PlaceholderText = TranslationServer.Translate(placeholderKey), - ClearButtonEnabled = true - }; - _search.TextChanged += _ => Rebuild(); - AddChild(_search); - - _list = new ItemList - { - CustomMinimumSize = new Vector2(0, minHeight), - SizeFlagsVertical = SizeFlags.ExpandFill, - AllowReselect = true - }; - _list.ItemSelected += OnItemSelected; - _list.ItemActivated += index => Activated?.Invoke(IdAt((int)index)); - AddChild(_list); - } - - /// Turns rows into an icon grid of px thumbnails. - public void UseIconGrid(int iconSize) - { - _list.FixedIconSize = new Vector2I(iconSize, iconSize); - _list.IconMode = ItemList.IconModeEnum.Top; - _list.MaxColumns = 0; - _list.SameColumnWidth = true; - _list.FixedColumnWidth = iconSize + 24; - } - - /// - /// Scales row icons to fit a box at most px high and never wider than the - /// list (minus room for the scroll bar): pictures of any size — a 1024×1920 portrait as well as a - /// 32 px icon — keep their aspect and the row stays inside the panel. The box follows the list - /// width, so narrowing the panel shrinks the pictures further. - /// - public void FitIcons(int maxSide) - { - _fitSide = maxSide; - _list.Resized += UpdateIconBox; - UpdateIconBox(); - } - - /// Replaces all items; keeps the current selection when its id is still present. - public void SetItems(IEnumerable<(string Id, string Text, Texture2D Icon)> items) - { - _items.Clear(); - _items.AddRange(items); - Rebuild(); - } - - /// Replaces all items with text-only rows where text equals id. - public void SetItems(IEnumerable ids) - { - _items.Clear(); - foreach (var id in ids) - _items.Add((id, id, null)); - Rebuild(); - } - - /// Selects the row with (null clears) without raising events. - public void Select(string id) - { - _selectedId = id; - _silent = true; - _list.DeselectAll(); - var index = _visibleIds.IndexOf(id); - if (index >= 0) - { - _list.Select(index); - _list.EnsureCurrentIsVisible(); - } - _silent = false; - } - - /// Number of items before filtering. - public int Count => _items.Count; - - private void Rebuild() - { - var filter = _search.Text.Trim(); - _silent = true; - _list.Clear(); - _visibleIds.Clear(); - - foreach (var item in _items) - { - if (filter.Length > 0 && item.Text.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0 - && item.Id.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) - continue; - - var index = item.Icon != null ? _list.AddItem(item.Text, item.Icon) : _list.AddItem(item.Text); - _list.SetItemTooltip(index, item.Id); - _visibleIds.Add(item.Id); - } - - _silent = false; - Select(_selectedId); - } - - private void OnItemSelected(long index) - { - if (_silent) - return; - - _selectedId = IdAt((int)index); - Chosen?.Invoke(_selectedId); - } - - private void UpdateIconBox() - { - var width = (int)_list.Size.X - IconBoxMargin; - _list.FixedIconSize = new Vector2I(Math.Clamp(width, 16, _fitSide), _fitSide); - } - - private string IdAt(int index) => index >= 0 && index < _visibleIds.Count ? _visibleIds[index] : null; - } -} diff --git a/src/Common/ModFileSelector.cs b/src/Common/ModFileSelector.cs deleted file mode 100644 index 1dd1c81..0000000 --- a/src/Common/ModFileSelector.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Cthangover.Core.UI.Theming; -using Cthangover.Mods.Interface.Theming; -using Cthangover.Tools.Services; -using Godot; - -namespace Cthangover.Tools.Common -{ - /// - /// Toolbar group "mod ▾ file ▾ +" for editors that own one kind of file - /// inside a mod folder (map/*.json, interactives/*.json). - /// Only writable (folder-backed) mods are offered, and mods that already - /// contain such files come first, so the author lands on real data instead - /// of an arbitrary engine mod with nothing to edit. - /// - /// The selector never loads anything itself: it raises - /// and the window decides (usually after an - /// "unsaved changes" confirmation). When the window declines, it calls - /// with the file still open to roll the dropdowns back. - /// A null path means "this mod has no such file yet". - /// - public partial class ModFileSelector : HBoxContainer - { - private readonly string _directory; - private readonly string _extension; - private readonly OptionButton _modSelect; - private readonly OptionButton _fileSelect; - private readonly List _mods = new(); - private readonly List _files = new(); - private bool _silent; - - /// Raised with (modId, mod-relative path or null) when the author picks or creates a file. - public event Action FileRequested; - - /// Mod shown in the selector. - public string CurrentMod => _modSelect.Selected >= 0 && _modSelect.Selected < _mods.Count ? _mods[_modSelect.Selected] : null; - - /// File shown in the selector, or null. - public string CurrentPath => _fileSelect.Selected >= 0 && _fileSelect.Selected < _files.Count ? _files[_fileSelect.Selected] : null; - - /// Creates the selector for files in with . - public ModFileSelector(string directory, string extension) - { - _directory = directory; - _extension = extension; - AddThemeConstantOverride("separation", UiTokens.Space2); - - _modSelect = new OptionButton { CustomMinimumSize = new Vector2(170, 0), TooltipText = ToolUi.Tr("tools/common/mod_tip") }; - _modSelect.ItemSelected += _ => OnModPicked(); - AddChild(_modSelect); - - _fileSelect = new OptionButton { CustomMinimumSize = new Vector2(220, 0), TooltipText = ToolUi.Tr("tools/common/file_tip") }; - _fileSelect.ItemSelected += _ => OnFilePicked(); - AddChild(_fileSelect); - - var newBtn = UiKit.IconButton("+", ToolUi.Tr("tools/common/new_file")); - newBtn.Pressed += OnNewPressed; - AddChild(newBtn); - } - - /// Re-reads mods and files and requests the first file of the first mod that has any. - public void Refresh() - { - var writable = ModFileService.ListWritableMods(); - _mods.Clear(); - _mods.AddRange(writable.Where(HasFiles)); - _mods.AddRange(writable.Where(m => !HasFiles(m))); - - _silent = true; - _modSelect.Clear(); - foreach (var mod in _mods) - _modSelect.AddItem(HasFiles(mod) ? mod : mod + " ·"); - _silent = false; - - if (_mods.Count == 0) - { - FileRequested?.Invoke(null, null); - return; - } - - _modSelect.Select(0); - FillFiles(_mods[0]); - FileRequested?.Invoke(_mods[0], CurrentPath); - } - - /// Silently shows / (a not-yet-saved path is added). - public void Show(string modId, string path) - { - _silent = true; - var modIndex = _mods.IndexOf(modId); - if (modIndex >= 0) - { - _modSelect.Select(modIndex); - FillFiles(modId); - if (path != null && !_files.Contains(path)) - { - _files.Add(path); - _fileSelect.AddItem(ShortName(path)); - } - _fileSelect.Select(path != null ? _files.IndexOf(path) : -1); - } - _silent = false; - } - - private bool HasFiles(string modId) => ModFileService.ListFiles(modId, _directory, _extension).Count > 0; - - private void FillFiles(string modId) - { - _files.Clear(); - _files.AddRange(ModFileService.ListFiles(modId, _directory, _extension)); - var silent = _silent; - _silent = true; - _fileSelect.Clear(); - foreach (var file in _files) - _fileSelect.AddItem(ShortName(file)); - _fileSelect.Disabled = _files.Count == 0; - if (_files.Count > 0) - _fileSelect.Select(0); - _silent = silent; - } - - private string ShortName(string path) - { - var prefix = _directory + "/"; - return path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) ? path.Substring(prefix.Length) : path; - } - - private void OnModPicked() - { - if (_silent) - return; - var mod = CurrentMod; - FillFiles(mod); - FileRequested?.Invoke(mod, CurrentPath); - } - - private void OnFilePicked() - { - if (!_silent) - FileRequested?.Invoke(CurrentMod, CurrentPath); - } - - private void OnNewPressed() - { - if (CurrentMod == null) - return; - - var dialog = new ConfirmationDialog { Title = ToolUi.Tr("tools/common/new_file") }; - var column = new VBoxContainer(); - column.AddChild(UiKit.Hint(string.Format(ToolUi.Tr("tools/common/new_file_hint"), CurrentMod, _directory, _extension))); - var nameEdit = new LineEdit { PlaceholderText = "new_file", CustomMinimumSize = new Vector2(320, 0) }; - column.AddChild(nameEdit); - dialog.AddChild(column); - dialog.RegisterTextEnter(nameEdit); - dialog.Confirmed += () => - { - var name = SanitizeName(nameEdit.Text); - dialog.QueueFree(); - if (name.Length == 0) - return; - FileRequested?.Invoke(CurrentMod, _directory + "/" + name + _extension); - }; - dialog.Canceled += () => dialog.QueueFree(); - AddChild(dialog); - dialog.PopupCentered(); - nameEdit.GrabFocus(); - } - - private string SanitizeName(string raw) - { - var name = (raw ?? string.Empty).Trim().Replace('\\', '/').Trim('/'); - if (name.EndsWith(_extension, StringComparison.OrdinalIgnoreCase)) - name = name.Substring(0, name.Length - _extension.Length); - var chars = name.Where(c => char.IsLetterOrDigit(c) || c == '_' || c == '-' || c == '/').ToArray(); - return new string(chars); - } - } -} diff --git a/src/Common/ToolUi.cs b/src/Common/ToolUi.cs deleted file mode 100644 index ee11882..0000000 --- a/src/Common/ToolUi.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using Cthangover.Core.UI.Theming; -using Cthangover.Mods.Interface.Theming; -using Cthangover.Mods.Interface.Tool; -using Godot; - -namespace Cthangover.Tools.Common -{ - /// - /// Building blocks shared by every editor window of the tools mod, so all - /// of them read the same way: uppercase muted section captions, "label + - /// control" rows with a fixed label column, numeric spin boxes and token - /// buttons from . Editors are built in code (the - /// skill-code exception for utility tools), so this is the place where - /// their look is kept consistent with the style guide instead of each - /// window inventing its own spacing and label styles. - /// - public static class ToolUi - { - /// Width of the label column in rows. - public const int LabelWidth = 110; - - /// Translates a locale key. - public static string Tr(string key) => TranslationServer.Translate(key); - - /// Uppercase muted section caption (style guide .ct-section-title) with a little air above. - public static Control Section(string titleKey) - { - var box = new MarginContainer { MouseFilter = Control.MouseFilterEnum.Ignore }; - box.AddThemeConstantOverride("margin_top", UiTokens.Space2); - box.AddChild(UiKit.SectionTitle(Tr(titleKey), UiTokens.FontSmall)); - return box; - } - - /// - /// Adds a "label | control" row to : the label is - /// muted, fixed width and carries the optional tooltip; the control fills - /// the rest. Returns the control for chaining. - /// - public static T Field(Container parent, string labelKey, T control, string tooltipKey = null) where T : Control - { - var row = new HBoxContainer(); - row.AddThemeConstantOverride("separation", UiTokens.Space3); - - var label = new Label - { - Text = Tr(labelKey), - CustomMinimumSize = new Vector2(LabelWidth, 0), - VerticalAlignment = VerticalAlignment.Center, - ClipText = true, - MouseFilter = Control.MouseFilterEnum.Pass, - TooltipText = tooltipKey != null ? Tr(tooltipKey) : string.Empty - }; - label.AddThemeColorOverride("font_color", UiTokens.Muted); - label.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall); - row.AddChild(label); - - control.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - row.AddChild(control); - parent.AddChild(row); - return control; - } - - /// - /// Numeric field. Values are typed and shown culture-independently by - /// , which also clamps and steps them, so the editors - /// no longer parse free text ("0,5" vs "0.5"). - /// - public static SpinBox Number(double min, double max, double step, Action changed) - { - var spin = new SpinBox - { - MinValue = min, - MaxValue = max, - Step = step, - SelectAllOnFocus = true, - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - CustomArrowStep = step - }; - spin.GetLineEdit().AddThemeFontOverride("font", Mono()); - if (changed != null) - spin.ValueChanged += value => changed(value); - return spin; - } - - /// Single-line text field with a placeholder and an optional change handler. - public static LineEdit Text(string placeholder = "", Action changed = null) - { - var edit = new LineEdit { PlaceholderText = placeholder ?? string.Empty }; - if (changed != null) - edit.TextChanged += text => changed(text); - return edit; - } - - /// Token button with a localized caption, optional tooltip and kind. - public static Button Button(string textKey, Action pressed, ButtonKind kind = ButtonKind.Secondary, string tooltipKey = null) - { - var button = UiKit.Button(Tr(textKey), kind); - if (tooltipKey != null) - button.TooltipText = Tr(tooltipKey); - if (pressed != null) - button.Pressed += pressed; - return button; - } - - /// Thin vertical divider between toolbar groups. - public static VSeparator Divider() => new() { CustomMinimumSize = new Vector2(UiTokens.Space3, 0) }; - - /// Faint helper text that wraps inside narrow sidebars. - public static Label Hint(string text) - { - var label = UiKit.Hint(text); - label.AutowrapMode = TextServer.AutowrapMode.WordSmart; - label.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - label.CustomMinimumSize = new Vector2(80, 0); - return label; - } - - /// Monospace font for IDs, numbers and code (style guide --ct-mono). - public static Font Mono() => ToolWindow.GetMonospaceFont(); - - /// Code editor preset: monospace, line numbers, JSON highlighting and no wrap; scenario/scene callers select their syntax. - public static CodeEdit Code(bool editable = true) - { - var edit = new CodeEdit - { - Editable = editable, - SyntaxHighlighter = SourceSyntax.CreateJsonHighlighter(), - GuttersDrawLineNumbers = true, - HighlightCurrentLine = true, - WrapMode = TextEdit.LineWrappingMode.None, - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill, - IndentUseSpaces = true, - IndentSize = 2, - AutoBraceCompletionEnabled = false - }; - edit.AddThemeFontOverride("font", Mono()); - edit.AddThemeFontSizeOverride("font_size", UiTokens.FontBody); - edit.AddThemeColorOverride("current_line_color", UiTokens.WhiteTrace); - edit.AddThemeColorOverride("line_number_color", UiTokens.Faint); - edit.AddThemeColorOverride("font_readonly_color", UiTokens.Body); - return edit; - } - } -} diff --git a/src/EntityEditor/EntityDocument.cs b/src/EntityEditor/EntityDocument.cs deleted file mode 100644 index c0b8fb8..0000000 --- a/src/EntityEditor/EntityDocument.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Encodings.Web; -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace Cthangover.Tools.EntityEditor -{ - /// - /// The entities of one kind ("Kind": "item", "character"…) in one - /// mod, as an editor changes them, free of any UI. - /// - /// Files are kept as JSON trees, not as typed DTOs: a form changes only the - /// properties it knows, and everything else — fields of other mods' C# - /// (heal, verb, Charset), loot tables, the property - /// order — survives a save untouched. A file is written only when its text - /// differs from what was loaded or last saved, so opening and saving does not - /// reformat files nobody changed. New entities go to , - /// created on the first save when the mod has no such file. - /// - /// Localized texts are part of the document too (): the - /// form shows a name in every language of the mod, and an undo step restores - /// texts and data together. Undo works on whole states, as in the map editor. - /// - public class EntityDocument - { - private const int HistoryLimit = 200; - - private static readonly JsonSerializerOptions WriteOptions = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - - private readonly Dictionary files = new(StringComparer.Ordinal); - private readonly Dictionary savedText = new(StringComparer.Ordinal); - private readonly List records = new(); - private readonly List undo = new(); - private readonly List redo = new(); - private Dictionary texts = new(StringComparer.Ordinal); - private Dictionary savedTexts = new(StringComparer.Ordinal); - - /// Entity kind of the document. - public string Kind { get; } - - /// File new entities are added to. - public string NewFile { get; } - - /// The entities in file order (files sorted by path). - public IReadOnlyList Records => records; - - /// True when there is a step to undo. - public bool CanUndo => undo.Count > 0; - - /// True when there is a step to redo. - public bool CanRedo => redo.Count > 0; - - private EntityDocument(string kind, string newFile) - { - Kind = kind; - NewFile = newFile; - } - - /// - /// Builds the document from the mod's JSON files (path, text). Files of - /// another kind, without a kind or with broken JSON are skipped; their - /// paths are returned in only when they declare - /// this kind but cannot be read, so the window can name them. - /// - public static EntityDocument Load(string kind, string newFile, IEnumerable<(string Path, string Text)> sources, out List skipped) - { - var document = new EntityDocument(kind, newFile); - skipped = new List(); - foreach (var (path, text) in sources.OrderBy(s => s.Path, StringComparer.Ordinal)) - { - JsonObject root; - try - { - root = JsonNode.Parse(text ?? string.Empty) as JsonObject; - } - catch (JsonException) - { - if (text != null && text.Contains($"\"{kind}\"")) - skipped.Add(path); - continue; - } - if (root == null || !string.Equals(root["Kind"]?.ToString(), kind, StringComparison.OrdinalIgnoreCase)) - continue; - if (root["Items"] is not JsonArray) - root["Items"] = new JsonArray(); - document.files[path] = root; - document.savedText[path] = Serialize(root); - } - document.Rebuild(); - return document; - } - - /// Paths of the document's files. - public IEnumerable Files => files.Keys; - - /// Adds an entity to (the file is created when missing) and returns it. - public EntityRecord Add(JsonObject entity) - { - if (!files.TryGetValue(NewFile, out var root)) - { - root = new JsonObject { ["Kind"] = Kind, ["Items"] = new JsonArray() }; - files[NewFile] = root; - } - ((JsonArray)root["Items"]).Add(entity); - Rebuild(); - return records.First(r => r.Data == entity); - } - - /// A copy of with id , placed right after it in its file. - public EntityRecord Duplicate(EntityRecord record, string newId) - { - var copy = (JsonObject)record.Data.DeepClone(); - copy["Id"] = newId; - var items = (JsonArray)files[record.File]["Items"]; - items.Insert(items.IndexOf(record.Data) + 1, copy); - Rebuild(); - return records.First(r => r.Data == copy); - } - - /// Removes an entity from its file (the file stays, maybe with no items). - public void Remove(EntityRecord record) - { - ((JsonArray)files[record.File]["Items"]).Remove(record.Data); - Rebuild(); - } - - /// when no entity has it, else wanted_2, wanted_3… - public string FreeId(string wanted) - { - var id = wanted; - for (var number = 2; records.Any(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase)); number++) - id = wanted + "_" + number; - return id; - } - - /// The record with this id, or null. - public EntityRecord Find(string id) => records.FirstOrDefault(r => r.Id == id); - - /// A localized text the document holds (see ), or null when it was never set or loaded. - public string GetText(string language, string key) - => texts.TryGetValue(TextKey(language, key), out var value) ? value : null; - - /// Remembers the text the mod's files have now (loading, not an edit). - public void LoadText(string language, string key, string value) - { - var textKey = TextKey(language, key); - texts[textKey] = value; - savedTexts[textKey] = value; - } - - /// Changes a localized text; it is written on save. - public void SetText(string language, string key, string value) => texts[TextKey(language, key)] = value ?? string.Empty; - - /// Remembers the current state as one undo step and forgets the redo branch. - public void BeginChange() - { - undo.Add(Snapshot()); - if (undo.Count > HistoryLimit) - undo.RemoveAt(0); - redo.Clear(); - } - - /// Steps back; false when there is nothing to undo. - public bool Undo() => Swap(undo, redo); - - /// Steps forward again; false when there is nothing to redo. - public bool Redo() => Swap(redo, undo); - - /// Files whose text changed since loading or the last . - public List<(string Path, string Text)> ChangedFiles() - => files.Select(f => (f.Key, Serialize(f.Value))) - .Where(f => !savedText.TryGetValue(f.Key, out var saved) || saved != f.Item2) - .ToList(); - - /// Texts changed since loading or the last : language, key, value. - public List<(string Language, string Key, string Value)> ChangedTexts() - => texts.Where(t => !savedTexts.TryGetValue(t.Key, out var saved) || saved != t.Value) - .Select(t => (t.Key.Substring(0, t.Key.IndexOf('\n')), t.Key.Substring(t.Key.IndexOf('\n') + 1), t.Value)) - .ToList(); - - /// The current files and texts are what is on disk now. - public void MarkSaved() - { - foreach (var (path, text) in ChangedFiles()) - savedText[path] = text; - savedTexts = new Dictionary(texts, StringComparer.Ordinal); - } - - /// Problems that would break the game's data, as locale keys with an argument; empty when it can be saved. - public List<(string Key, string Argument)> Validate() - { - var problems = new List<(string, string)>(); - foreach (var record in records.Where(r => string.IsNullOrWhiteSpace(r.Id))) - problems.Add(("tools/entity_editor/problem_id", record.File)); - foreach (var group in records.Where(r => !string.IsNullOrWhiteSpace(r.Id)).GroupBy(r => r.Id, StringComparer.OrdinalIgnoreCase)) - { - if (group.Count() > 1) - problems.Add(("tools/entity_editor/problem_duplicate", group.Key)); - } - return problems; - } - - /// A file's text as the document writes it: two-space indented, letters unescaped, LF line breaks (the writer would use CRLF on Windows), a final one. - public static string Serialize(JsonObject root) => root.ToJsonString(WriteOptions).Replace("\r\n", "\n") + "\n"; - - private static string TextKey(string language, string key) => language + "\n" + key; - - private void Rebuild() - { - records.Clear(); - foreach (var file in files.OrderBy(f => f.Key, StringComparer.Ordinal)) - { - foreach (var item in (JsonArray)file.Value["Items"]) - { - if (item is JsonObject entity) - records.Add(new EntityRecord(file.Key, entity)); - } - } - } - - private string Snapshot() - { - var state = new JsonObject - { - ["files"] = new JsonObject(files.Select(f => new KeyValuePair(f.Key, f.Value.DeepClone()))), - ["texts"] = new JsonObject(texts.Select(t => new KeyValuePair(t.Key, t.Value))) - }; - return state.ToJsonString(); - } - - private bool Swap(List from, List to) - { - if (from.Count == 0) - return false; - to.Add(Snapshot()); - var state = JsonNode.Parse(from[^1]).AsObject(); - from.RemoveAt(from.Count - 1); - files.Clear(); - foreach (var file in state["files"].AsObject()) - files[file.Key] = file.Value.DeepClone().AsObject(); - texts = state["texts"].AsObject().ToDictionary(t => t.Key, t => t.Value?.ToString() ?? string.Empty, StringComparer.Ordinal); - Rebuild(); - return true; - } - } -} diff --git a/src/EntityEditor/EntityEditorWindow.cs b/src/EntityEditor/EntityEditorWindow.cs deleted file mode 100644 index 58c1059..0000000 --- a/src/EntityEditor/EntityEditorWindow.cs +++ /dev/null @@ -1,740 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Nodes; -using Cthangover.Core.Mods; -using Cthangover.Core.Mods.Resolvers; -using Cthangover.Core.UI.Theming; -using Cthangover.Core.Utils; -using Cthangover.Mods.Interface.Theming; -using Cthangover.Mods.Interface.Tool; -using Cthangover.Mods.Schemas; -using Cthangover.Tools.Common; -using Cthangover.Tools.Schemas; -using Cthangover.Tools.Services; -using Godot; - -namespace Cthangover.Tools.EntityEditor -{ - /// - /// Base of the data editors (items, characters…): every entity of one kind in - /// one writable mod, a search list on the left and a form on the right built - /// from the editor's list. Layout and behaviour are - /// shared by all such editors, so they look and work alike; a concrete editor - /// only names its kind, its fields and how a new entity starts out. - /// - /// For a beginner the form is all there is: a name is typed in each language - /// of the mod (the locale key is made from the id), a picture is picked from a - /// grid, lists are check boxes. Save writes the mod's data and locale files and - /// makes the running game re-read them (entities, then texts), so the change is - /// visible at once. For a professional nothing is hidden: the locale key stays - /// editable, and every property the form does not know is shown as JSON under - /// "other fields", edited there and written back as it is. - /// - /// Fields other mods describe for the kind in their schemas (schemas/*.json: the - /// equipment mod adds Equip and Traits to items, the stats add Archetype - /// and Stats to characters) get their own block, "fields of other mods", with the - /// schema editors, so a data editor grows with the mods loaded next to it without - /// knowing them. - /// - /// Every change is undoable (Ctrl+Z / Ctrl+Y): one undo step per editing - /// session of one entity, and one per add, duplicate or delete. - /// - public abstract partial class EntityEditorWindow : ToolWindow - { - private readonly string kind; - private readonly string folder; - private readonly List fields; - private readonly Dictionary controls = new(); - private readonly Dictionary keyEdits = new(); - private readonly Dictionary> textEdits = new(); - private readonly Dictionary previews = new(); - private readonly Dictionary icons = new(); - private readonly List mods = new(); - private readonly List schemaEditors = new(); - - private OptionButton modPicker; - private FilteredList list; - private VBoxContainer form; - private Label emptyHint; - private Label fileLabel; - private LineEdit idEdit; - private CodeEdit otherEdit; - private EntityDocument document; - private EntityRecord selected; - private List languages = new(); - private string modId; - private bool suppress; - private bool editing; - - /// Builds the window for entities of ; new ones go to <folder>/<mod>.json. - protected EntityEditorWindow(string titleKey, string entityKind, string entityFolder, List entityFields) : base(titleKey) - { - kind = entityKind; - folder = entityFolder; - fields = entityFields; - CthangoverTheme.Apply(this); - BuildUi(); - SetHint(ToolUi.Tr("tools/entity_editor/hint")); - FillMods(); - } - - /// A new entity's JSON: the kind's required fields with starting values. - protected abstract JsonObject CreateEntity(string id); - - /// Id a new entity starts from (numbered when taken), e.g. <mod>/new_item. - protected abstract string NewIdBase(string mod); - - /// The edited mod, or null. - public string ModId => modId; - - /// The selected entity's id, or null. - public string SelectedId => selected?.Id; - - /// The edited entities (null before a mod is opened). - public EntityDocument Document => document; - - // ── layout ───────────────────────────────────────────────── - - private void BuildUi() - { - var outer = CreateFillContainer(); - AddChild(outer); - outer.AddChild(BuildToolbar()); - - var main = new HSplitContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, SizeFlagsVertical = Control.SizeFlags.ExpandFill }; - outer.AddChild(main); - - var listColumn = new VBoxContainer { SizeFlagsVertical = Control.SizeFlags.ExpandFill }; - listColumn.AddChild(ToolUi.Section("tools/entity_editor/section_list")); - list = new FilteredList(320f, "tools/entity_editor/search"); - list.SizeFlagsVertical = Control.SizeFlags.ExpandFill; - list.FitIcons(64); - list.Chosen += OnChosen; - listColumn.AddChild(list); - var listPanel = CreateSidebar(listColumn, 320); - main.AddChild(listPanel); - - var scroll = new ScrollContainer { HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled, SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; - main.AddChild(scroll); - var column = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; - column.AddThemeConstantOverride("separation", UiTokens.Space2); - scroll.AddChild(column); - emptyHint = ToolUi.Hint(ToolUi.Tr("tools/entity_editor/no_entity")); - column.AddChild(emptyHint); - form = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; - form.AddThemeConstantOverride("separation", UiTokens.Space2); - column.AddChild(form); - BuildForm(); - } - - private HBoxContainer BuildToolbar() - { - var toolbar = CreateToolbar(); - modPicker = new OptionButton { TooltipText = ToolUi.Tr("tools/entity_editor/mod_tip") }; - modPicker.ItemSelected += OnModPicked; - toolbar.AddChild(modPicker); - toolbar.AddChild(ToolUi.Button("tools/common/save", OnSavePressed, ButtonKind.Primary, "tools/common/save_tip")); - toolbar.AddChild(ToolUi.Divider()); - toolbar.AddChild(ToolUi.Button("tools/entity_editor/new", AddNew, ButtonKind.Secondary, "tools/entity_editor/new_tip")); - toolbar.AddChild(ToolUi.Button("tools/entity_editor/duplicate", DuplicateSelected, ButtonKind.Secondary, "tools/entity_editor/duplicate_tip")); - toolbar.AddChild(ToolUi.Button("tools/entity_editor/delete", DeleteSelected, ButtonKind.Secondary, "tools/entity_editor/delete_tip")); - toolbar.AddChild(ToolUi.Divider()); - toolbar.AddChild(ToolUi.Button("tools/entity_editor/undo", Undo, ButtonKind.Secondary, "tools/entity_editor/undo_tip")); - toolbar.AddChild(ToolUi.Button("tools/entity_editor/redo", Redo, ButtonKind.Secondary, "tools/entity_editor/redo_tip")); - return toolbar; - } - - private void BuildForm() - { - fileLabel = ToolUi.Hint(string.Empty); - form.AddChild(fileLabel); - idEdit = ToolUi.Field(form, "tools/entity_editor/id", ToolUi.Text("", OnTextChanged), "tools/entity_editor/id_tip"); - idEdit.AddThemeFontOverride("font", ToolUi.Mono()); - foreach (var field in fields) - BuildField(field); - BuildSchemaFields(); - - form.AddChild(ToolUi.Section("tools/entity_editor/section_other")); - form.AddChild(ToolUi.Hint(ToolUi.Tr("tools/entity_editor/other_hint"))); - otherEdit = ToolUi.Code(); - otherEdit.CustomMinimumSize = new Vector2(0, 160); - otherEdit.SizeFlagsVertical = Control.SizeFlags.Fill; - otherEdit.TextChanged += OnOtherChanged; - form.AddChild(otherEdit); - } - - private void BuildField(EntityField field) - { - switch (field.Kind) - { - case EntityFieldKind.Text: - controls[field] = ToolUi.Field(form, field.LabelKey, ToolUi.Text("", OnTextChanged), field.TooltipKey); - break; - case EntityFieldKind.Integer: - controls[field] = ToolUi.Field(form, field.LabelKey, ToolUi.Number(-999999, 999999, 1, OnNumberChanged), field.TooltipKey); - break; - case EntityFieldKind.Choice: - var choice = ToolUi.Field(form, field.LabelKey, new OptionButton(), field.TooltipKey); - choice.ItemSelected += OnChoicePicked; - controls[field] = choice; - break; - case EntityFieldKind.Flags: - case EntityFieldKind.List: - controls[field] = ToolUi.Field(form, field.LabelKey, new HFlowContainer(), field.TooltipKey); - break; - case EntityFieldKind.Picture: - BuildPicture(field); - break; - case EntityFieldKind.LocaleText: - // The texts get their rows when a mod is opened: its languages decide them. - keyEdits[field] = ToolUi.Field(form, field.LabelKey, ToolUi.Text("", OnTextChanged), "tools/entity_editor/locale_key_tip"); - keyEdits[field].AddThemeFontOverride("font", ToolUi.Mono()); - var texts = new VBoxContainer(); - form.AddChild(texts); - controls[field] = texts; - textEdits[field] = new Dictionary(); - break; - } - } - - private void BuildSchemaFields() - { - var described = Cthangover.Mods.Schemas.SchemaCatalog.Current.FieldsOf(kind) - .Where(f => !string.Equals(f.Key, "Id", StringComparison.OrdinalIgnoreCase) && !fields.Any(e => string.Equals(e.Key, f.Key, StringComparison.OrdinalIgnoreCase))) - .ToList(); - if (described.Count == 0) - return; - form.AddChild(ToolUi.Section("tools/entity_editor/section_schema")); - var sources = string.Join(", ", described.Select(f => f.SourceMod).Where(m => m != null).Distinct()); - form.AddChild(ToolUi.Hint(string.Format(ToolUi.Tr("tools/entity_editor/schema_hint"), sources))); - foreach (var field in described) - schemaEditors.Add(SchemaFieldEditors.Create(form, field, WriteForm)); - } - - private void BuildPicture(EntityField field) - { - var row = new HBoxContainer(); - var path = ToolUi.Text(field.Group + "/…", OnPictureTyped); - path.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - row.AddChild(path); - var pick = UiKit.IconButton("…", ToolUi.Tr("tools/entity_editor/picture_pick_tip")); - pick.Pressed += () => OpenPicker(field); - row.AddChild(pick); - ToolUi.Field(form, field.LabelKey, row, field.TooltipKey); - controls[field] = path; - var preview = new TextureRect - { - CustomMinimumSize = new Vector2(96, 96), - ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize, - StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered, - TextureFilter = CanvasItem.TextureFilterEnum.Nearest, - SizeFlagsHorizontal = Control.SizeFlags.ShrinkBegin - }; - var margin = new MarginContainer(); - margin.AddThemeConstantOverride("margin_left", ToolUi.LabelWidth + UiTokens.Space3); - margin.AddChild(preview); - form.AddChild(margin); - previews[field] = preview; - } - - // ── mods and the list ────────────────────────────────────── - - private void FillMods() - { - mods.Clear(); - mods.AddRange(ModFileService.ListWritableMods()); - modPicker.Clear(); - foreach (var mod in mods) - modPicker.AddItem(mod); - if (mods.Count == 0) - { - SetStatus(ToolUi.Tr("tools/common/no_writable_mods"), ToolStatusKind.Error); - return; - } - // Start in the first mod that already has such entities: usually the game's own content. - var start = mods.FindIndex(m => LoadDocument(m).Records.Count > 0); - OpenMod(mods[Math.Max(0, start)]); - } - - private EntityDocument LoadDocument(string mod) - { - var sources = ModFileService.ListFiles(mod, string.Empty, ".json") - .Select(path => (path, ModFileService.ReadText(mod, path))); - var loaded = EntityDocument.Load(kind, $"{folder}/{mod}.json", sources, out var skipped); - if (skipped.Count > 0) - SetStatus(string.Format(ToolUi.Tr("tools/entity_editor/broken_files"), string.Join(", ", skipped)), ToolStatusKind.Error); - return loaded; - } - - /// Opens the entities of a mod (asks first about unsaved changes). - public void OpenMod(string mod) - { - RunDiscardingChanges(() => DoOpenMod(mod), () => modPicker.Select(mods.IndexOf(modId))); - } - - private void DoOpenMod(string mod) - { - modId = mod; - modPicker.Select(mods.IndexOf(mod)); - document = LoadDocument(mod); - languages = LocaleFileService.Languages(mod); - foreach (var field in fields.Where(f => f.Kind == EntityFieldKind.LocaleText)) - BuildTextRows(field); - foreach (var locale in schemaEditors.OfType()) - locale.Open(document, mod, languages); - MarkClean(); - selected = null; - RefreshList(); - Select(document.Records.FirstOrDefault()); - SetStatus(string.Format(ToolUi.Tr("tools/entity_editor/opened"), document.Records.Count, mod)); - } - - private void BuildTextRows(EntityField field) - { - var box = (VBoxContainer)controls[field]; - ClearChildren(box); - textEdits[field].Clear(); - foreach (var language in languages) - { - var label = string.Format(ToolUi.Tr("tools/entity_editor/text_in"), language); - var edit = ToolUi.Text("", OnTextChanged); - var row = new HBoxContainer(); - row.AddThemeConstantOverride("separation", UiTokens.Space3); - row.AddChild(new Label { Text = label, CustomMinimumSize = new Vector2(ToolUi.LabelWidth, 0), HorizontalAlignment = HorizontalAlignment.Right }); - edit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - row.AddChild(edit); - box.AddChild(row); - textEdits[field][language] = edit; - } - } - - private void RefreshList() - { - var first = fields.FirstOrDefault(f => f.Kind == EntityFieldKind.Picture); - var name = fields.FirstOrDefault(f => f.Kind == EntityFieldKind.LocaleText); - list.SetItems(document.Records.Select(r => (r.Id, RowText(r, name), first == null ? null : Icon(first.Group, Str(r.Data, first.Key))))); - list.Select(selected?.Id); - } - - private string RowText(EntityRecord record, EntityField nameField) - { - var key = nameField == null ? null : Str(record.Data, nameField.Key); - var text = key == null ? null : document.GetText(languages.FirstOrDefault() ?? "ru", key) ?? LocaleFileService.Read(modId, languages.FirstOrDefault() ?? "ru", key); - return string.IsNullOrWhiteSpace(text) ? record.Id : $"{text} · {record.Id}"; - } - - private Texture2D Icon(string group, string id) - { - if (string.IsNullOrEmpty(id)) - return null; - var cacheKey = group + "|" + id; - if (!icons.TryGetValue(cacheKey, out var texture)) - { - texture = ModResourceService.LoadGroupImage(group, id); - icons[cacheKey] = texture; - } - return texture; - } - - private void OnChosen(string id) => Select(document?.Find(id)); - - private void OnModPicked(long index) - { - if (index >= 0 && index < mods.Count && mods[(int)index] != modId) - OpenMod(mods[(int)index]); - } - - // ── form ─────────────────────────────────────────────────── - - /// Shows an entity in the form (null: none). - public void Select(EntityRecord record) - { - selected = record; - editing = false; - emptyHint.Visible = record == null; - form.Visible = record != null; - list.Select(record?.Id); - if (record == null) - return; - - suppress = true; - fileLabel.Text = string.Format(ToolUi.Tr("tools/entity_editor/file"), modId, record.File); - idEdit.Text = record.Id; - foreach (var field in fields) - ShowField(field, record.Data); - foreach (var editor in schemaEditors) - editor.Load(record.Data.FirstOrDefault(p => string.Equals(p.Key, editor.Field.Key, StringComparison.OrdinalIgnoreCase)).Value); - otherEdit.Text = OtherJson(record.Data); - suppress = false; - } - - private void ShowField(EntityField field, JsonObject data) - { - switch (field.Kind) - { - case EntityFieldKind.Text: - ((LineEdit)controls[field]).Text = Str(data, field.Key) ?? string.Empty; - break; - case EntityFieldKind.Integer: - ((SpinBox)controls[field]).Value = data[field.Key] is JsonValue v && v.TryGetValue(out int number) ? number : 0; - break; - case EntityFieldKind.Choice: - FillChoice((OptionButton)controls[field], field, Str(data, field.Key)); - break; - case EntityFieldKind.Flags: - FillChecks((HFlowContainer)controls[field], field, (Str(data, field.Key) ?? string.Empty).Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)); - break; - case EntityFieldKind.List: - FillChecks((HFlowContainer)controls[field], field, (data[field.Key] as JsonArray)?.Select(n => n?.ToString()).Where(s => s != null).ToArray() ?? Array.Empty()); - break; - case EntityFieldKind.Picture: - var path = Str(data, field.Key) ?? string.Empty; - ((LineEdit)controls[field]).Text = path; - previews[field].Texture = ModResourceService.LoadGroupImage(field.Group, path); - break; - case EntityFieldKind.LocaleText: - var key = Str(data, field.Key) ?? string.Empty; - keyEdits[field].Text = key; - foreach (var pair in textEdits[field]) - pair.Value.Text = TextOf(pair.Key, key); - break; - } - } - - private string TextOf(string language, string key) - { - if (string.IsNullOrEmpty(key)) - return string.Empty; - var text = document.GetText(language, key); - if (text != null) - return text; - text = LocaleFileService.Read(modId, language, key) ?? string.Empty; - document.LoadText(language, key, text); - return text; - } - - private static void FillChoice(OptionButton picker, EntityField field, string value) - { - var options = (field.Options?.Invoke() ?? Enumerable.Empty()).ToList(); - if (!string.IsNullOrEmpty(value) && !options.Contains(value)) - options.Insert(0, value); - picker.Clear(); - foreach (var option in options) - picker.AddItem(option); - picker.Select(options.IndexOf(value ?? string.Empty)); - } - - private void FillChecks(HFlowContainer box, EntityField field, string[] values) - { - ClearChildren(box); - var options = (field.Options?.Invoke() ?? Enumerable.Empty()).ToList(); - options.AddRange(values.Where(v => !options.Contains(v))); - foreach (var option in options) - { - var check = new CheckBox { Text = option, ButtonPressed = values.Contains(option) }; - check.Toggled += OnChecked; - box.AddChild(check); - } - } - - private void OnTextChanged(string text) => WriteForm(); - - private void OnNumberChanged(double value) => WriteForm(); - - private void OnChoicePicked(long index) => WriteForm(); - - private void OnChecked(bool on) => WriteForm(); - - private void OnPictureTyped(string text) - { - WriteForm(); - foreach (var pair in previews) - pair.Value.Texture = ModResourceService.LoadGroupImage(pair.Key.Group, ((LineEdit)controls[pair.Key]).Text); - } - - private void OpenPicker(EntityField field) - { - if (selected == null) - return; - var dialog = new PicturePickerDialog(field.Group, ((LineEdit)controls[field]).Text); - dialog.Picked += id => ((LineEdit)controls[field]).Text = id; - dialog.Picked += OnPictureTyped; - AddChild(dialog); - dialog.PopupCentered(); - } - - /// Copies the form into the selected entity; the first change of a session records an undo step. - private void WriteForm() - { - if (suppress || selected == null) - return; - if (!editing) - { - document.BeginChange(); - editing = true; - } - - var data = selected.Data; - data["Id"] = idEdit.Text.Trim(); - foreach (var field in fields) - WriteField(field, data); - foreach (var editor in schemaEditors) - { - var key = data.FirstOrDefault(p => string.Equals(p.Key, editor.Field.Key, StringComparison.OrdinalIgnoreCase)).Key ?? editor.Field.Key; - var value = editor.Save(); - if (value == null) - data.Remove(key); - else - data[key] = value; - } - SetDirty(); - RefreshList(); - } - - private void WriteField(EntityField field, JsonObject data) - { - switch (field.Kind) - { - case EntityFieldKind.Text: - case EntityFieldKind.Picture: - SetOrRemove(data, field.Key, ((LineEdit)controls[field]).Text.Trim()); - break; - case EntityFieldKind.Integer: - var number = (int)((SpinBox)controls[field]).Value; - if (number != 0 || data.ContainsKey(field.Key)) - data[field.Key] = number; - break; - case EntityFieldKind.Choice: - var picker = (OptionButton)controls[field]; - if (picker.Selected >= 0) - data[field.Key] = picker.GetItemText(picker.Selected); - break; - case EntityFieldKind.Flags: - var flags = Checked((HFlowContainer)controls[field]); - if (flags.Count > 0 || data.ContainsKey(field.Key)) - data[field.Key] = flags.Count == 0 ? "None" : string.Join(",", flags); - break; - case EntityFieldKind.List: - var values = Checked((HFlowContainer)controls[field]); - if (values.Count > 0 || data.ContainsKey(field.Key)) - data[field.Key] = new JsonArray(values.Select(v => (JsonNode)v).ToArray()); - break; - case EntityFieldKind.LocaleText: - var key = keyEdits[field].Text.Trim(); - SetOrRemove(data, field.Key, key); - if (key.Length == 0) - break; - foreach (var pair in textEdits[field]) - document.SetText(pair.Key, key, pair.Value.Text); - break; - } - } - - private static List Checked(HFlowContainer box) - => box.GetChildren().OfType().Where(c => c.ButtonPressed).Select(c => c.Text).ToList(); - - /// Takes the children out of the tree at once (a search of the tree must not find them) and frees them. - private static void ClearChildren(Node box) - { - foreach (var child in box.GetChildren()) - { - box.RemoveChild(child); - child.QueueFree(); - } - } - - private static void SetOrRemove(JsonObject data, string key, string value) - { - if (value.Length > 0) - data[key] = value; - else - data.Remove(key); - } - - /// The properties no field shows, as an indented JSON object. - private string OtherJson(JsonObject data) - { - var other = new JsonObject(); - foreach (var pair in data.Where(p => !IsFormKey(p.Key))) - other[pair.Key] = pair.Value?.DeepClone(); - return EntityDocument.Serialize(other).TrimEnd(); - } - - private bool IsFormKey(string key) - => key == "Id" || fields.Any(f => f.Key == key) || schemaEditors.Any(e => string.Equals(e.Field.Key, key, StringComparison.OrdinalIgnoreCase)); - - private void OnOtherChanged() - { - if (suppress || selected == null) - return; - JsonObject other; - try - { - other = JsonNode.Parse(otherEdit.Text) as JsonObject; - } - catch (JsonException ex) - { - SetStatus(string.Format(ToolUi.Tr("tools/entity_editor/other_invalid"), ex.Message), ToolStatusKind.Error); - return; - } - if (other == null) - return; - if (!editing) - { - document.BeginChange(); - editing = true; - } - var data = selected.Data; - foreach (var key in data.Select(p => p.Key).Where(k => !IsFormKey(k)).ToList()) - data.Remove(key); - foreach (var pair in other.Where(p => !IsFormKey(p.Key)).ToList()) - data[pair.Key] = pair.Value?.DeepClone(); - SetDirty(); - SetStatus(ToolUi.Tr("tools/entity_editor/other_applied")); - } - - // ── commands ─────────────────────────────────────────────── - - private void AddNew() - { - if (document == null) - return; - document.BeginChange(); - var id = document.FreeId(NewIdBase(modId)); - var entity = CreateEntity(id); - foreach (var field in fields.Where(f => f.Kind == EntityFieldKind.LocaleText && f.KeyPattern != null)) - { - var key = string.Format(field.KeyPattern, id); - entity[field.Key] = key; - foreach (var language in languages) - document.SetText(language, key, field.Key == "Name" ? id.Substring(id.LastIndexOf('/') + 1) : string.Empty); - } - var record = document.Add(entity); - SetDirty(); - RefreshList(); - Select(record); - SetStatus(string.Format(ToolUi.Tr("tools/entity_editor/added"), id, record.File)); - } - - private void DuplicateSelected() - { - if (selected == null) - return; - document.BeginChange(); - var record = document.Duplicate(selected, document.FreeId(selected.Id)); - SetDirty(); - RefreshList(); - Select(record); - } - - private void DeleteSelected() - { - if (selected == null) - return; - ShowConfirm("tools/entity_editor/delete_title", "tools/entity_editor/delete_text", ConfirmDelete); - } - - private void ConfirmDelete() - { - if (selected == null) - return; - document.BeginChange(); - var id = selected.Id; - document.Remove(selected); - SetDirty(); - Select(null); - RefreshList(); - SetStatus(string.Format(ToolUi.Tr("tools/entity_editor/deleted"), id)); - } - - private void Undo() => StepHistory(document != null && document.Undo()); - - private void Redo() => StepHistory(document != null && document.Redo()); - - private void StepHistory(bool stepped) - { - if (!stepped) - return; - // The document holds new JSON objects now: find the selection again by id. - var id = selected?.Id; - SetDirty(); - RefreshList(); - Select(document.Find(id) ?? document.Records.FirstOrDefault()); - } - - /// Ctrl+S. - protected override void OnSaveShortcut() => Save(); - - private void OnSavePressed() => Save(); - - /// - /// Writes the changed data and locale files and makes the game re-read - /// them: the entity cache is dropped (factories rebuild from the new - /// snapshot) and the current language's texts are reloaded. - /// - public bool Save() - { - if (document == null) - return false; - var problems = document.Validate(); - if (problems.Count > 0) - { - SetStatus(string.Format(ToolUi.Tr(problems[0].Key), problems[0].Argument), ToolStatusKind.Error); - return false; - } - - var changedFiles = document.ChangedFiles(); - var changedTexts = document.ChangedTexts(); - foreach (var (path, text) in changedFiles) - { - if (!ModFileService.WriteText(modId, path, text)) - { - SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error); - return false; - } - } - foreach (var (language, key, value) in changedTexts) - LocaleFileService.Write(modId, language, key, value); - - document.MarkSaved(); - MarkClean(); - editing = false; - ModManager.Instance.GetResolver()?.ClearCache(); - ModManager.Instance.GetResolver()?.ClearCache(); - icons.Clear(); - if (changedTexts.Count > 0) - LocaleFileService.ReloadGame(); - GameLogger.Log("TOOLS", $"entity editor: saved {changedFiles.Count} {kind} file(s), {changedTexts.Count} text(s) of '{modId}'"); - SetStatus(string.Format(ToolUi.Tr("tools/entity_editor/saved"), changedFiles.Count, changedTexts.Count, modId), ToolStatusKind.Ok); - return true; - } - - /// Shortcuts: Ctrl+Z / Ctrl+Y undo and redo, Ctrl+N adds, Ctrl+D duplicates. - protected override bool HandleKey(InputEventKey key) - { - if (!key.CtrlPressed) - return false; - switch (key.Keycode) - { - case Key.Z when key.ShiftPressed: - case Key.Y: - Redo(); - return true; - case Key.Z: - Undo(); - return true; - case Key.N: - AddNew(); - return true; - case Key.D: - DuplicateSelected(); - return true; - } - return false; - } - - private static string Str(JsonObject data, string key) => data[key]?.ToString(); - } -} diff --git a/src/EntityEditor/EntityField.cs b/src/EntityEditor/EntityField.cs deleted file mode 100644 index b6c74f8..0000000 --- a/src/EntityEditor/EntityField.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Cthangover.Tools.EntityEditor -{ - /// - /// One property of an entity as an form shows - /// it. A concrete editor lists its fields; properties no field names are - /// shown and edited as raw JSON ("other fields"), so the form never hides or - /// drops data a mod added. - /// - public class EntityField - { - /// JSON property name. - public string Key { get; init; } - - /// Locale key of the label. - public string LabelKey { get; init; } - - /// Locale key of the tooltip, or null. - public string TooltipKey { get; init; } - - /// How the value is edited. - public EntityFieldKind Kind { get; init; } - - /// Picture: the file group of the pictures (items, characters…). - public string Group { get; init; } - - /// Choice, flags, list: the values offered (read when the form is built). - public Func> Options { get; init; } - - /// Locale text: how a new entity's key is made from its id ({0} = id). - public string KeyPattern { get; init; } - } -} diff --git a/src/EntityEditor/EntityFieldKind.cs b/src/EntityEditor/EntityFieldKind.cs deleted file mode 100644 index df5aaf3..0000000 --- a/src/EntityEditor/EntityFieldKind.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Cthangover.Tools.EntityEditor -{ - /// How an is shown and stored. - public enum EntityFieldKind - { - /// One line of text. - Text, - /// A locale key with its text in every language of the mod. - LocaleText, - /// A whole number. - Integer, - /// A picture path in a file group, with a preview and a picker. - Picture, - /// One value of a list (values outside the list stay possible). - Choice, - /// Several names of a list written as "A,B" (an enum with flags). - Flags, - /// A JSON array of strings picked from a list. - List - } -} diff --git a/src/EntityEditor/EntityRecord.cs b/src/EntityEditor/EntityRecord.cs deleted file mode 100644 index 053f07b..0000000 --- a/src/EntityEditor/EntityRecord.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json.Nodes; - -namespace Cthangover.Tools.EntityEditor -{ - /// One entity of an : its JSON object inside the Items array of its file. - public class EntityRecord - { - /// Mod-relative path of the file holding the entity. - public string File { get; } - - /// The entity itself; unknown properties are kept as they are. - public JsonObject Data { get; } - - /// The entity's Id (empty when missing). - public string Id => Data["Id"]?.ToString() ?? string.Empty; - - /// Wraps an entity of a file. - public EntityRecord(string file, JsonObject data) - { - File = file; - Data = data; - } - } -} diff --git a/src/EntityEditor/PicturePickerDialog.cs b/src/EntityEditor/PicturePickerDialog.cs deleted file mode 100644 index 106c5c9..0000000 --- a/src/EntityEditor/PicturePickerDialog.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Linq; -using Cthangover.Mods.Interface.Tool; -using Cthangover.Tools.Common; -using Cthangover.Tools.Services; -using Godot; - -namespace Cthangover.Tools.EntityEditor -{ - /// - /// Grid of every picture of a file group with a search, for a picture field - /// of an entity form: a double click or OK picks one. The pictures are read - /// when the dialog opens, so a file just dropped into a mod folder shows up - /// (after the game re-reads its files: the editor's Save does that). - /// - public partial class PicturePickerDialog : AcceptDialog - { - private readonly FilteredList list; - - /// A picture was picked: its id in the group. - public event Action Picked; - - /// Builds the dialog for the pictures of . - public PicturePickerDialog(string group, string current) - { - Theme = ToolTheme.Get(); - Title = string.Format(ToolUi.Tr("tools/entity_editor/picture_pick_title"), group); - MinSize = new Vector2I(640, 520); - list = new FilteredList(420f); - list.UseIconGrid(64); - list.SetItems(ModResourceService.GetGroupImageIds(group) - .Select(id => (id, id.Substring(id.LastIndexOf('/') + 1), ModResourceService.LoadGroupImage(group, id)))); - list.Select(current); - list.Activated += OnActivated; - AddChild(list); - Confirmed += OnConfirmed; - CloseRequested += QueueFree; - Canceled += QueueFree; - } - - private void OnActivated(string id) - { - if (id == null) - return; - Picked?.Invoke(id); - Hide(); - QueueFree(); - } - - private void OnConfirmed() - { - if (list.SelectedId != null) - Picked?.Invoke(list.SelectedId); - QueueFree(); - } - } -} diff --git a/src/InteractiveEditor/InteractiveEditorCanvas.cs b/src/InteractiveEditor/InteractiveEditorCanvas.cs deleted file mode 100644 index 244550b..0000000 --- a/src/InteractiveEditor/InteractiveEditorCanvas.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.Linq; -using Cthangover.Core.Interactive; -using Cthangover.Core.UI.Theming; -using Cthangover.Tools.Common; -using Godot; - -namespace Cthangover.Tools.InteractiveEditor -{ - /// - /// Preview of the interactives of one file over a scene background. Every - /// collider is drawn (others faint, the selected one filled), so the author - /// sees how objects of the same scene sit next to each other; clicking a - /// collider selects it. The selected collider gets handles: - /// - /// rect — top-left and bottom-right corners plus a centre handle - /// that moves the whole rect; - /// circle — centre and a radius handle; - /// polygon — one handle per vertex (right-click deletes it); a click - /// on empty background appends a vertex. - /// - /// Geometry is in the same normalised background space as - /// InteractiveObject: x/y/width/height are fractions of the - /// background size, a circle radius is a fraction of its shorter side. - /// - public partial class InteractiveEditorCanvas : BackgroundCanvas - { - private const int TopLeft = 0; - private const int BottomRight = 1; - private const int Move = 2; - private const int RadiusHandle = 1; - private const float MinExtent = 0.005f; - - /// Data being edited; set once by the window. - public InteractiveEditorController Controller; - - /// Raised whenever geometry of the selected item changes. - public event Action Changed; - - /// Raised when a click on the canvas selects another item (-1: none). - public event Action ItemPicked; - - public InteractiveEditorCanvas() - { - GuiInput += OnGuiInput; - } - - /// Recreates the handles of the selected item's collider. - public override void RebuildHandles() - { - ClearHandles(); - - var hit = Controller?.Selected?.HitArea; - if (hit == null || GetBackgroundRect().Size.X <= 0f) - return; - - switch (hit.Type) - { - case "circle": - AddHandle(new Vector2(hit.X, hit.Y), UiTokens.Info, Move, p => { hit.X = p.X; hit.Y = p.Y; Moved(); }); - AddHandle(RadiusPoint(hit), UiTokens.Info, RadiusHandle, p => - { - var shortSide = ShortSide(); - var centre = NormToLocal(new Vector2(hit.X, hit.Y)); - hit.Radius = Mathf.Max(MinExtent, (NormToLocal(p) - centre).Length() / shortSide); - Moved(); - }); - break; - case "polygon": - var vertices = hit.Vertices ?? Array.Empty(); - for (var i = 0; i < vertices.Length; i++) - { - var index = i; - var handle = AddHandle(vertices[i], UiTokens.Accent, i, p => { hit.Vertices[index] = p; Moved(); }); - handle.RightClicked = () => RemoveVertex(index); - } - break; - default: - AddHandle(new Vector2(hit.X, hit.Y), UiTokens.Success, TopLeft, p => - { - var right = hit.X + hit.Width; - var bottom = hit.Y + hit.Height; - hit.X = Mathf.Min(p.X, right - MinExtent); - hit.Y = Mathf.Min(p.Y, bottom - MinExtent); - hit.Width = right - hit.X; - hit.Height = bottom - hit.Y; - Moved(); - }); - AddHandle(new Vector2(hit.X + hit.Width, hit.Y + hit.Height), UiTokens.Success, BottomRight, p => - { - hit.Width = Mathf.Max(MinExtent, p.X - hit.X); - hit.Height = Mathf.Max(MinExtent, p.Y - hit.Y); - Moved(); - }); - AddHandle(new Vector2(hit.X + hit.Width / 2f, hit.Y + hit.Height / 2f), UiTokens.AccentBright, Move, p => - { - hit.X = Mathf.Clamp(p.X - hit.Width / 2f, 0f, 1f - hit.Width); - hit.Y = Mathf.Clamp(p.Y - hit.Height / 2f, 0f, 1f - hit.Height); - Moved(); - }, 12f); - break; - } - - foreach (var handle in _handles) - handle.DragEnd = () => { RebuildHandles(); Changed?.Invoke(); }; - } - - /// Keeps the other handles glued to the shape while one is dragged. - private void Moved() - { - var hit = Controller.Selected.HitArea; - foreach (var handle in _handles) - { - Vector2? target = hit.Type switch - { - "circle" => handle.Index == Move ? new Vector2(hit.X, hit.Y) : RadiusPoint(hit), - "polygon" => null, - _ => handle.Index switch - { - TopLeft => new Vector2(hit.X, hit.Y), - BottomRight => new Vector2(hit.X + hit.Width, hit.Y + hit.Height), - _ => new Vector2(hit.X + hit.Width / 2f, hit.Y + hit.Height / 2f) - } - }; - if (target.HasValue) - handle.CenterAt(GlobalPosition + NormToLocal(target.Value)); - } - - QueueRedraw(); - Changed?.Invoke(); - } - - private Vector2 RadiusPoint(HitAreaDefinition hit) - { - var rect = GetBackgroundRect(); - var offset = rect.Size.X > 0f ? hit.Radius * ShortSide() / rect.Size.X : 0f; - return new Vector2(hit.X + offset, hit.Y); - } - - private float ShortSide() - { - var rect = GetBackgroundRect(); - return Mathf.Max(1f, Mathf.Min(rect.Size.X, rect.Size.Y)); - } - - private void RemoveVertex(int index) - { - var hit = Controller.Selected?.HitArea; - if (hit?.Vertices == null || index >= hit.Vertices.Length) - return; - - hit.Vertices = hit.Vertices.Where((_, i) => i != index).ToArray(); - RebuildHandles(); - QueueRedraw(); - Changed?.Invoke(); - } - - private void OnGuiInput(InputEvent @event) - { - if (Controller == null || @event is not InputEventMouseButton mb || !mb.Pressed || mb.ButtonIndex != MouseButton.Left) - return; - - var norm = LocalToNorm(mb.Position); - var hitIndex = HitTest(norm); - var selected = Controller.Selected; - - if (hitIndex >= 0 && hitIndex != Controller.SelectedIndex) - { - Controller.Select(hitIndex); - RebuildHandles(); - QueueRedraw(); - ItemPicked?.Invoke(hitIndex); - } - else if (hitIndex < 0 && selected?.HitArea.Type == "polygon") - { - var vertices = selected.HitArea.Vertices ?? Array.Empty(); - selected.HitArea.Vertices = vertices.Append(Clamp01(norm)).ToArray(); - RebuildHandles(); - QueueRedraw(); - Changed?.Invoke(); - } - - AcceptEvent(); - } - - /// Topmost item whose collider contains the normalised point, or -1. - private int HitTest(Vector2 norm) - { - var rect = GetBackgroundRect(); - var local = NormToLocal(norm); - for (var i = Controller.Items.Count - 1; i >= 0; i--) - { - var hit = Controller.Items[i].HitArea; - if (hit == null) - continue; - - var inside = hit.Type switch - { - "circle" => (local - NormToLocal(new Vector2(hit.X, hit.Y))).Length() <= hit.Radius * Mathf.Min(rect.Size.X, rect.Size.Y), - "polygon" => hit.Vertices is { Length: >= 3 } && Geometry2D.IsPointInPolygon(norm, hit.Vertices), - _ => new Rect2(hit.X, hit.Y, hit.Width, hit.Height).HasPoint(norm) - }; - if (inside) - return i; - } - return -1; - } - - protected override void DrawContent(Rect2 rect) - { - if (Controller == null) - return; - - for (var i = 0; i < Controller.Items.Count; i++) - DrawCollider(Controller.Items[i], rect, i == Controller.SelectedIndex); - } - - private void DrawCollider(InteractiveDefinition def, Rect2 rect, bool selected) - { - var hit = def.HitArea; - if (hit == null) - return; - - var line = selected ? UiTokens.AccentBright : UiTokens.Alpha(UiTokens.Ink, 0.55f); - var fill = selected ? UiTokens.Alpha(UiTokens.Accent, 0.22f) : UiTokens.Alpha(UiTokens.Ink, 0.06f); - Vector2 labelAt; - - switch (hit.Type) - { - case "circle": - var centre = NormToLocal(new Vector2(hit.X, hit.Y)); - var radius = hit.Radius * Mathf.Min(rect.Size.X, rect.Size.Y); - DrawCircle(centre, radius, fill); - DrawArc(centre, radius, 0f, Mathf.Tau, 48, line, selected ? 2f : 1f, true); - labelAt = centre + new Vector2(-radius, -radius); - break; - case "polygon": - var vertices = hit.Vertices ?? Array.Empty(); - var points = vertices.Select(NormToLocal).ToArray(); - if (points.Length >= 3 && Geometry2D.TriangulatePolygon(points).Length > 0) - DrawColoredPolygon(points, fill); - if (points.Length >= 2) - DrawPolyline(points.Append(points[0]).ToArray(), line, selected ? 2f : 1f, true); - labelAt = points.Length > 0 ? points[0] : rect.Position; - break; - default: - var box = new Rect2(NormToLocal(new Vector2(hit.X, hit.Y)), new Vector2(hit.Width, hit.Height) * rect.Size); - DrawRect(box, fill); - DrawRect(box, line, false, selected ? 2f : 1f); - labelAt = box.Position; - break; - } - - var font = GetThemeDefaultFont(); - var size = UiTokens.FontSmall; - DrawString(font, labelAt + new Vector2(4f, -4f), def.ID ?? "?", HorizontalAlignment.Left, -1, size, - selected ? UiTokens.AccentBright : UiTokens.Body); - } - } -} diff --git a/src/InteractiveEditor/InteractiveEditorController.cs b/src/InteractiveEditor/InteractiveEditorController.cs deleted file mode 100644 index cc59404..0000000 --- a/src/InteractiveEditor/InteractiveEditorController.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Cthangover.Core.Interactive; -using Cthangover.Core.Mods; -using Cthangover.Core.Utils; -using Godot; - -namespace Cthangover.Tools.InteractiveEditor -{ - /// - /// Data model of the interactive editor: every definition of one - /// interactives/*.json file plus the selection. Definitions are - /// edited in place (the same objects the game deserialises), so the file - /// round-trips without a parallel field-by-field copy that could forget a - /// property. fills the optional sub-objects the editor - /// needs (hit area, highlight, actions) before a definition is shown. - /// - public class InteractiveEditorController - { - /// Collider kinds understood by InteractiveObject. - public static readonly string[] HitTypes = { "rect", "circle", "polygon" }; - - /// Render layers understood by the interactive layer host. - public static readonly string[] Layers = { "foreground", "background", "ui" }; - - /// Definitions of the open file, in file order. - public List Items { get; private set; } = new(); - - /// Index of the selected definition, or -1. - public int SelectedIndex { get; private set; } = -1; - - /// The selected definition, or null. - public InteractiveDefinition Selected => SelectedIndex >= 0 && SelectedIndex < Items.Count ? Items[SelectedIndex] : null; - - /// Replaces the items with the contents of a {"Items": [...]} file. Throws on malformed JSON. - public void FromJson(string json) - { - var data = string.IsNullOrWhiteSpace(json) - ? null - : JsonSerializer.Deserialize>(json, JsonOptionsProvider.ReadOptions); - Items = data?.Items ?? new List(); - foreach (var item in Items) - Ensure(item); - SelectedIndex = Items.Count > 0 ? 0 : -1; - } - - /// Serialises all items into the standard envelope (indented, nulls omitted). - public string ToJson() - { - var data = new FileData { Items = Items }; - return JsonSerializer.Serialize(data, JsonOptionsProvider.WriteIndentedIgnoreNullOptions); - } - - /// Drops all items. - public void Clear() - { - Items = new List(); - SelectedIndex = -1; - } - - /// Selects by index; out of range clears the selection. - public void Select(int index) - { - SelectedIndex = index >= 0 && index < Items.Count ? index : -1; - } - - /// Appends a new clickable rect in the middle of the background and selects it. - public InteractiveDefinition Add() - { - var def = new InteractiveDefinition - { - ID = MakeUniqueId("interactive"), - Cursor = "PointingHand", - HitArea = new HitAreaDefinition { Type = "rect", X = 0.45f, Y = 0.45f, Width = 0.1f, Height = 0.1f } - }; - Ensure(def); - Items.Add(def); - SelectedIndex = Items.Count - 1; - return def; - } - - /// Appends a deep copy of the selected definition with a fresh ID, slightly offset. - public InteractiveDefinition DuplicateSelected() - { - var source = Selected; - if (source == null) - return null; - - var json = JsonSerializer.Serialize(source, JsonOptionsProvider.WriteCompactOptions); - var copy = JsonSerializer.Deserialize(json, JsonOptionsProvider.ReadOptions); - copy.ID = MakeUniqueId(source.ID); - Ensure(copy); - Offset(copy.HitArea, new Vector2(0.02f, 0.02f)); - Items.Add(copy); - SelectedIndex = Items.Count - 1; - return copy; - } - - /// Removes the selected definition and keeps a sensible selection. - public void RemoveSelected() - { - if (Selected == null) - return; - - Items.RemoveAt(SelectedIndex); - SelectedIndex = Items.Count == 0 ? -1 : Math.Min(SelectedIndex, Items.Count - 1); - } - - /// - /// Changes the collider kind of a definition, converting the current - /// shape so it stays roughly where it was (rect ↔ circle by bounds, - /// polygon from the bounds' corners). - /// - public static void SetHitType(InteractiveDefinition def, string type) - { - var hit = def.HitArea; - if (hit.Type == type) - return; - - var bounds = Bounds(hit); - hit.Type = type; - switch (type) - { - case "circle": - hit.X = bounds.GetCenter().X; - hit.Y = bounds.GetCenter().Y; - hit.Radius = Mathf.Max(0.01f, Mathf.Min(bounds.Size.X, bounds.Size.Y) / 2f); - break; - case "polygon": - hit.Vertices = new[] - { - bounds.Position, - new Vector2(bounds.End.X, bounds.Position.Y), - bounds.End, - new Vector2(bounds.Position.X, bounds.End.Y) - }; - break; - default: - hit.X = bounds.Position.X; - hit.Y = bounds.Position.Y; - hit.Width = bounds.Size.X; - hit.Height = bounds.Size.Y; - break; - } - } - - /// - /// Normalised bounding box of a hit area (circle radius taken as a - /// fraction of both axes — an approximation good enough for - /// conversions and duplication offsets). - /// - public static Rect2 Bounds(HitAreaDefinition hit) - { - switch (hit?.Type) - { - case "circle": - return new Rect2(hit.X - hit.Radius, hit.Y - hit.Radius, hit.Radius * 2f, hit.Radius * 2f); - case "polygon" when hit.Vertices is { Length: > 0 }: - var min = hit.Vertices[0]; - var max = hit.Vertices[0]; - foreach (var v in hit.Vertices) - { - min = new Vector2(Mathf.Min(min.X, v.X), Mathf.Min(min.Y, v.Y)); - max = new Vector2(Mathf.Max(max.X, v.X), Mathf.Max(max.Y, v.Y)); - } - return new Rect2(min, max - min); - case null: - return new Rect2(0.45f, 0.45f, 0.1f, 0.1f); - default: - return new Rect2(hit.X, hit.Y, hit.Width, hit.Height); - } - } - - /// Makes sure the optional parts the editor binds to exist. - public static void Ensure(InteractiveDefinition def) - { - def.HitArea ??= new HitAreaDefinition(); - def.HitArea.Type = HitTypes.Contains(def.HitArea.Type) ? def.HitArea.Type : "rect"; - def.Highlight ??= new HighlightDefinition(); - def.Actions ??= new InteractiveActionDefinition(); - } - - /// ID not used by another item: base, base_2, base_3… - public string MakeUniqueId(string baseId) - { - var used = new HashSet(Items.Select(i => i.ID ?? string.Empty), StringComparer.OrdinalIgnoreCase); - if (!used.Contains(baseId)) - return baseId; - - var suffix = 2; - while (used.Contains($"{baseId}_{suffix}")) - suffix++; - return $"{baseId}_{suffix}"; - } - - private static void Offset(HitAreaDefinition hit, Vector2 delta) - { - hit.X += delta.X; - hit.Y += delta.Y; - if (hit.Vertices != null) - hit.Vertices = hit.Vertices.Select(v => v + delta).ToArray(); - } - } -} diff --git a/src/InteractiveEditor/InteractiveEditorToolProvider.cs b/src/InteractiveEditor/InteractiveEditorToolProvider.cs deleted file mode 100644 index df60944..0000000 --- a/src/InteractiveEditor/InteractiveEditorToolProvider.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Cthangover.Core.UI.Tool; -using Godot; - -using Cthangover.Mods.Interface.Tool; -namespace Cthangover.Tools.InteractiveEditor -{ - /// - /// Registers the interactive editor as a tool window in the developer toolbox. - /// Implements so the tool system can instantiate - /// on demand. - /// - public class InteractiveEditorToolProvider : IToolProvider - { - /// Unique tool identifier referenced by the toolbox UI. - public string Id => "interactive_editor"; - /// Translation key for the tool's display name. - public string LocaleKey => "tools/interactive_editor/title"; - /// Creates the editor window instance. - public Window CreateWindow() => new InteractiveEditorWindow(); - /// Adds the interactive editor's launcher button to the main menu tool block. - public void AddMenuButton(Container container) => ToolMenuButton.Attach(container, this); - } - - /// - /// Provides a button in the developer toolbox that opens the interactive editor. - /// Controls visibility via the policy. - /// - public class InteractiveEditorToolBoxButton : IToolBoxButton - { - /// Matches . - public string ToolId => "interactive_editor"; - /// No custom icon — uses the tool title text. - public string IconPath => ""; - /// Translation key for the button label. - public string LocaleKey => "tools/interactive_editor/title"; - /// Always visible in the toolbox. - public bool IsVisible() => true; - } -} diff --git a/src/InteractiveEditor/InteractiveEditorWindow.cs b/src/InteractiveEditor/InteractiveEditorWindow.cs deleted file mode 100644 index 3e2fa1d..0000000 --- a/src/InteractiveEditor/InteractiveEditorWindow.cs +++ /dev/null @@ -1,644 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Cthangover.Core.Interactive; -using Cthangover.Core.UI.Theming; -using Cthangover.Core.Utils; -using Cthangover.Mods.Interface.Theming; -using Cthangover.Mods.Interface.Tool; -using Cthangover.Tools.Common; -using Cthangover.Tools.Services; -using Godot; - -namespace Cthangover.Tools.InteractiveEditor -{ - /// - /// Editor of interactive object definitions (interactives/*.json). - /// The author opens a file of a writable mod, sees every object of the - /// file over its scene background, picks one in the list or on the canvas - /// and edits its collider by dragging handles or typing numbers, plus its - /// look (texture, layer, highlight) and actions (scenario, commands, hover - /// commands). Saving writes the whole file back, other objects included. - /// - /// The background is chosen automatically from the scene whose scenario - /// adds the selected object (interactive_add), when that is known. - /// - public partial class InteractiveEditorWindow : ToolWindow - { - private const string InteractivesDir = "interactives"; - - private readonly InteractiveEditorController _controller = new(); - private readonly List _backgroundIds = new(); - private Dictionary _interactiveScenes; - - private string _modId; - private string _path; - private bool _suppress; - - private ModFileSelector _fileSelector; - private FilteredList _itemList; - private FilteredList _bgList; - private InteractiveEditorCanvas _canvas; - private CodeEdit _jsonEdit; - - private Label _propsEmpty; - private VBoxContainer _propsBody; - private LineEdit _idEdit; - private LineEdit _textureEdit; - private OptionButton _layerSelect; - private SpinBox _zIndex; - private CheckBox _enabledCheck; - private CheckBox _visibleCheck; - private OptionButton _cursorSelect; - private OptionButton _hitTypeSelect; - private VBoxContainer _rectParams; - private VBoxContainer _circleParams; - private VBoxContainer _polyParams; - private SpinBox _rectX, _rectY, _rectW, _rectH; - private SpinBox _circleX, _circleY, _circleR; - private VBoxContainer _vertexList; - private ColorPickerButton _highlightColor; - private SpinBox _highlightScale; - private LineEdit _clickScenario; - private CodeEdit _clickCommands; - private LineEdit _hoverEnter; - private LineEdit _hoverLeave; - - public InteractiveEditorWindow() : base("tools/interactive_editor/title") - { - CthangoverTheme.Apply(this); - BuildUI(); - LoadBackgrounds(); - SetHint(ToolUi.Tr("tools/interactive_editor/hint")); - _fileSelector.Refresh(); - } - - // ── UI construction ──────────────────────────────────────── - - private void BuildUI() - { - var outer = CreateFillContainer(); - AddChild(outer); - - var toolbar = CreateToolbar(); - outer.AddChild(toolbar); - _fileSelector = new ModFileSelector(InteractivesDir, ".json"); - _fileSelector.FileRequested += OnFileRequested; - toolbar.AddChild(_fileSelector); - toolbar.AddChild(ToolUi.Button("tools/common/save", Save, ButtonKind.Primary, "tools/common/save_tip")); - toolbar.AddChild(UiKit.Spacer()); - var jsonToggle = ToolUi.Button("tools/common/json", null, ButtonKind.Tab, "tools/common/json_tip"); - jsonToggle.ToggleMode = true; - jsonToggle.Toggled += on => _jsonEdit.Visible = on; - toolbar.AddChild(jsonToggle); - - var main = new HSplitContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - outer.AddChild(main); - - var left = new VBoxContainer(); - main.AddChild(CreateSidebar(left, 260)); - BuildLeft(left); - - var centerSplit = new HSplitContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; - main.AddChild(centerSplit); - - var canvasColumn = new VSplitContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - centerSplit.AddChild(canvasColumn); - - var canvasPanel = new PanelContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - canvasPanel.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusMd, UiTokens.Well, UiTokens.Line)); - canvasColumn.AddChild(canvasPanel); - - _canvas = new InteractiveEditorCanvas { Controller = _controller }; - _canvas.Changed += OnCanvasChanged; - _canvas.ItemPicked += _ => RefreshAll(); - canvasPanel.AddChild(_canvas); - - _jsonEdit = ToolUi.Code(editable: false); - _jsonEdit.CustomMinimumSize = new Vector2(0, 160); - _jsonEdit.Visible = false; - canvasColumn.AddChild(_jsonEdit); - - var right = new VBoxContainer(); - centerSplit.AddChild(CreateSidebar(right, 330)); - BuildProperties(right); - } - - private void BuildLeft(VBoxContainer column) - { - column.AddChild(ToolUi.Section("tools/interactive_editor/items")); - _itemList = new FilteredList(180); - _itemList.Chosen += OnItemChosen; - column.AddChild(_itemList); - - var row = new HBoxContainer(); - row.AddThemeConstantOverride("separation", UiTokens.Space1); - column.AddChild(row); - AddExpanding(row, ToolUi.Button("tools/interactive_editor/add", OnAdd)); - AddExpanding(row, ToolUi.Button("tools/interactive_editor/duplicate", OnDuplicate)); - column.AddChild(ToolUi.Button("tools/interactive_editor/delete", OnDelete, ButtonKind.Danger)); - - column.AddChild(ToolUi.Section("tools/interactive_editor/background")); - _bgList = new FilteredList(160); - _bgList.SizeFlagsVertical = Control.SizeFlags.ExpandFill; - _bgList.Chosen += ShowBackground; - column.AddChild(_bgList); - } - - private static void AddExpanding(Container parent, Control control) - { - control.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - parent.AddChild(control); - } - - private void BuildProperties(VBoxContainer column) - { - column.AddChild(ToolUi.Section("tools/interactive_editor/props")); - _propsEmpty = ToolUi.Hint(ToolUi.Tr("tools/interactive_editor/none_selected")); - column.AddChild(_propsEmpty); - - var body = new VBoxContainer(); - body.AddThemeConstantOverride("separation", UiTokens.Space2); - column.AddChild(body); - _propsBody = body; - - _idEdit = ToolUi.Field(body, "tools/interactive_editor/label_id", ToolUi.Text("crypt_spot", _ => OnIdChanged())); - _idEdit.AddThemeFontOverride("font", ToolUi.Mono()); - _textureEdit = ToolUi.Field(body, "tools/interactive_editor/label_texture_key", ToolUi.Text(ToolUi.Tr("tools/interactive_editor/texture_placeholder"), _ => OnFieldsChanged()), - "tools/interactive_editor/texture_tip"); - - _layerSelect = ToolUi.Field(body, "tools/interactive_editor/label_layer", new OptionButton()); - foreach (var layer in InteractiveEditorController.Layers) - _layerSelect.AddItem(layer); - _layerSelect.ItemSelected += _ => OnFieldsChanged(); - - _zIndex = ToolUi.Field(body, "tools/interactive_editor/label_z", ToolUi.Number(-100, 100, 1, _ => OnFieldsChanged())); - - _cursorSelect = ToolUi.Field(body, "tools/interactive_editor/label_cursor", new OptionButton()); - _cursorSelect.AddItem(ToolUi.Tr("tools/interactive_editor/cursor_none")); - _cursorSelect.AddItem("PointingHand"); - _cursorSelect.ItemSelected += _ => OnFieldsChanged(); - - _enabledCheck = new CheckBox { Text = ToolUi.Tr("tools/interactive_editor/enabled") }; - _enabledCheck.Toggled += _ => OnFieldsChanged(); - body.AddChild(_enabledCheck); - _visibleCheck = new CheckBox { Text = ToolUi.Tr("tools/interactive_editor/visible") }; - _visibleCheck.Toggled += _ => OnFieldsChanged(); - body.AddChild(_visibleCheck); - - body.AddChild(ToolUi.Section("tools/interactive_editor/collider")); - _hitTypeSelect = ToolUi.Field(body, "tools/interactive_editor/label_collider_type", new OptionButton()); - foreach (var type in InteractiveEditorController.HitTypes) - _hitTypeSelect.AddItem(HitTypeLabel(type)); - _hitTypeSelect.ItemSelected += OnHitTypeChanged; - - _rectParams = new VBoxContainer(); - body.AddChild(_rectParams); - _rectX = ToolUi.Field(_rectParams, "tools/interactive_editor/label_x", ToolUi.Number(0, 1, 0.001, _ => OnGeometryTyped())); - _rectY = ToolUi.Field(_rectParams, "tools/interactive_editor/label_y", ToolUi.Number(0, 1, 0.001, _ => OnGeometryTyped())); - _rectW = ToolUi.Field(_rectParams, "tools/interactive_editor/label_w", ToolUi.Number(0.001, 1, 0.001, _ => OnGeometryTyped())); - _rectH = ToolUi.Field(_rectParams, "tools/interactive_editor/label_h", ToolUi.Number(0.001, 1, 0.001, _ => OnGeometryTyped())); - - _circleParams = new VBoxContainer(); - body.AddChild(_circleParams); - _circleX = ToolUi.Field(_circleParams, "tools/interactive_editor/label_cx", ToolUi.Number(0, 1, 0.001, _ => OnGeometryTyped())); - _circleY = ToolUi.Field(_circleParams, "tools/interactive_editor/label_cy", ToolUi.Number(0, 1, 0.001, _ => OnGeometryTyped())); - _circleR = ToolUi.Field(_circleParams, "tools/interactive_editor/label_r", ToolUi.Number(0.001, 1, 0.001, _ => OnGeometryTyped())); - - _polyParams = new VBoxContainer(); - body.AddChild(_polyParams); - _polyParams.AddChild(ToolUi.Hint(ToolUi.Tr("tools/interactive_editor/polygon_hint"))); - _vertexList = new VBoxContainer(); - _vertexList.AddThemeConstantOverride("separation", UiTokens.Space1); - _polyParams.AddChild(_vertexList); - - body.AddChild(ToolUi.Section("tools/interactive_editor/highlight")); - _highlightColor = ToolUi.Field(body, "tools/interactive_editor/label_highlight_color", new ColorPickerButton { CustomMinimumSize = new Vector2(0, 28) }); - _highlightColor.ColorChanged += _ => OnFieldsChanged(); - _highlightScale = ToolUi.Field(body, "tools/interactive_editor/label_highlight_scale", ToolUi.Number(0.5, 2, 0.01, _ => OnFieldsChanged())); - - body.AddChild(ToolUi.Section("tools/interactive_editor/actions")); - _clickScenario = ToolUi.Field(body, "tools/interactive_editor/label_onclick_scenario", ToolUi.Text("scenarios/…/file.scenario", _ => OnFieldsChanged())); - body.AddChild(UiKit.Label(ToolUi.Tr("tools/interactive_editor/label_onclick_commands"), UiTokens.FontBodySmall, UiTokens.Muted)); - _clickCommands = ToolUi.Code(); - _clickCommands.GuttersDrawLineNumbers = false; - _clickCommands.CustomMinimumSize = new Vector2(0, 70); - _clickCommands.SyntaxHighlighter = ScenarioSyntaxService.CreateHighlighter(); - _clickCommands.TextChanged += OnFieldsChanged; - body.AddChild(_clickCommands); - _hoverEnter = ToolUi.Field(body, "tools/interactive_editor/label_hover_enter", ToolUi.Text("", _ => OnFieldsChanged())); - _hoverLeave = ToolUi.Field(body, "tools/interactive_editor/label_hover_leave", ToolUi.Text("", _ => OnFieldsChanged())); - } - - // ── loading & saving ─────────────────────────────────────── - - private void LoadBackgrounds() - { - _backgroundIds.AddRange(ModResourceService.GetBackgroundIds()); - _bgList.SetItems(_backgroundIds); - } - - private void ShowBackground(string id) - { - if (string.IsNullOrEmpty(id)) - return; - - _bgList.Select(id); - _canvas.SetBackground(ModResourceService.LoadBackgroundTexture(id)); - } - - /// Shows the background of the scene that adds the selected object, when known. - private void ShowSceneBackground() - { - var id = _controller.Selected?.ID; - if (id == null) - return; - - _interactiveScenes ??= SceneBackgroundService.MapInteractivesToScenes(); - if (!_interactiveScenes.TryGetValue(id, out var scene)) - return; - - var background = SceneBackgroundService.ForScene(scene); - if (background == null || background == _bgList.SelectedId) - return; - - if (ModResourceService.LoadBackgroundTexture(background) == null) - SetStatus(string.Format(ToolUi.Tr("tools/common/background_missing"), background, scene), ToolStatusKind.Error); - else - ShowBackground(background); - } - - private void OnFileRequested(string modId, string path) - { - RunDiscardingChanges(() => Load(modId, path), () => _fileSelector.Show(_modId, _path)); - } - - private void Load(string modId, string path) - { - _modId = modId; - _path = path; - _controller.Clear(); - MarkClean(); - - if (modId == null) - SetStatus(ToolUi.Tr("tools/common/no_writable_mods"), ToolStatusKind.Error); - else if (path == null) - SetStatus(string.Format(ToolUi.Tr("tools/common/no_files"), modId)); - else - { - try - { - var text = ModFileService.ReadText(modId, path); - _controller.FromJson(text); - SetStatus(string.Format(ToolUi.Tr(string.IsNullOrEmpty(text) ? "tools/common/new_file_status" : "tools/common/loaded"), modId + "/" + path)); - } - catch (Exception ex) - { - SetStatus($"{ToolUi.Tr("tools/common/load_error")}: {ex.Message}", ToolStatusKind.Error); - GameLogger.Log("INTERACTIVE_EDITOR", $"load failed for '{modId}/{path}': {ex.Message}", LogLevel.Error); - } - } - - _fileSelector.Show(_modId, _path); - RefreshAll(); - ShowSceneBackground(); - } - - /// Ctrl+S. - protected override void OnSaveShortcut() => Save(); - - private void Save() - { - if (_path == null) - { - SetStatus(ToolUi.Tr("tools/common/no_file_to_save"), ToolStatusKind.Error); - return; - } - - var problem = Validate(); - if (problem != null) - { - SetStatus(problem, ToolStatusKind.Error); - return; - } - - if (!ModFileService.WriteText(_modId, _path, _controller.ToJson())) - { - SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error); - return; - } - - MarkClean(); - _fileSelector.Show(_modId, _path); - SetStatus(string.Format(ToolUi.Tr("tools/common/saved"), _modId + "/" + _path), ToolStatusKind.Ok); - } - - /// First blocking problem of the file (empty/duplicate ID, polygon with < 3 vertices), or null. - private string Validate() - { - foreach (var item in _controller.Items) - { - if (string.IsNullOrWhiteSpace(item.ID)) - return ToolUi.Tr("tools/interactive_editor/id_empty"); - if (item.HitArea.Type == "polygon" && (item.HitArea.Vertices?.Length ?? 0) < 3) - return string.Format(ToolUi.Tr("tools/interactive_editor/polygon_too_small"), item.ID); - } - - var duplicate = _controller.Items.GroupBy(i => i.ID, StringComparer.OrdinalIgnoreCase).FirstOrDefault(g => g.Count() > 1); - return duplicate != null ? $"{ToolUi.Tr("tools/interactive_editor/duplicate_id")} {duplicate.Key}" : null; - } - - // ── list actions ─────────────────────────────────────────── - - private void OnItemChosen(string indexText) - { - if (_suppress || !int.TryParse(indexText, out var index)) - return; - - _controller.Select(index); - RefreshAll(); - ShowSceneBackground(); - } - - private void OnAdd() - { - if (_path == null) - { - SetStatus(ToolUi.Tr("tools/common/no_file_to_save"), ToolStatusKind.Error); - return; - } - - _controller.Add(); - Changed(); - RefreshAll(); - } - - private void OnDuplicate() - { - if (_controller.DuplicateSelected() == null) - return; - Changed(); - RefreshAll(); - } - - private void OnDelete() - { - var item = _controller.Selected; - if (item == null) - return; - - ShowConfirm("tools/interactive_editor/delete", "tools/interactive_editor/delete_confirm", () => - { - _controller.RemoveSelected(); - Changed(); - RefreshAll(); - SetStatus(string.Format(ToolUi.Tr("tools/interactive_editor/deleted"), item.ID)); - }); - } - - /// Delete removes the selected object (with confirmation). - protected override bool HandleKey(InputEventKey key) - { - if (key.Keycode != Key.Delete || _controller.Selected == null) - return false; - OnDelete(); - return true; - } - - // ── property editing ─────────────────────────────────────── - - private void OnIdChanged() - { - var item = _controller.Selected; - if (_suppress || item == null) - return; - - item.ID = _idEdit.Text.Trim(); - Changed(); - RefreshItemList(); - } - - private void OnFieldsChanged() - { - var item = _controller.Selected; - if (_suppress || item == null) - return; - - item.Texture = NullIfEmpty(_textureEdit.Text); - item.Layer = InteractiveEditorController.Layers[Math.Max(0, _layerSelect.Selected)]; - item.ZIndex = (int)_zIndex.Value; - item.Enabled = _enabledCheck.ButtonPressed; - item.Visible = _visibleCheck.ButtonPressed; - item.Cursor = _cursorSelect.Selected == 1 ? "PointingHand" : null; - item.Highlight.ColorHex = "#" + _highlightColor.Color.ToHtml(true); - item.Highlight.Scale = (float)_highlightScale.Value; - - var commands = _clickCommands.Text.Split('\n').Select(l => l.Trim()).Where(l => l.Length > 0).ToArray(); - var scenario = NullIfEmpty(_clickScenario.Text); - item.Actions.OnClick = scenario != null || commands.Length > 0 - ? new ClickAction { Scenario = scenario, Commands = commands.Length > 0 ? commands : null } - : null; - item.Actions.OnHoverEnter = NullIfEmpty(_hoverEnter.Text); - item.Actions.OnHoverLeave = NullIfEmpty(_hoverLeave.Text); - Changed(); - } - - private void OnHitTypeChanged(long index) - { - var item = _controller.Selected; - if (_suppress || item == null) - return; - - InteractiveEditorController.SetHitType(item, InteractiveEditorController.HitTypes[index]); - Changed(); - RefreshAll(); - } - - private void OnGeometryTyped() - { - var hit = _controller.Selected?.HitArea; - if (_suppress || hit == null) - return; - - if (hit.Type == "circle") - { - hit.X = (float)_circleX.Value; - hit.Y = (float)_circleY.Value; - hit.Radius = (float)_circleR.Value; - } - else if (hit.Type == "rect") - { - hit.X = (float)_rectX.Value; - hit.Y = (float)_rectY.Value; - hit.Width = (float)_rectW.Value; - hit.Height = (float)_rectH.Value; - } - - Changed(); - _canvas.RebuildHandles(); - _canvas.QueueRedraw(); - } - - private void OnCanvasChanged() - { - Changed(); - _suppress = true; - WriteGeometry(_controller.Selected?.HitArea); - _suppress = false; - } - - private void Changed() - { - SetDirty(); - if (_jsonEdit.Visible) - _jsonEdit.Text = _controller.ToJson(); - } - - // ── refresh ──────────────────────────────────────────────── - - private void RefreshAll() - { - RefreshItemList(); - _suppress = true; - WriteSidebar(); - _suppress = false; - _jsonEdit.Text = _controller.ToJson(); - _canvas.RebuildHandles(); - _canvas.QueueRedraw(); - } - - private void RefreshItemList() - { - var items = _controller.Items - .Select((item, i) => (i.ToString(), (string.IsNullOrEmpty(item.ID) ? "?" : item.ID) + " · " + HitTypeLabel(item.HitArea.Type), (Texture2D)null)); - _itemList.SetItems(items); - _itemList.Select(_controller.SelectedIndex >= 0 ? _controller.SelectedIndex.ToString() : null); - } - - private void WriteSidebar() - { - var item = _controller.Selected; - _propsEmpty.Visible = item == null; - _propsBody.Visible = item != null; - if (item == null) - return; - - _idEdit.Text = item.ID ?? string.Empty; - _textureEdit.Text = item.Texture ?? string.Empty; - _layerSelect.Select(Math.Max(0, Array.IndexOf(InteractiveEditorController.Layers, item.Layer))); - _zIndex.Value = item.ZIndex; - _enabledCheck.ButtonPressed = item.Enabled; - _visibleCheck.ButtonPressed = item.Visible; - _cursorSelect.Select(string.IsNullOrEmpty(item.Cursor) ? 0 : 1); - _hitTypeSelect.Select(Math.Max(0, Array.IndexOf(InteractiveEditorController.HitTypes, item.HitArea.Type))); - _highlightColor.Color = item.Highlight.Color; - _highlightScale.Value = item.Highlight.Scale; - _clickScenario.Text = item.Actions.OnClick?.Scenario ?? string.Empty; - _clickCommands.Text = item.Actions.OnClick?.Commands != null ? string.Join("\n", item.Actions.OnClick.Commands) : string.Empty; - _hoverEnter.Text = item.Actions.OnHoverEnter ?? string.Empty; - _hoverLeave.Text = item.Actions.OnHoverLeave ?? string.Empty; - WriteGeometry(item.HitArea); - } - - private void WriteGeometry(HitAreaDefinition hit) - { - if (hit == null) - return; - - _rectParams.Visible = hit.Type == "rect"; - _circleParams.Visible = hit.Type == "circle"; - _polyParams.Visible = hit.Type == "polygon"; - - _rectX.Value = hit.X; - _rectY.Value = hit.Y; - _rectW.Value = hit.Width; - _rectH.Value = hit.Height; - _circleX.Value = hit.X; - _circleY.Value = hit.Y; - _circleR.Value = hit.Radius; - RefreshVertexList(hit); - } - - /// X/Y spin rows per polygon vertex; handlers read the vertex at change time. - private void RefreshVertexList(HitAreaDefinition hit) - { - var vertices = hit.Type == "polygon" ? hit.Vertices ?? Array.Empty() : Array.Empty(); - var rows = _vertexList.GetChildren(); - - // Reuse rows while dragging so focus and scroll are not lost on every motion. - if (rows.Count == vertices.Length) - { - for (var i = 0; i < vertices.Length; i++) - { - var row = rows[i]; - ((SpinBox)row.GetChild(1)).SetValueNoSignal(vertices[i].X); - ((SpinBox)row.GetChild(2)).SetValueNoSignal(vertices[i].Y); - } - return; - } - - foreach (var child in rows) - child.QueueFree(); - - for (var i = 0; i < vertices.Length; i++) - { - var index = i; - var row = new HBoxContainer(); - row.AddThemeConstantOverride("separation", UiTokens.Space1); - var number = UiKit.Label((i + 1).ToString(), UiTokens.FontSmall, UiTokens.Faint); - number.CustomMinimumSize = new Vector2(20, 0); - row.AddChild(number); - - var x = ToolUi.Number(0, 1, 0.001, null); - x.Value = vertices[i].X; - x.ValueChanged += v => OnVertexTyped(index, (float)v, null); - row.AddChild(x); - - var y = ToolUi.Number(0, 1, 0.001, null); - y.Value = vertices[i].Y; - y.ValueChanged += v => OnVertexTyped(index, null, (float)v); - row.AddChild(y); - - _vertexList.AddChild(row); - } - } - - private void OnVertexTyped(int index, float? x, float? y) - { - var hit = _controller.Selected?.HitArea; - if (_suppress || hit?.Vertices == null || index >= hit.Vertices.Length) - return; - - var v = hit.Vertices[index]; - hit.Vertices[index] = new Vector2(x ?? v.X, y ?? v.Y); - Changed(); - _canvas.RebuildHandles(); - _canvas.QueueRedraw(); - } - - private static string HitTypeLabel(string type) => ToolUi.Tr(type switch - { - "circle" => "tools/interactive_editor/hit_circle", - "polygon" => "tools/interactive_editor/hit_polygon", - _ => "tools/interactive_editor/hit_rect" - }); - - private static string NullIfEmpty(string text) - { - var trimmed = text?.Trim(); - return string.IsNullOrEmpty(trimmed) ? null : trimmed; - } - } -} diff --git a/src/LightEditor/LightEditorCanvas.cs b/src/LightEditor/LightEditorCanvas.cs deleted file mode 100644 index 9403298..0000000 --- a/src/LightEditor/LightEditorCanvas.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using Cthangover.Core.UI.Theming; -using Cthangover.Tools.Common; -using Godot; - -namespace Cthangover.Core.UI.Tool.LightEditor -{ - /// - /// Night-time preview of a scene with its static lights. The background is - /// drawn through the game's timed_sprite shader (set as this - /// control's material), so the preview lights the picture exactly like the - /// game; the lights' positions are pushed to the shader in this canvas' - /// coordinates and their radii scaled from the 1920-px game width to the - /// drawn background width. Radius rings and handles live in a separate - /// child overlay — anything drawn by this control itself would be shaded. - /// - /// A click on a handle selects that light and drags it; a double-click on - /// empty background adds a light there. - /// - public partial class LightEditorCanvas : BackgroundCanvas - { - private readonly Control _overlay; - - /// Data being edited; set once by the window. - public LightEditorController Controller; - - /// Preview material (may be null when the shader is missing). - public ShaderMaterial PreviewMaterial - { - get => Material as ShaderMaterial; - set => Material = value; - } - - /// Raised when a light moved or was added on the canvas. - public event Action Changed; - - /// Raised when the selection changed on the canvas. - public event Action SelectionChanged; - - public LightEditorCanvas() - { - _overlay = new Control { MouseFilter = MouseFilterEnum.Ignore }; - AddChild(_overlay); - _overlay.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); - _overlay.Connect("draw", Callable.From((Action)DrawRings)); - GuiInput += OnGuiInput; - } - - /// One handle per light, coloured like the light. - public override void RebuildHandles() - { - ClearHandles(); - if (Controller == null || GetBackgroundRect().Size.X <= 0f) - return; - - for (var i = 0; i < Controller.Lights.Count; i++) - { - var index = i; - var light = Controller.Lights[i]; - var handle = AddHandle(new Vector2(light.X, light.Y), light.ToColor(), i, p => - { - light.X = p.X; - light.Y = p.Y; - Refresh(); - Changed?.Invoke(); - }, index == Controller.SelectedIndex ? 20f : 16f); - handle.Clicked = () => SelectLight(index); - } - - Refresh(); - } - - /// Redraws the preview and re-uploads the shader uniforms. - public void Refresh() - { - QueueRedraw(); - _overlay.QueueRedraw(); - } - - private void SelectLight(int index) - { - if (index == Controller.SelectedIndex) - return; - - Controller.Select(index); - SelectionChanged?.Invoke(); - CallDeferred(nameof(RebuildHandles)); - } - - private void OnGuiInput(InputEvent @event) - { - if (Controller == null || @event is not InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left } mb) - return; - - if (mb.DoubleClick && Controller.Add(Clamp01(LocalToNorm(mb.Position))) != null) - { - RebuildHandles(); - Changed?.Invoke(); - SelectionChanged?.Invoke(); - } - - AcceptEvent(); - } - - protected override void DrawContent(Rect2 rect) - { - // The shader compares against world_pos (MODEL_MATRIX * VERTEX), i.e. - // canvas coordinates of the window, not this control's local ones. - Controller?.UpdatePreview(PreviewMaterial, n => GlobalPosition + NormToLocal(n), rect.Size.X / LightEditorController.GameWidth); - } - - private void DrawRings() - { - var rect = GetBackgroundRect(); - if (Controller == null || rect.Size.X <= 0f) - return; - - var scale = rect.Size.X / LightEditorController.GameWidth; - for (var i = 0; i < Controller.Lights.Count; i++) - { - var light = Controller.Lights[i]; - var selected = i == Controller.SelectedIndex; - var centre = NormToLocal(new Vector2(light.X, light.Y)); - var color = selected ? UiTokens.AccentBright : UiTokens.Alpha(light.ToColor(), 0.45f); - _overlay.DrawArc(centre, light.Radius * scale, 0f, Mathf.Tau, 64, color, selected ? 1.5f : 1f, true); - } - } - } -} diff --git a/src/LightEditor/LightEditorController.cs b/src/LightEditor/LightEditorController.cs deleted file mode 100644 index 6835508..0000000 --- a/src/LightEditor/LightEditorController.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using Cthangover.Core.Mods.Resolvers; -using Cthangover.Core.UI.Theming; -using Cthangover.Core.Utils; -using Cthangover.Mods.Time; -using Godot; - -namespace Cthangover.Core.UI.Tool.LightEditor -{ - /// - /// Data of the light editor: the static lights of one scenario and their - /// place in the scenario text. Static lights live in a light_set - /// line (light_set [{"x":…,"y":…,"radius":…,"influence":…,"color":…}]) - /// that UiLightController.SetStaticLights reads at runtime: x/y are - /// fractions of the viewport, radius is in viewport pixels at the game - /// resolution. The controller parses that line, edits the list and writes - /// the line back (or inserts one right after the scenario header), and - /// feeds the timed_sprite shader of the preview the same way the - /// game does, so the preview shows the real night lighting. - /// - public class LightEditorController - { - /// Static light slots the shader has (slot 0 is the dynamic light). - public const int MaxLights = 10; - - /// Viewport width the stored radii refer to. - public const float GameWidth = 1920f; - - private const string Command = "light_set"; - private const int ShaderSlots = 11; - private static readonly Vector2 Offscreen = new(-1000f, -1000f); - - /// The edited lights, in file order. - public List Lights { get; private set; } = new(); - - /// Index of the selected light, or -1. - public int SelectedIndex { get; private set; } = -1; - - /// The selected light, or null. - public LightDef Selected => SelectedIndex >= 0 && SelectedIndex < Lights.Count ? Lights[SelectedIndex] : null; - - /// Selects by index; out of range clears the selection. - public void Select(int index) => SelectedIndex = index >= 0 && index < Lights.Count ? index : -1; - - /// Adds a warm light at the given normalised point and selects it; null when all slots are used. - public LightDef Add(Vector2 position) - { - if (Lights.Count >= MaxLights) - return null; - - var light = new LightDef { X = position.X, Y = position.Y, Radius = 400f, Influence = 1f, ColorHex = "#ffcc66" }; - Lights.Add(light); - SelectedIndex = Lights.Count - 1; - return light; - } - - /// Removes the selected light. - public void RemoveSelected() - { - if (Selected == null) - return; - Lights.RemoveAt(SelectedIndex); - SelectedIndex = Math.Min(SelectedIndex, Lights.Count - 1); - } - - /// Compact JSON array exactly as written into the light_set line. - public string ToJson() => JsonSerializer.Serialize(Lights, JsonOptionsProvider.WriteCompactOptions); - - /// The scenario line for the current lights. - public string ToScenarioLine() => Command + " " + ToJson(); - - /// - /// Loads the lights of the first light_set line of a scenario. - /// Returns false when the scenario has none (the list is then - /// empty and saving inserts a new line). Throws on malformed JSON. - /// - public bool LoadFromScenario(string text) - { - Lights = new List(); - SelectedIndex = -1; - - var index = FindLine(text, out var lines); - if (index < 0) - return false; - - var json = ArgumentOf(lines[index]); - Lights = JsonSerializer.Deserialize>(json, JsonOptionsProvider.ReadOptions) ?? new List(); - SelectedIndex = Lights.Count > 0 ? 0 : -1; - return true; - } - - /// - /// Returns the scenario text with its light_set line replaced by - /// the current lights, or — when it has none — with a new line inserted - /// right after the --- header separator (at the top otherwise). - /// - public string WriteToScenario(string text) - { - var index = FindLine(text, out var lines); - var newline = text.Contains("\r\n") ? "\r\n" : "\n"; - if (index >= 0) - { - var indent = lines[index].Substring(0, lines[index].Length - lines[index].TrimStart().Length); - lines[index] = indent + ToScenarioLine(); - } - else - { - var separator = lines.FindIndex(l => l.Trim() == "---"); - lines.Insert(separator + 1, ToScenarioLine()); - } - - return string.Join(newline, lines); - } - - /// True when the scenario text contains a light_set line. - public static bool HasLights(string text) => FindLine(text, out _) >= 0; - - private static int FindLine(string text, out List lines) - { - lines = new List((text ?? string.Empty).Replace("\r\n", "\n").Split('\n')); - return lines.FindIndex(l => l.TrimStart().StartsWith(Command + " ", StringComparison.Ordinal)); - } - - /// Argument of a light_set line; the old quoted form ("[{\"x\":…}]") is unescaped. - private static string ArgumentOf(string line) - { - var arg = line.Trim().Substring(Command.Length).Trim(); - if (arg.Length >= 2 && arg[0] == '"' && arg[arg.Length - 1] == '"') - arg = arg.Substring(1, arg.Length - 2).Replace("\\\"", "\""); - return arg; - } - - // ── shader preview ───────────────────────────────────────── - - /// - /// Material of the preview: the game's timed_sprite shader at - /// night weights. null when the shader is not installed. - /// - public static ShaderMaterial CreatePreviewMaterial() - { - var shader = Shaders.Resolve("timed_sprite"); - if (shader == null) - return null; - - var material = new ShaderMaterial { Shader = shader }; - material.SetShaderParameter("time_weights", new Vector4(0, 0, 0, 1)); - material.SetShaderParameter("use_time", true); - return material; - } - - /// Binds the background and its optional depth/albedo masks to the preview material. - public static void SetPreviewTextures(ShaderMaterial material, Texture2D depth, Texture2D albedo) - { - if (material == null) - return; - material.SetShaderParameter("depth_mask", depth); - material.SetShaderParameter("albedo_map", albedo); - } - - /// - /// Uploads the lights into the shader uniforms. - /// maps a normalised position into the preview's canvas coordinates - /// (the space of the shader's world_pos); - /// shrinks game-resolution radii to the preview size. - /// - public void UpdatePreview(ShaderMaterial material, Func toCanvas, float radiusScale) - { - if (material == null) - return; - - var positions = new Godot.Collections.Array(); - var colors = new Godot.Collections.Array(); - var radii = new Godot.Collections.Array(); - var influence = new Godot.Collections.Array(); - - // Slot 0 is the dynamic (cursor) light at runtime; keep it off. - positions.Add(Offscreen); - colors.Add(UiTokens.White); - radii.Add(0f); - influence.Add(0f); - - var count = Math.Min(Lights.Count, MaxLights); - for (var i = 0; i < ShaderSlots - 1; i++) - { - var on = i < count; - positions.Add(on ? toCanvas(new Vector2(Lights[i].X, Lights[i].Y)) : Offscreen); - colors.Add(on ? Lights[i].ToColor() : UiTokens.White); - radii.Add(on ? Lights[i].Radius : 0f); - influence.Add(on ? Lights[i].Influence : 0f); - } - - material.SetShaderParameter("light_count", count + 1); - material.SetShaderParameter("light_positions", positions); - material.SetShaderParameter("light_colors", colors); - material.SetShaderParameter("light_radii", radii); - material.SetShaderParameter("light_influence", influence); - material.SetShaderParameter("light_radius_scale", radiusScale); - } - } -} diff --git a/src/LightEditor/LightEditorToolProvider.cs b/src/LightEditor/LightEditorToolProvider.cs deleted file mode 100644 index c87b495..0000000 --- a/src/LightEditor/LightEditorToolProvider.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Cthangover.Core.UI.Tool; -using Godot; - -using Cthangover.Mods.Interface.Tool; -namespace Cthangover.Core.UI.Tool.LightEditor -{ - /// Registers the light editor as a tool window. Creates instances. - public class LightEditorToolProvider : IToolProvider - { - /// Unique tool identifier. - public string Id => "light_editor"; - /// Translation key for the tool's display name. - public string LocaleKey => "tools/light_editor/title"; - /// Creates the editor window instance. - public Window CreateWindow() => new LightEditorWindow(); - /// Adds the light editor's launcher button to the main menu tool block. - public void AddMenuButton(Container container) => ToolMenuButton.Attach(container, this); - } - - /// Toolbox button that opens the light editor. Always visible. - public class LightEditorToolBoxButton : IToolBoxButton - { - /// Matches . - public string ToolId => "light_editor"; - /// No custom icon — uses text label. - public string IconPath => ""; - /// Translation key for the button label. - public string LocaleKey => "tools/light_editor/title"; - /// Always visible in the toolbox. - public bool IsVisible() => true; - } -} diff --git a/src/LightEditor/LightEditorWindow.cs b/src/LightEditor/LightEditorWindow.cs deleted file mode 100644 index 2aa5664..0000000 --- a/src/LightEditor/LightEditorWindow.cs +++ /dev/null @@ -1,387 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Cthangover.Core.Factories.Impls; -using Cthangover.Core.UI.Theming; -using Cthangover.Core.Utils; -using Cthangover.Mods.Interface.Theming; -using Cthangover.Mods.Interface.Tool; -using Cthangover.Tools.Common; -using Cthangover.Tools.Services; -using Godot; - -namespace Cthangover.Core.UI.Tool.LightEditor -{ - /// - /// Editor of the static lights of a scene. The author picks a scenario — - /// the ones that already set lights come first — and edits its - /// light_set line visually: drag lights on a night-time preview of - /// the scene background, double-click to add one, tune radius, influence - /// and colour. Saving rewrites that line in the scenario (or inserts it - /// after the header). The background follows the scenario's scene. - /// - public partial class LightEditorWindow : ToolWindow - { - private readonly LightEditorController _controller = new(); - private readonly List _scenarios = new(); - private readonly List _backgroundIds = new(); - - private ScenarioFileInfo _scenario; - private string _scenarioText; - private bool _suppress; - - private OptionButton _scenarioSelect; - private LightEditorCanvas _canvas; - private ItemList _lightList; - private Button _addBtn; - private Button _deleteBtn; - private VBoxContainer _propsBody; - private Label _propsEmpty; - private SpinBox _x; - private SpinBox _y; - private SpinBox _radius; - private SpinBox _influence; - private ColorPickerButton _color; - private FilteredList _bgList; - private CodeEdit _lineEdit; - - public LightEditorWindow() : base("tools/light_editor/title") - { - CthangoverTheme.Apply(this); - BuildUI(); - LoadBackgrounds(); - LoadScenarioList(); - SetHint(ToolUi.Tr("tools/light_editor/hint")); - } - - // ── UI construction ──────────────────────────────────────── - - private void BuildUI() - { - var outer = CreateFillContainer(); - AddChild(outer); - - var toolbar = CreateToolbar(); - outer.AddChild(toolbar); - _scenarioSelect = new OptionButton { CustomMinimumSize = new Vector2(420, 0), TooltipText = ToolUi.Tr("tools/light_editor/scenario_tip") }; - _scenarioSelect.ItemSelected += OnScenarioPicked; - toolbar.AddChild(_scenarioSelect); - toolbar.AddChild(ToolUi.Button("tools/common/save", Save, ButtonKind.Primary, "tools/light_editor/save_tip")); - toolbar.AddChild(ToolUi.Button("tools/light_editor/copy_line", CopyLine, ButtonKind.Secondary, "tools/light_editor/copy_line_tip")); - toolbar.AddChild(UiKit.Spacer()); - var lineToggle = ToolUi.Button("tools/light_editor/show_line", null, ButtonKind.Tab); - lineToggle.ToggleMode = true; - lineToggle.Toggled += on => _lineEdit.Visible = on; - toolbar.AddChild(lineToggle); - - var main = new HSplitContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - outer.AddChild(main); - - var side = new VBoxContainer(); - main.AddChild(CreateSidebar(side, 300)); - BuildSidebar(side); - - var canvasColumn = new VSplitContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - main.AddChild(canvasColumn); - - var canvasPanel = new PanelContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - canvasPanel.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusMd, UiTokens.Well, UiTokens.Line)); - canvasColumn.AddChild(canvasPanel); - - _canvas = new LightEditorCanvas - { - Controller = _controller, - PreviewMaterial = LightEditorController.CreatePreviewMaterial() - }; - _canvas.Changed += OnCanvasChanged; - _canvas.SelectionChanged += RefreshAll; - canvasPanel.AddChild(_canvas); - - _lineEdit = ToolUi.Code(editable: false); - _lineEdit.SyntaxHighlighter = ScenarioSyntaxService.CreateHighlighter(); - _lineEdit.CustomMinimumSize = new Vector2(0, 70); - _lineEdit.WrapMode = TextEdit.LineWrappingMode.Boundary; - _lineEdit.Visible = false; - canvasColumn.AddChild(_lineEdit); - } - - private void BuildSidebar(VBoxContainer column) - { - column.AddChild(ToolUi.Section("tools/light_editor/static_lights")); - _lightList = new ItemList { CustomMinimumSize = new Vector2(0, 150) }; - _lightList.ItemSelected += index => { _controller.Select((int)index); RefreshAll(); }; - column.AddChild(_lightList); - - var row = new HBoxContainer(); - row.AddThemeConstantOverride("separation", UiTokens.Space1); - column.AddChild(row); - _addBtn = ToolUi.Button("tools/light_editor/add_light", OnAdd); - _addBtn.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - row.AddChild(_addBtn); - _deleteBtn = ToolUi.Button("tools/light_editor/delete", OnDelete, ButtonKind.Danger); - _deleteBtn.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - row.AddChild(_deleteBtn); - - column.AddChild(ToolUi.Section("tools/light_editor/selected_props")); - _propsEmpty = ToolUi.Hint(ToolUi.Tr("tools/light_editor/none_selected")); - column.AddChild(_propsEmpty); - _propsBody = new VBoxContainer(); - _propsBody.AddThemeConstantOverride("separation", UiTokens.Space2); - column.AddChild(_propsBody); - - _x = ToolUi.Field(_propsBody, "tools/light_editor/x", ToolUi.Number(0, 1, 0.001, _ => OnFieldsChanged())); - _y = ToolUi.Field(_propsBody, "tools/light_editor/y", ToolUi.Number(0, 1, 0.001, _ => OnFieldsChanged())); - _radius = ToolUi.Field(_propsBody, "tools/light_editor/radius", ToolUi.Number(10, 3000, 10, _ => OnFieldsChanged()), "tools/light_editor/radius_tip"); - _influence = ToolUi.Field(_propsBody, "tools/light_editor/influence", ToolUi.Number(0, 1, 0.05, _ => OnFieldsChanged()), "tools/light_editor/influence_tip"); - _color = ToolUi.Field(_propsBody, "tools/light_editor/color", new ColorPickerButton { CustomMinimumSize = new Vector2(0, 28), EditAlpha = false }); - _color.ColorChanged += _ => OnFieldsChanged(); - - column.AddChild(ToolUi.Section("tools/light_editor/background")); - _bgList = new FilteredList(180); - _bgList.SizeFlagsVertical = Control.SizeFlags.ExpandFill; - _bgList.Chosen += ShowBackground; - column.AddChild(_bgList); - } - - // ── loading ──────────────────────────────────────────────── - - private void LoadBackgrounds() - { - _backgroundIds.AddRange(ModResourceService.GetBackgroundIds()); - _bgList.SetItems(_backgroundIds); - } - - /// Scenarios with a light_set line first (marked ●), then all others. - private void LoadScenarioList() - { - var withLights = new List(); - var others = new List(); - foreach (var file in ScenarioFileService.ScanScenarioFiles().SelectMany(m => m.Groups).SelectMany(g => g.Files)) - { - var text = ScenarioFileService.ReadAllText(file.ModId, file.RelativePath); - (text != null && LightEditorController.HasLights(text) ? withLights : others).Add(file); - } - - _scenarios.Clear(); - _scenarios.AddRange(withLights); - _scenarios.AddRange(others); - - _scenarioSelect.Clear(); - _scenarioSelect.AddItem(ToolUi.Tr("tools/light_editor/pick_scenario")); - foreach (var file in _scenarios) - _scenarioSelect.AddItem((withLights.Contains(file) ? "● " : " ") + file.ModId + ": " + file.RelativePath); - _scenarioSelect.Select(0); - - if (withLights.Count > 0) - { - _scenarioSelect.Select(1); - OpenScenario(withLights[0]); - } - else - { - RefreshAll(); - } - } - - private void OnScenarioPicked(long index) - { - var file = index > 0 && index - 1 < _scenarios.Count ? _scenarios[(int)index - 1] : null; - RunDiscardingChanges(() => OpenScenario(file), () => _scenarioSelect.Select(_scenario == null ? 0 : _scenarios.IndexOf(_scenario) + 1)); - } - - private void OpenScenario(ScenarioFileInfo file) - { - _scenario = file; - _scenarioText = null; - MarkClean(); - - if (file != null) - { - _scenarioText = ScenarioFileService.ReadAllText(file.ModId, file.RelativePath) ?? string.Empty; - try - { - var found = _controller.LoadFromScenario(_scenarioText); - SetStatus(string.Format(ToolUi.Tr(found ? "tools/light_editor/loaded" : "tools/light_editor/no_lights_yet"), file.RelativePath)); - } - catch (Exception ex) - { - SetStatus($"{ToolUi.Tr("tools/common/load_error")}: {ex.Message}", ToolStatusKind.Error); - GameLogger.Log("LIGHT_EDITOR", $"light_set parse failed in '{file.Key}': {ex.Message}", LogLevel.Error); - } - - var scene = ScenarioFileService.ParseMetadata(_scenarioText).Scene; - var background = SceneBackgroundService.ForScene(scene); - if (background != null && ModResourceService.LoadBackgroundTexture(background) == null) - SetStatus(string.Format(ToolUi.Tr("tools/common/background_missing"), background, scene), ToolStatusKind.Error); - else if (background != null) - ShowBackground(background); - } - else - { - _controller.LoadFromScenario(string.Empty); - } - - RefreshAll(); - } - - /// Shows a background with its optional _depth/_albedo masks in the shader. - private void ShowBackground(string id) - { - if (string.IsNullOrEmpty(id)) - return; - - _bgList.Select(id); - _canvas.SetBackground(ModResourceService.LoadBackgroundTexture(id)); - LightEditorController.SetPreviewTextures(_canvas.PreviewMaterial, - ModResourceService.LoadBackgroundTexture(id + "_depth"), - ModResourceService.LoadBackgroundTexture(id + "_albedo")); - _canvas.Refresh(); - } - - // ── saving ───────────────────────────────────────────────── - - /// Ctrl+S. - protected override void OnSaveShortcut() => Save(); - - private void Save() - { - if (_scenario == null) - { - SetStatus(ToolUi.Tr("tools/light_editor/pick_scenario"), ToolStatusKind.Error); - return; - } - - var updated = _controller.WriteToScenario(_scenarioText); - if (!ScenarioFileService.WriteAllText(_scenario.ModId, _scenario.RelativePath, updated)) - { - SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error); - return; - } - - _scenarioText = updated; - MarkClean(); - SetStatus(string.Format(ToolUi.Tr("tools/common/saved"), _scenario.ModId + "/" + _scenario.RelativePath), ToolStatusKind.Ok); - } - - private void CopyLine() - { - DisplayServer.ClipboardSet(_controller.ToScenarioLine()); - SetStatus(ToolUi.Tr("tools/light_editor/copied"), ToolStatusKind.Ok); - } - - // ── editing ──────────────────────────────────────────────── - - private void OnAdd() - { - if (_controller.Add(new Vector2(0.5f, 0.5f)) == null) - { - SetStatus(string.Format(ToolUi.Tr("tools/light_editor/limit"), LightEditorController.MaxLights), ToolStatusKind.Error); - return; - } - - SetDirty(); - RefreshAll(); - } - - private void OnDelete() - { - if (_controller.Selected == null) - return; - - _controller.RemoveSelected(); - SetDirty(); - RefreshAll(); - } - - /// Delete removes the selected light. - protected override bool HandleKey(InputEventKey key) - { - if (key.Keycode != Key.Delete || _controller.Selected == null) - return false; - OnDelete(); - return true; - } - - private void OnFieldsChanged() - { - var light = _controller.Selected; - if (_suppress || light == null) - return; - - light.X = (float)_x.Value; - light.Y = (float)_y.Value; - light.Radius = (float)_radius.Value; - light.Influence = (float)_influence.Value; - light.ColorHex = "#" + _color.Color.ToHtml(false); - SetDirty(); - RefreshList(); - _lineEdit.Text = _controller.ToScenarioLine(); - _canvas.RebuildHandles(); - } - - private void OnCanvasChanged() - { - SetDirty(); - _suppress = true; - WriteProperties(); - _suppress = false; - RefreshList(); - _lineEdit.Text = _controller.ToScenarioLine(); - } - - // ── refresh ──────────────────────────────────────────────── - - private void RefreshAll() - { - RefreshList(); - _suppress = true; - WriteProperties(); - _suppress = false; - _lineEdit.Text = _controller.ToScenarioLine(); - _addBtn.Disabled = _controller.Lights.Count >= LightEditorController.MaxLights; - _deleteBtn.Disabled = _controller.Selected == null; - _canvas.RebuildHandles(); - } - - private void RefreshList() - { - _lightList.Clear(); - for (var i = 0; i < _controller.Lights.Count; i++) - { - var light = _controller.Lights[i]; - var index = _lightList.AddItem($"● {i + 1}. {light.X:0.000}, {light.Y:0.000} r {light.Radius:0}"); - _lightList.SetItemCustomFgColor(index, light.ToColor()); - } - - if (_controller.SelectedIndex >= 0) - _lightList.Select(_controller.SelectedIndex); - } - - private void WriteProperties() - { - var light = _controller.Selected; - _propsEmpty.Visible = light == null; - _propsBody.Visible = light != null; - if (light == null) - return; - - _x.Value = light.X; - _y.Value = light.Y; - _radius.Value = light.Radius; - _influence.Value = light.Influence; - _color.Color = light.ToColor(); - } - } -} diff --git a/src/MapEditor/MapEditorCanvas.cs b/src/MapEditor/MapEditorCanvas.cs deleted file mode 100644 index 5b9e5dc..0000000 --- a/src/MapEditor/MapEditorCanvas.cs +++ /dev/null @@ -1,397 +0,0 @@ -using System; -using System.Collections.Generic; -using Cthangover.Core.Mods.Resolvers; -using Cthangover.Core.UI.Theming; -using Cthangover.Mods.Map; -using Cthangover.Tools.Common; -using Godot; - -namespace Cthangover.Tools.MapEditor -{ - /// - /// Interactive preview of the edited map: the background (laid out by - /// exactly like MapWidget), every zone - /// polygon, marker and text label, and the draggable handles of the - /// selected zone. Two modes: - /// - /// select — a click picks the topmost zone (polygon first, - /// then marker/label dots); handles move vertices and positions; - /// draw — clicks append polygon vertices to the selected - /// region while a dashed rubber band shows the next edge; a double-click - /// (or Enter/Esc via the window) finishes the polygon. - /// - /// Right-click deletes the polygon vertex nearest to the pointer; while - /// drawing, a click that misses every vertex removes the last one ("undo - /// point"), in select mode only a targeted vertex is deleted. - /// - /// The canvas owns no data: it reads and mutates - /// and raises so - /// the window can refresh the sidebar and JSON. Zone fills and outlines are - /// painted through — the definition the game's - /// MapZonesLayer uses — so the editor and the live map look the same. - /// - public partial class MapEditorCanvas : BackgroundCanvas - { - /// Mode constant: clicking selects zones and drags handles. - public const string ModeSelect = "select"; - - /// Mode constant: clicking appends polygon vertices. - public const string ModeDraw = "draw"; - - /// Handle index of the marker position. - public const int MarkerHandle = -2; - - /// Handle index of the label position. - public const int LabelHandle = -3; - - private const float PickRadius = 24f; - - private static readonly Color VertexColor = MapZoneStyle.VertexHandle; - private static readonly Color MarkerColor = MapZoneStyle.MarkerHandle; - private static readonly Color LabelColor = MapZoneStyle.LabelHandle; - - /// Data being edited; set once by the window. - public MapEditorController Controller; - - /// Current interaction mode, one of / . - public string Mode { get; private set; } = ModeSelect; - - /// Raised whenever the canvas mutates controller data (drag, draw, delete). - public event Action Changed; - - /// Raised with the newly picked zone index (-1 for empty space). - public event Action ZonePicked; - - /// Raised when the canvas leaves draw mode on its own (double-click finishing a polygon). - public event Action ModeChanged; - - private Vector2? _pointer; - - public MapEditorCanvas() - { - MapZoneStyle.Prepare(this); - GuiInput += OnGuiInput; - MouseExited += OnMouseExited; - } - - /// Switches the interaction mode and refreshes the handles. - public void SetMode(string mode) - { - Mode = mode == ModeDraw ? ModeDraw : ModeSelect; - MouseDefaultCursorShape = Mode == ModeDraw ? CursorShape.Cross : CursorShape.Arrow; - RebuildHandles(); - QueueRedraw(); - } - - /// Leaves draw mode (double-click, Enter, Esc) and tells the window. - public void FinishDrawing() - { - if (Mode != ModeDraw) - return; - - SetMode(ModeSelect); - _pointer = null; - ModeChanged?.Invoke(); - } - - /// Recreates the handles from the current selection. Call after structural changes. - public override void RebuildHandles() - { - ClearHandles(); - - var zone = Controller?.Selected; - if (zone == null || GetBackgroundRect().Size.X <= 0f) - return; - - if (zone.Points != null) - { - for (var i = 0; i < zone.Points.Count; i++) - { - var index = i; - var handle = AddHandle(zone.Points[i], VertexColor, i, p => OnHandleDrag(index, p)); - handle.RightClicked = () => RemoveVertexAt(index); - handle.DragEnd = () => Changed?.Invoke(); - } - } - - if (HasMarker(zone)) - AddHandle(zone.ImagePos, MarkerColor, MarkerHandle, p => OnHandleDrag(MarkerHandle, p), 18f).DragEnd = () => Changed?.Invoke(); - - if (zone.LabelPos.HasValue) - AddHandle(zone.LabelPos.Value, LabelColor, LabelHandle, p => OnHandleDrag(LabelHandle, p), 18f).DragEnd = () => Changed?.Invoke(); - } - - private void OnHandleDrag(int index, Vector2 normalized) - { - if (Controller == null) - return; - - if (index == MarkerHandle) - Controller.SetImagePos(normalized); - else if (index == LabelHandle) - Controller.SetLabelPos(normalized); - else - Controller.SetVertex(index, normalized); - - QueueRedraw(); - Changed?.Invoke(); - } - - private void OnGuiInput(InputEvent @event) - { - if (Controller == null) - return; - - if (@event is InputEventMouseMotion motion) - { - if (Mode == ModeDraw) - { - _pointer = motion.Position; - QueueRedraw(); - } - return; - } - - if (@event is not InputEventMouseButton mb || !mb.Pressed) - return; - - if (mb.ButtonIndex == MouseButton.Left) - { - if (Mode == ModeDraw) - { - // The first click of a double-click already placed the - // vertex; the double-click itself only finishes the polygon. - if (mb.DoubleClick) - { - FinishDrawing(); - } - else - { - Controller.AddVertex(Clamp01(LocalToNorm(mb.Position))); - RebuildHandles(); - Changed?.Invoke(); - QueueRedraw(); - } - } - else - { - var hit = HitTest(mb.Position); - if (hit != Controller.SelectedIndex) - { - Controller.Select(hit); - RebuildHandles(); - ZonePicked?.Invoke(hit); - QueueRedraw(); - } - } - - AcceptEvent(); - } - else if (mb.ButtonIndex == MouseButton.Right) - { - RemoveVertexAtPointer(mb.Position, Mode == ModeDraw); - AcceptEvent(); - } - } - - private void OnMouseExited() - { - _pointer = null; - QueueRedraw(); - } - - /// Topmost zone under a canvas-local point: polygons win over marker/label dots. - private int HitTest(Vector2 local) - { - var zones = Controller.Zones; - var rect = GetBackgroundRect(); - - for (var i = zones.Count - 1; i >= 0; i--) - { - var points = zones[i].Points; - if (points != null && points.Count >= 3 && Geometry2D.IsPointInPolygon(local, ToPixels(points, rect))) - return i; - } - - for (var i = zones.Count - 1; i >= 0; i--) - { - var zone = zones[i]; - if (HasMarker(zone) && (local - ToPixel(zone.ImagePos, rect)).Length() <= PickRadius) - return i; - if (zone.LabelPos.HasValue && (local - ToPixel(zone.LabelPos.Value, rect)).Length() <= PickRadius) - return i; - } - - return -1; - } - - protected override void DrawContent(Rect2 rect) - { - var zones = Controller?.Zones; - if (zones == null) - return; - - for (var i = 0; i < zones.Count; i++) - DrawZone(zones[i], rect, i == Controller.SelectedIndex); - - DrawRubberBand(rect); - } - - /// - /// In draw mode shows where the next vertex goes: a dashed segment from - /// the last vertex to the pointer and back to the first one, so the - /// author sees the polygon closing before clicking. - /// - private void DrawRubberBand(Rect2 rect) - { - var points = Controller.Selected?.Points; - if (Mode != ModeDraw || !_pointer.HasValue || points == null || points.Count == 0) - return; - - var pointer = _pointer.Value; - DrawDashedLine(ToPixel(points[points.Count - 1], rect), pointer, UiTokens.AccentBright, 1.5f, 6f); - if (points.Count >= 2) - DrawDashedLine(pointer, ToPixel(points[0], rect), UiTokens.AccentLine, 1f, 6f); - } - - private void DrawZone(MapZone zone, Rect2 rect, bool selected) - { - if (zone.Points != null && zone.Points.Count >= 2) - { - var locked = MapZoneRules.IsLocked(zone, !zone.Locked); - MapZoneStyle.Draw(this, ToPixels(zone.Points, rect), selected, false, locked, zone.Hidden); - } - - if (HasMarker(zone)) - DrawMarker(zone, ToPixel(zone.ImagePos, rect), selected); - - if (zone.LabelPos.HasValue) - DrawLabelPill(zone, ToPixel(zone.LabelPos.Value, rect), selected); - } - - /// - /// Draws the marker emblem. A marker without an icon is rendered as a - /// placeholder crosshair instead of disappearing, so a freshly created - /// marker stays visible, selectable and draggable while the author is - /// still choosing an icon. - /// - private void DrawMarker(MapZone zone, Vector2 center, bool selected) - { - var outline = selected ? UiTokens.Accent : UiTokens.AccentLine; - var accent = selected ? UiTokens.Accent : MarkerColor; - - var icon = string.IsNullOrEmpty(zone.Image) ? null : Textures.Resolve(zone.Image); - if (icon == null) - { - DrawCircle(center, 18f, UiTokens.Alpha(UiTokens.Surface0, 0.55f)); - DrawArc(center, 18f, 0f, Mathf.Tau, 32, accent, 2f, true); - DrawLine(center - new Vector2(7f, 0f), center + new Vector2(7f, 0f), accent, 2f); - DrawLine(center - new Vector2(0f, 7f), center + new Vector2(0f, 7f), accent, 2f); - return; - } - - DrawCircle(center, 18f, UiTokens.Alpha(UiTokens.White, 0.85f)); - DrawArc(center, 18f, 0f, Mathf.Tau, 32, outline, 1.5f, true); - DrawTextureRect(icon, new Rect2(center - new Vector2(14f, 14f), new Vector2(28f, 28f)), false); - } - - private void DrawLabelPill(MapZone zone, Vector2 center, bool selected) - { - var text = string.IsNullOrEmpty(zone.Title) ? zone.ID : TranslationServer.Translate(zone.Title).ToString(); - if (string.IsNullOrEmpty(text)) - text = zone.ID; - - var font = GetThemeDefaultFont(); - var fontSize = GetThemeDefaultFontSize(); - var textSize = font.GetStringSize(text, HorizontalAlignment.Left, -1, fontSize); - var pill = new Rect2(center - new Vector2(textSize.X / 2f + 8f, textSize.Y / 2f + 3f), textSize + new Vector2(16f, 6f)); - - DrawRect(pill, UiTokens.Alpha(UiTokens.Surface0, 0.85f)); - DrawRect(pill, selected ? UiTokens.Accent : UiTokens.Line, false, 1f); - DrawString(font, new Vector2(pill.Position.X + 8f, pill.Position.Y + pill.Size.Y / 2f + fontSize / 3f), - text, HorizontalAlignment.Left, -1, fontSize, UiTokens.Ink); - } - - private static Vector2[] ToPixels(IReadOnlyList normalized, Rect2 rect) - { - var result = new Vector2[normalized.Count]; - for (var i = 0; i < result.Length; i++) - result[i] = rect.Position + normalized[i] * rect.Size; - return result; - } - - private static Vector2 ToPixel(Vector2 normalized, Rect2 rect) - { - return rect.Position + normalized * rect.Size; - } - - /// - /// A zone is marker-capable when it has an icon, or when it is still a - /// bare marker placeholder — no polygon and no text label. The - /// placeholder keeps a freshly created marker visible and draggable - /// before an icon is picked; once the zone grows a region or becomes a - /// pure inscription, the marker is only shown when an icon exists. The - /// region currently being drawn is excluded while it has no vertices. - /// - private bool HasMarker(MapZone zone) - { - if (!string.IsNullOrEmpty(zone.Image)) - return true; - - if (Mode == ModeDraw && zone == Controller?.Selected) - return false; - - return (zone.Points == null || zone.Points.Count == 0) && !zone.LabelPos.HasValue; - } - - /// - /// Right-click on empty canvas: removes the polygon vertex nearest to - /// the pointer. While drawing, a click that misses every vertex removes - /// the last one instead, which makes right-click a natural "undo - /// point"; in select mode only a targeted vertex is deleted, so a stray - /// click cannot destroy geometry. - /// - private void RemoveVertexAtPointer(Vector2 local, bool fallbackToLast) - { - var zone = Controller?.Selected; - if (zone?.Points == null || zone.Points.Count == 0) - return; - - var rect = GetBackgroundRect(); - var index = FindNearestVertex(zone, local, rect); - if (index < 0 && fallbackToLast) - index = zone.Points.Count - 1; - if (index >= 0) - RemoveVertexAt(index); - } - - private void RemoveVertexAt(int index) - { - if (Controller == null) - return; - - Controller.RemoveVertex(index); - RebuildHandles(); - Changed?.Invoke(); - QueueRedraw(); - } - - private static int FindNearestVertex(MapZone zone, Vector2 local, Rect2 rect) - { - var best = -1; - var bestDistance = PickRadius; - - for (var i = 0; i < zone.Points.Count; i++) - { - var distance = (local - ToPixel(zone.Points[i], rect)).Length(); - if (distance <= bestDistance) - { - bestDistance = distance; - best = i; - } - } - - return best; - } - } -} diff --git a/src/MapEditor/MapEditorController.cs b/src/MapEditor/MapEditorController.cs deleted file mode 100644 index 6dbe94c..0000000 --- a/src/MapEditor/MapEditorController.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System; -using System.Collections.Generic; -using Cthangover.Mods.Map; -using Cthangover.Tools.Services; -using Godot; - -namespace Cthangover.Tools.MapEditor -{ - /// - /// Data model for the map editor. Holds the zone list being edited, the - /// current selection and all structural operations (add/remove zone, - /// add/remove/move vertex, move marker and label, toggle label mode). - /// Contains no UI and no runtime state: it edits raw authoring data that - /// serialises back into - /// mods/<mod>/map/*.json, while the live game state belongs to - /// MapZoneService. - /// - /// New zones get unique IDs automatically and a sensible default shape - /// (a small square for regions, centre position for markers and labels), - /// so an author can place an element first and refine it afterwards. All - /// coordinates are normalised [0..1] and clamped by the canvas, not here. - /// - public class MapEditorController - { - private const float DefaultRegionHalfSize = 0.06f; - private static readonly Vector2 DefaultCenter = new(0.5f, 0.5f); - - /// Zones currently being edited, in map file order. - public List Zones { get; private set; } = new(); - - /// Index of the selected zone, or -1 when nothing is selected. - public int SelectedIndex { get; private set; } = -1; - - /// The selected zone, or null when the selection is empty. - public MapZone Selected => SelectedIndex >= 0 && SelectedIndex < Zones.Count ? Zones[SelectedIndex] : null; - - /// Replaces all zones with the contents of a map file JSON envelope. - public void FromJson(string json) - { - Zones = MapFileService.ParseZones(json); - SelectedIndex = Zones.Count > 0 ? 0 : -1; - } - - /// Serialises the current zone list into the map file JSON envelope. - public string ToJson() => MapFileService.BuildJson(Zones); - - /// Drops all zones and the selection. - public void Clear() - { - Zones.Clear(); - SelectedIndex = -1; - } - - /// Creates a rectangular region around the map centre and selects it. - public MapZone AddRegion() - { - var zone = new MapZone - { - ID = MakeUniqueId("region"), - Title = string.Empty, - Points = new List - { - DefaultCenter + new Vector2(-DefaultRegionHalfSize, -DefaultRegionHalfSize), - DefaultCenter + new Vector2(DefaultRegionHalfSize, -DefaultRegionHalfSize), - DefaultCenter + new Vector2(DefaultRegionHalfSize, DefaultRegionHalfSize), - DefaultCenter + new Vector2(-DefaultRegionHalfSize, DefaultRegionHalfSize), - }, - }; - - return Append(zone); - } - - /// - /// Creates a region without vertices and selects it — the start of - /// drawing a polygon point by point on the canvas. - /// - public MapZone StartRegion() - { - var zone = new MapZone - { - ID = MakeUniqueId("region"), - Title = string.Empty, - Points = new List(), - }; - - return Append(zone); - } - - /// - /// Creates an icon marker at the map centre with no icon chosen yet and - /// selects it. The author assigns image and scene in the - /// sidebar. - /// - public MapZone AddMarker() - { - var zone = new MapZone - { - ID = MakeUniqueId("marker"), - Title = string.Empty, - ImagePos = DefaultCenter, - }; - - return Append(zone); - } - - /// - /// Creates a free-standing text label at the map centre and selects it. - /// The label exists as soon as label_pos is set; MapWidget - /// draws zones without an icon as inscriptions. - /// - public MapZone AddLabel() - { - var zone = new MapZone - { - ID = MakeUniqueId("label"), - Title = string.Empty, - LabelPos = DefaultCenter, - }; - - return Append(zone); - } - - /// Removes the zone at the given index and adjusts the selection. - public void RemoveAt(int index) - { - if (index < 0 || index >= Zones.Count) - return; - - Zones.RemoveAt(index); - - if (Zones.Count == 0) - SelectedIndex = -1; - else if (SelectedIndex > index) - SelectedIndex--; - else if (SelectedIndex == index) - SelectedIndex = Math.Min(index, Zones.Count - 1); - } - - /// Selects the zone at the given index; out-of-range clears the selection. - public void Select(int index) - { - SelectedIndex = index >= 0 && index < Zones.Count ? index : -1; - } - - /// Appends a vertex to the selected region's polygon. - public void AddVertex(Vector2 normalized) - { - var zone = Selected; - if (zone == null) - return; - - zone.Points ??= new List(); - zone.Points.Add(normalized); - } - - /// Moves one vertex of the selected region. - public void SetVertex(int index, Vector2 normalized) - { - var zone = Selected; - if (zone?.Points == null || index < 0 || index >= zone.Points.Count) - return; - - zone.Points[index] = normalized; - } - - /// Removes one vertex of the selected region. - public void RemoveVertex(int index) - { - var zone = Selected; - if (zone?.Points == null || index < 0 || index >= zone.Points.Count) - return; - - zone.Points.RemoveAt(index); - } - - /// Moves the icon marker of the selected zone. - public void SetImagePos(Vector2 normalized) - { - var zone = Selected; - if (zone != null) - zone.ImagePos = normalized; - } - - /// Moves the text label of the selected zone. - public void SetLabelPos(Vector2 normalized) - { - var zone = Selected; - if (zone != null) - zone.LabelPos = normalized; - } - - /// - /// Enables or disables the free-standing label of the selected zone. - /// Enabling without a known position places it at the map centre. - /// - public void SetLabelEnabled(bool enabled) - { - var zone = Selected; - if (zone == null) - return; - - zone.LabelPos = enabled ? zone.LabelPos ?? DefaultCenter : null; - } - - /// ID set of every zone in the list, for duplicate validation. - public HashSet UsedIds() - { - var ids = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var zone in Zones) - { - if (!string.IsNullOrEmpty(zone.ID)) - ids.Add(zone.ID); - } - return ids; - } - - /// - /// Builds an ID that is not used yet by appending a numeric suffix - /// (region, region_2, region_3, ...). - /// - public string MakeUniqueId(string baseId) - { - var used = UsedIds(); - if (!used.Contains(baseId)) - return baseId; - - var suffix = 2; - while (used.Contains($"{baseId}_{suffix}")) - suffix++; - - return $"{baseId}_{suffix}"; - } - - private MapZone Append(MapZone zone) - { - Zones.Add(zone); - SelectedIndex = Zones.Count - 1; - return zone; - } - } -} diff --git a/src/MapEditor/MapEditorToolBoxButton.cs b/src/MapEditor/MapEditorToolBoxButton.cs deleted file mode 100644 index 5555996..0000000 --- a/src/MapEditor/MapEditorToolBoxButton.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Cthangover.Core.UI.Tool; - -namespace Cthangover.Tools.MapEditor -{ - /// - /// Toolbox button that opens the map editor from the in-game toolbar. - /// Matches the tool id registered by ; - /// always visible because map authoring is part of the normal dev workflow. - /// - public class MapEditorToolBoxButton : IToolBoxButton - { - /// Matches . - public string ToolId => "map_editor"; - - /// No dedicated icon — the button uses its text label. - public string IconPath => ""; - - /// Translation key for the button label. - public string LocaleKey => "tools/map_editor/title"; - - /// Always visible in the toolbox. - public bool IsVisible() => true; - } -} diff --git a/src/MapEditor/MapEditorToolProvider.cs b/src/MapEditor/MapEditorToolProvider.cs deleted file mode 100644 index 5a0c295..0000000 --- a/src/MapEditor/MapEditorToolProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Cthangover.Core.UI.Tool; -using Godot; - -using Cthangover.Mods.Interface.Tool; -namespace Cthangover.Tools.MapEditor -{ - /// - /// Registers the map editor as a discoverable tool window. Discovery is - /// automatic: ToolFactory scans mod assemblies for - /// implementations, so no manifest entry is - /// needed. The provider only knows how to build the window; all editing - /// logic lives in . - /// - public class MapEditorToolProvider : IToolProvider - { - /// Unique tool identifier used by the toolbox button. - public string Id => "map_editor"; - - /// Translation key for the tool's display name. - public string LocaleKey => "tools/map_editor/title"; - - /// Creates the editor window instance. - public Window CreateWindow() => new MapEditorWindow(); - - /// Adds the map editor's launcher button to the main menu tool block. - public void AddMenuButton(Container container) => ToolMenuButton.Attach(container, this); - } -} diff --git a/src/MapEditor/MapEditorWindow.cs b/src/MapEditor/MapEditorWindow.cs deleted file mode 100644 index 6f82337..0000000 --- a/src/MapEditor/MapEditorWindow.cs +++ /dev/null @@ -1,1044 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Cthangover.Core.Localization; -using Cthangover.Core.Mods.Resolvers; -using Cthangover.Core.Scenes; -using Cthangover.Core.Settings; -using Cthangover.Core.UI; -using Cthangover.Core.UI.Theming; -using Cthangover.Core.Utils; -using Cthangover.Mods.Interface.Theming; -using Cthangover.Mods.Interface.Tool; -using Cthangover.Mods.Map; -using Cthangover.Tools.Common; -using Cthangover.Tools.Services; -using Godot; - -namespace Cthangover.Tools.MapEditor -{ - /// - /// Editor window for the world map. The author picks a writable mod and a - /// file under its map/ folder (or creates one), then draws region - /// polygons, places icon markers and free-standing text labels over a - /// background preview. - /// - /// Layout: toolbar (file, save, mode, background, preview), zone list on - /// the left, canvas in the middle, properties of the selected zone on the - /// right, and an optional read-only JSON view below the canvas. Switching - /// file or mod with unsaved edits asks first. - /// - /// The editor edits raw authoring data only: the produced JSON matches - /// MapZone. Runtime state — what scenarios open or reveal — is not - /// touched; after a successful save the running map widget is asked to - /// reload so the edit is visible immediately. - /// - public partial class MapEditorWindow : ToolWindow - { - private readonly MapEditorController _controller = new(); - - private readonly List _backgroundIds = new(); - private readonly List _sceneIds = new(); - - private string _modId; - private string _path; - - private ModFileSelector _fileSelector; - private Button _selectModeBtn; - private Button _drawModeBtn; - private OptionButton _backgroundSelect; - - private FilteredList _zoneList; - private Button _deleteBtn; - - private VBoxContainer _props; - private Label _propsEmpty; - private VBoxContainer _propsBody; - private LineEdit _idEdit; - private LineEdit _titleEdit; - private Label _titlePreview; - private OptionButton _sceneSelect; - private LineEdit _descEdit; - private LineEdit _imageEdit; - private TextureRect _imagePreview; - private FilteredList _iconList; - private SpinBox _posX; - private SpinBox _posY; - private CheckBox _labelCheck; - private SpinBox _labelX; - private SpinBox _labelY; - private CheckBox _lockedCheck; - private CheckBox _hiddenCheck; - private Label _availabilityLabel; - private VBoxContainer _vertexList; - private CollapsibleSection _markerSection; - private CollapsibleSection _labelSection; - private CollapsibleSection _vertexSection; - private MapZone _sectionsShownFor; - - private MapEditorCanvas _canvas; - private TextEdit _jsonEdit; - private Button _jsonToggle; - private MapWidget _preview; - - private bool _suppress; - - public MapEditorWindow() : base("tools/map_editor/title") - { - CthangoverTheme.Apply(this); - - ReloadLocalization(); - BuildUI(); - LoadSceneList(); - LoadTextureList(); - LoadBackgroundList(); - SetHint(ToolUi.Tr("tools/map_editor/hint")); - _fileSelector.Refresh(); - } - - // ── UI construction ──────────────────────────────────────── - - private void BuildUI() - { - var outer = CreateFillContainer(); - AddChild(outer); - - outer.AddChild(BuildToolbar()); - - var main = new HSplitContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - outer.AddChild(main); - - var zonesColumn = new VBoxContainer(); - main.AddChild(CreateSidebar(zonesColumn, 240)); - BuildZoneList(zonesColumn); - - var centerSplit = new HSplitContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; - main.AddChild(centerSplit); - - var canvasColumn = new VSplitContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - centerSplit.AddChild(canvasColumn); - - var canvasPanel = new PanelContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - canvasPanel.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusMd, UiTokens.Well, UiTokens.Line)); - canvasColumn.AddChild(canvasPanel); - - _canvas = new MapEditorCanvas { Controller = _controller }; - _canvas.Changed += OnCanvasChanged; - _canvas.ZonePicked += OnCanvasZonePicked; - _canvas.ModeChanged += SyncModeButtons; - canvasPanel.AddChild(_canvas); - - _jsonEdit = ToolUi.Code(editable: false); - _jsonEdit.CustomMinimumSize = new Vector2(0, 160); - _jsonEdit.Visible = false; - canvasColumn.AddChild(_jsonEdit); - - _props = new VBoxContainer(); - centerSplit.AddChild(CreateSidebar(_props, 320)); - BuildProperties(_props); - } - - private HBoxContainer BuildToolbar() - { - var toolbar = CreateToolbar(); - - _fileSelector = new ModFileSelector(MapFileService.MapDir, ".json"); - _fileSelector.FileRequested += OnFileRequested; - toolbar.AddChild(_fileSelector); - - toolbar.AddChild(ToolUi.Button("tools/common/save", Save, ButtonKind.Primary, "tools/common/save_tip")); - toolbar.AddChild(ToolUi.Button("tools/map_editor/reload_map", OnReloadMap, ButtonKind.Secondary, "tools/map_editor/reload_map_tip")); - toolbar.AddChild(ToolUi.Divider()); - - var modeGroup = new ButtonGroup(); - _selectModeBtn = ModeButton("tools/map_editor/select_mode", modeGroup, MapEditorCanvas.ModeSelect); - _selectModeBtn.ButtonPressed = true; - toolbar.AddChild(_selectModeBtn); - _drawModeBtn = ModeButton("tools/map_editor/draw_region", modeGroup, MapEditorCanvas.ModeDraw); - toolbar.AddChild(_drawModeBtn); - toolbar.AddChild(ToolUi.Divider()); - - _backgroundSelect = new OptionButton { TooltipText = ToolUi.Tr("tools/map_editor/background"), CustomMinimumSize = new Vector2(160, 0) }; - _backgroundSelect.ItemSelected += OnBackgroundSelected; - toolbar.AddChild(_backgroundSelect); - - toolbar.AddChild(UiKit.Spacer()); - - _jsonToggle = ToolUi.Button("tools/common/json", null, ButtonKind.Tab, "tools/common/json_tip"); - _jsonToggle.ToggleMode = true; - _jsonToggle.Toggled += on => _jsonEdit.Visible = on; - toolbar.AddChild(_jsonToggle); - - toolbar.AddChild(ToolUi.Button("tools/map_editor/preview", OnPreviewPressed, ButtonKind.Secondary, "tools/map_editor/preview_tip")); - return toolbar; - } - - private Button ModeButton(string textKey, ButtonGroup group, string mode) - { - var button = ToolUi.Button(textKey, null, ButtonKind.Tab); - button.ToggleMode = true; - button.ButtonGroup = group; - button.Toggled += on => - { - if (on && !_suppress && _canvas != null) - _canvas.SetMode(mode); - }; - return button; - } - - private void BuildZoneList(VBoxContainer column) - { - column.AddChild(ToolUi.Section("tools/map_editor/zones")); - - _zoneList = new FilteredList(240); - _zoneList.SizeFlagsVertical = Control.SizeFlags.ExpandFill; - _zoneList.Chosen += OnZoneChosen; - column.AddChild(_zoneList); - - var addRow = new HBoxContainer(); - addRow.AddThemeConstantOverride("separation", UiTokens.Space1); - column.AddChild(addRow); - AddExpanding(addRow, ToolUi.Button("tools/map_editor/add_region", OnAddRegion, ButtonKind.Secondary, "tools/map_editor/add_region_tip")); - AddExpanding(addRow, ToolUi.Button("tools/map_editor/add_marker", OnAddMarker)); - AddExpanding(addRow, ToolUi.Button("tools/map_editor/add_label", OnAddLabel)); - - _deleteBtn = ToolUi.Button("tools/map_editor/delete", OnDeleteZone, ButtonKind.Danger, "tools/map_editor/delete_tip"); - column.AddChild(_deleteBtn); - } - - private static void AddExpanding(Container parent, Control control) - { - control.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - parent.AddChild(control); - } - - private void BuildProperties(VBoxContainer column) - { - column.AddChild(ToolUi.Section("tools/map_editor/props")); - - _propsEmpty = ToolUi.Hint(ToolUi.Tr("tools/map_editor/zone_missing")); - column.AddChild(_propsEmpty); - - var body = new VBoxContainer(); - body.AddThemeConstantOverride("separation", UiTokens.Space2); - column.AddChild(body); - _propsBody = body; - - _idEdit = ToolUi.Field(body, "tools/map_editor/id", ToolUi.Text("region", _ => OnIdChanged())); - _idEdit.AddThemeFontOverride("font", ToolUi.Mono()); - _titleEdit = ToolUi.Field(body, "tools/map_editor/title_key", ToolUi.Text("ui/map/…", _ => OnTitleChanged())); - _titlePreview = ToolUi.Hint(string.Empty); - body.AddChild(_titlePreview); - _descEdit = ToolUi.Field(body, "tools/map_editor/description", ToolUi.Text("ui/map/…", _ => OnFieldsChanged())); - - _sceneSelect = ToolUi.Field(body, "tools/map_editor/scene", new OptionButton()); - _sceneSelect.ItemSelected += _ => OnFieldsChanged(); - - body.AddChild(ToolUi.Section("tools/map_editor/availability")); - _lockedCheck = new CheckBox { Text = ToolUi.Tr("tools/map_editor/locked") }; - _lockedCheck.Toggled += _ => OnFieldsChanged(); - body.AddChild(_lockedCheck); - _hiddenCheck = new CheckBox { Text = ToolUi.Tr("tools/map_editor/hidden") }; - _hiddenCheck.Toggled += _ => OnFieldsChanged(); - body.AddChild(_hiddenCheck); - _availabilityLabel = ToolUi.Hint(string.Empty); - body.AddChild(_availabilityLabel); - - _markerSection = new CollapsibleSection("tools/map_editor/marker"); - body.AddChild(_markerSection); - var marker = _markerSection.Body; - var imageRow = new HBoxContainer(); - _imagePreview = new TextureRect - { - CustomMinimumSize = new Vector2(32, 32), - ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize, - StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered - }; - _imageEdit = ToolUi.Text(ToolUi.Tr("tools/map_editor/image_placeholder"), _ => OnImageChanged()); - _imageEdit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - imageRow.AddChild(_imageEdit); - imageRow.AddChild(_imagePreview); - ToolUi.Field(marker, "tools/map_editor/image", imageRow); - - _iconList = new FilteredList(110, "tools/map_editor/icon_search"); - _iconList.Chosen += OnIconChosen; - marker.AddChild(_iconList); - - _posX = ToolUi.Field(marker, "tools/map_editor/x", ToolUi.Number(0, 1, 0.001, _ => OnPositionChanged())); - _posY = ToolUi.Field(marker, "tools/map_editor/y", ToolUi.Number(0, 1, 0.001, _ => OnPositionChanged())); - - _labelSection = new CollapsibleSection("tools/map_editor/label_section"); - body.AddChild(_labelSection); - _labelCheck = new CheckBox { Text = ToolUi.Tr("tools/map_editor/label") }; - _labelCheck.Toggled += OnLabelToggled; - _labelSection.Body.AddChild(_labelCheck); - _labelX = ToolUi.Field(_labelSection.Body, "tools/map_editor/x", ToolUi.Number(0, 1, 0.001, _ => OnPositionChanged())); - _labelY = ToolUi.Field(_labelSection.Body, "tools/map_editor/y", ToolUi.Number(0, 1, 0.001, _ => OnPositionChanged())); - - _vertexSection = new CollapsibleSection("tools/map_editor/vertices"); - body.AddChild(_vertexSection); - _vertexSection.Body.AddChild(ToolUi.Hint(ToolUi.Tr("tools/map_editor/vertices_hint"))); - _vertexList = new VBoxContainer(); - _vertexList.AddThemeConstantOverride("separation", UiTokens.Space1); - _vertexSection.Body.AddChild(_vertexList); - _vertexSection.Body.AddChild(ToolUi.Button("tools/map_editor/add_vertex", OnAddVertex)); - } - - // ── data loading ─────────────────────────────────────────── - - private void LoadSceneList() - { - _sceneIds.Clear(); - _sceneSelect.Clear(); - _sceneSelect.AddItem(ToolUi.Tr("tools/map_editor/scene_none")); - - var defs = SceneDefinitions.Collect(); - if (defs != null) - _sceneIds.AddRange(defs.Keys.OrderBy(key => key, StringComparer.OrdinalIgnoreCase)); - - foreach (var scene in _sceneIds) - _sceneSelect.AddItem(scene); - } - - private void LoadTextureList() - { - _iconList.SetItems(ModResourceService.GetIconIds()); - } - - private void LoadBackgroundList() - { - _backgroundIds.Clear(); - _backgroundSelect.Clear(); - - _backgroundIds.AddRange(ModResourceService.GetBackgroundIds()); - foreach (var id in _backgroundIds) - _backgroundSelect.AddItem(id); - - var index = _backgroundIds.FindIndex(id => string.Equals(id, "map", StringComparison.OrdinalIgnoreCase)); - if (index < 0 && _backgroundIds.Count > 0) - index = 0; - - if (index >= 0) - { - _backgroundSelect.Select(index); - _canvas.SetBackground(ModResourceService.LoadBackgroundTexture(_backgroundIds[index])); - } - } - - // ── file actions ─────────────────────────────────────────── - - private void OnFileRequested(string modId, string path) - { - RunDiscardingChanges(() => Load(modId, path), () => _fileSelector.Show(_modId, _path)); - } - - private void Load(string modId, string path) - { - _modId = modId; - _path = path; - _controller.Clear(); - MarkClean(); - - if (modId == null) - { - SetStatus(ToolUi.Tr("tools/common/no_writable_mods"), ToolStatusKind.Error); - } - else if (path == null) - { - SetStatus(string.Format(ToolUi.Tr("tools/common/no_files"), modId), ToolStatusKind.Info); - } - else - { - try - { - var text = ModFileService.ReadText(modId, path); - if (!string.IsNullOrEmpty(text)) - _controller.FromJson(text); - SetStatus(string.Format(ToolUi.Tr(string.IsNullOrEmpty(text) ? "tools/common/new_file_status" : "tools/common/loaded"), modId + "/" + path)); - } - catch (Exception ex) - { - SetStatus($"{ToolUi.Tr("tools/common/load_error")}: {ex.Message}", ToolStatusKind.Error); - GameLogger.Log("MAP_EDITOR", $"load failed for '{modId}/{path}': {ex.Message}", LogLevel.Error); - } - } - - _fileSelector.Show(_modId, _path); - _canvas.FinishDrawing(); - RefreshAll(); - } - - /// Ctrl+S. - protected override void OnSaveShortcut() => Save(); - - private void Save() - { - if (_path == null) - { - SetStatus(ToolUi.Tr("tools/common/no_file_to_save"), ToolStatusKind.Error); - return; - } - - var invalid = _controller.Zones.FirstOrDefault(z => string.IsNullOrWhiteSpace(z.ID)); - if (invalid != null) - { - SetStatus(ToolUi.Tr("tools/map_editor/id_empty"), ToolStatusKind.Error); - return; - } - - var duplicate = _controller.Zones.GroupBy(z => z.ID, StringComparer.OrdinalIgnoreCase).FirstOrDefault(g => g.Count() > 1); - if (duplicate != null) - { - SetStatus($"{ToolUi.Tr("tools/map_editor/duplicate_id")} {duplicate.Key}", ToolStatusKind.Error); - return; - } - - if (!ModFileService.WriteText(_modId, _path, _controller.ToJson())) - { - SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error); - return; - } - - MarkClean(); - _fileSelector.Show(_modId, _path); - SetStatus(string.Format(ToolUi.Tr("tools/common/saved"), _modId + "/" + _path), ToolStatusKind.Ok); - ReloadRunningMap(); - } - - /// - /// Pushes the saved data into the running game. When the map panel was - /// already opened once, its widget re-reads through the service and - /// rebuilds; otherwise only the service is reloaded and the widget - /// picks the data up on its first open. - /// - private static void ReloadRunningMap() - { - if (UiHost.Instance != null - && UiHost.Instance.TryGetInstance("map", out var control) - && control is MapWidget mapWidget) - { - mapWidget.Reload(); - return; - } - - MapZoneService.Instance.Reload(); - } - - private void OnReloadMap() - { - ReloadRunningMap(); - SetStatus(ToolUi.Tr("tools/map_editor/reloaded"), ToolStatusKind.Ok); - } - - // ── shortcuts ────────────────────────────────────────────── - - /// Esc/Enter finish drawing, Delete removes the selected zone (with confirmation). - protected override bool HandleKey(InputEventKey key) - { - if (_preview != null) - return false; - - switch (key.Keycode) - { - case Key.Escape: - case Key.Enter: - case Key.KpEnter: - if (_canvas.Mode != MapEditorCanvas.ModeDraw) - return false; - _canvas.FinishDrawing(); - return true; - case Key.Delete: - OnDeleteZone(); - return true; - default: - return false; - } - } - - // ── game-like preview ────────────────────────────────────── - - /// True while the game-like preview covers the editor. - public bool PreviewActive => _preview != null; - - /// - /// Opens the game-like preview on top of the editor: the real - /// map_widget.tscn fed with the current — possibly unsaved — - /// zone list, so hover highlights, marker pills, the location card and - /// the transition button behave exactly as in the running map. The - /// preview ends through the widget itself: its close button or Escape - /// hides it, and the editor resumes. Returns false when the map - /// widget scene cannot be resolved. - /// - public bool StartPreview() - { - if (_preview != null) - return true; - - ReloadLocalization(); - - var widget = TscnScenes.LoadAndBuild("scenes/ui/map_widget.tscn") as MapWidget; - if (widget == null) - { - SetStatus(ToolUi.Tr("tools/map_editor/preview_missing"), ToolStatusKind.Error); - return false; - } - - widget.SetPreviewZones(_controller.Zones); - widget.VisibilityChanged += OnPreviewVisibilityChanged; - AddChild(widget); - widget.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect); - _preview = widget; - - widget.EnsureConstructed(); - widget.Show(); - SetStatus(ToolUi.Tr("tools/map_editor/preview_on")); - return true; - } - - /// Closes the preview and returns to editing. - public void StopPreview() - { - if (_preview == null) - return; - - var widget = _preview; - _preview = null; - - if (GodotObject.IsInstanceValid(widget)) - { - widget.VisibilityChanged -= OnPreviewVisibilityChanged; - widget.QueueFree(); - } - - UpdateTitlePreview(); - _canvas.QueueRedraw(); - SetStatus(ToolUi.Tr("tools/map_editor/preview_off")); - } - - private void OnPreviewPressed() - { - StartPreview(); - } - - private void OnPreviewVisibilityChanged() - { - if (_preview != null && GodotObject.IsInstanceValid(_preview) && !_preview.Visible) - StopPreview(); - } - - /// - /// Re-reads every mod's locale files so keys edited after the game - /// booted — the common case while authoring titles and descriptions — - /// resolve to their new values in the editor and in the preview. - /// - private static void ReloadLocalization() - { - if (GameData.Instance == null) - return; - - try - { - LocaleLoader.LoadCurrentLanguage(); - } - catch (Exception ex) - { - GameLogger.Log("MAP_EDITOR", $"localisation reload failed: {ex.Message}", LogLevel.Warning); - } - } - - private void OnBackgroundSelected(long index) - { - if (index < 0 || index >= _backgroundIds.Count) - return; - - _canvas.SetBackground(ModResourceService.LoadBackgroundTexture(_backgroundIds[(int)index])); - } - - // ── zone structure actions ───────────────────────────────── - - private void OnAddRegion() - { - _controller.StartRegion(); - _canvas.SetMode(MapEditorCanvas.ModeDraw); - SyncModeButtons(); - SetDirty(); - RefreshAll(); - SetStatus(ToolUi.Tr("tools/map_editor/draw_started")); - } - - private void OnAddMarker() - { - _controller.AddMarker(); - _canvas.FinishDrawing(); - SetDirty(); - RefreshAll(); - } - - private void OnAddLabel() - { - _controller.AddLabel(); - _canvas.FinishDrawing(); - SetDirty(); - RefreshAll(); - } - - private void OnDeleteZone() - { - var zone = _controller.Selected; - if (zone == null) - { - SetStatus(ToolUi.Tr("tools/map_editor/zone_missing"), ToolStatusKind.Error); - return; - } - - var index = _controller.SelectedIndex; - ShowConfirm("tools/map_editor/delete", "tools/map_editor/delete_confirm", () => - { - _controller.RemoveAt(index); - _canvas.FinishDrawing(); - SetDirty(); - RefreshAll(); - SetStatus(string.Format(ToolUi.Tr("tools/map_editor/deleted"), zone.ID)); - }); - } - - private void OnZoneChosen(string indexText) - { - if (_suppress || !int.TryParse(indexText, out var index)) - return; - - _canvas.FinishDrawing(); - _controller.Select(index); - RefreshAll(); - } - - private void OnCanvasZonePicked(int index) - { - RefreshSelectionOnly(); - } - - private void OnCanvasChanged() - { - SetDirty(); - RefreshValuesOnly(); - } - - private void SyncModeButtons() - { - _suppress = true; - if (_canvas.Mode == MapEditorCanvas.ModeDraw) - _drawModeBtn.ButtonPressed = true; - else - _selectModeBtn.ButtonPressed = true; - _suppress = false; - } - - private void OnAddVertex() - { - var zone = _controller.Selected; - if (zone == null) - { - SetStatus(ToolUi.Tr("tools/map_editor/zone_missing"), ToolStatusKind.Error); - return; - } - - _controller.AddVertex(zone.Points != null && zone.Points.Count > 0 - ? zone.Points[zone.Points.Count - 1] + new Vector2(0.02f, 0.02f) - : new Vector2(0.5f, 0.5f)); - SetDirty(); - RefreshAll(); - } - - private void RemoveVertex(int index) - { - _controller.RemoveVertex(index); - SetDirty(); - RefreshAll(); - } - - // ── property editors ─────────────────────────────────────── - - private void OnIdChanged() - { - if (_suppress) - return; - - var zone = _controller.Selected; - if (zone == null) - return; - - var id = _idEdit.Text.Trim(); - if (ValidateId(id, out var error)) - { - zone.ID = id; - SetDirty(); - SetStatus(string.Empty); - _idEdit.RemoveThemeColorOverride("font_color"); - } - else - { - SetStatus(error, ToolStatusKind.Error); - _idEdit.AddThemeColorOverride("font_color", UiTokens.DangerText); - } - - RefreshJson(); - RefreshZoneList(); - } - - private bool ValidateId(string id, out string error) - { - error = null; - if (string.IsNullOrWhiteSpace(id)) - { - error = ToolUi.Tr("tools/map_editor/id_empty"); - return false; - } - - foreach (var zone in _controller.Zones) - { - if (!ReferenceEquals(zone, _controller.Selected) - && string.Equals(zone.ID, id, StringComparison.OrdinalIgnoreCase)) - { - error = ToolUi.Tr("tools/map_editor/duplicate_id"); - return false; - } - } - - return true; - } - - private void OnTitleChanged() - { - if (_suppress || _controller.Selected == null) - return; - - _controller.Selected.Title = _titleEdit.Text.Trim(); - UpdateTitlePreview(); - SetDirty(); - RefreshJson(); - _canvas.QueueRedraw(); - } - - private void OnFieldsChanged() - { - if (_suppress) - return; - - var zone = _controller.Selected; - if (zone == null) - return; - - zone.Scene = _sceneSelect.Selected <= 0 ? string.Empty : _sceneIds[_sceneSelect.Selected - 1]; - zone.Description = _descEdit.Text.Trim(); - zone.Locked = _lockedCheck.ButtonPressed; - zone.Hidden = _hiddenCheck.ButtonPressed; - - SetDirty(); - RefreshJson(); - UpdateAvailability(zone); - _canvas.QueueRedraw(); - } - - private void OnImageChanged() - { - if (_suppress || _controller.Selected == null) - return; - - _controller.Selected.Image = _imageEdit.Text.Trim(); - _iconList.Select(_controller.Selected.Image); - AfterImageChanged(); - } - - private void OnIconChosen(string id) - { - if (_suppress || _controller.Selected == null || id == null) - return; - - _controller.Selected.Image = id; - _suppress = true; - _imageEdit.Text = id; - _suppress = false; - AfterImageChanged(); - } - - private void AfterImageChanged() - { - UpdateImagePreview(_controller.Selected); - SetDirty(); - _canvas.RebuildHandles(); - _canvas.QueueRedraw(); - RefreshJson(); - } - - private void OnPositionChanged() - { - if (_suppress) - return; - - var zone = _controller.Selected; - if (zone == null) - return; - - zone.ImagePos = new Vector2((float)_posX.Value, (float)_posY.Value); - if (zone.LabelPos.HasValue) - zone.LabelPos = new Vector2((float)_labelX.Value, (float)_labelY.Value); - - SetDirty(); - _canvas.RebuildHandles(); - _canvas.QueueRedraw(); - RefreshJson(); - } - - private void OnLabelToggled(bool enabled) - { - if (_suppress) - return; - - _controller.SetLabelEnabled(enabled); - SetDirty(); - RefreshValuesOnly(); - _canvas.RebuildHandles(); - _canvas.QueueRedraw(); - } - - // ── refresh ──────────────────────────────────────────────── - - private void RefreshAll() - { - RefreshZoneList(); - RefreshValuesOnly(); - _canvas.RebuildHandles(); - _canvas.QueueRedraw(); - } - - private void RefreshSelectionOnly() - { - RefreshZoneList(); - RefreshValuesOnly(); - } - - private void RefreshValuesOnly() - { - _suppress = true; - try - { - WriteSidebarFromController(); - } - finally - { - _suppress = false; - } - - RefreshVertexList(); - RefreshJson(); - } - - /// Zone rows are "kind glyph + id"; the row id is the zone index. - private void RefreshZoneList() - { - var items = new List<(string, string, Texture2D)>(); - for (var i = 0; i < _controller.Zones.Count; i++) - { - var zone = _controller.Zones[i]; - var id = string.IsNullOrEmpty(zone.ID) ? "?" : zone.ID; - items.Add((i.ToString(), KindGlyph(zone) + " " + id, null)); - } - - _zoneList.SetItems(items); - _zoneList.Select(_controller.SelectedIndex >= 0 ? _controller.SelectedIndex.ToString() : null); - _deleteBtn.Disabled = _controller.Selected == null; - } - - private static string KindGlyph(MapZone zone) - { - if (zone.Points != null && zone.Points.Count > 0) - return "▱"; - if (!string.IsNullOrEmpty(zone.Image) || !zone.LabelPos.HasValue) - return "◆"; - return "T"; - } - - private void WriteSidebarFromController() - { - var zone = _controller.Selected; - var hasZone = zone != null; - - _propsEmpty.Visible = !hasZone; - _propsBody.Visible = hasZone; - - if (!hasZone) - return; - - _idEdit.Text = zone.ID ?? string.Empty; - _idEdit.RemoveThemeColorOverride("font_color"); - _titleEdit.Text = zone.Title ?? string.Empty; - _descEdit.Text = zone.Description ?? string.Empty; - _imageEdit.Text = zone.Image ?? string.Empty; - _lockedCheck.ButtonPressed = zone.Locked; - _hiddenCheck.ButtonPressed = zone.Hidden; - _labelCheck.ButtonPressed = zone.LabelPos.HasValue; - _labelX.Editable = zone.LabelPos.HasValue; - _labelY.Editable = zone.LabelPos.HasValue; - - var sceneIndex = _sceneIds.FindIndex(id => string.Equals(id, zone.Scene, StringComparison.OrdinalIgnoreCase)); - _sceneSelect.Select(sceneIndex >= 0 ? sceneIndex + 1 : 0); - - _iconList.Select(zone.Image); - UpdateImagePreview(zone); - - _posX.Value = zone.ImagePos.X; - _posY.Value = zone.ImagePos.Y; - var labelPos = zone.LabelPos ?? Vector2.Zero; - _labelX.Value = labelPos.X; - _labelY.Value = labelPos.Y; - - UpdateTitlePreview(); - UpdateAvailability(zone); - ExpandRelevantSections(zone); - } - - /// - /// On a new selection expands only the sections that matter for the - /// zone's kind (polygon → vertices, marker → icon, label → label); the - /// author may open the others by hand, which sticks until the next pick. - /// - private void ExpandRelevantSections(MapZone zone) - { - if (ReferenceEquals(zone, _sectionsShownFor)) - return; - - _sectionsShownFor = zone; - var hasPolygon = zone.Points is { Count: > 0 } || _canvas.Mode == MapEditorCanvas.ModeDraw; - _vertexSection.Expanded = hasPolygon; - _labelSection.Expanded = zone.LabelPos.HasValue; - _markerSection.Expanded = !string.IsNullOrEmpty(zone.Image) || (!hasPolygon && !zone.LabelPos.HasValue); - } - - private void UpdateImagePreview(MapZone zone) - { - _imagePreview.Texture = zone == null || string.IsNullOrEmpty(zone.Image) ? null : Textures.Resolve(zone.Image); - } - - /// - /// Shows the availability the current authoring data produces in the - /// game. The sidebar flags alone are not the whole truth: the player - /// also refuses to travel to a zone with an empty or unregistered - /// scene, so the hint goes through the shared - /// and the editor cannot disagree with the map the player sees. - /// - private void UpdateAvailability(MapZone zone) - { - if (zone == null) - { - _availabilityLabel.Text = string.Empty; - return; - } - - string state; - var color = UiTokens.DangerText; - if (zone.Hidden) - state = ToolUi.Tr("tools/map_editor/effective_hidden"); - else if (zone.Locked) - state = ToolUi.Tr("tools/map_editor/effective_locked"); - else if (string.IsNullOrEmpty(zone.Scene)) - state = ToolUi.Tr("tools/map_editor/effective_no_scene"); - else if (MapZoneRules.IsLocked(zone, true)) - state = ToolUi.Tr("tools/map_editor/effective_unknown_scene"); - else - { - state = ToolUi.Tr("tools/map_editor/effective_open"); - color = UiTokens.Ok; - } - - _availabilityLabel.Text = $"{ToolUi.Tr("tools/map_editor/effective")} {state}"; - _availabilityLabel.AddThemeColorOverride("font_color", color); - } - - private void UpdateTitlePreview() - { - var key = _titleEdit.Text.Trim(); - if (string.IsNullOrEmpty(key)) - { - _titlePreview.Text = string.Empty; - return; - } - - var translated = TranslationServer.Translate(key).ToString(); - _titlePreview.Text = string.Equals(translated, key, StringComparison.Ordinal) - ? ToolUi.Tr("tools/map_editor/no_translation") - : "« " + translated + " »"; - } - - /// - /// One row per vertex with X/Y spin boxes. Handlers read the vertex at - /// change time — capturing the original point made editing Y revert X. - /// - private void RefreshVertexList() - { - foreach (var child in _vertexList.GetChildren()) - child.QueueFree(); - - var zone = _controller.Selected; - if (zone?.Points == null) - return; - - for (var i = 0; i < zone.Points.Count; i++) - { - var index = i; - var row = new HBoxContainer(); - row.AddThemeConstantOverride("separation", UiTokens.Space1); - - var number = UiKit.Label((i + 1).ToString(), UiTokens.FontSmall, UiTokens.Faint); - number.CustomMinimumSize = new Vector2(20, 0); - row.AddChild(number); - - var xSpin = ToolUi.Number(0, 1, 0.001, null); - xSpin.Value = zone.Points[i].X; - xSpin.ValueChanged += value => OnVertexEdited(index, (float)value, null); - row.AddChild(xSpin); - - var ySpin = ToolUi.Number(0, 1, 0.001, null); - ySpin.Value = zone.Points[i].Y; - ySpin.ValueChanged += value => OnVertexEdited(index, null, (float)value); - row.AddChild(ySpin); - - var deleteBtn = UiKit.IconButton("✕", ToolUi.Tr("tools/map_editor/delete_vertex")); - deleteBtn.Pressed += () => RemoveVertex(index); - row.AddChild(deleteBtn); - - _vertexList.AddChild(row); - } - } - - private void OnVertexEdited(int index, float? x, float? y) - { - var zone = _controller.Selected; - if (zone?.Points == null || index >= zone.Points.Count) - return; - - var current = zone.Points[index]; - _controller.SetVertex(index, new Vector2(x ?? current.X, y ?? current.Y)); - SetDirty(); - _canvas.RebuildHandles(); - _canvas.QueueRedraw(); - RefreshJson(); - } - - private void RefreshJson() - { - _jsonEdit.Text = _controller.ToJson(); - } - } -} diff --git a/src/ScenarioEditor/ScenarioEditTabs.cs b/src/ScenarioEditor/ScenarioEditTabs.cs deleted file mode 100644 index 366a112..0000000 --- a/src/ScenarioEditor/ScenarioEditTabs.cs +++ /dev/null @@ -1,315 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Cthangover.Tools.Common; -using Cthangover.Tools.Services; -using Cthangover.Core.Utils; -using Godot; - -namespace Cthangover.Core.UI.Tool.ScenarioEditor -{ - /// - /// Tabbed editor area of the scenario editor: a permanent graph tab plus - /// one text tab per opened .scenario file, with dirty tracking and a - /// monospace syntax-highlighted code editor with line numbers. Only one - /// exists; switching tabs swaps its text with the - /// tab's , which therefore holds unsaved edits - /// of background tabs. - /// - public partial class ScenarioEditTabs : VBoxContainer - { - private const int GraphTabIndex = 0; - - private TabBar _tabBar; - private CodeEdit _textEdit; - private ScenarioGraphView _graphView; - - private readonly List _tabs = new(); - private int _activeTabIndex = -1; - private bool _suppressTextChanged; - - public CodeEdit CodeEditor => _textEdit; - public string CurrentText => _textEdit.Text; - public string CurrentKey => - _activeTabIndex >= 0 && _activeTabIndex < _tabs.Count ? _tabs[_activeTabIndex].Key : null; - public string CurrentModId => - _activeTabIndex >= 0 && _activeTabIndex < _tabs.Count ? _tabs[_activeTabIndex].ModId : null; - public string CurrentRelativePath => - _activeTabIndex >= 0 && _activeTabIndex < _tabs.Count ? _tabs[_activeTabIndex].RelativePath : null; - public bool HasActiveFile => _activeTabIndex >= 0 && _activeTabIndex < _tabs.Count; - public bool IsCurrentDirty => - _activeTabIndex >= 0 && _activeTabIndex < _tabs.Count && _tabs[_activeTabIndex].IsDirty; - - public event Action TextTabSelected; - public event Action GraphTabSelected; - public event Action DirtyChanged; - public event Action GraphFileRequested; - - /// Raised on every user edit of the active text tab. - public event Action BufferChanged; - - public ScenarioEditTabs(ScenarioGraphView graphView) - { - _graphView = graphView; - - SizeFlagsHorizontal = SizeFlags.ExpandFill; - SizeFlagsVertical = SizeFlags.ExpandFill; - - _tabBar = new TabBar(); - _tabBar.TabCloseDisplayPolicy = TabBar.CloseButtonDisplayPolicy.ShowActiveOnly; - _tabBar.TabSelected += OnTabSelected; - _tabBar.TabClosePressed += OnTabClosePressed; - _tabBar.AddTab(TranslationServer.Translate("tools/scenario_editor/tab_graph")); - AddChild(_tabBar); - - _textEdit = ToolUi.Code(); - _textEdit.SyntaxHighlighter = ScenarioSyntaxService.CreateHighlighter(); - _textEdit.TextChanged += OnTextChanged; - AddChild(_textEdit); - - _graphView.ScenarioFileRequested += OnGraphScenarioFileRequested; - AddChild(_graphView); - } - - /// Opens the scenario in a new tab, or just activates its existing tab. - public void OpenFile(string key) - { - for (int i = 0; i < _tabs.Count; i++) - { - if (_tabs[i].Key == key) - { - SwitchToTextTab(i); - return; - } - } - - if (!ScenarioFileService.TryParseKey(key, out var modId, out var relativePath)) - return; - - try - { - var text = ScenarioFileService.ReadAllText(modId, relativePath); - if (text == null) - { - GameLogger.Log("SCENARIO_EDITOR", $"Failed to read scenario '{key}'", LogLevel.Error); - return; - } - - var tab = new TabData - { - Key = key, - ModId = modId, - RelativePath = relativePath, - FileName = Path.GetFileNameWithoutExtension(relativePath), - Buffer = text, - IsDirty = false - }; - - _tabs.Add(tab); - _tabBar.AddTab(tab.FileName); - _tabBar.SetTabTooltip(_tabBar.TabCount - 1, ScenarioFileService.GetDisplayPath(modId, relativePath)); - - SwitchToTextTab(_tabs.Count - 1); - } - catch (Exception ex) - { - GameLogger.Log("SCENARIO_EDITOR", $"Failed to load file: {ex.Message}", LogLevel.Error); - } - } - - /// Marks the active tab clean, remembering the editor text as the saved baseline. - public void SaveCurrentFile() - { - if (_activeTabIndex < 0 || _activeTabIndex >= _tabs.Count) - return; - - var tab = _tabs[_activeTabIndex]; - tab.Buffer = _textEdit.Text; - if (tab.IsDirty) - { - tab.IsDirty = false; - UpdateTabTitle(_activeTabIndex); - UpdateGlobalDirty(); - } - } - - /// Replaces the editor text without firing the text-changed handler. - public void SetTextSilent(string text) - { - _suppressTextChanged = true; - _textEdit.Text = text; - _suppressTextChanged = false; - } - - /// Switches to the graph tab when no file tabs are open. - public void SetViewToGraph() - { - if (_tabs.Count > 0) - return; - - _tabBar.CurrentTab = GraphTabIndex; - _textEdit.Visible = false; - _graphView.Visible = true; - GraphTabSelected?.Invoke(); - } - - /// Rebuilds the graph from the current scenario files, if the graph tab is visible. - public void RefreshGraph() - { - if (_graphView.Visible) - { - var data = ScenarioFileService.BuildSceneGraph(); - _graphView.Populate(data); - } - } - - /// Returns false when any tab still has unsaved changes. - public bool ConfirmCloseAll() - { - for (int i = _tabs.Count - 1; i >= 0; i--) - { - if (_tabs[i].IsDirty) - return false; - } - return true; - } - - private void OnTabSelected(long index) - { - if (index == GraphTabIndex) - { - SaveActiveTextEditContent(); - _activeTabIndex = -1; - _textEdit.Visible = false; - _graphView.Visible = true; - GraphTabSelected?.Invoke(); - } - else - { - _graphView.Visible = false; - _textEdit.Visible = true; - SwitchToTextTab((int)index - 1); - } - } - - private void OnTabClosePressed(long index) - { - if (index == GraphTabIndex) - return; - CloseTextTab((int)index - 1); - } - - private void SwitchToTextTab(int index) - { - if (index < 0 || index >= _tabs.Count || index == _activeTabIndex) - return; - - if (_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count) - _tabs[_activeTabIndex].Buffer = _textEdit.Text; - - _activeTabIndex = index; - _tabBar.CurrentTab = index + 1; - - var tab = _tabs[index]; - - SetTextSilent(tab.Buffer); - - TextTabSelected?.Invoke(tab.Buffer); - } - - private void CloseTextTab(int index) - { - if (index < 0 || index >= _tabs.Count) - return; - - if (_tabs[index].IsDirty) - { - var dialog = new ConfirmationDialog(); - dialog.Title = TranslationServer.Translate("tools/scenario_editor/unsaved_title"); - dialog.DialogText = TranslationServer.Translate("tools/scenario_editor/unsaved_switch_text"); - int capturedIndex = index; - dialog.Confirmed += () => - { - dialog.QueueFree(); - DoCloseTextTab(capturedIndex); - }; - dialog.Canceled += () => dialog.QueueFree(); - AddChild(dialog); - dialog.PopupCentered(); - return; - } - - DoCloseTextTab(index); - } - - private void DoCloseTextTab(int index) - { - _tabs.RemoveAt(index); - _tabBar.RemoveTab(index + 1); - - if (_tabs.Count == 0) - { - _activeTabIndex = -1; - SetTextSilent(""); - TextTabSelected?.Invoke(""); - UpdateGlobalDirty(); - return; - } - - if (index == _activeTabIndex) - { - var newIndex = Math.Min(index, _tabs.Count - 1); - _activeTabIndex = -1; - SwitchToTextTab(newIndex); - } - else if (index < _activeTabIndex) - { - _activeTabIndex--; - _tabBar.CurrentTab = _activeTabIndex + 1; - } - - UpdateGlobalDirty(); - } - - private void OnTextChanged() - { - if (_suppressTextChanged || _activeTabIndex < 0 || _activeTabIndex >= _tabs.Count) - return; - - var tab = _tabs[_activeTabIndex]; - if (!tab.IsDirty) - { - tab.IsDirty = true; - UpdateTabTitle(_activeTabIndex); - UpdateGlobalDirty(); - } - BufferChanged?.Invoke(); - } - - private void UpdateTabTitle(int index) - { - if (index < 0 || index >= _tabs.Count) - return; - var tab = _tabs[index]; - _tabBar.SetTabTitle(index + 1, tab.IsDirty ? tab.FileName + " *" : tab.FileName); - } - - private void UpdateGlobalDirty() - { - bool anyDirty = _tabs.Any(t => t.IsDirty); - DirtyChanged?.Invoke(anyDirty); - } - - private void SaveActiveTextEditContent() - { - if (_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count) - _tabs[_activeTabIndex].Buffer = _textEdit.Text; - } - - private void OnGraphScenarioFileRequested(string filePath) - { - GraphFileRequested?.Invoke(filePath); - } - } -} diff --git a/src/ScenarioEditor/ScenarioEditorToolProvider.cs b/src/ScenarioEditor/ScenarioEditorToolProvider.cs deleted file mode 100644 index a0b13a9..0000000 --- a/src/ScenarioEditor/ScenarioEditorToolProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Cthangover.Core.UI.Tool; -using Godot; - -using Cthangover.Mods.Interface.Tool; -namespace Cthangover.Core.UI.Tool.ScenarioEditor -{ - /// Registers the scenario editor as a tool window. Creates instances. - public class ScenarioEditorToolProvider : IToolProvider - { - /// Unique tool identifier. - public string Id => "scenario_editor"; - /// Translation key for the tool's display name. - public string LocaleKey => "tools/scenario_editor/title"; - /// Creates the editor window instance. - public Window CreateWindow() => new ScenarioEditorWindow(); - /// Adds the scenario editor's launcher button to the main menu tool block. - public void AddMenuButton(Container container) => ToolMenuButton.Attach(container, this); - } -} diff --git a/src/ScenarioEditor/ScenarioEditorWindow.cs b/src/ScenarioEditor/ScenarioEditorWindow.cs deleted file mode 100644 index e776b56..0000000 --- a/src/ScenarioEditor/ScenarioEditorWindow.cs +++ /dev/null @@ -1,411 +0,0 @@ -using System; -using Cthangover.Core.Scenes; -using Cthangover.Core.UI.Theming; -using Cthangover.Core.Utils; -using Cthangover.Mods.Interface.Theming; -using Cthangover.Mods.Interface.Tool; -using Cthangover.Tools.Common; -using Cthangover.Tools.Services; -using Godot; - -namespace Cthangover.Core.UI.Tool.ScenarioEditor -{ - /// - /// Tool window hosting the scenario editor: a searchable tree of every - /// mod's .scenario files, tabbed code/graph editors, a metadata panel that - /// follows the text as it is typed, background thumbnails, plus a - /// play-test mode that runs the current scenario in-game. - /// - /// Shortcuts: Ctrl+S saves the active file, F5 play-tests it. - /// - public partial class ScenarioEditorWindow : ToolWindow - { - private const double InfoRefreshDelay = 0.4; - - private Control _editLayout; - - private ScenarioFileTreePanel _fileTreePanel; - private ScenarioEditTabs _editTabs; - private ScenarioInfoPanel _infoPanel; - private ScenarioThumbnailPanel _thumbPanel; - private Timer _infoTimer; - - private Button _saveBtn; - private Button _openFileBtn; - private Button _playBtn; - private Label _filePathLabel; - - private bool _playActive; - private string _pendingKey; - private string _returnRoute; - private string _returnScene; - private CanvasLayer _stopOverlay; - - public ScenarioEditorWindow() : base("tools/scenario_editor/title") - { - CthangoverTheme.Apply(this); - BuildUI(); - SetHint(ToolUi.Tr("tools/scenario_editor/hint")); - } - - /// Opens with the graph view so the editor starts on the cross-scene map, or on the file asked for by . - public override void _Ready() - { - _editTabs.SetViewToGraph(); - if (_pendingKey != null) - OpenScenario(_pendingKey); - } - - /// - /// Opens a scenario file (key mod:path) in a text tab and selects it in the - /// tree — for other tools, e.g. the scene editor's "open" button. Before the window - /// is in the tree the key waits for , which would otherwise - /// switch the view back to the graph. - /// - public void OpenScenario(string key) - { - if (!IsInsideTree()) - { - _pendingKey = key; - return; - } - - _pendingKey = null; - _fileTreePanel.SelectFile(key); - _editTabs.OpenFile(key); - } - - /// Returns from play-test mode as soon as the in-game test play ends. - public override void _Process(double delta) - { - if (_playActive && !SceneManager.IsTestPlayActive) - ReturnFromPlay(); - } - - private void BuildUI() - { - _editLayout = new Control - { - AnchorRight = 1f, - AnchorBottom = 1f - }; - AddChild(_editLayout); - - var outerVBox = CreateFillContainer(); - _editLayout.AddChild(outerVBox); - - var toolbar = CreateToolbar(); - outerVBox.AddChild(toolbar); - - _saveBtn = ToolUi.Button("tools/common/save", OnSavePressed, ButtonKind.Primary, "tools/common/save_tip"); - toolbar.AddChild(_saveBtn); - _playBtn = ToolUi.Button("tools/scenario_editor/play", OnPlayPressed, ButtonKind.Secondary, "tools/scenario_editor/play_tooltip"); - toolbar.AddChild(_playBtn); - _openFileBtn = ToolUi.Button("tools/scenario_editor/open_file", OnOpenFilePressed, ButtonKind.Secondary, "tools/scenario_editor/open_file_tip"); - toolbar.AddChild(_openFileBtn); - toolbar.AddChild(ToolUi.Divider()); - toolbar.AddChild(ToolUi.Button("tools/scenario_editor/rescan", OnRescanPressed, ButtonKind.Secondary, "tools/scenario_editor/rescan_tip")); - - _filePathLabel = new Label - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Center, - TextOverrunBehavior = TextServer.OverrunBehavior.TrimEllipsis - }; - _filePathLabel.AddThemeFontOverride("font", ToolUi.Mono()); - _filePathLabel.AddThemeColorOverride("font_color", UiTokens.Muted); - _filePathLabel.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall); - toolbar.AddChild(_filePathLabel); - - var mainSplit = new HSplitContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill - }; - outerVBox.AddChild(mainSplit); - - var leftVBox = new VBoxContainer { SizeFlagsVertical = Control.SizeFlags.ExpandFill }; - var leftPanel = CreateSidebar(leftVBox, 320); - mainSplit.AddChild(leftPanel); - - var tabsContainer = new TabContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill, - CustomMinimumSize = new Vector2(0, 420) - }; - leftVBox.AddChild(tabsContainer); - - _fileTreePanel = (ScenarioFileTreePanel)TscnScenes.LoadAndBuild("scenes/scenario_file_tree_panel.tscn"); - _fileTreePanel.FileSelected += OnFileSelected; - tabsContainer.AddChild(_fileTreePanel); - tabsContainer.SetTabTitle(0, ToolUi.Tr("tools/scenario_editor/tab_files")); - - _infoPanel = (ScenarioInfoPanel)TscnScenes.LoadAndBuild("scenes/scenario_info_panel.tscn"); - tabsContainer.AddChild(_infoPanel); - tabsContainer.SetTabTitle(1, ToolUi.Tr("tools/scenario_editor/tab_info")); - - leftVBox.AddChild(ToolUi.Section("tools/scenario_editor/bg_previews")); - _thumbPanel = (ScenarioThumbnailPanel)TscnScenes.LoadAndBuild("scenes/scenario_thumbnail_panel.tscn"); - leftVBox.AddChild(_thumbPanel); - - _editTabs = new ScenarioEditTabs(new ScenarioGraphView()); - _editTabs.TextTabSelected += OnTextTabSelected; - _editTabs.GraphTabSelected += OnGraphTabSelected; - _editTabs.DirtyChanged += OnDirtyChanged; - _editTabs.GraphFileRequested += OnGraphFileRequested; - _editTabs.BufferChanged += () => _infoTimer.Start(); - mainSplit.AddChild(_editTabs); - - _infoTimer = new Timer { OneShot = true, WaitTime = InfoRefreshDelay }; - _infoTimer.Timeout += () => _infoPanel.Update(_editTabs.CurrentText); - AddChild(_infoTimer); - } - - private void OnFileSelected(string filePath) - { - _editTabs.OpenFile(filePath); - } - - private void OnGraphFileRequested(string filePath) - { - _fileTreePanel.SelectFile(filePath); - } - - private void OnRescanPressed() - { - _fileTreePanel.Reload(); - _editTabs.RefreshGraph(); - SetStatus(ToolUi.Tr("tools/scenario_editor/rescanned")); - } - - private void OnTextTabSelected(string text) - { - var hasFile = _editTabs.CurrentKey != null; - _filePathLabel.Text = hasFile - ? ScenarioFileService.GetDisplayPath(_editTabs.CurrentModId, _editTabs.CurrentRelativePath) : ""; - SetFileButtonsEnabled(hasFile); - _infoPanel.Update(text); - _thumbPanel.Load(text); - } - - private void OnGraphTabSelected() - { - _filePathLabel.Text = ""; - SetFileButtonsEnabled(false); - _infoPanel.Clear(); - _thumbPanel.Clear(); - _editTabs.RefreshGraph(); - } - - private void SetFileButtonsEnabled(bool enabled) - { - _saveBtn.Disabled = !enabled; - _openFileBtn.Disabled = !enabled; - _playBtn.Disabled = !enabled; - } - - private void OnDirtyChanged(bool anyDirty) - { - if (anyDirty) - SetDirty(); - else - MarkClean(); - } - - /// Ctrl+S. - protected override void OnSaveShortcut() => OnSavePressed(); - - /// F5 play-tests the active scenario. - protected override bool HandleKey(InputEventKey key) - { - if (key.Keycode != Key.F5 || !_editTabs.HasActiveFile) - return false; - - OnPlayPressed(); - return true; - } - - private void OnSavePressed() - { - if (!_editTabs.HasActiveFile) - return; - - var modId = _editTabs.CurrentModId; - var relativePath = _editTabs.CurrentRelativePath; - var text = _editTabs.CurrentText; - try - { - if (!ScenarioFileService.WriteAllText(modId, relativePath, text)) - { - SetStatus(ToolUi.Tr("tools/scenario_editor/readonly_zip"), ToolStatusKind.Error); - return; - } - - _editTabs.SaveCurrentFile(); - // The running game caches scenario headers and texts: drop them, so a new or - // edited scenario plays on the next entry into its scene without a mod reload. - SceneManager.ReloadContent(); - SetStatus(string.Format(ToolUi.Tr("tools/common/saved"), ScenarioFileService.GetDisplayPath(modId, relativePath)), ToolStatusKind.Ok); - _infoPanel.Update(text); - _thumbPanel.Load(text); - GameLogger.Log("SCENARIO_EDITOR", $"Saved: {modId}/{relativePath}"); - } - catch (Exception ex) - { - GameLogger.Log("SCENARIO_EDITOR", $"Save failed: {ex.Message}", LogLevel.Error); - SetStatus(string.Format(ToolUi.Tr("tools/scenario_editor/save_error"), ex.Message), ToolStatusKind.Error); - } - } - - private void OnOpenFilePressed() - { - if (_editTabs.CurrentKey == null) - return; - - var path = ScenarioFileService.GetAbsolutePath(_editTabs.CurrentModId, _editTabs.CurrentRelativePath); - if (string.IsNullOrEmpty(path)) - { - SetStatus(ToolUi.Tr("tools/scenario_editor/readonly_zip"), ToolStatusKind.Error); - return; - } - - var err = OS.ShellOpen(path); - if (err != Error.Ok) - { - GameLogger.Log("SCENARIO_EDITOR", $"ShellOpen '{path}' failed: {err}", LogLevel.Error); - SetStatus($"{ToolUi.Tr("tools/scenario_editor/open_file_error")}: {err}", ToolStatusKind.Error); - } - } - - private void OnPlayPressed() - { - if (!_editTabs.HasActiveFile) - return; - - var text = _editTabs.CurrentText; - var metadata = ScenarioFileService.ParseMetadata(text); - - if (metadata.Scene == "-") - { - SetStatus(ToolUi.Tr("tools/scenario_editor/no_scene_error"), ToolStatusKind.Error); - return; - } - - var modId = _editTabs.CurrentModId; - var relativePath = _editTabs.CurrentRelativePath; - try - { - if (ScenarioFileService.WriteAllText(modId, relativePath, text)) - { - _editTabs.SaveCurrentFile(); - } - else - { - GameLogger.Log("SCENARIO_EDITOR", - $"Test play: '{modId}/{relativePath}' is read-only, playing from the editor buffer", - LogLevel.Warning); - } - } - catch (Exception ex) - { - SetStatus(string.Format(ToolUi.Tr("tools/scenario_editor/save_error"), ex.Message), ToolStatusKind.Error); - return; - } - - SceneManager.IsTestPlayActive = true; - SceneManager.TestScenarioText = text; - - GameLogger.Log("SCENARIO_EDITOR", $"Test play START: scene='{metadata.Scene}', queue={text.Split('\n').Length} lines"); - - Visible = false; - - // The screen the editor was opened from (a mod menu, a mod - // route) is freed by the shell load below; remember its route so - // the play-test returns there instead of to the engine's main menu. - var sceneService = GetNodeOrNull("/root/GodotSceneService"); - var current = sceneService?.CurrentGodotSceneName; - _returnRoute = !string.IsNullOrEmpty(current) && SceneRouteRegistry.TryGet(current, out _) ? current : null; - - // Opened inside a running game: come back into the scene the - // player was in, not to the main menu (there is no way back to the - // editor from there). - var sceneManager = GetNodeOrNull("/root/SceneManager"); - var gameScene = sceneManager?.CurrentSceneName; - _returnScene = _returnRoute == null && current != "mainmenu" && !string.IsNullOrEmpty(gameScene) && sceneManager.HasScene(gameScene) - ? gameScene - : null; - - _stopOverlay = CreateStopOverlay(); - var tree = Godot.Engine.GetMainLoop() as SceneTree; - tree?.Root.AddChild(_stopOverlay); - - if (sceneManager != null) - { - sceneManager.Initialize(); - sceneManager.PendingSceneName = metadata.Scene; - } - - sceneService?.LoadShell(); - - _playActive = true; - } - - private void OnStopPressed() - { - SceneManager.IsTestPlayActive = false; - ReturnFromPlay(); - } - - private void ReturnFromPlay() - { - _playActive = false; - Visible = true; - - if (_stopOverlay != null) - { - _stopOverlay.QueueFree(); - _stopOverlay = null; - } - - var sceneService = GetNodeOrNull("/root/GodotSceneService"); - var sceneManager = GetNodeOrNull("/root/SceneManager"); - if (_returnRoute != null && SceneRouteRegistry.TryGet(_returnRoute, out var route)) - { - sceneService?.SwitchRoute(route); - } - else if (_returnScene != null && sceneManager != null) - { - sceneManager.PendingSceneName = _returnScene; - sceneService?.LoadShell(); - } - else - { - sceneService?.SwitchToMenu(); - } - - _returnRoute = null; - _returnScene = null; - SetStatus(ToolUi.Tr("tools/scenario_editor/play_stopped")); - } - - /// Stop button shown over the running game while the scenario is play-tested. - private CanvasLayer CreateStopOverlay() - { - var layer = new CanvasLayer { Layer = 128 }; - - var btn = ToolUi.Button("tools/scenario_editor/stop", OnStopPressed, ButtonKind.Danger); - btn.Theme = ToolTheme.Get(); - btn.CustomMinimumSize = new Vector2(120, 36); - - var margin = new MarginContainer(); - margin.AddThemeConstantOverride("margin_top", UiTokens.Space3); - margin.AddThemeConstantOverride("margin_left", UiTokens.Space3); - margin.AddChild(btn); - - layer.AddChild(margin); - return layer; - } - } -} diff --git a/src/ScenarioEditor/ScenarioFileTreePanel.cs b/src/ScenarioEditor/ScenarioFileTreePanel.cs deleted file mode 100644 index b3054b1..0000000 --- a/src/ScenarioEditor/ScenarioFileTreePanel.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using Cthangover.Core.UI.Theming; -using Cthangover.Tools.Services; -using Godot; - -namespace Cthangover.Core.UI.Tool.ScenarioEditor -{ - /// - /// Left-hand tree of every mod's scenario files, grouped by mod and - /// subdirectory, with a search field that keeps only matching files (and - /// expands their groups). Raises with the - /// scenario key on picks. - /// - /// Built from scenario_file_tree_panel.tscn: the tscn builder creates - /// the script node through its constructor before the child nodes exist, - /// so children are looked up lazily () — not in the - /// constructor, which is what used to crash the whole editor on open. - /// - public partial class ScenarioFileTreePanel : VBoxContainer - { - private Tree _fileTree; - private LineEdit _search; - private List _mods = new(); - private readonly Dictionary _itemToKey = new(); - private readonly Dictionary _keyToItem = new(); - private bool _isProgrammaticSelection; - private string _selectedKey; - - public event Action FileSelected; - - /// Binds child nodes and fills the tree once the panel enters the tree. - public override void _Ready() - { - Reload(); - } - - /// Re-scans the mods' scenario files and rebuilds the tree. - public void Reload() - { - Bind(); - _mods = ScenarioFileService.ScanScenarioFiles(); - Populate(); - } - - private void Bind() - { - if (_fileTree != null) - return; - - _fileTree = GetNode("FileTree"); - _fileTree.CellSelected += OnCellSelected; - _search = GetNode("Search"); - _search.PlaceholderText = TranslationServer.Translate("tools/scenario_editor/search_files"); - _search.TextChanged += _ => Populate(); - } - - private void Populate() - { - _fileTree.Clear(); - _itemToKey.Clear(); - _keyToItem.Clear(); - - var filter = _search.Text.Trim(); - var root = _fileTree.CreateItem(); - - foreach (var mod in _mods) - { - TreeItem modItem = null; - foreach (var group in mod.Groups) - { - TreeItem groupItem = null; - foreach (var file in group.Files) - { - if (filter.Length > 0 && file.RelativePath.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) - continue; - - if (modItem == null) - { - modItem = _fileTree.CreateItem(root); - modItem.SetText(0, mod.ModId); - modItem.SetSelectable(0, false); - modItem.SetCustomColor(0, UiTokens.AccentBright); - } - - if (groupItem == null) - { - if (string.IsNullOrEmpty(group.DirectoryPath)) - { - groupItem = modItem; - } - else - { - groupItem = _fileTree.CreateItem(modItem); - groupItem.SetText(0, group.DirectoryPath + "/"); - groupItem.SetSelectable(0, false); - groupItem.SetCustomColor(0, UiTokens.Muted); - groupItem.Collapsed = filter.Length == 0; - } - } - - var fileItem = _fileTree.CreateItem(groupItem); - fileItem.SetText(0, file.Name); - fileItem.SetTooltipText(0, ScenarioFileService.GetDisplayPath(file.ModId, file.RelativePath)); - _itemToKey[fileItem] = file.Key; - _keyToItem[file.Key] = fileItem; - } - } - } - - if (_selectedKey != null && _keyToItem.TryGetValue(_selectedKey, out var selected)) - SelectSilently(selected); - } - - /// Selects the tree row of a scenario key and raises . - public void SelectFile(string key) - { - Bind(); - _selectedKey = key; - if (!_keyToItem.TryGetValue(key, out var item)) - { - _search.Text = string.Empty; - Populate(); - if (!_keyToItem.TryGetValue(key, out item)) - return; - } - - SelectSilently(item); - FileSelected?.Invoke(key); - } - - private void SelectSilently(TreeItem item) - { - for (var parent = item.GetParent(); parent != null; parent = parent.GetParent()) - parent.Collapsed = false; - - _isProgrammaticSelection = true; - item.Select(0); - _fileTree.ScrollToItem(item); - _isProgrammaticSelection = false; - } - - private void OnCellSelected() - { - if (_isProgrammaticSelection) - return; - - var item = _fileTree.GetSelected(); - if (item == null || !_itemToKey.TryGetValue(item, out var key)) - return; - - _selectedKey = key; - FileSelected?.Invoke(key); - } - } -} diff --git a/src/ScenarioEditor/ScenarioGraphLayout.cs b/src/ScenarioEditor/ScenarioGraphLayout.cs deleted file mode 100644 index 352d3f8..0000000 --- a/src/ScenarioEditor/ScenarioGraphLayout.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Cthangover.Tools.Services; -using Godot; - -namespace Cthangover.Core.UI.Tool.ScenarioEditor -{ - /// - /// Layered left-to-right layout of the scene graph: a scene's column is - /// its distance (in scene switches) from the start scenes — the - /// start_scene node, or every scene nothing switches to when there - /// is none — so the story reads from left to right. Scenes no start - /// reaches are stacked in one extra column on the right. Inside a column - /// scenes are sorted by name, which keeps the picture stable between - /// rebuilds. - /// - public static class ScenarioGraphLayout - { - private const float ColumnWidth = 300f; - private const float RowHeight = 130f; - private const float Margin = 40f; - - /// Canvas position per lower-cased scene name. - public static Dictionary Compute(SceneGraphData data) - { - var names = data.Nodes.Select(n => n.SceneName.ToLowerInvariant()).Distinct().ToList(); - var outgoing = names.ToDictionary(n => n, _ => new List()); - var incoming = new HashSet(); - foreach (var edge in data.Edges) - { - var from = edge.FromScene.ToLowerInvariant(); - var to = edge.ToScene.ToLowerInvariant(); - if (!outgoing.ContainsKey(from) || !outgoing.ContainsKey(to) || from == to) - continue; - outgoing[from].Add(to); - incoming.Add(to); - } - - var starts = data.Nodes.Where(n => n.IsStartScene).Select(n => n.SceneName.ToLowerInvariant()).ToList(); - if (starts.Count == 0) - starts = names.Where(n => !incoming.Contains(n)).ToList(); - - var depth = new Dictionary(); - var queue = new Queue(); - foreach (var start in starts) - { - depth[start] = 0; - queue.Enqueue(start); - } - while (queue.Count > 0) - { - var current = queue.Dequeue(); - foreach (var next in outgoing[current]) - { - if (depth.ContainsKey(next)) - continue; - depth[next] = depth[current] + 1; - queue.Enqueue(next); - } - } - - var unreachableColumn = depth.Count > 0 ? depth.Values.Max() + 1 : 0; - var result = new Dictionary(); - foreach (var column in names.GroupBy(n => depth.TryGetValue(n, out var d) ? d : unreachableColumn).OrderBy(g => g.Key)) - { - var row = 0; - foreach (var name in column.OrderBy(n => n, StringComparer.Ordinal)) - result[name] = new Vector2(Margin + column.Key * ColumnWidth, Margin + row++ * RowHeight); - } - return result; - } - } -} diff --git a/src/ScenarioEditor/ScenarioGraphView.cs b/src/ScenarioEditor/ScenarioGraphView.cs deleted file mode 100644 index deae56a..0000000 --- a/src/ScenarioEditor/ScenarioGraphView.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Cthangover.Core.UI.Theming; -using Cthangover.Tools.Services; -using Godot; - -using Cthangover.Mods.Interface.Theming; -namespace Cthangover.Core.UI.Tool.ScenarioEditor -{ - /// Cross-scene graph of all scenario files: a GraphEdit of scene nodes linked by switch targets, with a detail panel for the selected scenario. - public partial class ScenarioGraphView : HSplitContainer - { - private GraphEdit _graph; - private RichTextLabel _infoLabel; - private Dictionary _nodes; - private Dictionary _lookup; - - public event Action ScenarioFileRequested; - - public ScenarioGraphView() - { - SizeFlagsHorizontal = SizeFlags.ExpandFill; - SizeFlagsVertical = SizeFlags.ExpandFill; - - _graph = new GraphEdit - { - SizeFlagsHorizontal = SizeFlags.ExpandFill, - SizeFlagsVertical = SizeFlags.ExpandFill, - RightDisconnects = false, - ConnectionLinesThickness = 2f, - ConnectionLinesAntialiased = true, - MinimapEnabled = true, - ShowGrid = true, - SnappingEnabled = true, - SnappingDistance = 20 - }; - _graph.ConnectionRequest += OnConnectionRequest; - _graph.DisconnectionRequest += OnDisconnectionRequest; - _graph.NodeSelected += OnNodeSelected; - _graph.NodeDeselected += OnNodeDeselected; - AddChild(_graph); - - var panel = new VBoxContainer { CustomMinimumSize = new Vector2(260, 0) }; - panel.SizeFlagsVertical = SizeFlags.ExpandFill; - AddChild(panel); - - var hint = UiKit.Hint(TranslationServer.Translate("tools/scenario_editor/graph_select_node")); - hint.AutowrapMode = TextServer.AutowrapMode.WordSmart; - panel.AddChild(hint); - - _infoLabel = new RichTextLabel - { - BbcodeEnabled = true, - SizeFlagsVertical = SizeFlags.ExpandFill - }; - _infoLabel.AddThemeFontSizeOverride("normal_font_size", UiTokens.FontBody); - _infoLabel.AddThemeFontSizeOverride("bold_font_size", UiTokens.FontBodyLarge); - _infoLabel.AddThemeColorOverride("default_color", UiTokens.Body); - _infoLabel.MetaClicked += OnLinkClicked; - panel.AddChild(_infoLabel); - } - - /// Rebuilds the graph nodes and edges, preserving the nodes' saved canvas positions. - public void Populate(SceneGraphData data) - { - var savedPositions = SaveNodePositions(); - ClearGraph(); - - _nodes = new Dictionary(); - _lookup = new Dictionary(); - - var layout = ScenarioGraphLayout.Compute(data); - - foreach (var nodeData in data.Nodes) - { - var key = nodeData.SceneName.ToLowerInvariant(); - if (_nodes.ContainsKey(key)) - continue; - - var bgCount = nodeData.Backgrounds?.Count ?? 0; - var firstBg = bgCount > 0 ? nodeData.Backgrounds[0] : ""; - - var position = savedPositions.TryGetValue(key, out var saved) - ? saved - : layout[key]; - - var gn = new GraphNode - { - Title = nodeData.SceneName, - Size = new Vector2(240, 90), - PositionOffset = position - }; - - var nodeLabel = new Label - { - Text = string.Format(Tr("tools/scenario_editor/graph_node"), nodeData.ModId, nodeData.ScenarioCount) + - (bgCount > 0 ? "\n" + string.Format(Tr("tools/scenario_editor/graph_node_bg"), firstBg) + - (bgCount > 1 ? $" +{bgCount - 1}" : "") : "") - }; - nodeLabel.AddThemeColorOverride("font_color", UiTokens.Body); - nodeLabel.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall); - gn.AddChild(nodeLabel); - - var color = UiTokens.White; - if (nodeData.IsStartScene) - { - color = UiTokens.Success; - gn.AddThemeStyleboxOverride("titlebar", CreateTitlebarStyle(UiTokens.Alpha(UiTokens.Success, 0.3f))); - } - else if (nodeData.HasErrors) - { - color = UiTokens.DangerText; - gn.AddThemeStyleboxOverride("titlebar", CreateTitlebarStyle(UiTokens.Alpha(UiTokens.Danger, 0.5f))); - } - gn.SetSlot(0, true, 0, UiTokens.White, true, 0, color); - - _graph.AddChild(gn); - _nodes[key] = gn; - _lookup[key] = nodeData; - - } - - foreach (var edge in data.Edges) - { - var srcK = edge.FromScene.ToLowerInvariant(); - var dstK = edge.ToScene.ToLowerInvariant(); - if (!_nodes.ContainsKey(srcK) || !_nodes.ContainsKey(dstK)) - continue; - _graph.ConnectNode(_nodes[srcK].Name, 0, _nodes[dstK].Name, 0); - } - - } - - private Dictionary SaveNodePositions() - { - var positions = new Dictionary(); - if (_nodes == null) - return positions; - foreach (var kv in _nodes) - positions[kv.Key] = kv.Value.PositionOffset; - return positions; - } - - private static StyleBoxFlat CreateTitlebarStyle(Color bgColor) - { - var style = UiKit.Frame(UiTokens.RadiusMd, bgColor); - style.CornerRadiusBottomLeft = 0; - style.CornerRadiusBottomRight = 0; - return style; - } - - private void ClearGraph() - { - foreach (var c in _graph.GetChildren()) - if (c is GraphNode gn) - gn.QueueFree(); - _infoLabel.Text = ""; - _nodes?.Clear(); - _lookup?.Clear(); - } - - private void OnConnectionRequest(StringName fromNode, long fromPort, StringName toNode, long toPort) { } - - private void OnDisconnectionRequest(StringName fromNode, long fromPort, StringName toNode, long toPort) { } - - private void OnNodeSelected(Node n) - { - if (n is not GraphNode gn) - return; - var key = gn.Title.ToString().ToLowerInvariant(); - if (!_lookup.TryGetValue(key, out var nodeData)) - return; - - var sb = new System.Text.StringBuilder(); - sb.AppendLine($"[b]{nodeData.SceneName}[/b]"); - sb.AppendLine(string.Format(Tr("tools/scenario_editor/graph_detail_mod"), nodeData.ModId)); - if (nodeData.Backgrounds.Count > 0) - sb.AppendLine(string.Format(Tr("tools/scenario_editor/graph_node_bg"), string.Join(", ", nodeData.Backgrounds))); - sb.AppendLine(); - sb.AppendLine(string.Format(Tr("tools/scenario_editor/graph_detail_scenarios"), nodeData.Scenarios.Count)); - - foreach (var sc in nodeData.Scenarios.OrderBy(s => s.Priority)) - { - var url = Uri.EscapeDataString(sc.Key); - sb.AppendLine($" [{sc.Priority}] [url={url}]{sc.Name}[/url]"); - } - - _infoLabel.Text = sb.ToString(); - } - - private void OnNodeDeselected(Node n) - { - _infoLabel.Text = ""; - } - - private static string Tr(string key) => TranslationServer.Translate(key); - - private void OnLinkClicked(Variant meta) - { - var path = Uri.UnescapeDataString(meta.AsString()); - ScenarioFileRequested?.Invoke(path); - } - } -} diff --git a/src/ScenarioEditor/ScenarioInfoPanel.cs b/src/ScenarioEditor/ScenarioInfoPanel.cs deleted file mode 100644 index 1e9535d..0000000 --- a/src/ScenarioEditor/ScenarioInfoPanel.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Linq; -using Cthangover.Tools.Services; -using Cthangover.Core.UI.Theming; -using Cthangover.Mods.Interface.Tool; -using Godot; - -namespace Cthangover.Core.UI.Tool.ScenarioEditor -{ - /// Read-only metadata panel for the scenario in the active text tab: scene, priority, condition, switch targets, locale keys and quest references. - public partial class ScenarioInfoPanel : VBoxContainer - { - private Label _infoScene; - private Label _infoPriority; - private Label _infoCondition; - private Label _infoSwitchTargets; - private Label _infoLocaleKeys; - private Label _infoQuestRefs; - - /// Binds the rows once the panel enters the tree (see ). - public override void _Ready() - { - Bind(); - } - - /// - /// Looks the rows up lazily: the tscn builder runs the constructor before - /// the child nodes exist, so lookups there would fail. - /// - private void Bind() - { - if (_infoScene != null) - return; - - SetHint("HintScene", "tools/scenario_editor/info_scene"); - SetHint("HintPriority", "tools/scenario_editor/info_priority"); - SetHint("HintCondition", "tools/scenario_editor/info_condition"); - SetHint("HintSwitchTargets", "tools/scenario_editor/info_switch_targets"); - SetHint("HintLocaleKeys", "tools/scenario_editor/info_locale_keys"); - SetHint("HintQuestRefs", "tools/scenario_editor/info_quest_refs"); - - _infoScene = Value("InfoScene"); - _infoPriority = Value("InfoPriority"); - _infoCondition = Value("InfoCondition"); - _infoSwitchTargets = Value("InfoSwitchTargets"); - _infoLocaleKeys = Value("InfoLocaleKeys"); - _infoQuestRefs = Value("InfoQuestRefs"); - } - - private void SetHint(string node, string key) - { - var label = GetNode