Compare commits
25
Commits
d82733e3f7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
152096337b | ||
|
|
8944ba756c | ||
|
|
5be5bed0c5 | ||
|
|
cbc9472738 | ||
|
|
84894b1e98 | ||
|
|
80c86063c4 | ||
|
|
93ebb12c58 | ||
|
|
033192dcc9 | ||
|
|
3ddb61d61e | ||
|
|
e1a1d770cf | ||
|
|
6ca1fff35f | ||
|
|
3b2c1ddb95 | ||
|
|
972df5ccff | ||
|
|
2b8eaa5452 | ||
|
|
5f3f4b0d82 | ||
|
|
bd9147e1b6 | ||
|
|
bf4ac5e27a | ||
|
|
2bb0a07edb | ||
|
|
754b854817 | ||
|
|
7b6ae80616 | ||
|
|
55ef9416ef | ||
|
|
8742aa3da5 | ||
|
|
578ba7c5d5 | ||
|
|
e9b9205514 | ||
|
|
03ee4dbc8f |
No files matched your search
@@ -0,0 +1,30 @@
|
||||
# Line endings: LF everywhere, on every OS, whatever core.autocrlf says.
|
||||
* text=auto eol=lf
|
||||
|
||||
# cmd.exe misreads labels and goto in LF batch files, so they stay CRLF.
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# Binaries: never converted, never diffed as text.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.webp binary
|
||||
*.svg text
|
||||
*.ogg binary
|
||||
*.wav binary
|
||||
*.mp3 binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.dll binary
|
||||
*.so binary
|
||||
*.dylib binary
|
||||
*.pdb binary
|
||||
*.exe binary
|
||||
*.zip binary
|
||||
*.pck binary
|
||||
*.moc3 binary
|
||||
*.res binary
|
||||
*.scn binary
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# Godot 4.x
|
||||
.godot/
|
||||
.idea/
|
||||
sdk/
|
||||
bin/
|
||||
obj/
|
||||
|
||||
#ai
|
||||
.omo/
|
||||
.opencode/
|
||||
|
||||
gen/
|
||||
obj/
|
||||
bin/
|
||||
art_sources/
|
||||
|
||||
*.tres~
|
||||
*.tscn~
|
||||
|
||||
# GDScript LSP
|
||||
.godot/
|
||||
|
||||
# Export
|
||||
*.exe
|
||||
*.dmg
|
||||
*.apk
|
||||
*.aab
|
||||
*.pck
|
||||
*.zip
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor
|
||||
*.import
|
||||
*.godot.uid
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"id": "dev_tools",
|
||||
"name": "Developer Tools",
|
||||
"author": "ct",
|
||||
"version": "1.0.0",
|
||||
"description": "Mod authoring toolset: the scenario graph editor, content validators and the mod manager.",
|
||||
"mechanics": [
|
||||
{
|
||||
"id": "editor",
|
||||
"name": "Scenario Editor",
|
||||
"description": "Graph-based scenario and scene authoring tools.",
|
||||
"mods": ["tools>=1"]
|
||||
},
|
||||
{
|
||||
"id": "validation",
|
||||
"name": "Validation",
|
||||
"description": "Optional content validation suite.",
|
||||
"mods": ["validator>=0.1"],
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "manager",
|
||||
"name": "Mod Manager",
|
||||
"description": "Catalog browser and installer.",
|
||||
"mods": ["mod_manager>=1"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
+73
@@ -0,0 +1,73 @@
|
||||
# Editor Tools — making scenes and scenarios inside the running game
|
||||
|
||||
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.
|
||||
|
||||
## Opening the tools
|
||||
|
||||
In a game, the hammer button on the toolbar opens the tool box:
|
||||
|
||||

|
||||
|
||||
- **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.
|
||||
|
||||
The windows stay open across scene changes, can be moved and resized, and close with the cross.
|
||||
|
||||
## A whole small edit: a new place
|
||||
|
||||
The novel template has a town, a bar and a home. Here an author adds a park that can be reached from the town, using only the editors.
|
||||
|
||||
**1. A new scene.** In the scene editor press **+ New scene**, type the name, choose the mod to save it to and add a background:
|
||||
|
||||

|
||||
|
||||
Save (Ctrl+S) writes `scenes/my_novel_park.json` into `my_novel_game`; the game knows the scene at once.
|
||||
|
||||
**2. Its scenario.** **+ New scenario** opens the scenario editor with a file for the scene. The park offers one way back:
|
||||
|
||||

|
||||
|
||||
**3. A way there.** In the town's scenario one answer and one label are added:
|
||||
|
||||

|
||||
|
||||
```scenario
|
||||
option "Go to the park" -> :park
|
||||
|
||||
:park
|
||||
switch_scene my_novel_park
|
||||
end
|
||||
```
|
||||
|
||||
Saving a scenario drops the game's scenario caches, so the next entry into the town plays the new text.
|
||||
|
||||
**4. Play it.** **Go to scene** in the scene editor, or simply playing on, enters the town, and the new answer is there:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## The editors
|
||||
|
||||
- **Scene editor** — the places of a novel: name, the mod to save to, backgrounds (one is picked at random on entry), ambient sound with a listen button, the default scenario and the scene's scenarios. **Go to scene** ends the running dialog and enters the scene.
|
||||
- **Scenario editor** — the files of every mod, a summary and a graph of scenes and transitions; DSL highlighting; Ctrl+S saves; **Play** (F5) saves and plays the scenario inside the running game, and Stop returns to where the game was; **Open in external editor** opens the file in the system's editor.
|
||||
- **Map Editor** — the zones of the world map.
|
||||
- **Interactive Editor** — clickable objects of the current scene.
|
||||
- **Light Editor** — the lights of the current scene.
|
||||
- **Scene builder** — Godot `.tscn` scenes for mods.
|
||||
- **Catalog export** — packs a mod into a zip and writes its entry for a mod catalog with checksums.
|
||||
|
||||
Undo works while editing (Ctrl+Z in the text and in the forms of the entity editors). Checking a mod for broken references is the Validator's job; the editors do not stop you from saving a scenario with a mistake.
|
||||
|
||||
## Whose files are written
|
||||
|
||||
The tools write only into mods installed as folders. A scene is the sum of the `scenes/<name>.json` files of all mods, so the scene editor writes the file of the mod chosen in **Save to**. Changing a scene that belongs to another or a packed mod creates a file in your mod that adds to it; backgrounds that come from other files are shown locked. Scenarios are saved in the mod they belong to.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
1. **The hammer button is missing.** The game does not include the tools; add the Developer Tools toolset in its collection or run with `--with-mods=editor_tools`.
|
||||
2. **A new scenario does not play.** Its header names another scene, or another scenario of the scene is chosen first (compare their `priority`).
|
||||
3. **Save to lists no mods.** Only mods installed as folders can be written to.
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# Editor Tools — crear escenas y escenarios dentro del juego en marcha
|
||||
|
||||
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.
|
||||
|
||||
## Cómo abrir las herramientas
|
||||
|
||||
Dentro de un juego, el botón del martillo de la barra de herramientas abre la caja de herramientas:
|
||||
|
||||

|
||||
|
||||
- **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 ventanas siguen abiertas al cambiar de escena, se pueden mover y redimensionar, y se cierran con la cruz.
|
||||
|
||||
## Una edición pequeña completa: un lugar nuevo
|
||||
|
||||
La plantilla de novela tiene un pueblo, un bar y una casa. Aquí añadiremos un parque al que se llega desde el pueblo usando solo los editores.
|
||||
|
||||
**1. Una escena nueva.** En el editor de escenas pulsa **+ Nueva escena**, escribe el nombre, elige el mod donde guardarla y añade un fondo:
|
||||
|
||||

|
||||
|
||||
Guardar (Ctrl+S) escribe `scenes/my_novel_park.json` en `my_novel_game`; el juego conoce la escena al instante.
|
||||
|
||||
**2. Su escenario.** **+ Nuevo escenario** abre el editor de escenarios con un archivo para la escena. El parque ofrece una única salida:
|
||||
|
||||

|
||||
|
||||
**3. Un camino hasta allí.** En el escenario del pueblo se añaden una respuesta y una etiqueta:
|
||||
|
||||

|
||||
|
||||
```scenario
|
||||
option "Ir al parque" -> :park
|
||||
|
||||
:park
|
||||
switch_scene my_novel_park
|
||||
end
|
||||
```
|
||||
|
||||
Guardar un escenario descarta las cachés de escenarios del juego, así que la próxima entrada al pueblo ya reproduce el texto nuevo.
|
||||
|
||||
**4. Juégalo.** **Ir a la escena** en el editor de escenas, o simplemente seguir jugando, entra en el pueblo, y la nueva respuesta ya está ahí:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Los editores
|
||||
|
||||
- **Editor de escenas**: los lugares de una novela: nombre, el mod donde guardar, fondos (al entrar se elige uno al azar), sonido ambiente con botón de escucha, el escenario por defecto y los escenarios de la escena. **Ir a la escena** termina el diálogo en curso y entra en la escena.
|
||||
- **Editor de escenarios**: los archivos de todos los mods, un resumen y un grafo de escenas y transiciones; resaltado de DSL; Ctrl+S guarda; **Jugar** (F5) guarda y reproduce el escenario dentro del juego en marcha, y Detener vuelve a donde estaba el juego; **Abrir en un editor externo** abre el archivo en el editor del sistema.
|
||||
- **Editor de mapas**: las zonas del mapa del mundo.
|
||||
- **Editor de interactivos**: los objetos clicables de la escena actual.
|
||||
- **Editor de luces**: las luces de la escena actual.
|
||||
- **Constructor de escenas**: escenas `.tscn` de Godot para los mods.
|
||||
- **Exportación al catálogo**: empaqueta un mod en un zip y escribe su entrada para un catálogo de mods con sumas de comprobación.
|
||||
|
||||
Deshacer funciona mientras editas (Ctrl+Z en el texto y en los formularios de los editores de objetos y personajes). Comprobar un mod en busca de referencias rotas es tarea de Validator; los editores no te impiden guardar un escenario con un error.
|
||||
|
||||
## De quién son los archivos que se escriben
|
||||
|
||||
Las herramientas solo escriben en mods instalados como carpetas. Una escena es la suma de los archivos `scenes/<name>.json` de todos los mods, así que el editor de escenas escribe el archivo del mod elegido en **Guardar en**. Si cambias una escena que pertenece a otro mod o a un mod empaquetado, se crea un archivo en tu mod que la amplía; los fondos que vienen de otros archivos se muestran bloqueados. Los escenarios se guardan en el mod al que pertenecen.
|
||||
|
||||
## Si algo sale mal
|
||||
|
||||
1. **No aparece el botón del martillo.** El juego no incluye las herramientas; añade el conjunto Developer Tools a su colección o ejecútalo con `--with-mods=editor_tools`.
|
||||
2. **Un escenario nuevo no se reproduce.** Su cabecera nombra otra escena, o antes se elige otro escenario de la escena (compara sus `priority`).
|
||||
3. **Guardar en no muestra ningún mod.** Solo se puede escribir en mods instalados como carpetas.
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# Editor Tools — 実行中のゲーム内でシーンとシナリオを作る
|
||||
|
||||
Editor Tools は、実行中のゲームの上に開く作者向けのウィンドウ群です。シーンエディター、シナリオエディター、ワールドマップ・インタラクティブ・ライトの各エディター、Godot シーンのビルダー、MOD カタログの書き出しが含まれます。保存されるのはどれも普通の MOD ファイルで、ゲームは変更をすぐに取り込みます。そのため、場所を書いて、その場に入って遊ぶまでを、再起動なしで行えます。
|
||||
|
||||
この MOD には、アイテムエディターとキャラクターエディターの土台も入っています(本体は別の MOD、Items Editor と Characters Editor です)。ゲームにこれらのツールを入れるには、Developer Tools ツールセットか、そのゲーム独自のコレクションを使います。
|
||||
|
||||
## ツールを開く
|
||||
|
||||
ゲーム内では、ツールバーのハンマーのボタンでツールボックスが開きます:
|
||||
|
||||

|
||||
|
||||
- ツールボックスには**シーンエディター**、**マップエディター**、**インタラクティブエディター**、**ライトエディター**が入っています。他のツール MOD もここに項目を追加します(アイテム、キャラクター、RPG マップの各エディター、データエディター)。
|
||||
- **シナリオエディター**はシーンエディターから開きます。シーンを選び、そのシナリオのひとつを選んで**開く**を押すか、**+ 新しいシナリオ**を押します。
|
||||
- メインメニューでツールを読み込んでいる場合(たとえば `--all-mods`)、メニューのツール欄にも各ツールのボタンが並びます。シーンビルダーとカタログの書き出しも含まれます。
|
||||
|
||||
ウィンドウはシーンを切り替えても開いたまま残り、移動やサイズ変更ができ、×で閉じます。
|
||||
|
||||
## 小さな編集をひと通り: 新しい場所
|
||||
|
||||
ノベルのテンプレートには、町、バー、家があります。ここではエディターだけを使って、町から行ける公園を追加します。
|
||||
|
||||
**1. 新しいシーン。** シーンエディターで **+ 新しいシーン**を押し、名前を入力し、保存先の MOD を選んで背景を追加します:
|
||||
|
||||

|
||||
|
||||
保存(Ctrl+S)すると `scenes/my_novel_park.json` が `my_novel_game` に書き込まれ、ゲームはそのシーンをすぐに認識します。
|
||||
|
||||
**2. そのシナリオ。** **+ 新しいシナリオ**を押すと、そのシーンのファイルを開いたシナリオエディターが立ち上がります。公園からは戻る道が一本だけあります:
|
||||
|
||||

|
||||
|
||||
**3. そこへ行く道。** 町のシナリオに、答えをひとつとラベルをひとつ追加します:
|
||||
|
||||

|
||||
|
||||
```scenario
|
||||
option "公園へ行く" -> :park
|
||||
|
||||
:park
|
||||
switch_scene my_novel_park
|
||||
end
|
||||
```
|
||||
|
||||
シナリオを保存するとゲームのシナリオキャッシュが破棄されるので、次に町へ入ったときには新しいテキストが再生されます。
|
||||
|
||||
**4. 遊んでみる。** シーンエディターの**シーンへ移動**を押すか、そのまま遊び続けると町に入り、新しい答えがそこにあります:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 各エディター
|
||||
|
||||
- **シーンエディター** — ノベルの場所: 名前、保存先の MOD、背景(入るたびにひとつがランダムで選ばれます)、試聴ボタン付きの環境音、既定のシナリオ、そのシーンのシナリオ。**シーンへ移動**は進行中のダイアログを終わらせてシーンに入ります。
|
||||
- **シナリオエディター** — すべての MOD のファイル、概要、シーンと遷移のグラフ、DSL のハイライト、Ctrl+S で保存。**プレイ**(F5)は保存して実行中のゲーム内でシナリオを再生し、停止するとゲームが元いた場所へ戻ります。**外部エディターで開く**はシステムのエディターでファイルを開きます。
|
||||
- **マップエディター** — ワールドマップのゾーン。
|
||||
- **インタラクティブエディター** — 現在のシーンのクリックできるオブジェクト。
|
||||
- **ライトエディター** — 現在のシーンのライト。
|
||||
- **シーンビルダー** — MOD 用の Godot `.tscn` シーン。
|
||||
- **カタログの書き出し** — MOD を zip にまとめ、チェックサム付きで MOD カタログ用のエントリーを書き出します。
|
||||
|
||||
編集中は元に戻せます(テキスト内と、アイテム・キャラクターエディターのフォームで Ctrl+Z)。壊れた参照の検査は Validator の仕事で、エディターは間違いのあるシナリオの保存を止めません。
|
||||
|
||||
## どの MOD のファイルが書かれるか
|
||||
|
||||
ツールが書き込むのは、フォルダーとしてインストールされた MOD だけです。シーンは全 MOD の `scenes/<name>.json` の合計なので、シーンエディターは**保存先**で選んだ MOD のファイルを書き込みます。他の MOD やパッケージ済みの MOD に属するシーンを変更すると、それを補うファイルがあなたの MOD に作られ、他のファイル由来の背景はロックされて表示されます。シナリオは所属する MOD に保存されます。
|
||||
|
||||
## うまくいかないとき
|
||||
|
||||
1. **ハンマーのボタンがない。** そのゲームにツールが入っていません。コレクションに Developer Tools ツールセットを追加するか、`--with-mods=editor_tools` で起動してください。
|
||||
2. **新しいシナリオが再生されない。** ヘッダーが別のシーンを指しているか、そのシーンの別のシナリオが先に選ばれています(`priority` を比べてください)。
|
||||
3. **保存先に MOD が出ない。** 書き込めるのはフォルダーとしてインストールされた MOD だけです。
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# Editor Tools — 실행 중인 게임 안에서 장면과 시나리오 만들기
|
||||
|
||||
Editor Tools는 실행 중인 게임 위에 열리는 창작용 창 모음입니다. 장면 에디터, 시나리오 에디터, 월드 맵·상호작용·조명 에디터, Godot 장면 빌더, 모드 카탈로그 내보내기가 들어 있습니다. 저장되는 것은 모두 평범한 모드 파일이고 게임은 변경을 곧바로 받아들입니다. 그래서 장소를 쓰고, 바로 들어가 플레이하는 일을 재시작 없이 할 수 있습니다.
|
||||
|
||||
이 모드에는 아이템·캐릭터 에디터의 기반도 들어 있습니다(본체는 별도 모드인 Items Editor와 Characters Editor입니다). 게임에 이 도구들을 넣으려면 Developer Tools 도구 모음이나 그 게임의 컬렉션을 사용합니다.
|
||||
|
||||
## 도구 열기
|
||||
|
||||
게임 안에서 도구 모음의 망치 버튼을 누르면 도구 상자가 열립니다:
|
||||
|
||||

|
||||
|
||||
- 도구 상자에는 **장면 에디터**, **맵 에디터**, **상호작용 에디터**, **조명 에디터**가 있습니다. 다른 도구 모드도 여기에 항목을 추가합니다(아이템, 캐릭터, RPG 맵 에디터, 데이터 에디터).
|
||||
- **시나리오 에디터**는 장면 에디터에서 엽니다. 장면을 고르고 그 시나리오 중 하나를 선택한 뒤 **열기**를 누르거나 **+ 새 시나리오**를 누릅니다.
|
||||
- 메인 메뉴에서 도구를 불러오는 경우(예: `--all-mods`), 메뉴의 도구 영역에도 각 도구 버튼이 생깁니다. 장면 빌더와 카탈로그 내보내기도 포함됩니다.
|
||||
|
||||
창은 장면을 바꿔도 열린 채로 남고, 옮기고 크기를 바꿀 수 있으며 ×로 닫습니다.
|
||||
|
||||
## 작은 편집 하나를 끝까지: 새 장소
|
||||
|
||||
소설 템플릿에는 마을, 술집, 집이 있습니다. 여기서는 에디터만 써서 마을에서 갈 수 있는 공원을 추가해 보겠습니다.
|
||||
|
||||
**1. 새 장면.** 장면 에디터에서 **+ 새 장면**을 누르고 이름을 입력하고 저장할 모드를 고른 다음 배경을 추가합니다:
|
||||
|
||||

|
||||
|
||||
저장(Ctrl+S)하면 `scenes/my_novel_park.json`이 `my_novel_game`에 기록되고, 게임은 그 장면을 곧바로 인식합니다.
|
||||
|
||||
**2. 그 장면의 시나리오.** **+ 새 시나리오**를 누르면 그 장면의 파일이 열린 시나리오 에디터가 뜹니다. 공원에서는 돌아가는 길이 하나뿐입니다:
|
||||
|
||||

|
||||
|
||||
**3. 그곳으로 가는 길.** 마을 시나리오에 답 하나와 레이블 하나를 추가합니다:
|
||||
|
||||

|
||||
|
||||
```scenario
|
||||
option "공원으로 가기" -> :park
|
||||
|
||||
:park
|
||||
switch_scene my_novel_park
|
||||
end
|
||||
```
|
||||
|
||||
시나리오를 저장하면 게임의 시나리오 캐시가 비워지므로, 다음에 마을에 들어가면 새 텍스트가 재생됩니다.
|
||||
|
||||
**4. 플레이해 보기.** 장면 에디터의 **장면으로 이동**을 누르거나 그냥 계속 플레이하면 마을로 들어가고, 새 답이 그 자리에 있습니다:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 각 에디터
|
||||
|
||||
- **장면 에디터** — 소설의 장소: 이름, 저장할 모드, 배경(들어갈 때 하나가 무작위로 선택됩니다), 청취 버튼이 있는 환경음, 기본 시나리오, 장면의 시나리오. **장면으로 이동**은 진행 중인 대화를 끝내고 장면으로 들어갑니다.
|
||||
- **시나리오 에디터** — 모든 모드의 파일, 요약, 장면과 전환의 그래프, DSL 강조 표시, Ctrl+S 저장. **플레이**(F5)는 저장하고 실행 중인 게임 안에서 시나리오를 재생하며, 정지는 게임이 있던 곳으로 돌아갑니다. **외부 에디터로 열기**는 시스템 편집기로 파일을 엽니다.
|
||||
- **맵 에디터** — 월드 맵의 구역.
|
||||
- **상호작용 에디터** — 현재 장면의 클릭 가능한 객체.
|
||||
- **조명 에디터** — 현재 장면의 조명.
|
||||
- **장면 빌더** — 모드용 Godot `.tscn` 장면.
|
||||
- **카탈로그 내보내기** — 모드를 zip으로 묶고 체크섬과 함께 모드 카탈로그용 항목을 기록합니다.
|
||||
|
||||
편집 중에는 되돌리기가 됩니다(텍스트와 아이템·캐릭터 에디터의 폼에서 Ctrl+Z). 깨진 참조 검사는 Validator의 일이며, 에디터가 실수가 있는 시나리오의 저장을 막지는 않습니다.
|
||||
|
||||
## 어떤 모드의 파일이 기록되는가
|
||||
|
||||
도구는 폴더로 설치된 모드에만 씁니다. 장면은 모든 모드의 `scenes/<name>.json` 파일을 합친 것이므로, 장면 에디터는 **저장할 곳**에서 고른 모드의 파일을 씁니다. 다른 모드나 압축된 모드에 속한 장면을 바꾸면 그 장면을 보충하는 파일이 당신의 모드에 만들어지고, 다른 파일에서 온 배경은 잠긴 상태로 표시됩니다. 시나리오는 속한 모드에 저장됩니다.
|
||||
|
||||
## 문제가 생겼을 때
|
||||
|
||||
1. **망치 버튼이 없습니다.** 그 게임에 도구가 들어 있지 않습니다. 컬렉션에 Developer Tools 도구 모음을 추가하거나 `--with-mods=editor_tools`로 실행하세요.
|
||||
2. **새 시나리오가 재생되지 않습니다.** 머리말이 다른 장면을 가리키고 있거나, 그 장면의 다른 시나리오가 먼저 선택됩니다(`priority`를 비교해 보세요).
|
||||
3. **저장할 곳에 모드가 없습니다.** 폴더로 설치된 모드에만 쓸 수 있습니다.
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# Editor Tools — сцены и сценарии прямо в запущенной игре
|
||||
|
||||
Editor Tools — это авторские окна поверх работающей игры: редактор сцен, редактор сценариев, редакторы карты мира, интерактивных объектов и света, конструктор сцен Godot и экспорт каталога мода. Всё, что они сохраняют, — обычные файлы модов, и игра подхватывает правки сразу: можно описать место, тут же войти в него и сыграть, ничего не перезапуская.
|
||||
|
||||
Здесь же лежит основа редакторов предметов и персонажей (сами они — отдельные моды: Items Editor и Characters Editor). В игру эти инструменты попадают через набор Developer Tools или через её собственную коллекцию.
|
||||
|
||||
## Как открыть инструменты
|
||||
|
||||
Молоток на панели инструментов в игре открывает набор инструментов:
|
||||
|
||||

|
||||
|
||||
- **Редактор сцен**, **Редактор карты**, **Редактор интерактивов** и **Редактор освещения** входят в набор. Другие инструментальные моды добавляют туда свои пункты (редакторы предметов, персонажей и RPG-карт, редактор данных).
|
||||
- **Редактор сценариев** открывается из редактора сцен: выберите сцену, укажите один из её сценариев и нажмите **Открыть** — или **+ Новый сценарий**.
|
||||
- Если инструменты загружены в главном меню (например, с `--all-mods`), у каждого есть кнопка и в блоке инструментов меню, включая конструктор сцен и экспорт каталога.
|
||||
|
||||
Окна остаются открытыми при смене сцен, их можно двигать и растягивать, а закрываются они крестиком.
|
||||
|
||||
## Целая небольшая правка: новое место
|
||||
|
||||
В шаблоне новеллы есть город, бар и дом. Добавим туда парк, до которого можно дойти из города, — только средствами редакторов.
|
||||
|
||||
**1. Новая сцена.** В редакторе сцен нажмите **+ Новая сцена**, введите имя, выберите мод для сохранения и добавьте фон:
|
||||
|
||||

|
||||
|
||||
Сохранение (Ctrl+S) записывает `scenes/my_novel_park.json` в `my_novel_game` — и игра знает сцену сразу.
|
||||
|
||||
**2. Её сценарий.** **+ Новый сценарий** открывает редактор сценариев с файлом для сцены. Из парка есть один путь назад:
|
||||
|
||||

|
||||
|
||||
**3. Путь туда.** В сценарии города добавляются один ответ и одна метка:
|
||||
|
||||

|
||||
|
||||
```scenario
|
||||
option "Пойти в парк" -> :park
|
||||
|
||||
:park
|
||||
switch_scene my_novel_park
|
||||
end
|
||||
```
|
||||
|
||||
Сохранение сценария сбрасывает кэш сценариев игры, так что при следующем входе в город сыграет уже новый текст.
|
||||
|
||||
**4. Сыграйте.** **Перейти в сцену** в редакторе сцен — или просто продолжайте играть — и вы попадёте в город, где новый ответ уже на месте:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Редакторы
|
||||
|
||||
- **Редактор сцен** — места новеллы: имя, мод для сохранения, фоны (при входе один выбирается случайно), звук окружения с кнопкой прослушивания, сценарий по умолчанию и сценарии сцены. **Перейти в сцену** завершает текущий диалог и входит в сцену.
|
||||
- **Редактор сценариев** — файлы всех модов, сводка и граф сцен и переходов; подсветка DSL; Ctrl+S сохраняет; **Запустить** (F5) сохраняет и проигрывает сценарий прямо в запущенной игре, а «Стоп» возвращает туда, где игра была; **Открыть во внешнем редакторе** открывает файл в системном редакторе.
|
||||
- **Редактор карты** — зоны карты мира.
|
||||
- **Редактор интерактивов** — кликабельные объекты текущей сцены.
|
||||
- **Редактор освещения** — источники света текущей сцены.
|
||||
- **Конструктор сцен** — сцены Godot `.tscn` для модов.
|
||||
- **Экспорт каталога** — упаковывает мод в zip и пишет его запись для каталога модов с контрольными суммами.
|
||||
|
||||
Отмена работает во время редактирования (Ctrl+Z в тексте и в формах редакторов предметов и персонажей). Проверка мода на битые ссылки — задача Validator; редакторы не мешают сохранить сценарий с ошибкой.
|
||||
|
||||
## Чьи файлы пишутся
|
||||
|
||||
Инструменты пишут только в моды, установленные папками. Сцена — это сумма файлов `scenes/<name>.json` всех модов, поэтому редактор сцен пишет файл того мода, который выбран в **Сохранять в**. Если вы меняете сцену, принадлежащую другому моду или упакованному моду, создаётся файл в вашем моде, который её дополняет; фоны из чужих файлов показываются заблокированными. Сценарии сохраняются в том моде, которому принадлежат.
|
||||
|
||||
## Если что-то пошло не так
|
||||
|
||||
1. **Нет кнопки-молотка.** В игре нет инструментов — добавьте набор Developer Tools в её коллекцию или запустите с `--with-mods=editor_tools`.
|
||||
2. **Новый сценарий не играет.** В его заголовке указана другая сцена, или вперёд выбран другой сценарий этой сцены (сравните их `priority`).
|
||||
3. **В «Сохранять в» нет модов.** Писать можно только в моды, установленные папками.
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# Editor Tools — dựng cảnh và kịch bản ngay trong trò chơi đang chạy
|
||||
|
||||
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ó.
|
||||
|
||||
## 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ụ:
|
||||
|
||||

|
||||
|
||||
- **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á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 ×.
|
||||
|
||||
## Một chỉnh sửa nhỏ trọn vẹn: một địa điểm mới
|
||||
|
||||
Mẫu tiểu thuyết có một thị trấn, một quán bar và một ngôi nhà. Ở đây ta thêm một công viên đi được từ thị trấn, chỉ bằng các trình sửa.
|
||||
|
||||
**1. Một cảnh mới.** Trong trình sửa cảnh nhấn **+ Cảnh mới**, gõ tên, chọn mod để lưu và thêm phông nền:
|
||||
|
||||

|
||||
|
||||
Lưu (Ctrl+S) ghi `scenes/my_novel_park.json` vào `my_novel_game`; trò chơi biết cảnh này ngay.
|
||||
|
||||
**2. Kịch bản của cảnh.** **+ Kịch bản mới** mở trình sửa kịch bản với một tệp cho cảnh. Công viên chỉ có một lối về:
|
||||
|
||||

|
||||
|
||||
**3. Một lối đến đó.** Trong kịch bản của thị trấn, thêm một câu trả lời và một nhãn:
|
||||
|
||||

|
||||
|
||||
```scenario
|
||||
option "Ra công viên" -> :park
|
||||
|
||||
:park
|
||||
switch_scene my_novel_park
|
||||
end
|
||||
```
|
||||
|
||||
Lưu kịch bản sẽ xóa bộ nhớ đệm kịch bản của game, nên lần vào thị trấn tiếp theo sẽ chơi đoạn văn bản mới.
|
||||
|
||||
**4. Chơi thử.** **Đến cảnh** trong trình sửa cảnh, hoặc cứ chơi tiếp, sẽ vào thị trấn, và câu trả lời mới đã ở đó:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Các trình sửa
|
||||
|
||||
- **Trình sửa cảnh** — các địa điểm của tiểu thuyết: tên, mod để lưu, phông nền (mỗi lần vào chọn ngẫu nhiên một), âm thanh nền có nút nghe thử, kịch bản mặc định và các kịch bản của cảnh. **Đến cảnh** kết thúc đoạn hội thoại đang chạy và vào cảnh.
|
||||
- **Trình sửa kịch bản** — tệp của mọi mod, bản tóm tắt và đồ thị cảnh cùng chuyển cảnh; tô sáng DSL; Ctrl+S để lưu; **Chơi** (F5) lưu và phát kịch bản ngay trong game đang chạy, còn Dừng đưa game về chỗ cũ; **Mở bằng trình sửa ngoài** mở tệp bằng chương trình mặc định của hệ thống.
|
||||
- **Trình sửa bản đồ** — các vùng của bản đồ thế giới.
|
||||
- **Trình sửa tương tác** — những vật bấm được của cảnh hiện tại.
|
||||
- **Trình sửa ánh sáng** — đèn của cảnh hiện tại.
|
||||
- **Trình dựng cảnh** — cảnh `.tscn` của Godot cho các mod.
|
||||
- **Xuất danh mục** — đóng gói mod thành zip và ghi mục danh mục kèm tổng kiểm tra.
|
||||
|
||||
Hoàn tác hoạt động trong khi sửa (Ctrl+Z trong phần văn bản và trong biểu mẫu của trình sửa vật phẩm, nhân vật). Kiểm tra mod có tham chiếu hỏng là việc của Validator; các trình sửa không cản bạn lưu một kịch bản còn lỗi.
|
||||
|
||||
## Tệp của ai được ghi
|
||||
|
||||
Các công cụ chỉ ghi vào mod được cài dưới dạng thư mục. Một cảnh là tổng các tệp `scenes/<name>.json` của mọi mod, nên trình sửa cảnh ghi tệp của mod được chọn trong **Lưu vào**. Khi sửa một cảnh thuộc mod khác hoặc mod đã đóng gói, một tệp bổ sung cho nó sẽ được tạo trong mod của bạn; những phông nền đến từ tệp khác hiện ở trạng thái khóa. Kịch bản được lưu vào mod mà chúng thuộc về.
|
||||
|
||||
## Khi có trục trặc
|
||||
|
||||
1. **Không thấy nút chiếc búa.** Trò chơi không có công cụ; hãy thêm bộ Developer Tools vào bộ sưu tập của nó hoặc chạy với `--with-mods=editor_tools`.
|
||||
2. **Kịch bản mới không phát.** Phần đầu của nó ghi cảnh khác, hoặc một kịch bản khác của cảnh được chọn trước (so sánh `priority` của chúng).
|
||||
3. **Mục Lưu vào không có mod nào.** Chỉ ghi được vào mod cài dưới dạng thư mục.
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# Editor Tools — 在运行中的游戏里制作场景与剧本
|
||||
|
||||
Editor Tools 是叠在运行中游戏之上的作者工具窗口:场景编辑器、剧本编辑器、世界地图编辑器、交互物编辑器和光照编辑器,还有一个 Godot 场景构建器与模组目录导出。它们保存的全是普通的模组文件,游戏会立刻读到改动,所以作者可以写下一个地点,马上走进去试玩,无需重启。
|
||||
|
||||
本模组还包含物品与角色编辑器的底层部分(它们是两个独立的模组:Items Editor 和 Characters Editor)。游戏通过 Developer Tools 工具集或自己的集合获得这些工具。
|
||||
|
||||
## 打开工具
|
||||
|
||||
在游戏中,工具栏上的锤子按钮会打开工具箱:
|
||||
|
||||

|
||||
|
||||
- 工具箱里有**场景编辑器**、**地图编辑器**、**交互编辑器**和**光照编辑器**。其他工具模组会往里面添加自己的条目(物品、角色和 RPG 地图编辑器,以及数据编辑器)。
|
||||
- **剧本编辑器**从场景编辑器打开:选中一个场景,挑一个它的剧本,按**打开**,或者按 **+ 新剧本**。
|
||||
- 如果在主菜单就加载了工具(比如用 `--all-mods`),菜单的工具区里每个工具也都有按钮,包括场景构建器和目录导出。
|
||||
|
||||
这些窗口在切换场景后依然开着,可以拖动和调整大小,点叉号关闭。
|
||||
|
||||
## 一次完整的小改动:一个新地点
|
||||
|
||||
小说模板里有小镇、酒吧和家。下面只用编辑器,给小镇加一个能走过去的公园。
|
||||
|
||||
**1. 新场景。** 在场景编辑器里按 **+ 新场景**,输入名称,选择保存到哪个模组,再添加一个背景:
|
||||
|
||||

|
||||
|
||||
保存(Ctrl+S)会把 `scenes/my_novel_park.json` 写进 `my_novel_game`,游戏马上就知道这个场景了。
|
||||
|
||||
**2. 它的剧本。** **+ 新剧本**会打开剧本编辑器,里面是该场景的一个文件。公园只有一条回去的路:
|
||||
|
||||

|
||||
|
||||
**3. 通往那里的路。** 在小镇的剧本里加一个回答和一个标签:
|
||||
|
||||

|
||||
|
||||
```scenario
|
||||
option "去公园" -> :park
|
||||
|
||||
:park
|
||||
switch_scene my_novel_park
|
||||
end
|
||||
```
|
||||
|
||||
保存剧本会清掉游戏的剧本缓存,所以下次进小镇时播放的就是新文本。
|
||||
|
||||
**4. 试玩。** 在场景编辑器里点**前往场景**,或者干脆继续玩下去,就会进入小镇,新的回答已经在那里了:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 各个编辑器
|
||||
|
||||
- **场景编辑器** —— 小说的地点:名称、保存到的模组、背景(进入时随机选一个)、带试听按钮的环境音、默认剧本和场景的剧本。**前往场景**会结束当前对话并进入该场景。
|
||||
- **剧本编辑器** —— 所有模组的文件、一份概要,以及场景与转场的图;DSL 高亮;Ctrl+S 保存;**试玩**(F5)保存并在运行中的游戏里播放剧本,停止则回到游戏原来的位置;**在外部编辑器中打开**会用系统默认程序打开文件。
|
||||
- **地图编辑器** —— 世界地图上的区域。
|
||||
- **交互编辑器** —— 当前场景里可点击的对象。
|
||||
- **光照编辑器** —— 当前场景的灯光。
|
||||
- **场景构建器** —— 为模组制作 Godot `.tscn` 场景。
|
||||
- **目录导出** —— 把模组打包成 zip,并写出带校验和的模组目录条目。
|
||||
|
||||
编辑期间可以撤销(文本里和物品、角色编辑器的表单里都支持 Ctrl+Z)。检查模组有没有失效引用是 Validator 的活儿;编辑器不会拦着你保存一个有错误的剧本。
|
||||
|
||||
## 写的是谁的文件
|
||||
|
||||
这些工具只写安装成文件夹的模组。一个场景是所有模组 `scenes/<name>.json` 文件的总和,所以场景编辑器写的是**保存到**里选中的那个模组的文件。改动属于别的模组或打包模组的场景时,会在你的模组里新建一个文件来补充它;来自其他文件的背景会显示为锁定。剧本则保存在它所属的模组里。
|
||||
|
||||
## 出问题了怎么办
|
||||
|
||||
1. **找不到锤子按钮。** 这个游戏没带工具;在它的集合里加上 Developer Tools 工具集,或者用 `--with-mods=editor_tools` 启动。
|
||||
2. **新剧本不播放。** 它的头部写的是另一个场景,或者这个场景里有别的剧本先被选中了(比较它们的 `priority`)。
|
||||
3. **「保存到」里没有模组。** 只有安装成文件夹的模组才能写入。
|
||||
@@ -1,2 +1,5 @@
|
||||
# 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
|
||||
@@ -0,0 +1,5 @@
|
||||
# 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
|
||||
@@ -0,0 +1,5 @@
|
||||
# Catalog export tool — extra keys
|
||||
mods/catalog_select_mod = MOD を選択:
|
||||
mods/catalog_add_locale = 言語を追加
|
||||
mods/catalog_locale_name = この言語での名前
|
||||
mods/catalog_locale_desc = この言語での説明
|
||||
@@ -0,0 +1,5 @@
|
||||
# Catalog export tool — extra keys
|
||||
mods/catalog_select_mod = 모드 선택:
|
||||
mods/catalog_add_locale = 언어 추가
|
||||
mods/catalog_locale_name = 이 언어의 이름
|
||||
mods/catalog_locale_desc = 이 언어의 설명
|
||||
@@ -1,2 +1,5 @@
|
||||
# Catalog export tool — дополнительные ключи
|
||||
mods/catalog_select_mod = Выберите мод:
|
||||
mods/catalog_add_locale = Добавить язык
|
||||
mods/catalog_locale_name = Название на этом языке
|
||||
mods/catalog_locale_desc = Описание на этом языке
|
||||
@@ -0,0 +1,5 @@
|
||||
# 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
|
||||
@@ -0,0 +1,5 @@
|
||||
# Catalog export tool — extra keys
|
||||
mods/catalog_select_mod = 选择模组:
|
||||
mods/catalog_add_locale = 添加语言
|
||||
mods/catalog_locale_name = 此语言的名称
|
||||
mods/catalog_locale_desc = 此语言的描述
|
||||
@@ -0,0 +1,6 @@
|
||||
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
|
||||
@@ -0,0 +1,6 @@
|
||||
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
|
||||
@@ -0,0 +1,6 @@
|
||||
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 = + ステータス
|
||||
@@ -0,0 +1,6 @@
|
||||
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 = + 능력치
|
||||
@@ -0,0 +1,6 @@
|
||||
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 = + Стат
|
||||
@@ -0,0 +1,6 @@
|
||||
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ố
|
||||
@@ -0,0 +1,6 @@
|
||||
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 = + 属性
|
||||
+313
-146
@@ -1,39 +1,145 @@
|
||||
mod_tools_name = Tools
|
||||
mod_tools_desc = Development tools
|
||||
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/light_editor/title = Light Editor
|
||||
tools/light_editor/background = Background
|
||||
tools/light_editor/static_lights = Static lights (max. 10)
|
||||
tools/light_editor/add_light = + Add
|
||||
tools/light_editor/selected_props = Selected properties
|
||||
tools/light_editor/radius = Radius
|
||||
tools/light_editor/influence = Influence
|
||||
tools/light_editor/color = Color
|
||||
tools/light_editor/import_json = Import from JSON
|
||||
tools/light_editor/norm_coords = Norm. coords. (0-1). Drag markers.
|
||||
tools/light_editor/light_n = Light {0} ({1:F3},{2:F3})
|
||||
tools/light_editor/btn_select = S
|
||||
tools/light_editor/btn_delete = D
|
||||
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/base_scene = Base scene:
|
||||
tools/scene_builder/mod = Mod:
|
||||
tools/scene_builder/scene = Scene:
|
||||
tools/scene_builder/hierarchy = Hierarchy
|
||||
tools/scene_builder/tscn = TSCN
|
||||
tools/scene_builder/output = Output
|
||||
tools/scene_builder/clear = Clear
|
||||
tools/scene_builder/refresh = Refresh
|
||||
tools/scene_builder/reload = Reload
|
||||
tools/scene_builder/save = Save
|
||||
tools/scene_builder/select_scene = -- Select a scene --
|
||||
tools/scene_builder/select_mod = -- Select a mod --
|
||||
tools/scene_builder/empty_scene = (empty scene)
|
||||
tools/scene_builder/error_no_scene = Error: scene not selected.
|
||||
tools/scene_builder/error_save = Save error.
|
||||
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/reloaded = Reloaded.
|
||||
tools/scene_builder/inspector = Inspector
|
||||
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)
|
||||
@@ -42,129 +148,190 @@ 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/autosaved = Auto-saved.
|
||||
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/add_node_tip = Add child node of the selected type (to selection, or scene root)
|
||||
tools/scene_builder/delete_node_tip = Delete selected mod node
|
||||
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/scene_builder/tab_inspector = Inspector
|
||||
tools/scene_builder/tab_tscn = TSCN
|
||||
tools/scene_builder/light_editor = Light Editor
|
||||
tools/scene_builder/scene_builder = Scene Builder
|
||||
|
||||
tools/scenario_editor/title = Scenario Editor
|
||||
tools/scenario_editor/save = Save
|
||||
tools/scenario_editor/open_file = Open file
|
||||
tools/scenario_editor/tab_files = Files
|
||||
tools/scenario_editor/tab_info = Info
|
||||
tools/scenario_editor/tab_graph = Graph
|
||||
tools/scenario_editor/graph_select_node = Select a node to see details
|
||||
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 transitions
|
||||
tools/scenario_editor/info_locale_keys = Locale keys
|
||||
tools/scenario_editor/info_quest_refs = Quests
|
||||
tools/scenario_editor/bg_previews = Background previews
|
||||
tools/scenario_editor/no_backgrounds = No backgrounds
|
||||
tools/scenario_editor/status_modified = Modified (unsaved)
|
||||
tools/scenario_editor/status_saved = Saved
|
||||
tools/scenario_editor/save_error = Save error: {0}
|
||||
tools/scenario_editor/readonly_zip = File is provided by a zip mod and is read-only
|
||||
tools/scenario_editor/unsaved_switch_text = You have unsaved changes. Switch file without saving?
|
||||
|
||||
tools/unsaved_title = Unsaved changes
|
||||
tools/unsaved_text = You have unsaved changes. Close without saving?
|
||||
|
||||
mods/catalog_select_mod = Select a mod:
|
||||
|
||||
tools/scenario_editor/play = ▶ Play
|
||||
tools/scenario_editor/play_tooltip = Run scenario in test mode
|
||||
tools/scenario_editor/stop = ⏹ Stop
|
||||
tools/scenario_editor/no_scene_error = Error: scene (scene:) not specified in scenario metadata
|
||||
|
||||
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/interactive_editor/btn_load_json = Load JSON
|
||||
tools/interactive_editor/btn_save = Save
|
||||
tools/interactive_editor/btn_import_mod = Import from mod
|
||||
tools/interactive_editor/dlg_select_def = Select Definition
|
||||
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/interactive_editor/tab_background = Background
|
||||
tools/interactive_editor/tab_interactive = Interactive
|
||||
tools/interactive_editor/tab_scenario = Scenarios
|
||||
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/interactive_editor/label_id = ID:
|
||||
tools/interactive_editor/label_texture_key = Texture key:
|
||||
tools/interactive_editor/label_layer = Layer:
|
||||
tools/interactive_editor/label_bg_preview = Background preview:
|
||||
tools/interactive_editor/label_collider_type = Collider type:
|
||||
tools/interactive_editor/label_rect_params = Rect (norm. 0-1):
|
||||
tools/interactive_editor/label_x = x:
|
||||
tools/interactive_editor/label_y = y:
|
||||
tools/interactive_editor/label_w = w:
|
||||
tools/interactive_editor/label_h = h:
|
||||
tools/interactive_editor/label_circle_params = Circle (norm. 0-1):
|
||||
tools/interactive_editor/label_cx = cx:
|
||||
tools/interactive_editor/label_cy = cy:
|
||||
tools/interactive_editor/label_r = r:
|
||||
tools/interactive_editor/label_polygon_vertices = Vertices:
|
||||
tools/interactive_editor/btn_add_vertex = + Add vertex
|
||||
tools/interactive_editor/label_highlight_color = Highlight color:
|
||||
tools/interactive_editor/label_highlight_scale = Highlight scale:
|
||||
tools/interactive_editor/label_cursor = Cursor:
|
||||
tools/interactive_editor/cursor_none = (none)
|
||||
tools/interactive_editor/label_onclick_scenario = OnClick scenario:
|
||||
tools/interactive_editor/label_onclick_commands = OnClick commands:
|
||||
tools/interactive_editor/label_hover_enter = OnHoverEnter DSL:
|
||||
tools/interactive_editor/label_hover_leave = OnHoverLeave DSL:
|
||||
|
||||
tools/map_editor/title = Map Editor
|
||||
tools/map_editor/mod = Mod:
|
||||
tools/map_editor/file = File:
|
||||
tools/map_editor/load = Load
|
||||
tools/map_editor/save = Save
|
||||
tools/map_editor/save_ok = Saved.
|
||||
tools/map_editor/reloaded = Map reloaded.
|
||||
tools/map_editor/load_error = Load error
|
||||
tools/map_editor/readonly = Cannot save: mod is not writable (zip) or file path is invalid.
|
||||
tools/map_editor/no_mods = No writable mods
|
||||
tools/map_editor/no_file = (new file)
|
||||
tools/map_editor/reload_map = Reload map
|
||||
tools/map_editor/background = Background:
|
||||
tools/map_editor/zones = Zones
|
||||
tools/map_editor/props = Selected zone
|
||||
tools/map_editor/add_region = + Region
|
||||
tools/map_editor/add_marker = + Marker
|
||||
tools/map_editor/add_label = + Label
|
||||
tools/map_editor/delete = Delete
|
||||
tools/map_editor/select_mode = Select
|
||||
tools/map_editor/draw_region = Draw region
|
||||
tools/map_editor/id = ID:
|
||||
tools/map_editor/id_empty = ID must not be empty.
|
||||
tools/map_editor/duplicate_id = Duplicate zone ID.
|
||||
tools/map_editor/title_key = Title locale key:
|
||||
tools/map_editor/no_translation = no translation
|
||||
tools/map_editor/scene = Scene:
|
||||
tools/map_editor/scene_none = (none)
|
||||
tools/map_editor/description = Description locale key:
|
||||
tools/map_editor/image = Marker icon:
|
||||
tools/map_editor/image_pos = Marker position (0-1):
|
||||
tools/map_editor/label = Text label on map
|
||||
tools/map_editor/locked = Closed for travel (locked)
|
||||
tools/map_editor/hidden = Hidden from map
|
||||
tools/map_editor/vertices = Polygon vertices
|
||||
tools/map_editor/add_vertex = + Vertex
|
||||
tools/map_editor/x = x:
|
||||
tools/map_editor/y = y:
|
||||
tools/map_editor/zone_missing = No zone selected.
|
||||
tools/map_editor/preview = ▶ Preview
|
||||
tools/map_editor/preview_tip = View the map as in game: hover, location card, markers
|
||||
tools/map_editor/preview_on = Preview mode: Esc or ✕ returns to the editor.
|
||||
tools/map_editor/preview_off = Edit mode.
|
||||
tools/map_editor/preview_missing = Failed to load the map widget scene (map_widget.tscn).
|
||||
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
|
||||
@@ -0,0 +1,337 @@
|
||||
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
|
||||
@@ -0,0 +1,337 @@
|
||||
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} が複数回出てきます
|
||||
@@ -0,0 +1,337 @@
|
||||
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}이(가) 두 번 이상 나옵니다
|
||||
+313
-145
@@ -1,40 +1,145 @@
|
||||
# Русская локализация для инструментов разработки
|
||||
mod_tools_name = Инструменты
|
||||
mod_tools_desc = Инструменты разработчика
|
||||
mod_editor_tools_name = Редакторы
|
||||
mod_editor_tools_desc = Авторские окна поверх запущенной игры: редакторы сцен, сценариев, карты, интерактива и света, а также конструктор сцен Godot. Всё, что они сохраняют, — обычные файлы модов, которые игра подхватывает сразу.
|
||||
|
||||
tools/light_editor/title = Редактор освещения
|
||||
tools/light_editor/background = Фон
|
||||
tools/light_editor/static_lights = Статичные источники (макс. 10)
|
||||
tools/light_editor/add_light = + Добавить
|
||||
tools/light_editor/selected_props = Свойства выбранного
|
||||
tools/light_editor/radius = Радиус
|
||||
tools/light_editor/influence = Влияние
|
||||
tools/light_editor/color = Цвет
|
||||
tools/light_editor/import_json = Импорт из JSON
|
||||
tools/light_editor/norm_coords = Норм. коорд. (0-1). Перетащите маркеры.
|
||||
tools/light_editor/light_n = Источник {0} ({1:F3},{2:F3})
|
||||
tools/light_editor/btn_select = В
|
||||
tools/light_editor/btn_delete = У
|
||||
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/base_scene = Базовая сцена:
|
||||
tools/scene_builder/mod = Мод:
|
||||
tools/scene_builder/scene = Сцена:
|
||||
tools/scene_builder/hierarchy = Иерархия
|
||||
tools/scene_builder/tscn = TSCN
|
||||
tools/scene_builder/output = Вывод
|
||||
tools/scene_builder/clear = Очистить
|
||||
tools/scene_builder/refresh = Обновить
|
||||
tools/scene_builder/reload = Перезагрузить
|
||||
tools/scene_builder/save = Сохранить
|
||||
tools/scene_builder/select_scene = -- Выберите сцену --
|
||||
tools/scene_builder/select_mod = -- Выберите мод --
|
||||
tools/scene_builder/empty_scene = (пустая сцена)
|
||||
tools/scene_builder/error_no_scene = Ошибка: сцена не выбрана.
|
||||
tools/scene_builder/error_save = Ошибка сохранения.
|
||||
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/reloaded = Перезагружено.
|
||||
tools/scene_builder/inspector = Инспектор
|
||||
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 (узлы вложенных сцен пока не редактируются)
|
||||
@@ -43,127 +148,190 @@ 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/autosaved = Автосохранено.
|
||||
tools/scene_builder/inspector_bad_value = Значение не может быть записано в tscn.
|
||||
tools/scene_builder/inspector_in_file_tooltip = Записано в tscn-файле
|
||||
tools/scene_builder/inspector_container_banner = Узлом управляет контейнер: якоря и отступы пересчитываются родителем и здесь не действуют.
|
||||
tools/scene_builder/add_node_tip = Добавить дочерний узел выбранного типа (в выделение или корень сцены)
|
||||
tools/scene_builder/delete_node_tip = Удалить выбранный узел мод-сцены
|
||||
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/scene_builder/tab_inspector = Инспектор
|
||||
tools/scene_builder/tab_tscn = TSCN
|
||||
tools/scene_builder/light_editor = Редактор освещения
|
||||
tools/scene_builder/scene_builder = Конструктор сцен
|
||||
|
||||
tools/scenario_editor/title = Редактор сценариев
|
||||
tools/scenario_editor/save = Сохранить
|
||||
tools/scenario_editor/open_file = Открыть файл
|
||||
tools/scenario_editor/tab_files = Файлы
|
||||
tools/scenario_editor/tab_info = Инфо
|
||||
tools/scenario_editor/tab_graph = Граф
|
||||
tools/scenario_editor/graph_select_node = Выберите узел для просмотра деталей
|
||||
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/no_backgrounds = Нет фонов
|
||||
tools/scenario_editor/status_modified = Изменено (не сохранено)
|
||||
tools/scenario_editor/status_saved = Сохранено
|
||||
tools/scenario_editor/save_error = Ошибка сохранения: {0}
|
||||
tools/scenario_editor/readonly_zip = Файл из zip-мода доступен только для чтения
|
||||
tools/scenario_editor/unsaved_switch_text = У вас есть несохранённые изменения. Сменить файл без сохранения?
|
||||
|
||||
tools/unsaved_title = Есть изменения
|
||||
tools/unsaved_text = У вас есть несохранённые изменения. Закрыть без сохранения?
|
||||
|
||||
tools/scenario_editor/play = ▶ Запустить
|
||||
tools/scenario_editor/play_tooltip = Запустить сценарий в тестовом режиме
|
||||
tools/scenario_editor/stop = ⏹ Стоп
|
||||
tools/scenario_editor/no_scene_error = Ошибка: не указана сцена (scene:) в метаданных сценария
|
||||
|
||||
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/interactive_editor/btn_load_json = Загрузить JSON
|
||||
tools/interactive_editor/btn_save = Сохранить
|
||||
tools/interactive_editor/btn_import_mod = Импорт из мода
|
||||
tools/interactive_editor/dlg_select_def = Выбор определения
|
||||
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/interactive_editor/tab_background = Задник
|
||||
tools/interactive_editor/tab_interactive = Интерактив
|
||||
tools/interactive_editor/tab_scenario = Сценарии
|
||||
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/interactive_editor/label_id = ID:
|
||||
tools/interactive_editor/label_texture_key = Ключ текстуры:
|
||||
tools/interactive_editor/label_layer = Слой:
|
||||
tools/interactive_editor/label_bg_preview = Превью задника:
|
||||
tools/interactive_editor/label_collider_type = Тип коллайдера:
|
||||
tools/interactive_editor/label_rect_params = Прямоугольник (норм. 0-1):
|
||||
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_circle_params = Окружность (норм. 0-1):
|
||||
tools/interactive_editor/label_cx = cx:
|
||||
tools/interactive_editor/label_cy = cy:
|
||||
tools/interactive_editor/label_r = r:
|
||||
tools/interactive_editor/label_polygon_vertices = Вершины:
|
||||
tools/interactive_editor/btn_add_vertex = + Добавить вершину
|
||||
tools/interactive_editor/label_highlight_color = Цвет подсветки:
|
||||
tools/interactive_editor/label_highlight_scale = Масштаб подсветки:
|
||||
tools/interactive_editor/label_cursor = Курсор:
|
||||
tools/interactive_editor/cursor_none = (нет)
|
||||
tools/interactive_editor/label_onclick_scenario = Сценарий по клику:
|
||||
tools/interactive_editor/label_onclick_commands = Команды по клику:
|
||||
tools/interactive_editor/label_hover_enter = OnHoverEnter DSL:
|
||||
tools/interactive_editor/label_hover_leave = OnHoverLeave DSL:
|
||||
|
||||
tools/map_editor/title = Редактор карты
|
||||
tools/map_editor/mod = Мод:
|
||||
tools/map_editor/file = Файл:
|
||||
tools/map_editor/load = Загрузить
|
||||
tools/map_editor/save = Сохранить
|
||||
tools/map_editor/save_ok = Сохранено.
|
||||
tools/map_editor/reloaded = Карта перезагружена.
|
||||
tools/map_editor/load_error = Ошибка загрузки
|
||||
tools/map_editor/readonly = Не удалось сохранить: мод не для записи (zip) или неверный путь файла.
|
||||
tools/map_editor/no_mods = Нет модов для записи
|
||||
tools/map_editor/no_file = (новый файл)
|
||||
tools/map_editor/reload_map = Перезагрузить карту
|
||||
tools/map_editor/background = Фон:
|
||||
tools/map_editor/zones = Зоны
|
||||
tools/map_editor/props = Выбранная зона
|
||||
tools/map_editor/add_region = + Регион
|
||||
tools/map_editor/add_marker = + Метка
|
||||
tools/map_editor/add_label = + Надпись
|
||||
tools/map_editor/delete = Удалить
|
||||
tools/map_editor/select_mode = Выбор
|
||||
tools/map_editor/draw_region = Рисовать регион
|
||||
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/no_translation = нет перевода
|
||||
tools/map_editor/scene = Сцена:
|
||||
tools/map_editor/scene_none = (нет)
|
||||
tools/map_editor/description = Ключ описания:
|
||||
tools/map_editor/image = Иконка метки:
|
||||
tools/map_editor/image_pos = Позиция метки (0-1):
|
||||
tools/map_editor/label = Текстовая надпись на карте
|
||||
tools/map_editor/locked = Закрыта для посещения
|
||||
tools/map_editor/hidden = Скрыта с карты
|
||||
tools/map_editor/vertices = Вершины полигона
|
||||
tools/map_editor/add_vertex = + Вершина
|
||||
tools/map_editor/x = x:
|
||||
tools/map_editor/y = y:
|
||||
tools/map_editor/zone_missing = Зона не выбрана.
|
||||
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/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} встречается несколько раз
|
||||
@@ -0,0 +1,337 @@
|
||||
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
|
||||
@@ -0,0 +1,337 @@
|
||||
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} 出现了不止一次
|
||||
+12
-4
@@ -1,12 +1,20 @@
|
||||
{
|
||||
"id": "tools",
|
||||
"name": "Tools",
|
||||
"id": "editor_tools",
|
||||
"name": "Editor Tools",
|
||||
"version": "1.0.0",
|
||||
"author": "ct",
|
||||
"description": "Development tools",
|
||||
"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.",
|
||||
"category": "tools, editor",
|
||||
"sources": [
|
||||
"src/**/*.cs"
|
||||
],
|
||||
"depends": ["visual_novel_core>=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"]}
|
||||
]
|
||||
}
|
||||
@@ -5,6 +5,10 @@
|
||||
[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
|
||||
|
||||
@@ -9,10 +9,11 @@ theme_override_constants/separation = 6
|
||||
|
||||
[node name="Scroll" type="ScrollContainer" parent="."]
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 3
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="Scroll"]
|
||||
theme_override_constants/separation = 8
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="HintScene" type="Label" parent="Scroll/VBox"]
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -4,8 +4,10 @@ using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Core.UI.Tool;
|
||||
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
|
||||
@@ -16,11 +18,11 @@ namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
/// mods menu so that mod-management UI can live in the <c>mod_manager</c>
|
||||
/// mod while this authoring utility stays a tool.
|
||||
/// <para/>
|
||||
/// The window lets the author pick any discovered mod, fill in the
|
||||
/// catalog-only metadata (download URL, category, tags, changelog, status,
|
||||
/// minimum game version, icon URL, localized names/descriptions), preview
|
||||
/// the resulting JSON, copy it to the clipboard, or export it next to the
|
||||
/// mod's zip via <see cref="ModCatalogExporter"/>.
|
||||
/// 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
|
||||
/// <see cref="ModCatalogExporter"/> (archive and hashes are computed there).
|
||||
/// </summary>
|
||||
public partial class CatalogExportWindow : ToolWindow
|
||||
{
|
||||
@@ -29,57 +31,59 @@ namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
|
||||
private OptionButton _modSelect;
|
||||
private VBoxContainer _formHost;
|
||||
private CodeEdit _previewEdit;
|
||||
|
||||
private ModInfo _modInfo;
|
||||
private ModManifest _manifest;
|
||||
private LineEdit _urlEdit;
|
||||
private ItemList _categoryList;
|
||||
private readonly List<CheckBox> _categoryChecks = new();
|
||||
private LineEdit _tagsEdit;
|
||||
private TextEdit _changelogEdit;
|
||||
private OptionButton _statusOpt;
|
||||
private LineEdit _minGameVerEdit;
|
||||
private LineEdit _iconUrlEdit;
|
||||
private VBoxContainer _localeContainer;
|
||||
private TextEdit _previewEdit;
|
||||
|
||||
public CatalogExportWindow() : base("mods/catalog_title")
|
||||
{
|
||||
Size = new Vector2I(620, 720);
|
||||
Size = new Vector2I(1200, 820);
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
var vbox = CreateFillContainer();
|
||||
vbox.AddThemeConstantOverride("separation", 8);
|
||||
AddChild(vbox);
|
||||
var outer = CreateFillContainer();
|
||||
AddChild(outer);
|
||||
|
||||
var selectRow = new HBoxContainer();
|
||||
selectRow.AddThemeConstantOverride("separation", 8);
|
||||
var selectLabel = new Label { Text = TranslationServer.Translate("mods/catalog_select_mod") };
|
||||
selectLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
selectRow.AddChild(selectLabel);
|
||||
|
||||
_modSelect = new OptionButton
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
var toolbar = CreateToolbar();
|
||||
outer.AddChild(toolbar);
|
||||
_modSelect = new OptionButton { CustomMinimumSize = new Vector2(320, 0), TooltipText = ToolUi.Tr("mods/catalog_select_mod") };
|
||||
_modSelect.ItemSelected += OnModSelected;
|
||||
selectRow.AddChild(_modSelect);
|
||||
vbox.AddChild(selectRow);
|
||||
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 scroll = new ScrollContainer
|
||||
var split = new HSplitContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill,
|
||||
HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
vbox.AddChild(scroll);
|
||||
outer.AddChild(split);
|
||||
|
||||
_formHost = new VBoxContainer
|
||||
_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
|
||||
};
|
||||
_formHost.AddThemeConstantOverride("separation", 8);
|
||||
scroll.AddChild(_formHost);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -92,7 +96,7 @@ namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
if (info == null)
|
||||
continue;
|
||||
|
||||
_modSelect.AddItem(info.DisplayTitle);
|
||||
_modSelect.AddItem($"{info.DisplayTitle} ({id})");
|
||||
_modSelect.SetItemMetadata(_modSelect.ItemCount - 1, id);
|
||||
}
|
||||
|
||||
@@ -112,175 +116,101 @@ namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
{
|
||||
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;
|
||||
|
||||
AddSectionLabel(_formHost, "Mod Info");
|
||||
AddReadOnlyField(_formHost, "mods/id", _modInfo.Id);
|
||||
AddReadOnlyField(_formHost, "mods/name", _modInfo.DisplayTitle);
|
||||
AddReadOnlyField(_formHost, "mods/version", _modInfo.Version ?? "—");
|
||||
AddReadOnlyField(_formHost, "mods/author", _modInfo.Author ?? "—");
|
||||
_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 ?? "—");
|
||||
|
||||
AddSectionLabel(_formHost, "Catalog Metadata");
|
||||
_urlEdit = AddLineEdit(_formHost, "mods/catalog_download_url");
|
||||
_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()));
|
||||
|
||||
_categoryList = new ItemList
|
||||
{
|
||||
CustomMinimumSize = new Vector2(0, 130),
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SelectMode = ItemList.SelectModeEnum.Toggle,
|
||||
};
|
||||
_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;
|
||||
for (var i = 0; i < Categories.Length; i++)
|
||||
foreach (var category in Categories)
|
||||
{
|
||||
_categoryList.AddItem(GetCategoryDisplayName(Categories[i]));
|
||||
if ((preselected & Categories[i]) != 0)
|
||||
_categoryList.Select(i, false);
|
||||
var check = new CheckBox { Text = GetCategoryDisplayName(category), ButtonPressed = (preselected & category) != 0 };
|
||||
check.Toggled += _ => UpdatePreview();
|
||||
_categoryChecks.Add(check);
|
||||
grid.AddChild(check);
|
||||
}
|
||||
AddLabeledControl(_formHost, "mods/catalog_category", _categoryList);
|
||||
|
||||
_tagsEdit = AddLineEdit(_formHost, "mods/catalog_tags");
|
||||
_formHost.AddChild(grid);
|
||||
|
||||
_formHost.AddChild(ToolUi.Section("mods/catalog_changelog"));
|
||||
_changelogEdit = new TextEdit
|
||||
{
|
||||
CustomMinimumSize = new Vector2(0, 60),
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
CustomMinimumSize = new Vector2(0, 80),
|
||||
WrapMode = TextEdit.LineWrappingMode.Boundary,
|
||||
};
|
||||
AddLabeledControl(_formHost, "mods/catalog_changelog", _changelogEdit);
|
||||
|
||||
_statusOpt = AddOptionButton(_formHost, "mods/catalog_status", Statuses);
|
||||
|
||||
_minGameVerEdit = AddLineEdit(_formHost, "mods/catalog_min_game_version");
|
||||
_iconUrlEdit = AddLineEdit(_formHost, "mods/catalog_icon_url");
|
||||
|
||||
AddSectionLabel(_formHost, "JSON Preview");
|
||||
_previewEdit = new TextEdit
|
||||
{
|
||||
CustomMinimumSize = new Vector2(0, 160),
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
Editable = false,
|
||||
WrapMode = TextEdit.LineWrappingMode.None,
|
||||
};
|
||||
_previewEdit.AddThemeFontSizeOverride("font_size", UiTokens.FontSmall);
|
||||
_formHost.AddChild(_previewEdit);
|
||||
|
||||
var btnRow = new HBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
btnRow.AddThemeConstantOverride("separation", 8);
|
||||
|
||||
var copyBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("mods/catalog_copy"),
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
copyBtn.Pressed += OnCopyPressed;
|
||||
btnRow.AddChild(copyBtn);
|
||||
|
||||
var exportBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("mods/catalog_export_file"),
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
exportBtn.Pressed += OnExportPressed;
|
||||
btnRow.AddChild(exportBtn);
|
||||
|
||||
var closeBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("mods/catalog_close"),
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
closeBtn.Pressed += () => QueueFree();
|
||||
btnRow.AddChild(closeBtn);
|
||||
|
||||
_formHost.AddChild(btnRow);
|
||||
|
||||
AddSectionLabel(_formHost, "Locales");
|
||||
_localeContainer = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
_localeContainer.AddThemeConstantOverride("separation", 4);
|
||||
_formHost.AddChild(_localeContainer);
|
||||
|
||||
var addLocaleBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("mods/catalog_add_locale"),
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
addLocaleBtn.Pressed += () => AddLocaleRow(string.Empty, string.Empty, string.Empty);
|
||||
_formHost.AddChild(addLocaleBtn);
|
||||
|
||||
_urlEdit.TextChanged += _ => UpdatePreview();
|
||||
_categoryList.ItemSelected += _ => UpdatePreview();
|
||||
_categoryList.MultiSelected += (_, _) => UpdatePreview();
|
||||
_tagsEdit.TextChanged += _ => UpdatePreview();
|
||||
_changelogEdit.TextChanged += UpdatePreview;
|
||||
_statusOpt.ItemSelected += _ => UpdatePreview();
|
||||
_minGameVerEdit.TextChanged += _ => UpdatePreview();
|
||||
_iconUrlEdit.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 => TranslationServer.Translate("mods/category_story"),
|
||||
CategoryType.Characters => TranslationServer.Translate("mods/category_characters"),
|
||||
CategoryType.Gameplay => TranslationServer.Translate("mods/category_gameplay"),
|
||||
CategoryType.Visual => TranslationServer.Translate("mods/category_visual"),
|
||||
CategoryType.Audio => TranslationServer.Translate("mods/category_audio"),
|
||||
CategoryType.UI => TranslationServer.Translate("mods/category_ui"),
|
||||
CategoryType.Locale => TranslationServer.Translate("mods/category_locale"),
|
||||
CategoryType.Library => TranslationServer.Translate("mods/category_library"),
|
||||
CategoryType.Engine => TranslationServer.Translate("mods/category_engine"),
|
||||
CategoryType.Overhaul => TranslationServer.Translate("mods/category_overhaul"),
|
||||
CategoryType.Tools => TranslationServer.Translate("mods/category_tools"),
|
||||
CategoryType.Editor => TranslationServer.Translate("mods/category_editor"),
|
||||
CategoryType.Debug => TranslationServer.Translate("mods/category_debug"),
|
||||
CategoryType.Other => TranslationServer.Translate("mods/category_other"),
|
||||
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
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
row.AddThemeConstantOverride("separation", 4);
|
||||
var row = new HBoxContainer();
|
||||
row.AddThemeConstantOverride("separation", UiTokens.Space1);
|
||||
|
||||
var langEdit = new LineEdit
|
||||
{
|
||||
CustomMinimumSize = new Vector2(44, 0),
|
||||
PlaceholderText = "en",
|
||||
MaxLength = 5,
|
||||
Text = lang,
|
||||
};
|
||||
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 = TranslationServer.Translate("mods/catalog_locale_name"),
|
||||
Text = name,
|
||||
};
|
||||
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 = TranslationServer.Translate("mods/catalog_locale_desc"),
|
||||
Text = description,
|
||||
};
|
||||
var descEdit = new LineEdit { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, PlaceholderText = ToolUi.Tr("mods/catalog_locale_desc"), Text = description };
|
||||
row.AddChild(descEdit);
|
||||
|
||||
var removeBtn = new Button { Text = "×", CustomMinimumSize = new Vector2(28, 0) };
|
||||
var removeBtn = UiKit.IconButton("✕", ToolUi.Tr("tools/catalog_export/remove_locale"));
|
||||
removeBtn.Pressed += () => RemoveLocaleRow(row);
|
||||
row.AddChild(removeBtn);
|
||||
|
||||
@@ -289,6 +219,7 @@ namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
descEdit.TextChanged += _ => UpdatePreview();
|
||||
|
||||
_localeContainer.AddChild(row);
|
||||
UpdatePreview();
|
||||
}
|
||||
|
||||
private void RemoveLocaleRow(HBoxContainer row)
|
||||
@@ -300,28 +231,13 @@ namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
|
||||
private void UpdatePreview()
|
||||
{
|
||||
if (_previewEdit == null || _modInfo == null || _manifest == null)
|
||||
if (_previewEdit == null || _modInfo == null || _manifest == null || _changelogEdit == null)
|
||||
return;
|
||||
|
||||
var entry = BuildEntryFromManifest(_manifest);
|
||||
entry.Downloads = new List<CatalogEntryDownload>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Version = entry.Version,
|
||||
Urls = new List<string> { _urlEdit.Text.Trim() },
|
||||
SizeBytes = 0,
|
||||
ArchiveSha256 = "—",
|
||||
InstallSha256 = "—",
|
||||
},
|
||||
};
|
||||
entry.Category = CollectSelectedCategories(_categoryList, Categories);
|
||||
entry.Tags = ParseTags(_tagsEdit.Text);
|
||||
entry.Downloads[0].Changelog = NullIfEmpty(_changelogEdit.Text);
|
||||
entry.Status = Statuses[_statusOpt.Selected];
|
||||
entry.MinGameVersion = NullIfEmpty(_minGameVerEdit.Text);
|
||||
entry.IconUrl = NullIfEmpty(_iconUrlEdit.Text);
|
||||
entry.Locale = CollectLocales();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -330,32 +246,23 @@ namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
var list = new List<CatalogLocaleEntry>();
|
||||
foreach (var child in _localeContainer.GetChildren())
|
||||
{
|
||||
if (child is not HBoxContainer row || row.GetChildCount() < 3)
|
||||
if (child is not HBoxContainer row || row.IsQueuedForDeletion() || row.GetChildCount() < 3)
|
||||
continue;
|
||||
|
||||
var lang = ((LineEdit)row.GetChild(0)).Text.Trim();
|
||||
var name = ((LineEdit)row.GetChild(1)).Text.Trim();
|
||||
var description = ((LineEdit)row.GetChild(2)).Text.Trim();
|
||||
if (lang.Length == 0)
|
||||
continue;
|
||||
|
||||
list.Add(new CatalogLocaleEntry
|
||||
{
|
||||
Lang = lang,
|
||||
Name = NullIfEmpty(name),
|
||||
Description = NullIfEmpty(description),
|
||||
Name = NullIfEmpty(((LineEdit)row.GetChild(1)).Text),
|
||||
Description = NullIfEmpty(((LineEdit)row.GetChild(2)).Text),
|
||||
});
|
||||
}
|
||||
|
||||
if (list.Count == 0 && _manifest != null)
|
||||
{
|
||||
list.Add(new CatalogLocaleEntry
|
||||
{
|
||||
Lang = "en",
|
||||
Name = _manifest.Name,
|
||||
Description = _manifest.Description,
|
||||
});
|
||||
}
|
||||
if (list.Count == 0)
|
||||
list.Add(new CatalogLocaleEntry { Lang = "en", Name = _manifest.Name, Description = _manifest.Description });
|
||||
|
||||
return list;
|
||||
}
|
||||
@@ -370,12 +277,12 @@ namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
Version = entry.Version,
|
||||
Urls = new List<string> { _urlEdit.Text.Trim() },
|
||||
SizeBytes = 0,
|
||||
Changelog = NullIfEmpty(_changelogEdit.Text),
|
||||
},
|
||||
};
|
||||
entry.Category = CollectSelectedCategories(_categoryList, Categories);
|
||||
entry.Category = CollectSelectedCategories();
|
||||
entry.Tags = ParseTags(_tagsEdit.Text);
|
||||
entry.Downloads[0].Changelog = NullIfEmpty(_changelogEdit.Text);
|
||||
entry.Status = Statuses[_statusOpt.Selected];
|
||||
entry.Status = Statuses[Math.Max(0, _statusOpt.Selected)];
|
||||
entry.MinGameVersion = NullIfEmpty(_minGameVerEdit.Text);
|
||||
entry.IconUrl = NullIfEmpty(_iconUrlEdit.Text);
|
||||
entry.Locale = CollectLocales();
|
||||
@@ -385,7 +292,7 @@ namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
private void OnCopyPressed()
|
||||
{
|
||||
DisplayServer.ClipboardSet(_previewEdit.Text);
|
||||
GameLogger.Log("CATALOG_EXPORT", $"Copied catalog JSON for '{_modInfo?.Id}' to clipboard");
|
||||
SetStatus(ToolUi.Tr("tools/catalog_export/copied"), ToolStatusKind.Ok);
|
||||
}
|
||||
|
||||
private void OnExportPressed()
|
||||
@@ -393,15 +300,27 @@ namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
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);
|
||||
if (isZip)
|
||||
ModCatalogExporter.ExportFromZip(_modInfo, entry, modsRoot);
|
||||
else
|
||||
ModCatalogExporter.ExportFromFolder(_modInfo, entry, modsRoot);
|
||||
|
||||
QueueFree();
|
||||
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<string> ParseTags(string text)
|
||||
@@ -434,66 +353,15 @@ namespace Cthangover.Core.UI.Tool.CatalogExport
|
||||
};
|
||||
}
|
||||
|
||||
private static CategoryType CollectSelectedCategories(ItemList list, CategoryType[] categories)
|
||||
private CategoryType CollectSelectedCategories()
|
||||
{
|
||||
var selected = (CategoryType)0;
|
||||
for (var i = 0; i < categories.Length && i < list.ItemCount; i++)
|
||||
for (var i = 0; i < Categories.Length && i < _categoryChecks.Count; i++)
|
||||
{
|
||||
if (list.IsSelected(i))
|
||||
selected |= categories[i];
|
||||
if (_categoryChecks[i].ButtonPressed)
|
||||
selected |= Categories[i];
|
||||
}
|
||||
return selected != 0 ? selected : CategoryType.Other;
|
||||
}
|
||||
|
||||
private static void AddLabeledControl(Container parent, string labelKey, Control control)
|
||||
{
|
||||
var box = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
var label = new Label
|
||||
{
|
||||
Text = TranslationServer.Translate(labelKey),
|
||||
};
|
||||
label.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
|
||||
label.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
box.AddChild(label);
|
||||
box.AddChild(control);
|
||||
parent.AddChild(box);
|
||||
}
|
||||
|
||||
private static void AddReadOnlyField(Container parent, string labelKey, string value)
|
||||
{
|
||||
var label = new Label
|
||||
{
|
||||
Text = $"{TranslationServer.Translate(labelKey)}: {value}",
|
||||
};
|
||||
label.AddThemeFontSizeOverride("font_size", UiTokens.FontBody);
|
||||
label.AddThemeColorOverride("font_color", UiTokens.Body);
|
||||
parent.AddChild(label);
|
||||
}
|
||||
|
||||
private static LineEdit AddLineEdit(Container parent, string labelKey)
|
||||
{
|
||||
var edit = new LineEdit { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
AddLabeledControl(parent, labelKey, edit);
|
||||
return edit;
|
||||
}
|
||||
|
||||
private static OptionButton AddOptionButton(Container parent, string labelKey, string[] items)
|
||||
{
|
||||
var opt = new OptionButton { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
foreach (var item in items)
|
||||
opt.AddItem(item);
|
||||
AddLabeledControl(parent, labelKey, opt);
|
||||
return opt;
|
||||
}
|
||||
|
||||
private static void AddSectionLabel(Container parent, string text)
|
||||
{
|
||||
var sep = new HSeparator();
|
||||
parent.AddChild(sep);
|
||||
var label = new Label { Text = text };
|
||||
label.AddThemeFontSizeOverride("font_size", UiTokens.FontBodyLarge);
|
||||
label.AddThemeColorOverride("font_color", UiTokens.Ink);
|
||||
parent.AddChild(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 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".
|
||||
/// <para/>
|
||||
/// Subclasses paint their overlay in <see cref="DrawContent"/> and rebuild
|
||||
/// their <see cref="DragHandle"/>s in <see cref="RebuildHandles"/>, which is
|
||||
/// also called whenever the canvas is resized or the background changes.
|
||||
/// </summary>
|
||||
public abstract partial class BackgroundCanvas : Control
|
||||
{
|
||||
private static readonly Vector2 FallbackSize = new(1920, 1024);
|
||||
|
||||
/// <summary>Live handles of the current selection.</summary>
|
||||
protected readonly List<DragHandle> _handles = new();
|
||||
|
||||
/// <summary>Reference background; <c>null</c> draws a flat placeholder.</summary>
|
||||
public Texture2D Background { get; private set; }
|
||||
|
||||
protected BackgroundCanvas()
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Stop;
|
||||
ClipContents = true;
|
||||
Resized += OnResized;
|
||||
Connect("draw", Callable.From((Action)DrawAll));
|
||||
}
|
||||
|
||||
/// <summary>Assigns the background texture and relayouts handles.</summary>
|
||||
public void SetBackground(Texture2D texture)
|
||||
{
|
||||
Background = texture;
|
||||
RebuildHandles();
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
/// <summary>Recreates the handles from the current selection.</summary>
|
||||
public abstract void RebuildHandles();
|
||||
|
||||
/// <summary>Paints the overlay above the background inside <paramref name="rect"/>.</summary>
|
||||
protected abstract void DrawContent(Rect2 rect);
|
||||
|
||||
/// <summary>
|
||||
/// Aspect-preserving rectangle the background occupies inside the
|
||||
/// canvas — identical to the runtime "fit" layout.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Canvas-local pixel of a normalised point.</summary>
|
||||
public Vector2 NormToLocal(Vector2 normalized)
|
||||
{
|
||||
var rect = GetBackgroundRect();
|
||||
return rect.Position + normalized * rect.Size;
|
||||
}
|
||||
|
||||
/// <summary>Normalised point of a canvas-local pixel (not clamped).</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Clamps both axes into [0..1].</summary>
|
||||
public static Vector2 Clamp01(Vector2 value)
|
||||
{
|
||||
return new Vector2(Mathf.Clamp(value.X, 0f, 1f), Mathf.Clamp(value.Y, 0f, 1f));
|
||||
}
|
||||
|
||||
/// <summary>Frees every handle.</summary>
|
||||
protected void ClearHandles()
|
||||
{
|
||||
foreach (var handle in _handles)
|
||||
handle.QueueFree();
|
||||
_handles.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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). <paramref name="drag"/>
|
||||
/// receives the clamped normalised point while dragging.
|
||||
/// </summary>
|
||||
protected DragHandle AddHandle(Vector2 normalized, Color color, int index, Action<Vector2> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Sidebar section with a clickable uppercase caption (style guide
|
||||
/// <c>.ct-section-title</c> plus a ▸/▾ marker) that shows or hides its
|
||||
/// <see cref="Body"/>. 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.
|
||||
/// </summary>
|
||||
public partial class CollapsibleSection : VBoxContainer
|
||||
{
|
||||
private readonly Button _header;
|
||||
private readonly string _title;
|
||||
|
||||
/// <summary>Container for the section's controls.</summary>
|
||||
public VBoxContainer Body { get; }
|
||||
|
||||
/// <summary>Whether the body is shown.</summary>
|
||||
public bool Expanded
|
||||
{
|
||||
get => Body.Visible;
|
||||
set
|
||||
{
|
||||
Body.Visible = value;
|
||||
_header.Text = (value ? "▾ " : "▸ ") + _title;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a section titled by <paramref name="titleKey"/>.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <c>GuiInput</c> event and drawing through the <c>draw</c>
|
||||
/// signal — the forms that dispatch reliably for mod assemblies.
|
||||
/// <para/>
|
||||
/// <see cref="Index"/> is free for the owner: a vertex index or a negative
|
||||
/// role constant (marker, label, rect corner...).
|
||||
/// </summary>
|
||||
public partial class DragHandle : Control
|
||||
{
|
||||
/// <summary>Owner-defined identity of the handle.</summary>
|
||||
public int Index;
|
||||
|
||||
/// <summary>Called while dragging with the handle centre in global coordinates.</summary>
|
||||
public Action<Vector2> DragUpdate;
|
||||
|
||||
/// <summary>Called on left press.</summary>
|
||||
public Action Clicked;
|
||||
|
||||
/// <summary>Called on left release after a press.</summary>
|
||||
public Action DragEnd;
|
||||
|
||||
/// <summary>Called on right press.</summary>
|
||||
public Action RightClicked;
|
||||
|
||||
private readonly Color _fill;
|
||||
private bool _dragging;
|
||||
private bool _hovered;
|
||||
private Vector2 _dragOffset;
|
||||
|
||||
/// <summary>Creates a handle of the given fill colour, role index and diameter.</summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>Places the handle so that its centre sits at <paramref name="globalCenter"/>; call after it is in the tree.</summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="Chosen"/> /
|
||||
/// <see cref="Activated"/> with the id. <see cref="Select"/> is silent so
|
||||
/// windows can mirror their model into the list without feedback loops.
|
||||
/// </summary>
|
||||
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<string> _visibleIds = new();
|
||||
private string _selectedId;
|
||||
private bool _silent;
|
||||
private int _fitSide;
|
||||
|
||||
/// <summary>Raised when the user selects a row (click or keyboard).</summary>
|
||||
public event Action<string> Chosen;
|
||||
|
||||
/// <summary>Raised on double-click / Enter.</summary>
|
||||
public event Action<string> Activated;
|
||||
|
||||
/// <summary>Currently selected id, or <c>null</c>.</summary>
|
||||
public string SelectedId => _selectedId;
|
||||
|
||||
/// <summary>Creates the list; <paramref name="minHeight"/> is the list part's minimum height.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Turns rows into an icon grid of <paramref name="iconSize"/> px thumbnails.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales row icons to fit a box at most <paramref name="maxSide"/> 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.
|
||||
/// </summary>
|
||||
public void FitIcons(int maxSide)
|
||||
{
|
||||
_fitSide = maxSide;
|
||||
_list.Resized += UpdateIconBox;
|
||||
UpdateIconBox();
|
||||
}
|
||||
|
||||
/// <summary>Replaces all items; keeps the current selection when its id is still present.</summary>
|
||||
public void SetItems(IEnumerable<(string Id, string Text, Texture2D Icon)> items)
|
||||
{
|
||||
_items.Clear();
|
||||
_items.AddRange(items);
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
/// <summary>Replaces all items with text-only rows where text equals id.</summary>
|
||||
public void SetItems(IEnumerable<string> ids)
|
||||
{
|
||||
_items.Clear();
|
||||
foreach (var id in ids)
|
||||
_items.Add((id, id, null));
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
/// <summary>Selects the row with <paramref name="id"/> (null clears) without raising events.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Number of items before filtering.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Toolbar group "mod ▾ file ▾ +" for editors that own one kind of file
|
||||
/// inside a mod folder (<c>map/*.json</c>, <c>interactives/*.json</c>).
|
||||
/// 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.
|
||||
/// <para/>
|
||||
/// The selector never loads anything itself: it raises
|
||||
/// <see cref="FileRequested"/> and the window decides (usually after an
|
||||
/// "unsaved changes" confirmation). When the window declines, it calls
|
||||
/// <see cref="Show"/> with the file still open to roll the dropdowns back.
|
||||
/// A <c>null</c> path means "this mod has no such file yet".
|
||||
/// </summary>
|
||||
public partial class ModFileSelector : HBoxContainer
|
||||
{
|
||||
private readonly string _directory;
|
||||
private readonly string _extension;
|
||||
private readonly OptionButton _modSelect;
|
||||
private readonly OptionButton _fileSelect;
|
||||
private readonly List<string> _mods = new();
|
||||
private readonly List<string> _files = new();
|
||||
private bool _silent;
|
||||
|
||||
/// <summary>Raised with (modId, mod-relative path or null) when the author picks or creates a file.</summary>
|
||||
public event Action<string, string> FileRequested;
|
||||
|
||||
/// <summary>Mod shown in the selector.</summary>
|
||||
public string CurrentMod => _modSelect.Selected >= 0 && _modSelect.Selected < _mods.Count ? _mods[_modSelect.Selected] : null;
|
||||
|
||||
/// <summary>File shown in the selector, or <c>null</c>.</summary>
|
||||
public string CurrentPath => _fileSelect.Selected >= 0 && _fileSelect.Selected < _files.Count ? _files[_fileSelect.Selected] : null;
|
||||
|
||||
/// <summary>Creates the selector for files in <paramref name="directory"/> with <paramref name="extension"/>.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Re-reads mods and files and requests the first file of the first mod that has any.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Silently shows <paramref name="modId"/>/<paramref name="path"/> (a not-yet-saved path is added).</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="UiKit"/>. 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.
|
||||
/// </summary>
|
||||
public static class ToolUi
|
||||
{
|
||||
/// <summary>Width of the label column in <see cref="Field"/> rows.</summary>
|
||||
public const int LabelWidth = 110;
|
||||
|
||||
/// <summary>Translates a locale key.</summary>
|
||||
public static string Tr(string key) => TranslationServer.Translate(key);
|
||||
|
||||
/// <summary>Uppercase muted section caption (style guide <c>.ct-section-title</c>) with a little air above.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a "label | control" row to <paramref name="parent"/>: the label is
|
||||
/// muted, fixed width and carries the optional tooltip; the control fills
|
||||
/// the rest. Returns the control for chaining.
|
||||
/// </summary>
|
||||
public static T Field<T>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Numeric field. Values are typed and shown culture-independently by
|
||||
/// <see cref="SpinBox"/>, which also clamps and steps them, so the editors
|
||||
/// no longer parse free text ("0,5" vs "0.5").
|
||||
/// </summary>
|
||||
public static SpinBox Number(double min, double max, double step, Action<double> 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;
|
||||
}
|
||||
|
||||
/// <summary>Single-line text field with a placeholder and an optional change handler.</summary>
|
||||
public static LineEdit Text(string placeholder = "", Action<string> changed = null)
|
||||
{
|
||||
var edit = new LineEdit { PlaceholderText = placeholder ?? string.Empty };
|
||||
if (changed != null)
|
||||
edit.TextChanged += text => changed(text);
|
||||
return edit;
|
||||
}
|
||||
|
||||
/// <summary>Token button with a localized caption, optional tooltip and kind.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Thin vertical divider between toolbar groups.</summary>
|
||||
public static VSeparator Divider() => new() { CustomMinimumSize = new Vector2(UiTokens.Space3, 0) };
|
||||
|
||||
/// <summary>Faint helper text that wraps inside narrow sidebars.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Monospace font for IDs, numbers and code (style guide <c>--ct-mono</c>).</summary>
|
||||
public static Font Mono() => ToolWindow.GetMonospaceFont();
|
||||
|
||||
/// <summary>Code editor preset: monospace, line numbers, JSON highlighting and no wrap; scenario/scene callers select their syntax.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// The entities of one kind (<c>"Kind": "item"</c>, <c>"character"</c>…) in one
|
||||
/// mod, as an editor changes them, free of any UI.
|
||||
/// <para/>
|
||||
/// 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#
|
||||
/// (<c>heal</c>, <c>verb</c>, <c>Charset</c>), 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 <see cref="NewFile"/>,
|
||||
/// created on the first save when the mod has no such file.
|
||||
/// <para/>
|
||||
/// Localized texts are part of the document too (<see cref="SetText"/>): 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.
|
||||
/// </summary>
|
||||
public class EntityDocument
|
||||
{
|
||||
private const int HistoryLimit = 200;
|
||||
|
||||
private static readonly JsonSerializerOptions WriteOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
private readonly Dictionary<string, JsonObject> files = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, string> savedText = new(StringComparer.Ordinal);
|
||||
private readonly List<EntityRecord> records = new();
|
||||
private readonly List<string> undo = new();
|
||||
private readonly List<string> redo = new();
|
||||
private Dictionary<string, string> texts = new(StringComparer.Ordinal);
|
||||
private Dictionary<string, string> savedTexts = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>Entity kind of the document.</summary>
|
||||
public string Kind { get; }
|
||||
|
||||
/// <summary>File new entities are added to.</summary>
|
||||
public string NewFile { get; }
|
||||
|
||||
/// <summary>The entities in file order (files sorted by path).</summary>
|
||||
public IReadOnlyList<EntityRecord> Records => records;
|
||||
|
||||
/// <summary>True when there is a step to undo.</summary>
|
||||
public bool CanUndo => undo.Count > 0;
|
||||
|
||||
/// <summary>True when there is a step to redo.</summary>
|
||||
public bool CanRedo => redo.Count > 0;
|
||||
|
||||
private EntityDocument(string kind, string newFile)
|
||||
{
|
||||
Kind = kind;
|
||||
NewFile = newFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <paramref name="skipped"/> only when they declare
|
||||
/// this kind but cannot be read, so the window can name them.
|
||||
/// </summary>
|
||||
public static EntityDocument Load(string kind, string newFile, IEnumerable<(string Path, string Text)> sources, out List<string> skipped)
|
||||
{
|
||||
var document = new EntityDocument(kind, newFile);
|
||||
skipped = new List<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Paths of the document's files.</summary>
|
||||
public IEnumerable<string> Files => files.Keys;
|
||||
|
||||
/// <summary>Adds an entity to <see cref="NewFile"/> (the file is created when missing) and returns it.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>A copy of <paramref name="record"/> with id <paramref name="newId"/>, placed right after it in its file.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Removes an entity from its file (the file stays, maybe with no items).</summary>
|
||||
public void Remove(EntityRecord record)
|
||||
{
|
||||
((JsonArray)files[record.File]["Items"]).Remove(record.Data);
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
/// <summary><paramref name="wanted"/> when no entity has it, else <c>wanted_2</c>, <c>wanted_3</c>…</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>The record with this id, or null.</summary>
|
||||
public EntityRecord Find(string id) => records.FirstOrDefault(r => r.Id == id);
|
||||
|
||||
/// <summary>A localized text the document holds (see <see cref="SetText"/>), or null when it was never set or loaded.</summary>
|
||||
public string GetText(string language, string key)
|
||||
=> texts.TryGetValue(TextKey(language, key), out var value) ? value : null;
|
||||
|
||||
/// <summary>Remembers the text the mod's files have now (loading, not an edit).</summary>
|
||||
public void LoadText(string language, string key, string value)
|
||||
{
|
||||
var textKey = TextKey(language, key);
|
||||
texts[textKey] = value;
|
||||
savedTexts[textKey] = value;
|
||||
}
|
||||
|
||||
/// <summary>Changes a localized text; it is written on save.</summary>
|
||||
public void SetText(string language, string key, string value) => texts[TextKey(language, key)] = value ?? string.Empty;
|
||||
|
||||
/// <summary>Remembers the current state as one undo step and forgets the redo branch.</summary>
|
||||
public void BeginChange()
|
||||
{
|
||||
undo.Add(Snapshot());
|
||||
if (undo.Count > HistoryLimit)
|
||||
undo.RemoveAt(0);
|
||||
redo.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Steps back; false when there is nothing to undo.</summary>
|
||||
public bool Undo() => Swap(undo, redo);
|
||||
|
||||
/// <summary>Steps forward again; false when there is nothing to redo.</summary>
|
||||
public bool Redo() => Swap(redo, undo);
|
||||
|
||||
/// <summary>Files whose text changed since loading or the last <see cref="MarkSaved"/>.</summary>
|
||||
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();
|
||||
|
||||
/// <summary>Texts changed since loading or the last <see cref="MarkSaved"/>: language, key, value.</summary>
|
||||
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();
|
||||
|
||||
/// <summary>The current files and texts are what is on disk now.</summary>
|
||||
public void MarkSaved()
|
||||
{
|
||||
foreach (var (path, text) in ChangedFiles())
|
||||
savedText[path] = text;
|
||||
savedTexts = new Dictionary<string, string>(texts, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>Problems that would break the game's data, as locale keys with an argument; empty when it can be saved.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>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.</summary>
|
||||
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<string, JsonNode>(f.Key, f.Value.DeepClone()))),
|
||||
["texts"] = new JsonObject(texts.Select(t => new KeyValuePair<string, JsonNode>(t.Key, t.Value)))
|
||||
};
|
||||
return state.ToJsonString();
|
||||
}
|
||||
|
||||
private bool Swap(List<string> from, List<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,740 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="EntityField"/> 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.
|
||||
/// <para/>
|
||||
/// 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.
|
||||
/// <para/>
|
||||
/// Fields other mods describe for the kind in their schemas (<c>schemas/*.json</c>: the
|
||||
/// equipment mod adds <c>Equip</c> and <c>Traits</c> to items, the stats add <c>Archetype</c>
|
||||
/// and <c>Stats</c> 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.
|
||||
/// <para/>
|
||||
/// Every change is undoable (Ctrl+Z / Ctrl+Y): one undo step per editing
|
||||
/// session of one entity, and one per add, duplicate or delete.
|
||||
/// </summary>
|
||||
public abstract partial class EntityEditorWindow : ToolWindow
|
||||
{
|
||||
private readonly string kind;
|
||||
private readonly string folder;
|
||||
private readonly List<EntityField> fields;
|
||||
private readonly Dictionary<EntityField, Control> controls = new();
|
||||
private readonly Dictionary<EntityField, LineEdit> keyEdits = new();
|
||||
private readonly Dictionary<EntityField, Dictionary<string, LineEdit>> textEdits = new();
|
||||
private readonly Dictionary<EntityField, TextureRect> previews = new();
|
||||
private readonly Dictionary<string, Texture2D> icons = new();
|
||||
private readonly List<string> mods = new();
|
||||
private readonly List<ISchemaFieldEditor> 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<string> languages = new();
|
||||
private string modId;
|
||||
private bool suppress;
|
||||
private bool editing;
|
||||
|
||||
/// <summary>Builds the window for entities of <paramref name="entityKind"/>; new ones go to <c><folder>/<mod>.json</c>.</summary>
|
||||
protected EntityEditorWindow(string titleKey, string entityKind, string entityFolder, List<EntityField> entityFields) : base(titleKey)
|
||||
{
|
||||
kind = entityKind;
|
||||
folder = entityFolder;
|
||||
fields = entityFields;
|
||||
CthangoverTheme.Apply(this);
|
||||
BuildUi();
|
||||
SetHint(ToolUi.Tr("tools/entity_editor/hint"));
|
||||
FillMods();
|
||||
}
|
||||
|
||||
/// <summary>A new entity's JSON: the kind's required fields with starting values.</summary>
|
||||
protected abstract JsonObject CreateEntity(string id);
|
||||
|
||||
/// <summary>Id a new entity starts from (numbered when taken), e.g. <c><mod>/new_item</c>.</summary>
|
||||
protected abstract string NewIdBase(string mod);
|
||||
|
||||
/// <summary>The edited mod, or null.</summary>
|
||||
public string ModId => modId;
|
||||
|
||||
/// <summary>The selected entity's id, or null.</summary>
|
||||
public string SelectedId => selected?.Id;
|
||||
|
||||
/// <summary>The edited entities (null before a mod is opened).</summary>
|
||||
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<string, LineEdit>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Opens the entities of a mod (asks first about unsaved changes).</summary>
|
||||
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<LocaleFieldEditor>())
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Shows an entity in the form (null: none).</summary>
|
||||
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<string>());
|
||||
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<string>()).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<string>()).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();
|
||||
}
|
||||
|
||||
/// <summary>Copies the form into the selected entity; the first change of a session records an undo step.</summary>
|
||||
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<string> Checked(HFlowContainer box)
|
||||
=> box.GetChildren().OfType<CheckBox>().Where(c => c.ButtonPressed).Select(c => c.Text).ToList();
|
||||
|
||||
/// <summary>Takes the children out of the tree at once (a search of the tree must not find them) and frees them.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>The properties no field shows, as an indented JSON object.</summary>
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>Ctrl+S.</summary>
|
||||
protected override void OnSaveShortcut() => Save();
|
||||
|
||||
private void OnSavePressed() => Save();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<EntityResolver>()?.ClearCache();
|
||||
ModManager.Instance.GetResolver<FileListResolver>()?.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;
|
||||
}
|
||||
|
||||
/// <summary>Shortcuts: Ctrl+Z / Ctrl+Y undo and redo, Ctrl+N adds, Ctrl+D duplicates.</summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cthangover.Tools.EntityEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// One property of an entity as an <see cref="EntityEditorWindow"/> 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.
|
||||
/// </summary>
|
||||
public class EntityField
|
||||
{
|
||||
/// <summary>JSON property name.</summary>
|
||||
public string Key { get; init; }
|
||||
|
||||
/// <summary>Locale key of the label.</summary>
|
||||
public string LabelKey { get; init; }
|
||||
|
||||
/// <summary>Locale key of the tooltip, or null.</summary>
|
||||
public string TooltipKey { get; init; }
|
||||
|
||||
/// <summary>How the value is edited.</summary>
|
||||
public EntityFieldKind Kind { get; init; }
|
||||
|
||||
/// <summary>Picture: the file group of the pictures (<c>items</c>, <c>characters</c>…).</summary>
|
||||
public string Group { get; init; }
|
||||
|
||||
/// <summary>Choice, flags, list: the values offered (read when the form is built).</summary>
|
||||
public Func<IEnumerable<string>> Options { get; init; }
|
||||
|
||||
/// <summary>Locale text: how a new entity's key is made from its id (<c>{0}</c> = id).</summary>
|
||||
public string KeyPattern { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Cthangover.Tools.EntityEditor
|
||||
{
|
||||
/// <summary>How an <see cref="EntityField"/> is shown and stored.</summary>
|
||||
public enum EntityFieldKind
|
||||
{
|
||||
/// <summary>One line of text.</summary>
|
||||
Text,
|
||||
/// <summary>A locale key with its text in every language of the mod.</summary>
|
||||
LocaleText,
|
||||
/// <summary>A whole number.</summary>
|
||||
Integer,
|
||||
/// <summary>A picture path in a file group, with a preview and a picker.</summary>
|
||||
Picture,
|
||||
/// <summary>One value of a list (values outside the list stay possible).</summary>
|
||||
Choice,
|
||||
/// <summary>Several names of a list written as <c>"A,B"</c> (an enum with flags).</summary>
|
||||
Flags,
|
||||
/// <summary>A JSON array of strings picked from a list.</summary>
|
||||
List
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Cthangover.Tools.EntityEditor
|
||||
{
|
||||
/// <summary>One entity of an <see cref="EntityDocument"/>: its JSON object inside the <c>Items</c> array of its file.</summary>
|
||||
public class EntityRecord
|
||||
{
|
||||
/// <summary>Mod-relative path of the file holding the entity.</summary>
|
||||
public string File { get; }
|
||||
|
||||
/// <summary>The entity itself; unknown properties are kept as they are.</summary>
|
||||
public JsonObject Data { get; }
|
||||
|
||||
/// <summary>The entity's <c>Id</c> (empty when missing).</summary>
|
||||
public string Id => Data["Id"]?.ToString() ?? string.Empty;
|
||||
|
||||
/// <summary>Wraps an entity of a file.</summary>
|
||||
public EntityRecord(string file, JsonObject data)
|
||||
{
|
||||
File = file;
|
||||
Data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
using Cthangover.Tools.Common;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.EntityEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public partial class PicturePickerDialog : AcceptDialog
|
||||
{
|
||||
private readonly FilteredList list;
|
||||
|
||||
/// <summary>A picture was picked: its id in the group.</summary>
|
||||
public event Action<string> Picked;
|
||||
|
||||
/// <summary>Builds the dialog for the pictures of <paramref name="group"/>.</summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Interactive;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Tools.Common;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.InteractiveEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// 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:
|
||||
/// <list type="bullet">
|
||||
/// <item>rect — top-left and bottom-right corners plus a centre handle
|
||||
/// that moves the whole rect;</item>
|
||||
/// <item>circle — centre and a radius handle;</item>
|
||||
/// <item>polygon — one handle per vertex (right-click deletes it); a click
|
||||
/// on empty background appends a vertex.</item>
|
||||
/// </list>
|
||||
/// Geometry is in the same normalised background space as
|
||||
/// <c>InteractiveObject</c>: x/y/width/height are fractions of the
|
||||
/// background size, a circle radius is a fraction of its shorter side.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>Data being edited; set once by the window.</summary>
|
||||
public InteractiveEditorController Controller;
|
||||
|
||||
/// <summary>Raised whenever geometry of the selected item changes.</summary>
|
||||
public event Action Changed;
|
||||
|
||||
/// <summary>Raised when a click on the canvas selects another item (-1: none).</summary>
|
||||
public event Action<int> ItemPicked;
|
||||
|
||||
public InteractiveEditorCanvas()
|
||||
{
|
||||
GuiInput += OnGuiInput;
|
||||
}
|
||||
|
||||
/// <summary>Recreates the handles of the selected item's collider.</summary>
|
||||
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<Vector2>();
|
||||
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(); };
|
||||
}
|
||||
|
||||
/// <summary>Keeps the other handles glued to the shape while one is dragged.</summary>
|
||||
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<Vector2>();
|
||||
selected.HitArea.Vertices = vertices.Append(Clamp01(norm)).ToArray();
|
||||
RebuildHandles();
|
||||
QueueRedraw();
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
AcceptEvent();
|
||||
}
|
||||
|
||||
/// <summary>Topmost item whose collider contains the normalised point, or -1.</summary>
|
||||
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<Vector2>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,244 +1,206 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Cthangover.Core.Interactive;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.InteractiveEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Data model for the interactive editor. Holds interactive definition
|
||||
/// fields, manages collider vertex list for polygon editing, and produces
|
||||
/// the output JSON.
|
||||
/// Data model of the interactive editor: every definition of one
|
||||
/// <c>interactives/*.json</c> 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. <see cref="Ensure"/> fills the optional sub-objects the editor
|
||||
/// needs (hit area, highlight, actions) before a definition is shown.
|
||||
/// </summary>
|
||||
public class InteractiveEditorController
|
||||
{
|
||||
/// <summary>Unique identifier for the interactive object resource.</summary>
|
||||
public string Id { get; set; } = "new_interactive";
|
||||
/// <summary>Texture key mapped to a sprite resource by the resource system.</summary>
|
||||
public string Texture { get; set; } = "";
|
||||
/// <summary>Render layer: <c>"foreground"</c>, <c>"background"</c>, or <c>"ui"</c>.</summary>
|
||||
public string Layer { get; set; } = "foreground";
|
||||
/// <summary>Z-index for draw order within the layer.</summary>
|
||||
public int ZIndex { get; set; }
|
||||
/// <summary>Whether the interactive is initially enabled.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
/// <summary>Whether the interactive is visible.</summary>
|
||||
public bool Visible { get; set; } = true;
|
||||
/// <summary>Mouse cursor override name, e.g. <c>"PointingHand"</c>. Empty for default.</summary>
|
||||
public string Cursor { get; set; } = "";
|
||||
/// <summary>Collider shape type: <c>"rect"</c>, <c>"circle"</c>, or <c>"polygon"</c>.</summary>
|
||||
public string HitType { get; set; } = "rect";
|
||||
/// <summary>Collider kinds understood by <c>InteractiveObject</c>.</summary>
|
||||
public static readonly string[] HitTypes = { "rect", "circle", "polygon" };
|
||||
|
||||
/// <summary>Normalised X position of the rect collider (0–1).</summary>
|
||||
public float RectX { get; set; }
|
||||
/// <summary>Normalised Y position of the rect collider (0–1).</summary>
|
||||
public float RectY { get; set; }
|
||||
/// <summary>Normalised width of the rect collider (0–1).</summary>
|
||||
public float RectW { get; set; } = 0.1f;
|
||||
/// <summary>Normalised height of the rect collider (0–1).</summary>
|
||||
public float RectH { get; set; } = 0.1f;
|
||||
/// <summary>Render layers understood by the interactive layer host.</summary>
|
||||
public static readonly string[] Layers = { "foreground", "background", "ui" };
|
||||
|
||||
/// <summary>Normalised X centre of the circle collider (0–1).</summary>
|
||||
public float CircleX { get; set; } = 0.5f;
|
||||
/// <summary>Normalised Y centre of the circle collider (0–1).</summary>
|
||||
public float CircleY { get; set; } = 0.5f;
|
||||
/// <summary>Normalised radius of the circle collider (0–1).</summary>
|
||||
public float CircleRadius { get; set; } = 0.05f;
|
||||
/// <summary>Definitions of the open file, in file order.</summary>
|
||||
public List<InteractiveDefinition> Items { get; private set; } = new();
|
||||
|
||||
/// <summary>List of normalised (0–1) vertices defining the polygon collider.</summary>
|
||||
public List<Vector2> PolygonVertices { get; set; } = new();
|
||||
/// <summary>Index of the selected definition, or -1.</summary>
|
||||
public int SelectedIndex { get; private set; } = -1;
|
||||
|
||||
/// <summary>Hex colour string (e.g. <c>"#FFFF0033"</c>) for the highlight overlay.</summary>
|
||||
public string HighlightColorHex { get; set; } = "#FFFF0033";
|
||||
/// <summary>Scale multiplier applied to the sprite when highlighted.</summary>
|
||||
public float HighlightScale { get; set; } = 1.02f;
|
||||
/// <summary>The selected definition, or <c>null</c>.</summary>
|
||||
public InteractiveDefinition Selected => SelectedIndex >= 0 && SelectedIndex < Items.Count ? Items[SelectedIndex] : null;
|
||||
|
||||
/// <summary>Scenario file path executed on click.</summary>
|
||||
public string OnClickScenario { get; set; } = "";
|
||||
/// <summary>Inline DSL commands executed on click, one per line.</summary>
|
||||
public string OnClickCommands { get; set; } = "";
|
||||
/// <summary>DSL commands executed when the mouse enters the interactive area.</summary>
|
||||
public string OnHoverEnter { get; set; } = "";
|
||||
/// <summary>DSL commands executed when the mouse leaves the interactive area.</summary>
|
||||
public string OnHoverLeave { get; set; } = "";
|
||||
|
||||
/// <summary>Builds an InteractiveDefinition from current state.</summary>
|
||||
public InteractiveDefinition ToDefinition()
|
||||
/// <summary>Replaces the items with the contents of a <c>{"Items": [...]}</c> file. Throws on malformed JSON.</summary>
|
||||
public void FromJson(string json)
|
||||
{
|
||||
var hitArea = new HitAreaDefinition { Type = HitType };
|
||||
|
||||
switch (HitType)
|
||||
{
|
||||
case "rect":
|
||||
hitArea.X = RectX;
|
||||
hitArea.Y = RectY;
|
||||
hitArea.Width = RectW;
|
||||
hitArea.Height = RectH;
|
||||
break;
|
||||
case "circle":
|
||||
hitArea.X = CircleX;
|
||||
hitArea.Y = CircleY;
|
||||
hitArea.Radius = CircleRadius;
|
||||
break;
|
||||
case "polygon":
|
||||
hitArea.Vertices = PolygonVertices.ToArray();
|
||||
break;
|
||||
var data = string.IsNullOrWhiteSpace(json)
|
||||
? null
|
||||
: JsonSerializer.Deserialize<FileData<InteractiveDefinition>>(json, JsonOptionsProvider.ReadOptions);
|
||||
Items = data?.Items ?? new List<InteractiveDefinition>();
|
||||
foreach (var item in Items)
|
||||
Ensure(item);
|
||||
SelectedIndex = Items.Count > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
var clickAction = new ClickAction();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(OnClickScenario))
|
||||
clickAction.Scenario = OnClickScenario.Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(OnClickCommands))
|
||||
{
|
||||
var lines = OnClickCommands.Split('\n', System.StringSplitOptions.RemoveEmptyEntries);
|
||||
var cleanLines = new List<string>();
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (!string.IsNullOrEmpty(trimmed))
|
||||
cleanLines.Add(trimmed);
|
||||
}
|
||||
if (cleanLines.Count > 0)
|
||||
clickAction.Commands = cleanLines.ToArray();
|
||||
}
|
||||
|
||||
return new InteractiveDefinition
|
||||
{
|
||||
ID = Id,
|
||||
Texture = string.IsNullOrWhiteSpace(Texture) ? null : Texture.Trim(),
|
||||
Layer = Layer,
|
||||
ZIndex = ZIndex,
|
||||
Enabled = Enabled,
|
||||
Visible = Visible,
|
||||
Cursor = string.IsNullOrWhiteSpace(Cursor) ? null : Cursor.Trim(),
|
||||
HitArea = hitArea,
|
||||
Highlight = new HighlightDefinition
|
||||
{
|
||||
ColorHex = HighlightColorHex,
|
||||
Scale = HighlightScale
|
||||
},
|
||||
Actions = new InteractiveActionDefinition
|
||||
{
|
||||
OnClick = (clickAction.Scenario != null || clickAction.Commands != null) ? clickAction : null,
|
||||
OnHoverEnter = string.IsNullOrWhiteSpace(OnHoverEnter) ? null : OnHoverEnter.Trim(),
|
||||
OnHoverLeave = string.IsNullOrWhiteSpace(OnHoverLeave) ? null : OnHoverLeave.Trim()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Loads state from an InteractiveDefinition.</summary>
|
||||
public void FromDefinition(InteractiveDefinition def)
|
||||
{
|
||||
if (def == null) return;
|
||||
|
||||
Id = def.ID ?? "new_interactive";
|
||||
Texture = def.Texture ?? "";
|
||||
Layer = def.Layer ?? "foreground";
|
||||
ZIndex = def.ZIndex;
|
||||
Enabled = def.Enabled;
|
||||
Visible = def.Visible;
|
||||
Cursor = def.Cursor ?? "";
|
||||
|
||||
if (def.HitArea != null)
|
||||
{
|
||||
HitType = def.HitArea.Type ?? "rect";
|
||||
RectX = def.HitArea.X;
|
||||
RectY = def.HitArea.Y;
|
||||
RectW = def.HitArea.Width;
|
||||
RectH = def.HitArea.Height;
|
||||
CircleX = def.HitArea.X;
|
||||
CircleY = def.HitArea.Y;
|
||||
CircleRadius = def.HitArea.Radius;
|
||||
PolygonVertices = def.HitArea.Vertices != null
|
||||
? new List<Vector2>(def.HitArea.Vertices)
|
||||
: new List<Vector2>();
|
||||
}
|
||||
else
|
||||
{
|
||||
HitType = "rect";
|
||||
}
|
||||
|
||||
if (def.Highlight != null)
|
||||
{
|
||||
HighlightColorHex = def.Highlight.ColorHex ?? "#FFFF0033";
|
||||
HighlightScale = def.Highlight.Scale;
|
||||
}
|
||||
|
||||
OnClickScenario = def.Actions?.OnClick?.Scenario ?? "";
|
||||
OnClickCommands = def.Actions?.OnClick?.Commands != null
|
||||
? string.Join("\n", def.Actions.OnClick.Commands)
|
||||
: "";
|
||||
OnHoverEnter = def.Actions?.OnHoverEnter ?? "";
|
||||
OnHoverLeave = def.Actions?.OnHoverLeave ?? "";
|
||||
}
|
||||
|
||||
/// <summary>Serializes the definition to formatted JSON (FileData envelope).</summary>
|
||||
/// <summary>Serialises all items into the standard envelope (indented, nulls omitted).</summary>
|
||||
public string ToJson()
|
||||
{
|
||||
var def = ToDefinition();
|
||||
var wrapper = new
|
||||
var data = new FileData<InteractiveDefinition> { Items = Items };
|
||||
return JsonSerializer.Serialize(data, JsonOptionsProvider.WriteIndentedIgnoreNullOptions);
|
||||
}
|
||||
|
||||
/// <summary>Drops all items.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
Items = new[] { def }
|
||||
Items = new List<InteractiveDefinition>();
|
||||
SelectedIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>Selects by index; out of range clears the selection.</summary>
|
||||
public void Select(int index)
|
||||
{
|
||||
SelectedIndex = index >= 0 && index < Items.Count ? index : -1;
|
||||
}
|
||||
|
||||
/// <summary>Appends a new clickable rect in the middle of the background and selects it.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
/// <summary>Appends a deep copy of the selected definition with a fresh ID, slightly offset.</summary>
|
||||
public InteractiveDefinition DuplicateSelected()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
var source = Selected;
|
||||
if (source == null)
|
||||
return null;
|
||||
|
||||
var json = JsonSerializer.Serialize(source, JsonOptionsProvider.WriteCompactOptions);
|
||||
var copy = JsonSerializer.Deserialize<InteractiveDefinition>(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;
|
||||
}
|
||||
|
||||
/// <summary>Removes the selected definition and keeps a sensible selection.</summary>
|
||||
public void RemoveSelected()
|
||||
{
|
||||
if (Selected == null)
|
||||
return;
|
||||
|
||||
Items.RemoveAt(SelectedIndex);
|
||||
SelectedIndex = Items.Count == 0 ? -1 : Math.Min(SelectedIndex, Items.Count - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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)
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(wrapper, options);
|
||||
}
|
||||
|
||||
/// <summary>Parses JSON back into the controller. Returns false on failure.</summary>
|
||||
public bool FromJson(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, ReadCommentHandling = System.Text.Json.JsonCommentHandling.Skip };
|
||||
var wrapper = JsonSerializer.Deserialize<FileDataWrapper>(json, options);
|
||||
if (wrapper?.Items != null && wrapper.Items.Count > 0)
|
||||
{
|
||||
FromDefinition(wrapper.Items[0]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
break;
|
||||
default:
|
||||
hit.X = bounds.Position.X;
|
||||
hit.Y = bounds.Position.Y;
|
||||
hit.Width = bounds.Size.X;
|
||||
hit.Height = bounds.Size.Y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private class FileDataWrapper
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public static Rect2 Bounds(HitAreaDefinition hit)
|
||||
{
|
||||
public List<InteractiveDefinition> Items { get; set; }
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Updates a polygon vertex at the given index.</summary>
|
||||
public void SetPolygonVertex(int index, Vector2 value)
|
||||
/// <summary>Makes sure the optional parts the editor binds to exist.</summary>
|
||||
public static void Ensure(InteractiveDefinition def)
|
||||
{
|
||||
while (PolygonVertices.Count <= index)
|
||||
PolygonVertices.Add(Vector2.Zero);
|
||||
PolygonVertices[index] = value;
|
||||
def.HitArea ??= new HitAreaDefinition();
|
||||
def.HitArea.Type = HitTypes.Contains(def.HitArea.Type) ? def.HitArea.Type : "rect";
|
||||
def.Highlight ??= new HighlightDefinition();
|
||||
def.Actions ??= new InteractiveActionDefinition();
|
||||
}
|
||||
|
||||
/// <summary>Removes a polygon vertex at the given index.</summary>
|
||||
public void RemovePolygonVertex(int index)
|
||||
/// <summary>ID not used by another item: <c>base</c>, <c>base_2</c>, <c>base_3</c>…</summary>
|
||||
public string MakeUniqueId(string baseId)
|
||||
{
|
||||
if (index >= 0 && index < PolygonVertices.Count)
|
||||
PolygonVertices.RemoveAt(index);
|
||||
var used = new HashSet<string>(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}";
|
||||
}
|
||||
|
||||
/// <summary>Adds a new polygon vertex at the default position (center of viewport).</summary>
|
||||
public void AddPolygonVertex()
|
||||
private static void Offset(HitAreaDefinition hit, Vector2 delta)
|
||||
{
|
||||
var offset = PolygonVertices.Count * 0.04f;
|
||||
PolygonVertices.Add(new Vector2(0.5f + offset, 0.5f + offset));
|
||||
hit.X += delta.X;
|
||||
hit.Y += delta.Y;
|
||||
if (hit.Vertices != null)
|
||||
hit.Vertices = hit.Vertices.Select(v => v + delta).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.InteractiveEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Draggable 16x16 handle representing a collider vertex on the preview area.
|
||||
/// Used for rect corners, circle radius marker, and polygon vertices.
|
||||
/// Subscribes to the GuiInput C# event rather than overriding _GuiInput
|
||||
/// for reliable event delivery in mod assemblies.
|
||||
/// </summary>
|
||||
public partial class InteractiveEditorHandle : ColorRect
|
||||
{
|
||||
/// <summary>Index of the vertex this handle represents (0-based).</summary>
|
||||
public int VertexIndex;
|
||||
/// <summary>Invoked during drag with the centre position of the handle in global coordinates.</summary>
|
||||
public System.Action<Vector2> DragUpdate;
|
||||
/// <summary>Invoked when the handle is clicked (mouse down).</summary>
|
||||
public System.Action Clicked;
|
||||
/// <summary>Invoked when the drag ends (mouse up). Used to sync sidebar values.</summary>
|
||||
public System.Action DragEnd;
|
||||
|
||||
private bool _dragging;
|
||||
private Vector2 _dragOffset;
|
||||
|
||||
/// <summary>Creates a handle with the given colour, size 16×16, and index for vertex identification.</summary>
|
||||
public InteractiveEditorHandle(Color color, int index)
|
||||
{
|
||||
VertexIndex = index;
|
||||
Size = new Vector2(16, 16);
|
||||
Color = color;
|
||||
MouseFilter = Control.MouseFilterEnum.Stop;
|
||||
MouseDefaultCursorShape = Control.CursorShape.PointingHand;
|
||||
GuiInput += OnGuiInput;
|
||||
}
|
||||
|
||||
private void OnGuiInput(InputEvent @event)
|
||||
{
|
||||
if (@event is InputEventMouseButton mb && mb.ButtonIndex == MouseButton.Left)
|
||||
{
|
||||
if (mb.Pressed)
|
||||
{
|
||||
_dragging = true;
|
||||
_dragOffset = GetGlobalMousePosition() - GlobalPosition;
|
||||
GameLogger.Log("INTERACTIVE_HANDLE", $"drag start idx={VertexIndex} pos={GlobalPosition}");
|
||||
Clicked?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
_dragging = false;
|
||||
GameLogger.Log("INTERACTIVE_HANDLE", $"drag end idx={VertexIndex}");
|
||||
DragEnd?.Invoke();
|
||||
}
|
||||
AcceptEvent();
|
||||
}
|
||||
else if (@event is InputEventMouseMotion && _dragging)
|
||||
{
|
||||
var center = GlobalPosition + Size / 2;
|
||||
GlobalPosition = GetGlobalMousePosition() - _dragOffset;
|
||||
DragUpdate?.Invoke(center);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
namespace Cthangover.Tools.InteractiveEditor
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Tools.Common;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.UI.Tool.LightEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Night-time preview of a scene with its static lights. The background is
|
||||
/// drawn through the game's <c>timed_sprite</c> 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.
|
||||
/// <para/>
|
||||
/// A click on a handle selects that light and drags it; a double-click on
|
||||
/// empty background adds a light there.
|
||||
/// </summary>
|
||||
public partial class LightEditorCanvas : BackgroundCanvas
|
||||
{
|
||||
private readonly Control _overlay;
|
||||
|
||||
/// <summary>Data being edited; set once by the window.</summary>
|
||||
public LightEditorController Controller;
|
||||
|
||||
/// <summary>Preview material (may be null when the shader is missing).</summary>
|
||||
public ShaderMaterial PreviewMaterial
|
||||
{
|
||||
get => Material as ShaderMaterial;
|
||||
set => Material = value;
|
||||
}
|
||||
|
||||
/// <summary>Raised when a light moved or was added on the canvas.</summary>
|
||||
public event Action Changed;
|
||||
|
||||
/// <summary>Raised when the selection changed on the canvas.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>One handle per light, coloured like the light.</summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Redraws the preview and re-uploads the shader uniforms.</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,176 +1,202 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.UI.Lights;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Mods.Time;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.UI.Tool.LightEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Data controller for the light editor tool. Maintains a list of
|
||||
/// <see cref="LightDef"/> objects and feeds up to 10 lights plus one
|
||||
/// sentinel entry into the <c>"timed_sprite"</c> shader material for
|
||||
/// real-time preview. Exports lights as JSON (<see cref="ExportLightsJson"/>)
|
||||
/// or as a scenario DSL snippet (<see cref="ExportScenarioSnippet"/>).
|
||||
/// Uses a dirty-flag pattern so shader uniform updates only occur when
|
||||
/// the light list changes.
|
||||
/// Data of the light editor: the static lights of one scenario and their
|
||||
/// place in the scenario text. Static lights live in a <c>light_set</c>
|
||||
/// line (<c>light_set [{"x":…,"y":…,"radius":…,"influence":…,"color":…}]</c>)
|
||||
/// that <c>UiLightController.SetStaticLights</c> 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 <c>timed_sprite</c> shader of the preview the same way the
|
||||
/// game does, so the preview shows the real night lighting.
|
||||
/// </summary>
|
||||
public class LightEditorController
|
||||
{
|
||||
/// <summary>The editable list of light definitions. Capped at 10 for shader preview.</summary>
|
||||
public List<LightDef> Lights { get; } = new();
|
||||
/// <summary>Static light slots the shader has (slot 0 is the dynamic light).</summary>
|
||||
public const int MaxLights = 10;
|
||||
|
||||
private readonly Vector2[] previewPositions = new Vector2[11];
|
||||
private readonly Color[] previewColors = new Color[11];
|
||||
private readonly float[] previewRadii = new float[11];
|
||||
private readonly float[] previewInfluence = new float[11];
|
||||
/// <summary>Viewport width the stored radii refer to.</summary>
|
||||
public const float GameWidth = 1920f;
|
||||
|
||||
private bool dirty = true;
|
||||
private const string Command = "light_set";
|
||||
private const int ShaderSlots = 11;
|
||||
private static readonly Vector2 Offscreen = new(-1000f, -1000f);
|
||||
|
||||
/// <summary>Initialises preview arrays with out-of-bounds sentinel values.</summary>
|
||||
public LightEditorController()
|
||||
/// <summary>The edited lights, in file order.</summary>
|
||||
public List<LightDef> Lights { get; private set; } = new();
|
||||
|
||||
/// <summary>Index of the selected light, or -1.</summary>
|
||||
public int SelectedIndex { get; private set; } = -1;
|
||||
|
||||
/// <summary>The selected light, or <c>null</c>.</summary>
|
||||
public LightDef Selected => SelectedIndex >= 0 && SelectedIndex < Lights.Count ? Lights[SelectedIndex] : null;
|
||||
|
||||
/// <summary>Selects by index; out of range clears the selection.</summary>
|
||||
public void Select(int index) => SelectedIndex = index >= 0 && index < Lights.Count ? index : -1;
|
||||
|
||||
/// <summary>Adds a warm light at the given normalised point and selects it; null when all slots are used.</summary>
|
||||
public LightDef Add(Vector2 position)
|
||||
{
|
||||
for (int i = 0; i < 11; i++)
|
||||
{
|
||||
previewPositions[i] = new Vector2(-1000f, -1000f);
|
||||
previewRadii[i] = 0f;
|
||||
previewInfluence[i] = 0f;
|
||||
previewColors[i] = UiTokens.White;
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Removes the selected light.</summary>
|
||||
public void RemoveSelected()
|
||||
{
|
||||
if (Selected == null)
|
||||
return;
|
||||
Lights.RemoveAt(SelectedIndex);
|
||||
SelectedIndex = Math.Min(SelectedIndex, Lights.Count - 1);
|
||||
}
|
||||
|
||||
/// <summary>Compact JSON array exactly as written into the <c>light_set</c> line.</summary>
|
||||
public string ToJson() => JsonSerializer.Serialize(Lights, JsonOptionsProvider.WriteCompactOptions);
|
||||
|
||||
/// <summary>The scenario line for the current lights.</summary>
|
||||
public string ToScenarioLine() => Command + " " + ToJson();
|
||||
|
||||
/// <summary>
|
||||
/// Loads the lights of the first <c>light_set</c> line of a scenario.
|
||||
/// Returns <c>false</c> when the scenario has none (the list is then
|
||||
/// empty and saving inserts a new line). Throws on malformed JSON.
|
||||
/// </summary>
|
||||
public bool LoadFromScenario(string text)
|
||||
{
|
||||
Lights = new List<LightDef>();
|
||||
SelectedIndex = -1;
|
||||
|
||||
var index = FindLine(text, out var lines);
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
var json = ArgumentOf(lines[index]);
|
||||
Lights = JsonSerializer.Deserialize<List<LightDef>>(json, JsonOptionsProvider.ReadOptions) ?? new List<LightDef>();
|
||||
SelectedIndex = Lights.Count > 0 ? 0 : -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ShaderMaterial"/> using the <c>"timed_sprite"</c> shader
|
||||
/// with night-time weights applied. Returns <c>null</c> if the shader is not found.
|
||||
/// Returns the scenario text with its <c>light_set</c> line replaced by
|
||||
/// the current lights, or — when it has none — with a new line inserted
|
||||
/// right after the <c>---</c> header separator (at the top otherwise).
|
||||
/// </summary>
|
||||
public ShaderMaterial CreatePreviewMaterial()
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>True when the scenario text contains a <c>light_set</c> line.</summary>
|
||||
public static bool HasLights(string text) => FindLine(text, out _) >= 0;
|
||||
|
||||
private static int FindLine(string text, out List<string> lines)
|
||||
{
|
||||
lines = new List<string>((text ?? string.Empty).Replace("\r\n", "\n").Split('\n'));
|
||||
return lines.FindIndex(l => l.TrimStart().StartsWith(Command + " ", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>Argument of a <c>light_set</c> line; the old quoted form (<c>"[{\"x\":…}]"</c>) is unescaped.</summary>
|
||||
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 ─────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Material of the preview: the game's <c>timed_sprite</c> shader at
|
||||
/// night weights. <c>null</c> when the shader is not installed.
|
||||
/// </summary>
|
||||
public static ShaderMaterial CreatePreviewMaterial()
|
||||
{
|
||||
var shader = Shaders.Resolve("timed_sprite");
|
||||
if (shader == null)
|
||||
return null;
|
||||
|
||||
var mat = new ShaderMaterial { Shader = shader };
|
||||
var night = new Vector4(0, 0, 0, 1);
|
||||
mat.SetShaderParameter("time_weights", night);
|
||||
mat.SetShaderParameter("use_time", true);
|
||||
return mat;
|
||||
var material = new ShaderMaterial { Shader = shader };
|
||||
material.SetShaderParameter("time_weights", new Vector4(0, 0, 0, 1));
|
||||
material.SetShaderParameter("use_time", true);
|
||||
return material;
|
||||
}
|
||||
|
||||
/// <summary>Sets the background texture uniform on the preview shader material.</summary>
|
||||
public void SetBackgroundForPreview(Texture2D texture, ShaderMaterial material)
|
||||
/// <summary>Binds the background and its optional depth/albedo masks to the preview material.</summary>
|
||||
public static void SetPreviewTextures(ShaderMaterial material, Texture2D depth, Texture2D albedo)
|
||||
{
|
||||
if (material != null)
|
||||
material.SetShaderParameter("background_map", texture);
|
||||
}
|
||||
|
||||
/// <summary>Sets the depth mask texture uniform on the preview shader material.</summary>
|
||||
public void SetDepthForPreview(Texture2D texture, ShaderMaterial material)
|
||||
{
|
||||
if (material != null)
|
||||
material.SetShaderParameter("depth_mask", texture);
|
||||
}
|
||||
|
||||
/// <summary>Sets the albedo texture uniform on the preview shader material.</summary>
|
||||
public void SetAlbedoForPreview(Texture2D texture, ShaderMaterial material)
|
||||
{
|
||||
if (material != null)
|
||||
material.SetShaderParameter("albedo_map", texture);
|
||||
}
|
||||
|
||||
/// <summary>Appends a <see cref="LightDef"/> to the list and marks the preview dirty.</summary>
|
||||
public void AddLight(LightDef light)
|
||||
{
|
||||
Lights.Add(light);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
/// <summary>Removes the light at <paramref name="index"/>. No-op if out of range.</summary>
|
||||
public void RemoveLight(int index)
|
||||
{
|
||||
if (index < 0 || index >= Lights.Count)
|
||||
if (material == null)
|
||||
return;
|
||||
Lights.RemoveAt(index);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
/// <summary>Removes all lights from the list and marks dirty.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
Lights.Clear();
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
/// <summary>Forces the preview to be recalculated on the next <see cref="UpdatePreview"/> call.</summary>
|
||||
public void MarkDirty()
|
||||
{
|
||||
dirty = true;
|
||||
material.SetShaderParameter("depth_mask", depth);
|
||||
material.SetShaderParameter("albedo_map", albedo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Packs up to 10 lights plus a sentinel into shader uniform arrays.
|
||||
/// Only executes if <c>dirty</c> is <c>true</c>. Converts each light's
|
||||
/// normalised position to pixel coordinates relative to the preview area.
|
||||
/// Uploads the lights into the shader uniforms. <paramref name="toCanvas"/>
|
||||
/// maps a normalised position into the preview's canvas coordinates
|
||||
/// (the space of the shader's <c>world_pos</c>); <paramref name="radiusScale"/>
|
||||
/// shrinks game-resolution radii to the preview size.
|
||||
/// </summary>
|
||||
public void UpdatePreview(ShaderMaterial material, Vector2 previewGlobalPos, Vector2 previewSize)
|
||||
public void UpdatePreview(ShaderMaterial material, Func<Vector2, Vector2> toCanvas, float radiusScale)
|
||||
{
|
||||
if (material == null || !dirty)
|
||||
if (material == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < 11; i++)
|
||||
var positions = new Godot.Collections.Array<Vector2>();
|
||||
var colors = new Godot.Collections.Array<Color>();
|
||||
var radii = new Godot.Collections.Array<float>();
|
||||
var influence = new Godot.Collections.Array<float>();
|
||||
|
||||
// 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++)
|
||||
{
|
||||
previewPositions[i] = new Vector2(-1000f, -1000f);
|
||||
previewRadii[i] = 0f;
|
||||
previewInfluence[i] = 0f;
|
||||
previewColors[i] = UiTokens.White;
|
||||
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);
|
||||
}
|
||||
|
||||
int count = Mathf.Min(Lights.Count, 10);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var pixelPos = Lights[i].ToPixelPos(previewSize);
|
||||
previewPositions[i] = new Vector2(
|
||||
pixelPos.X + previewGlobalPos.X,
|
||||
pixelPos.Y + previewGlobalPos.Y);
|
||||
previewRadii[i] = Lights[i].Radius;
|
||||
previewInfluence[i] = Lights[i].Influence;
|
||||
previewColors[i] = Lights[i].ToColor();
|
||||
}
|
||||
|
||||
var posArray = new Godot.Collections.Array<Vector2>();
|
||||
var colArray = new Godot.Collections.Array<Color>();
|
||||
var radArray = new Godot.Collections.Array<float>();
|
||||
var infArray = new Godot.Collections.Array<float>();
|
||||
for (int i = 0; i < 11; i++)
|
||||
{
|
||||
posArray.Add(previewPositions[i]);
|
||||
colArray.Add(previewColors[i]);
|
||||
radArray.Add(previewRadii[i]);
|
||||
infArray.Add(previewInfluence[i]);
|
||||
}
|
||||
|
||||
material.SetShaderParameter("light_count", count);
|
||||
material.SetShaderParameter("light_positions", posArray);
|
||||
material.SetShaderParameter("light_colors", colArray);
|
||||
material.SetShaderParameter("light_radii", radArray);
|
||||
material.SetShaderParameter("light_influence", infArray);
|
||||
material.SetShaderParameter("light_radius_scale", 1.0f);
|
||||
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
/// <summary>Serialises the light list as a compact JSON string.</summary>
|
||||
public string ExportLightsJson()
|
||||
{
|
||||
return JsonSerializer.Serialize(Lights, Cthangover.Core.Utils.JsonOptionsProvider.WriteCompactOptions);
|
||||
}
|
||||
|
||||
/// <summary>Produces a <c>light_set</c> scenario DSL command with the lights JSON as its argument.</summary>
|
||||
public string ExportScenarioSnippet()
|
||||
{
|
||||
var json = ExportLightsJson();
|
||||
return $"light_set \"{json.Replace("\"", "\\\"")}\"";
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using Cthangover.Core.UI.Lights;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.UI.Tool.LightEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Draggable 24×24 coloured handle representing a light source on the preview.
|
||||
/// Overrides <c>_GuiInput</c> (Godot virtual) for mouse drag support; the colour
|
||||
/// is taken from <see cref="LightDef.ToColor"/>. Raises <see cref="DragUpdate"/>
|
||||
/// with the handle's centre position in global coordinates for the window to
|
||||
/// convert back to normalised coordinates.
|
||||
/// </summary>
|
||||
public partial class LightEditorHandle : ColorRect
|
||||
{
|
||||
/// <summary>Index of the light in <see cref="LightEditorController.Lights"/>.</summary>
|
||||
public int LightIndex;
|
||||
/// <summary>Invoked during drag with the centre position in global coordinates.</summary>
|
||||
public System.Action<Vector2> DragUpdate;
|
||||
/// <summary>Invoked when the handle is clicked (mouse down).</summary>
|
||||
public System.Action Clicked;
|
||||
|
||||
private bool dragging;
|
||||
private Vector2 dragOffset;
|
||||
|
||||
/// <summary>Creates a 24×24 handle coloured to match the light's <see cref="LightDef.ToColor"/> value.</summary>
|
||||
public LightEditorHandle(LightDef light, int index)
|
||||
{
|
||||
LightIndex = index;
|
||||
Size = new Vector2(24, 24);
|
||||
Color = light.ToColor();
|
||||
MouseFilter = MouseFilterEnum.Stop;
|
||||
}
|
||||
|
||||
/// <summary>Handles mouse drag and click via Godot's virtual input system (<c>_GuiInput</c>).</summary>
|
||||
public override void _GuiInput(InputEvent @event)
|
||||
{
|
||||
if (@event is InputEventMouseButton mb && mb.ButtonIndex == MouseButton.Left)
|
||||
{
|
||||
if (mb.Pressed)
|
||||
{
|
||||
dragging = true;
|
||||
dragOffset = GetGlobalMousePosition() - GlobalPosition;
|
||||
Clicked?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
dragging = false;
|
||||
}
|
||||
AcceptEvent();
|
||||
}
|
||||
else if (@event is InputEventMouseMotion && dragging)
|
||||
{
|
||||
GlobalPosition = GetGlobalMousePosition() - dragOffset;
|
||||
DragUpdate?.Invoke(GlobalPosition + Size / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
namespace Cthangover.Core.UI.Tool.LightEditor
|
||||
{
|
||||
/// <summary>Registers the light editor as a tool window. Creates <see cref="LightEditorWindow"/> instances.</summary>
|
||||
|
||||
@@ -1,379 +1,387 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.UI.Lights;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window for placing and configuring static lights on a background.
|
||||
/// Provides a sidebar with background selection, a light list with add/delete/select,
|
||||
/// per-light property editors (radius, influence, colour), draggable handles on a
|
||||
/// shader-powered preview, and JSON export. Uses <see cref="LightEditorController"/>
|
||||
/// for data management and <see cref="ModResourceService"/> for background textures.
|
||||
/// Editor of the static lights of a scene. The author picks a scenario —
|
||||
/// the ones that already set lights come first — and edits its
|
||||
/// <c>light_set</c> 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.
|
||||
/// </summary>
|
||||
public partial class LightEditorWindow : ToolWindow
|
||||
{
|
||||
private LightEditorController controller;
|
||||
private readonly LightEditorController _controller = new();
|
||||
private readonly List<ScenarioFileInfo> _scenarios = new();
|
||||
private readonly List<string> _backgroundIds = new();
|
||||
|
||||
private ItemList backgroundList;
|
||||
private VBoxContainer lightListPanel;
|
||||
private Button addLightBtn;
|
||||
private TextureRect preview;
|
||||
private ShaderMaterial previewMaterial;
|
||||
private ScenarioFileInfo _scenario;
|
||||
private string _scenarioText;
|
||||
private bool _suppress;
|
||||
|
||||
private LineEdit radiusEdit;
|
||||
private LineEdit influenceEdit;
|
||||
private ColorPickerButton colorPicker;
|
||||
private TextEdit jsonEdit;
|
||||
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;
|
||||
|
||||
private readonly List<LightEditorHandle> handles = new();
|
||||
private int selectedIndex = -1;
|
||||
|
||||
private readonly List<string> bgIds = new();
|
||||
|
||||
/// <summary>Constructs the window, creates the controller, builds UI, and loads the background list.</summary>
|
||||
public LightEditorWindow() : base("tools/light_editor/title")
|
||||
{
|
||||
controller = new LightEditorController();
|
||||
CthangoverTheme.Apply(this);
|
||||
BuildUI();
|
||||
LoadBackgroundList();
|
||||
LoadBackgrounds();
|
||||
LoadScenarioList();
|
||||
SetHint(ToolUi.Tr("tools/light_editor/hint"));
|
||||
}
|
||||
|
||||
protected override void Cleanup()
|
||||
{
|
||||
foreach (var h in handles)
|
||||
h.QueueFree();
|
||||
handles.Clear();
|
||||
}
|
||||
|
||||
private Vector2 PreviewSize()
|
||||
{
|
||||
var sz = preview.Size;
|
||||
if (sz.X <= 0 || sz.Y <= 0)
|
||||
sz = new Vector2(1920, 1080);
|
||||
return sz;
|
||||
}
|
||||
|
||||
private void RefreshAll()
|
||||
{
|
||||
RepositionHandles();
|
||||
controller.MarkDirty();
|
||||
controller.UpdatePreview(previewMaterial, preview.GlobalPosition, PreviewSize());
|
||||
UpdateLightList();
|
||||
UpdateJsonEdit();
|
||||
}
|
||||
// ── UI construction ────────────────────────────────────────
|
||||
|
||||
private void BuildUI()
|
||||
{
|
||||
var outerVBox = CreateFillContainer();
|
||||
AddChild(outerVBox);
|
||||
var outer = CreateFillContainer();
|
||||
AddChild(outer);
|
||||
|
||||
var mainHBox = new HBoxContainer();
|
||||
mainHBox.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
mainHBox.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
outerVBox.AddChild(mainHBox);
|
||||
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 sidebarVBox = new VBoxContainer();
|
||||
var sidebar = CreateSidebar(sidebarVBox);
|
||||
mainHBox.AddChild(sidebar);
|
||||
|
||||
sidebarVBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/background") });
|
||||
|
||||
backgroundList = new ItemList();
|
||||
backgroundList.CustomMinimumSize = new Vector2(0, 140);
|
||||
backgroundList.ItemSelected += OnBackgroundSelected;
|
||||
sidebarVBox.AddChild(backgroundList);
|
||||
|
||||
sidebarVBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/static_lights") });
|
||||
|
||||
lightListPanel = new VBoxContainer();
|
||||
sidebarVBox.AddChild(lightListPanel);
|
||||
|
||||
addLightBtn = new Button { Text = TranslationServer.Translate("tools/light_editor/add_light") };
|
||||
addLightBtn.Pressed += OnAddLight;
|
||||
sidebarVBox.AddChild(addLightBtn);
|
||||
|
||||
sidebarVBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/selected_props") });
|
||||
|
||||
var radiusHBox = new HBoxContainer();
|
||||
radiusHBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/radius") });
|
||||
radiusEdit = new LineEdit { Text = "300" };
|
||||
radiusEdit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
radiusEdit.TextChanged += OnRadiusChanged;
|
||||
radiusHBox.AddChild(radiusEdit);
|
||||
sidebarVBox.AddChild(radiusHBox);
|
||||
|
||||
var infHBox = new HBoxContainer();
|
||||
infHBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/influence") });
|
||||
influenceEdit = new LineEdit { Text = "1" };
|
||||
influenceEdit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
influenceEdit.TextChanged += OnInfluenceChanged;
|
||||
infHBox.AddChild(influenceEdit);
|
||||
sidebarVBox.AddChild(infHBox);
|
||||
|
||||
var colorHBox = new HBoxContainer();
|
||||
colorHBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/color") });
|
||||
colorPicker = new ColorPickerButton { Color = Colors.Yellow };
|
||||
colorPicker.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
colorPicker.ColorChanged += OnColorChanged;
|
||||
colorHBox.AddChild(colorPicker);
|
||||
sidebarVBox.AddChild(colorHBox);
|
||||
|
||||
var importBtn = new Button { Text = TranslationServer.Translate("tools/light_editor/import_json") };
|
||||
importBtn.Pressed += OnImport;
|
||||
sidebarVBox.AddChild(importBtn);
|
||||
|
||||
sidebarVBox.AddChild(new Label { Text = TranslationServer.Translate("tools/light_editor/norm_coords") });
|
||||
|
||||
var previewContainer = new PanelContainer();
|
||||
previewContainer.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
previewContainer.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
mainHBox.AddChild(previewContainer);
|
||||
|
||||
preview = new TextureRect();
|
||||
preview.ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize;
|
||||
preview.StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered;
|
||||
preview.MouseFilter = Control.MouseFilterEnum.Stop;
|
||||
preview.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
preview.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
previewContainer.AddChild(preview);
|
||||
|
||||
previewMaterial = controller.CreatePreviewMaterial();
|
||||
preview.Material = previewMaterial;
|
||||
|
||||
jsonEdit = new TextEdit();
|
||||
jsonEdit.CustomMinimumSize = new Vector2(0, 70);
|
||||
jsonEdit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
jsonEdit.WrapMode = TextEdit.LineWrappingMode.Boundary;
|
||||
jsonEdit.TextChanged += OnJsonEditChanged;
|
||||
outerVBox.AddChild(jsonEdit);
|
||||
}
|
||||
|
||||
private void OnJsonEditChanged()
|
||||
var main = new HSplitContainer
|
||||
{
|
||||
}
|
||||
|
||||
private void UpdateJsonEdit()
|
||||
{
|
||||
var json = controller.ExportLightsJson();
|
||||
if (jsonEdit.Text != json)
|
||||
jsonEdit.Text = json;
|
||||
}
|
||||
|
||||
private void LoadBackgroundList()
|
||||
{
|
||||
bgIds.Clear();
|
||||
backgroundList.Clear();
|
||||
|
||||
var ids = ModResourceService.GetBackgroundIds();
|
||||
bgIds.AddRange(ids);
|
||||
foreach (var id in ids)
|
||||
backgroundList.AddItem(id);
|
||||
}
|
||||
|
||||
private void OnBackgroundSelected(long index)
|
||||
{
|
||||
var bgId = bgIds[(int)index];
|
||||
var tex = BackgroundFactory.Instance.Get(bgId);
|
||||
preview.Texture = tex;
|
||||
controller.SetBackgroundForPreview(tex, previewMaterial);
|
||||
|
||||
var depthTex = BackgroundFactory.Instance.Get(bgId + "_depth");
|
||||
if (depthTex != null)
|
||||
controller.SetDepthForPreview(depthTex, previewMaterial);
|
||||
|
||||
var albedoTex = BackgroundFactory.Instance.Get(bgId + "_albedo");
|
||||
if (albedoTex != null)
|
||||
controller.SetAlbedoForPreview(albedoTex, previewMaterial);
|
||||
}
|
||||
|
||||
private void OnAddLight()
|
||||
{
|
||||
if (handles.Count >= 10)
|
||||
return;
|
||||
|
||||
var light = new LightDef
|
||||
{
|
||||
X = 0.4f,
|
||||
Y = 0.3f,
|
||||
Radius = 300f,
|
||||
Influence = 1f,
|
||||
ColorHex = "#ffff00"
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
outer.AddChild(main);
|
||||
|
||||
controller.AddLight(light);
|
||||
AddHandle(light, controller.Lights.Count - 1);
|
||||
RefreshAll();
|
||||
}
|
||||
var side = new VBoxContainer();
|
||||
main.AddChild(CreateSidebar(side, 300));
|
||||
BuildSidebar(side);
|
||||
|
||||
private void AddHandle(LightDef light, int index)
|
||||
var canvasColumn = new VSplitContainer
|
||||
{
|
||||
var handle = new LightEditorHandle(light, index);
|
||||
handle.DragUpdate += globalCenter =>
|
||||
{
|
||||
var localCenter = globalCenter - preview.GlobalPosition;
|
||||
var currentSize = PreviewSize();
|
||||
light.FromPixelPos(localCenter, currentSize);
|
||||
RefreshAll();
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
handle.Clicked += () => SelectHandle(index);
|
||||
preview.AddChild(handle);
|
||||
handles.Add(handle);
|
||||
RepositionHandle(handle, light);
|
||||
}
|
||||
main.AddChild(canvasColumn);
|
||||
|
||||
private void RepositionHandle(LightEditorHandle handle, LightDef light)
|
||||
var canvasPanel = new PanelContainer
|
||||
{
|
||||
var size = PreviewSize();
|
||||
handle.GlobalPosition = preview.GlobalPosition
|
||||
+ new Vector2(light.X * size.X - 12, light.Y * size.Y - 12);
|
||||
}
|
||||
|
||||
private void RepositionHandles()
|
||||
{
|
||||
for (int i = 0; i < handles.Count && i < controller.Lights.Count; i++)
|
||||
RepositionHandle(handles[i], controller.Lights[i]);
|
||||
}
|
||||
|
||||
private void SelectHandle(int index)
|
||||
{
|
||||
selectedIndex = index;
|
||||
if (index < 0 || index >= controller.Lights.Count)
|
||||
{
|
||||
radiusEdit.Text = "300";
|
||||
influenceEdit.Text = "1";
|
||||
colorPicker.Color = Colors.Yellow;
|
||||
return;
|
||||
}
|
||||
|
||||
var light = controller.Lights[index];
|
||||
radiusEdit.Text = light.Radius.ToString("F0");
|
||||
influenceEdit.Text = light.Influence.ToString("F2");
|
||||
colorPicker.Color = light.ToColor();
|
||||
}
|
||||
|
||||
private void OnRadiusChanged(string text)
|
||||
{
|
||||
if (selectedIndex < 0 || selectedIndex >= controller.Lights.Count)
|
||||
return;
|
||||
if (float.TryParse(text, out var val))
|
||||
{
|
||||
controller.Lights[selectedIndex].Radius = val;
|
||||
RefreshAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInfluenceChanged(string text)
|
||||
{
|
||||
if (selectedIndex < 0 || selectedIndex >= controller.Lights.Count)
|
||||
return;
|
||||
if (float.TryParse(text, out var val))
|
||||
{
|
||||
controller.Lights[selectedIndex].Influence = val;
|
||||
RefreshAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnColorChanged(Color color)
|
||||
{
|
||||
if (selectedIndex < 0 || selectedIndex >= controller.Lights.Count)
|
||||
return;
|
||||
controller.Lights[selectedIndex].ColorHex = color.ToHtml();
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void UpdateLightList()
|
||||
{
|
||||
foreach (var child in lightListPanel.GetChildren())
|
||||
child.QueueFree();
|
||||
|
||||
for (int i = 0; i < controller.Lights.Count; i++)
|
||||
{
|
||||
var light = controller.Lights[i];
|
||||
var hBox = new HBoxContainer();
|
||||
var label = new Label
|
||||
{
|
||||
Text = string.Format(TranslationServer.Translate("tools/light_editor/light_n"), i + 1, light.X, light.Y),
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
hBox.AddChild(label);
|
||||
canvasPanel.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusMd, UiTokens.Well, UiTokens.Line));
|
||||
canvasColumn.AddChild(canvasPanel);
|
||||
|
||||
var selBtn = new Button { Text = TranslationServer.Translate("tools/light_editor/btn_select") };
|
||||
int idx = i;
|
||||
selBtn.Pressed += () => SelectHandle(idx);
|
||||
hBox.AddChild(selBtn);
|
||||
|
||||
var delBtn = new Button { Text = TranslationServer.Translate("tools/light_editor/btn_delete") };
|
||||
delBtn.Pressed += () => RemoveLight(idx);
|
||||
hBox.AddChild(delBtn);
|
||||
|
||||
lightListPanel.AddChild(hBox);
|
||||
}
|
||||
|
||||
addLightBtn.Disabled = controller.Lights.Count >= 10;
|
||||
}
|
||||
|
||||
private void RemoveLight(int index)
|
||||
_canvas = new LightEditorCanvas
|
||||
{
|
||||
if (index < 0 || index >= controller.Lights.Count)
|
||||
return;
|
||||
Controller = _controller,
|
||||
PreviewMaterial = LightEditorController.CreatePreviewMaterial()
|
||||
};
|
||||
_canvas.Changed += OnCanvasChanged;
|
||||
_canvas.SelectionChanged += RefreshAll;
|
||||
canvasPanel.AddChild(_canvas);
|
||||
|
||||
controller.RemoveLight(index);
|
||||
|
||||
if (index < handles.Count)
|
||||
{
|
||||
handles[index].QueueFree();
|
||||
handles.RemoveAt(index);
|
||||
_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);
|
||||
}
|
||||
|
||||
for (int i = 0; i < handles.Count; i++)
|
||||
handles[i].LightIndex = i;
|
||||
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);
|
||||
|
||||
if (selectedIndex == index)
|
||||
selectedIndex = -1;
|
||||
else if (selectedIndex > index)
|
||||
selectedIndex--;
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Scenarios with a <c>light_set</c> line first (marked ●), then all others.</summary>
|
||||
private void LoadScenarioList()
|
||||
{
|
||||
var withLights = new List<ScenarioFileInfo>();
|
||||
var others = new List<ScenarioFileInfo>();
|
||||
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 OnImport()
|
||||
private void OnScenarioPicked(long index)
|
||||
{
|
||||
var json = jsonEdit.Text;
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return;
|
||||
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 lights = JsonSerializer.Deserialize<List<LightDef>>(json, Cthangover.Core.Utils.JsonOptionsProvider.ReadOptions);
|
||||
if (lights == null || lights.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var h in handles)
|
||||
h.QueueFree();
|
||||
handles.Clear();
|
||||
controller.Clear();
|
||||
|
||||
foreach (var light in lights)
|
||||
{
|
||||
controller.AddLight(light);
|
||||
AddHandle(light, controller.Lights.Count - 1);
|
||||
}
|
||||
|
||||
selectedIndex = -1;
|
||||
RefreshAll();
|
||||
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)
|
||||
{
|
||||
GameLogger.Log("LIGHT_EDITOR", $"Import JSON failed: {ex.Message}", LogLevel.Error);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Shows a background with its optional <c>_depth</c>/<c>_albedo</c> masks in the shader.</summary>
|
||||
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 ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Ctrl+S.</summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Delete removes the selected light.</summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
-156
@@ -2,39 +2,35 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Cthangover.Mods.Map;
|
||||
using Cthangover.Tools.Common;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.MapEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Interactive preview of the edited map: draws the background with the
|
||||
/// exact aspect-preserving layout used by <c>MapWidget</c>, overlays every
|
||||
/// zone polygon, marker and text label, and hosts the draggable handles.
|
||||
/// Implements the two editor modes:
|
||||
/// Interactive preview of the edited map: the background (laid out by
|
||||
/// <see cref="BackgroundCanvas"/> exactly like <c>MapWidget</c>), every zone
|
||||
/// polygon, marker and text label, and the draggable handles of the
|
||||
/// selected zone. Two modes:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>select</b> — clicking picks the topmost zone (polygon first,
|
||||
/// <item><b>select</b> — a click picks the topmost zone (polygon first,
|
||||
/// then marker/label dots); handles move vertices and positions;</item>
|
||||
/// <item><b>draw</b> — clicking appends polygon vertices to the selected
|
||||
/// region and double-click finishes the polygon and returns to select
|
||||
/// mode.</item>
|
||||
/// <item><b>draw</b> — 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.</item>
|
||||
/// </list>
|
||||
/// Right-click deletes a polygon vertex in either mode: the 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
|
||||
/// <see cref="MapEditorController"/> and raises <see cref="Changed"/> so the
|
||||
/// window can refresh the sidebar and JSON preview. Drawing is connected
|
||||
/// through the "draw" signal and input through the GuiInput event, because
|
||||
/// virtual method overrides are unreliable in mod assemblies.
|
||||
/// 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.
|
||||
/// <para/>
|
||||
/// Zone fills and outlines are painted through <see cref="MapZoneStyle"/> —
|
||||
/// the same definition the game's <c>MapZonesLayer</c> and the window's
|
||||
/// game-like preview use — so a zone the author sees here and the same zone
|
||||
/// on the live map are guaranteed to look identical.
|
||||
/// The canvas owns no data: it reads and mutates
|
||||
/// <see cref="MapEditorController"/> and raises <see cref="Changed"/> so
|
||||
/// the window can refresh the sidebar and JSON. Zone fills and outlines are
|
||||
/// painted through <see cref="MapZoneStyle"/> — the definition the game's
|
||||
/// <c>MapZonesLayer</c> uses — so the editor and the live map look the same.
|
||||
/// </summary>
|
||||
public partial class MapEditorCanvas : Control
|
||||
public partial class MapEditorCanvas : BackgroundCanvas
|
||||
{
|
||||
/// <summary>Mode constant: clicking selects zones and drags handles.</summary>
|
||||
public const string ModeSelect = "select";
|
||||
@@ -42,129 +38,97 @@ namespace Cthangover.Tools.MapEditor
|
||||
/// <summary>Mode constant: clicking appends polygon vertices.</summary>
|
||||
public const string ModeDraw = "draw";
|
||||
|
||||
/// <summary>Handle index of the marker position.</summary>
|
||||
public const int MarkerHandle = -2;
|
||||
|
||||
/// <summary>Handle index of the label position.</summary>
|
||||
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;
|
||||
|
||||
private readonly List<MapEditorHandle> _handles = new();
|
||||
|
||||
/// <summary>Data being edited; set once by the window.</summary>
|
||||
public MapEditorController Controller;
|
||||
|
||||
/// <summary>Reference background; <c>null</c> falls back to a flat rectangle.</summary>
|
||||
public Texture2D Background;
|
||||
|
||||
/// <summary>Current interaction mode, one of <see cref="ModeSelect"/> / <see cref="ModeDraw"/>.</summary>
|
||||
public string Mode { get; private set; } = ModeSelect;
|
||||
|
||||
/// <summary>Raised whenever the canvas mutates controller data (drag, draw, pick).</summary>
|
||||
/// <summary>Raised whenever the canvas mutates controller data (drag, draw, delete).</summary>
|
||||
public event Action Changed;
|
||||
|
||||
/// <summary>Raised with the newly picked zone index (-1 for empty space).</summary>
|
||||
public event Action<int> ZonePicked;
|
||||
|
||||
/// <summary>Raised when the canvas leaves draw mode on its own (double-click finishing a polygon).</summary>
|
||||
public event Action ModeChanged;
|
||||
|
||||
private Vector2? _pointer;
|
||||
|
||||
public MapEditorCanvas()
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Stop;
|
||||
ClipContents = true;
|
||||
MapZoneStyle.Prepare(this);
|
||||
GuiInput += OnGuiInput;
|
||||
MouseExited += OnMouseExited;
|
||||
Resized += OnResized;
|
||||
Connect("draw", Callable.From((Action)DrawAll));
|
||||
}
|
||||
|
||||
/// <summary>Switches the interaction mode and refreshes the handles.</summary>
|
||||
public void SetMode(string mode)
|
||||
{
|
||||
Mode = mode == ModeDraw ? ModeDraw : ModeSelect;
|
||||
MouseDefaultCursorShape = Mode == ModeDraw ? CursorShape.Cross : CursorShape.Arrow;
|
||||
RebuildHandles();
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
/// <summary>Assigns the background texture and rebuilds the layout.</summary>
|
||||
public void SetBackground(Texture2D texture)
|
||||
/// <summary>Leaves draw mode (double-click, Enter, Esc) and tells the window.</summary>
|
||||
public void FinishDrawing()
|
||||
{
|
||||
Background = texture;
|
||||
RebuildHandles();
|
||||
QueueRedraw();
|
||||
if (Mode != ModeDraw)
|
||||
return;
|
||||
|
||||
SetMode(ModeSelect);
|
||||
_pointer = null;
|
||||
ModeChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Recreates the handles from the current selection. Call after structural changes.</summary>
|
||||
public void RebuildHandles()
|
||||
public override void RebuildHandles()
|
||||
{
|
||||
foreach (var handle in _handles)
|
||||
handle.QueueFree();
|
||||
_handles.Clear();
|
||||
ClearHandles();
|
||||
|
||||
var zone = Controller?.Selected;
|
||||
if (zone == null)
|
||||
return;
|
||||
|
||||
var rect = GetBackgroundRect();
|
||||
if (rect.Size.X <= 0f || rect.Size.Y <= 0f)
|
||||
if (zone == null || GetBackgroundRect().Size.X <= 0f)
|
||||
return;
|
||||
|
||||
if (zone.Points != null)
|
||||
{
|
||||
for (var i = 0; i < zone.Points.Count; i++)
|
||||
AddHandle(rect, zone.Points[i], VertexColor, 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(rect, zone.ImagePos, MarkerColor, MapEditorHandle.MarkerHandle);
|
||||
AddHandle(zone.ImagePos, MarkerColor, MarkerHandle, p => OnHandleDrag(MarkerHandle, p), 18f).DragEnd = () => Changed?.Invoke();
|
||||
|
||||
if (zone.LabelPos.HasValue)
|
||||
AddHandle(rect, zone.LabelPos.Value, LabelColor, MapEditorHandle.LabelHandle);
|
||||
AddHandle(zone.LabelPos.Value, LabelColor, LabelHandle, p => OnHandleDrag(LabelHandle, p), 18f).DragEnd = () => Changed?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aspect-preserving rectangle the background occupies inside the
|
||||
/// canvas. Must stay identical to the runtime layout: the saved
|
||||
/// normalised coordinates mean nothing if the editor and the game
|
||||
/// disagree about this rectangle.
|
||||
/// </summary>
|
||||
public Rect2 GetBackgroundRect()
|
||||
{
|
||||
var controlSize = Size;
|
||||
if (controlSize.X <= 0f || controlSize.Y <= 0f)
|
||||
return new Rect2();
|
||||
|
||||
var textureSize = Background?.GetSize() ?? new Vector2(1920, 1080);
|
||||
if (textureSize.X <= 0f || textureSize.Y <= 0f)
|
||||
textureSize = new Vector2(1920, 1080);
|
||||
|
||||
var scale = Mathf.Min(controlSize.X / textureSize.X, controlSize.Y / textureSize.Y);
|
||||
var size = textureSize * scale;
|
||||
return new Rect2((controlSize - size) / 2f, size);
|
||||
}
|
||||
|
||||
private void AddHandle(Rect2 rect, Vector2 normalized, Color color, int index)
|
||||
{
|
||||
var handle = new MapEditorHandle(color, index);
|
||||
handle.DragUpdate = center => OnHandleDrag(index, center);
|
||||
handle.DragEnd = () => Changed?.Invoke();
|
||||
handle.RightClicked = () => RemoveVertexHandle(index);
|
||||
AddChild(handle);
|
||||
handle.GlobalPosition = GlobalPosition + rect.Position + Clamp01(normalized) * rect.Size - handle.Size / 2f;
|
||||
_handles.Add(handle);
|
||||
}
|
||||
|
||||
private void OnHandleDrag(int index, Vector2 globalCenter)
|
||||
private void OnHandleDrag(int index, Vector2 normalized)
|
||||
{
|
||||
if (Controller == null)
|
||||
return;
|
||||
|
||||
var rect = GetBackgroundRect();
|
||||
if (rect.Size.X <= 0f || rect.Size.Y <= 0f)
|
||||
return;
|
||||
|
||||
var local = globalCenter - GlobalPosition - rect.Position;
|
||||
var normalized = Clamp01(new Vector2(local.X / rect.Size.X, local.Y / rect.Size.Y));
|
||||
|
||||
if (index == MapEditorHandle.MarkerHandle)
|
||||
if (index == MarkerHandle)
|
||||
Controller.SetImagePos(normalized);
|
||||
else if (index == MapEditorHandle.LabelHandle)
|
||||
else if (index == LabelHandle)
|
||||
Controller.SetLabelPos(normalized);
|
||||
else
|
||||
Controller.SetVertex(index, normalized);
|
||||
@@ -175,20 +139,40 @@ namespace Cthangover.Tools.MapEditor
|
||||
|
||||
private void OnGuiInput(InputEvent @event)
|
||||
{
|
||||
if (Controller == null || @event is not InputEventMouseButton mb || !mb.Pressed)
|
||||
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)
|
||||
{
|
||||
Controller.AddVertex(Clamp01(PixelToNorm(mb.Position)));
|
||||
// The first click of a double-click already placed the
|
||||
// vertex; the double-click itself only finishes the polygon.
|
||||
if (mb.DoubleClick)
|
||||
Mode = ModeSelect;
|
||||
{
|
||||
FinishDrawing();
|
||||
}
|
||||
else
|
||||
{
|
||||
Controller.AddVertex(Clamp01(LocalToNorm(mb.Position)));
|
||||
RebuildHandles();
|
||||
Changed?.Invoke();
|
||||
QueueRedraw();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var hit = HitTest(mb.Position);
|
||||
@@ -197,7 +181,6 @@ namespace Cthangover.Tools.MapEditor
|
||||
Controller.Select(hit);
|
||||
RebuildHandles();
|
||||
ZonePicked?.Invoke(hit);
|
||||
Changed?.Invoke();
|
||||
QueueRedraw();
|
||||
}
|
||||
}
|
||||
@@ -213,12 +196,7 @@ namespace Cthangover.Tools.MapEditor
|
||||
|
||||
private void OnMouseExited()
|
||||
{
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
private void OnResized()
|
||||
{
|
||||
RebuildHandles();
|
||||
_pointer = null;
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
@@ -238,41 +216,51 @@ namespace Cthangover.Tools.MapEditor
|
||||
for (var i = zones.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var zone = zones[i];
|
||||
if (HasMarker(zone) && (local - ToPixel(zone.ImagePos, rect)).Length() <= 24f)
|
||||
if (HasMarker(zone) && (local - ToPixel(zone.ImagePos, rect)).Length() <= PickRadius)
|
||||
return i;
|
||||
if (zone.LabelPos.HasValue && (local - ToPixel(zone.LabelPos.Value, rect)).Length() <= 24f)
|
||||
if (zone.LabelPos.HasValue && (local - ToPixel(zone.LabelPos.Value, rect)).Length() <= PickRadius)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void DrawAll()
|
||||
protected override void DrawContent(Rect2 rect)
|
||||
{
|
||||
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);
|
||||
|
||||
var zones = Controller?.Zones;
|
||||
if (zones == null)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < zones.Count; i++)
|
||||
{
|
||||
var selected = i == Controller.SelectedIndex;
|
||||
DrawZone(zones[i], rect, selected);
|
||||
DrawZone(zones[i], rect, i == Controller.SelectedIndex);
|
||||
|
||||
DrawRubberBand(rect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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)
|
||||
MapZoneStyle.Draw(this, ToPixels(zone.Points, rect), selected, false, zone.Locked);
|
||||
{
|
||||
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);
|
||||
@@ -337,41 +325,31 @@ namespace Cthangover.Tools.MapEditor
|
||||
return rect.Position + normalized * rect.Size;
|
||||
}
|
||||
|
||||
private Vector2 PixelToNorm(Vector2 local)
|
||||
{
|
||||
var rect = GetBackgroundRect();
|
||||
if (rect.Size.X <= 0f || rect.Size.Y <= 0f)
|
||||
return Vector2.Zero;
|
||||
|
||||
return new Vector2((local.X - rect.Position.X) / rect.Size.X, (local.Y - rect.Position.Y) / rect.Size.Y);
|
||||
}
|
||||
|
||||
private static Vector2 Clamp01(Vector2 value)
|
||||
{
|
||||
return new Vector2(Mathf.Clamp(value.X, 0f, 1f), Mathf.Clamp(value.Y, 0f, 1f));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// pure inscription, the marker is only shown when an icon exists. The
|
||||
/// region currently being drawn is excluded while it has no vertices.
|
||||
/// </summary>
|
||||
private static bool HasMarker(MapZone zone)
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Right-click handler: 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.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private void RemoveVertexAtPointer(Vector2 local, bool fallbackToLast)
|
||||
{
|
||||
@@ -383,7 +361,13 @@ namespace Cthangover.Tools.MapEditor
|
||||
var index = FindNearestVertex(zone, local, rect);
|
||||
if (index < 0 && fallbackToLast)
|
||||
index = zone.Points.Count - 1;
|
||||
if (index < 0)
|
||||
if (index >= 0)
|
||||
RemoveVertexAt(index);
|
||||
}
|
||||
|
||||
private void RemoveVertexAt(int index)
|
||||
{
|
||||
if (Controller == null)
|
||||
return;
|
||||
|
||||
Controller.RemoveVertex(index);
|
||||
@@ -395,7 +379,7 @@ namespace Cthangover.Tools.MapEditor
|
||||
private static int FindNearestVertex(MapZone zone, Vector2 local, Rect2 rect)
|
||||
{
|
||||
var best = -1;
|
||||
var bestDistance = 24f;
|
||||
var bestDistance = PickRadius;
|
||||
|
||||
for (var i = 0; i < zone.Points.Count; i++)
|
||||
{
|
||||
@@ -409,20 +393,5 @@ namespace Cthangover.Tools.MapEditor
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a vertex from its handle's right-click. Marker and label
|
||||
/// handles (negative indexes) are not vertices and are ignored.
|
||||
/// </summary>
|
||||
private void RemoveVertexHandle(int index)
|
||||
{
|
||||
if (index < 0 || Controller == null)
|
||||
return;
|
||||
|
||||
Controller.RemoveVertex(index);
|
||||
RebuildHandles();
|
||||
Changed?.Invoke();
|
||||
QueueRedraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Cthangover.Mods.Map;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
@@ -70,6 +70,22 @@ namespace Cthangover.Tools.MapEditor
|
||||
return Append(zone);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a region without vertices and selects it — the start of
|
||||
/// drawing a polygon point by point on the canvas.
|
||||
/// </summary>
|
||||
public MapZone StartRegion()
|
||||
{
|
||||
var zone = new MapZone
|
||||
{
|
||||
ID = MakeUniqueId("region"),
|
||||
Title = string.Empty,
|
||||
Points = new List<Vector2>(),
|
||||
};
|
||||
|
||||
return Append(zone);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an icon marker at the map centre with no icon chosen yet and
|
||||
/// selects it. The author assigns <c>image</c> and <c>scene</c> in the
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.MapEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Draggable 16x16 handle on the map editor canvas. One handle represents
|
||||
/// either a polygon vertex, the marker position or the label position,
|
||||
/// distinguished by <see cref="Index"/> with negative values reserved for
|
||||
/// the position handles. Subscribes to the GuiInput C# event rather than
|
||||
/// overriding <c>_GuiInput</c> for reliable event delivery in mod
|
||||
/// assemblies, following <c>InteractiveEditorHandle</c>.
|
||||
/// </summary>
|
||||
public partial class MapEditorHandle : ColorRect
|
||||
{
|
||||
/// <summary>Handle role: 0..n — vertex index, <see cref="MarkerHandle"/> or <see cref="LabelHandle"/>.</summary>
|
||||
public const int MarkerHandle = -2;
|
||||
|
||||
/// <summary>Handle role used for the label position handle.</summary>
|
||||
public const int LabelHandle = -3;
|
||||
|
||||
/// <summary>Vertex index or one of the negative role constants.</summary>
|
||||
public int Index;
|
||||
|
||||
/// <summary>Invoked during drag with the handle centre in global coordinates.</summary>
|
||||
public System.Action<Vector2> DragUpdate;
|
||||
|
||||
/// <summary>Invoked when the handle is clicked (mouse down).</summary>
|
||||
public System.Action Clicked;
|
||||
|
||||
/// <summary>Invoked when the drag ends (mouse up).</summary>
|
||||
public System.Action DragEnd;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked on a right-click over the handle. Handles stop mouse input,
|
||||
/// so without this hook a right-click aimed exactly at a vertex would
|
||||
/// never reach the canvas and the vertex could not be deleted there.
|
||||
/// </summary>
|
||||
public System.Action RightClicked;
|
||||
|
||||
private bool _dragging;
|
||||
private Vector2 _dragOffset;
|
||||
|
||||
/// <summary>Creates a handle with the given colour and role index.</summary>
|
||||
public MapEditorHandle(Color color, int index)
|
||||
{
|
||||
Index = index;
|
||||
Size = new Vector2(16, 16);
|
||||
Color = color;
|
||||
MouseFilter = MouseFilterEnum.Stop;
|
||||
MouseDefaultCursorShape = CursorShape.PointingHand;
|
||||
GuiInput += OnGuiInput;
|
||||
}
|
||||
|
||||
private void OnGuiInput(InputEvent @event)
|
||||
{
|
||||
if (@event is InputEventMouseButton mb && mb.ButtonIndex == MouseButton.Left)
|
||||
{
|
||||
if (mb.Pressed)
|
||||
{
|
||||
_dragging = true;
|
||||
_dragOffset = GetGlobalMousePosition() - GlobalPosition;
|
||||
Clicked?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
_dragging = false;
|
||||
DragEnd?.Invoke();
|
||||
}
|
||||
AcceptEvent();
|
||||
}
|
||||
else if (@event is InputEventMouseButton right && right.ButtonIndex == MouseButton.Right && right.Pressed)
|
||||
{
|
||||
RightClicked?.Invoke();
|
||||
AcceptEvent();
|
||||
}
|
||||
else if (@event is InputEventMouseMotion && _dragging)
|
||||
{
|
||||
var center = GlobalPosition + Size / 2;
|
||||
GlobalPosition = GetGlobalMousePosition() - _dragOffset;
|
||||
DragUpdate?.Invoke(center);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
namespace Cthangover.Tools.MapEditor
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
+445
-420
File diff suppressed because it is too large.
Load diff
@@ -2,36 +2,34 @@ 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
|
||||
{
|
||||
public sealed class TabData
|
||||
{
|
||||
public string Key;
|
||||
public string ModId;
|
||||
public string RelativePath;
|
||||
public string FileName;
|
||||
public string SavedText;
|
||||
public bool IsDirty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="CodeEdit"/> exists; switching tabs swaps its text with the
|
||||
/// tab's <see cref="TabData.Buffer"/>, which therefore holds unsaved edits
|
||||
/// of background tabs.
|
||||
/// </summary>
|
||||
public partial class ScenarioEditTabs : VBoxContainer
|
||||
{
|
||||
private const int GraphTabIndex = 0;
|
||||
|
||||
private TabBar _tabBar;
|
||||
private TextEdit _textEdit;
|
||||
private CodeEdit _textEdit;
|
||||
private ScenarioGraphView _graphView;
|
||||
private Font _monospaceFont;
|
||||
|
||||
private readonly List<TabData> _tabs = new();
|
||||
private int _activeTabIndex = -1;
|
||||
private bool _suppressTextChanged;
|
||||
|
||||
public TextEdit CodeEditor => _textEdit;
|
||||
public CodeEdit CodeEditor => _textEdit;
|
||||
public string CurrentText => _textEdit.Text;
|
||||
public string CurrentKey =>
|
||||
_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count ? _tabs[_activeTabIndex].Key : null;
|
||||
@@ -48,10 +46,12 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
public event Action<bool> DirtyChanged;
|
||||
public event Action<string> GraphFileRequested;
|
||||
|
||||
public ScenarioEditTabs(ScenarioGraphView graphView, Font monospaceFont)
|
||||
/// <summary>Raised on every user edit of the active text tab.</summary>
|
||||
public event Action BufferChanged;
|
||||
|
||||
public ScenarioEditTabs(ScenarioGraphView graphView)
|
||||
{
|
||||
_graphView = graphView;
|
||||
_monospaceFont = monospaceFont;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||
@@ -63,15 +63,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_tabBar.AddTab(TranslationServer.Translate("tools/scenario_editor/tab_graph"));
|
||||
AddChild(_tabBar);
|
||||
|
||||
_textEdit = new TextEdit
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
Editable = true,
|
||||
SyntaxHighlighter = ScenarioSyntaxService.CreateHighlighter(),
|
||||
WrapMode = TextEdit.LineWrappingMode.None
|
||||
};
|
||||
_textEdit.AddThemeFontOverride("font", _monospaceFont);
|
||||
_textEdit = ToolUi.Code();
|
||||
_textEdit.SyntaxHighlighter = ScenarioSyntaxService.CreateHighlighter();
|
||||
_textEdit.TextChanged += OnTextChanged;
|
||||
AddChild(_textEdit);
|
||||
|
||||
@@ -79,6 +72,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
AddChild(_graphView);
|
||||
}
|
||||
|
||||
/// <summary>Opens the scenario in a new tab, or just activates its existing tab.</summary>
|
||||
public void OpenFile(string key)
|
||||
{
|
||||
for (int i = 0; i < _tabs.Count; i++)
|
||||
@@ -108,12 +102,13 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
ModId = modId,
|
||||
RelativePath = relativePath,
|
||||
FileName = Path.GetFileNameWithoutExtension(relativePath),
|
||||
SavedText = text,
|
||||
Buffer = text,
|
||||
IsDirty = false
|
||||
};
|
||||
|
||||
_tabs.Add(tab);
|
||||
_tabBar.AddTab(tab.FileName);
|
||||
_tabBar.SetTabTooltip(_tabBar.TabCount - 1, ScenarioFileService.GetDisplayPath(modId, relativePath));
|
||||
|
||||
SwitchToTextTab(_tabs.Count - 1);
|
||||
}
|
||||
@@ -123,13 +118,14 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Marks the active tab clean, remembering the editor text as the saved baseline.</summary>
|
||||
public void SaveCurrentFile()
|
||||
{
|
||||
if (_activeTabIndex < 0 || _activeTabIndex >= _tabs.Count)
|
||||
return;
|
||||
|
||||
var tab = _tabs[_activeTabIndex];
|
||||
tab.SavedText = _textEdit.Text;
|
||||
tab.Buffer = _textEdit.Text;
|
||||
if (tab.IsDirty)
|
||||
{
|
||||
tab.IsDirty = false;
|
||||
@@ -138,6 +134,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Replaces the editor text without firing the text-changed handler.</summary>
|
||||
public void SetTextSilent(string text)
|
||||
{
|
||||
_suppressTextChanged = true;
|
||||
@@ -145,6 +142,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_suppressTextChanged = false;
|
||||
}
|
||||
|
||||
/// <summary>Switches to the graph tab when no file tabs are open.</summary>
|
||||
public void SetViewToGraph()
|
||||
{
|
||||
if (_tabs.Count > 0)
|
||||
@@ -156,6 +154,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
GraphTabSelected?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Rebuilds the graph from the current scenario files, if the graph tab is visible.</summary>
|
||||
public void RefreshGraph()
|
||||
{
|
||||
if (_graphView.Visible)
|
||||
@@ -165,6 +164,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns false when any tab still has unsaved changes.</summary>
|
||||
public bool ConfirmCloseAll()
|
||||
{
|
||||
for (int i = _tabs.Count - 1; i >= 0; i--)
|
||||
@@ -206,16 +206,16 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
return;
|
||||
|
||||
if (_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count)
|
||||
_tabs[_activeTabIndex].SavedText = _textEdit.Text;
|
||||
_tabs[_activeTabIndex].Buffer = _textEdit.Text;
|
||||
|
||||
_activeTabIndex = index;
|
||||
_tabBar.CurrentTab = index + 1;
|
||||
|
||||
var tab = _tabs[index];
|
||||
|
||||
SetTextSilent(tab.SavedText);
|
||||
SetTextSilent(tab.Buffer);
|
||||
|
||||
TextTabSelected?.Invoke(tab.SavedText);
|
||||
TextTabSelected?.Invoke(tab.Buffer);
|
||||
}
|
||||
|
||||
private void CloseTextTab(int index)
|
||||
@@ -284,6 +284,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
UpdateTabTitle(_activeTabIndex);
|
||||
UpdateGlobalDirty();
|
||||
}
|
||||
BufferChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void UpdateTabTitle(int index)
|
||||
@@ -303,7 +304,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
private void SaveActiveTextEditContent()
|
||||
{
|
||||
if (_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count)
|
||||
_tabs[_activeTabIndex].SavedText = _textEdit.Text;
|
||||
_tabs[_activeTabIndex].Buffer = _textEdit.Text;
|
||||
}
|
||||
|
||||
private void OnGraphScenarioFileRequested(string filePath)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
/// <summary>Registers the scenario editor as a tool window. Creates <see cref="ScenarioEditorWindow"/> instances.</summary>
|
||||
|
||||
@@ -1,46 +1,81 @@
|
||||
using System;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para/>
|
||||
/// Shortcuts: Ctrl+S saves the active file, F5 play-tests it.
|
||||
/// </summary>
|
||||
public partial class ScenarioEditorWindow : ToolWindow
|
||||
{
|
||||
private const double InfoRefreshDelay = 0.4;
|
||||
|
||||
private Control _editLayout;
|
||||
private Control _playLayout;
|
||||
|
||||
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 Label _statusLabel;
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
/// <summary>Opens with the graph view so the editor starts on the cross-scene map, or on the file asked for by <see cref="OpenScenario"/>.</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
_editTabs.SetViewToGraph();
|
||||
if (_pendingKey != null)
|
||||
OpenScenario(_pendingKey);
|
||||
}
|
||||
|
||||
protected override void Cleanup()
|
||||
/// <summary>
|
||||
/// Opens a scenario file (key <c>mod:path</c>) 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 <see cref="_Ready"/>, which would otherwise
|
||||
/// switch the view back to the graph.
|
||||
/// </summary>
|
||||
public void OpenScenario(string key)
|
||||
{
|
||||
if (!IsInsideTree())
|
||||
{
|
||||
_pendingKey = key;
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingKey = null;
|
||||
_fileTreePanel.SelectFile(key);
|
||||
_editTabs.OpenFile(key);
|
||||
}
|
||||
|
||||
/// <summary>Returns from play-test mode as soon as the in-game test play ends.</summary>
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (_playActive && !SceneManager.IsTestPlayActive)
|
||||
@@ -52,8 +87,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_editLayout = new Control
|
||||
{
|
||||
AnchorRight = 1f,
|
||||
AnchorBottom = 1f,
|
||||
Visible = true
|
||||
AnchorBottom = 1f
|
||||
};
|
||||
AddChild(_editLayout);
|
||||
|
||||
@@ -63,111 +97,70 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
var toolbar = CreateToolbar();
|
||||
outerVBox.AddChild(toolbar);
|
||||
|
||||
_saveBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/save"),
|
||||
Disabled = true
|
||||
};
|
||||
_saveBtn.Pressed += OnSavePressed;
|
||||
_saveBtn = ToolUi.Button("tools/common/save", OnSavePressed, ButtonKind.Primary, "tools/common/save_tip");
|
||||
toolbar.AddChild(_saveBtn);
|
||||
|
||||
_openFileBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/open_file"),
|
||||
Disabled = true
|
||||
};
|
||||
_openFileBtn.Pressed += OnOpenFilePressed;
|
||||
toolbar.AddChild(_openFileBtn);
|
||||
|
||||
_playBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/play"),
|
||||
TooltipText = TranslationServer.Translate("tools/scenario_editor/play_tooltip"),
|
||||
Disabled = true
|
||||
};
|
||||
_playBtn.Pressed += OnPlayPressed;
|
||||
_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 { Text = "" };
|
||||
_filePathLabel.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
_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();
|
||||
mainSplit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
mainSplit.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
outerVBox.AddChild(mainSplit);
|
||||
|
||||
var leftPanel = new PanelContainer
|
||||
var mainSplit = new HSplitContainer
|
||||
{
|
||||
CustomMinimumSize = new Vector2(340, 0),
|
||||
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 leftVBox = new VBoxContainer();
|
||||
leftPanel.AddChild(leftVBox);
|
||||
|
||||
var tabsContainer = new TabContainer();
|
||||
tabsContainer.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
tabsContainer.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
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, TranslationServer.Translate("tools/scenario_editor/tab_files"));
|
||||
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, TranslationServer.Translate("tools/scenario_editor/tab_info"));
|
||||
|
||||
leftVBox.AddChild(new Label { Text = TranslationServer.Translate("tools/scenario_editor/bg_previews") + ":" });
|
||||
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);
|
||||
|
||||
var rightPanel = new PanelContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
mainSplit.AddChild(rightPanel);
|
||||
|
||||
_editTabs = new ScenarioEditTabs(new ScenarioGraphView(), GetMonospaceFont());
|
||||
_editTabs = new ScenarioEditTabs(new ScenarioGraphView());
|
||||
_editTabs.TextTabSelected += OnTextTabSelected;
|
||||
_editTabs.GraphTabSelected += OnGraphTabSelected;
|
||||
_editTabs.DirtyChanged += OnDirtyChanged;
|
||||
_editTabs.GraphFileRequested += OnGraphFileRequested;
|
||||
rightPanel.AddChild(_editTabs);
|
||||
_editTabs.BufferChanged += () => _infoTimer.Start();
|
||||
mainSplit.AddChild(_editTabs);
|
||||
|
||||
var statusBar = new HBoxContainer();
|
||||
outerVBox.AddChild(statusBar);
|
||||
|
||||
_statusLabel = new Label { Text = "" };
|
||||
statusBar.AddChild(_statusLabel);
|
||||
|
||||
_playLayout = new Control
|
||||
{
|
||||
AnchorRight = 1f,
|
||||
AnchorBottom = 1f,
|
||||
Visible = false
|
||||
};
|
||||
AddChild(_playLayout);
|
||||
|
||||
var stopContainer = new HBoxContainer();
|
||||
stopContainer.Alignment = BoxContainer.AlignmentMode.Center;
|
||||
stopContainer.AnchorRight = 0.5f;
|
||||
stopContainer.AnchorLeft = 0.5f;
|
||||
stopContainer.OffsetTop = 8;
|
||||
_playLayout.AddChild(stopContainer);
|
||||
|
||||
var stopBtn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/stop"),
|
||||
CustomMinimumSize = new Vector2(120, 40)
|
||||
};
|
||||
stopBtn.Pressed += OnStopPressed;
|
||||
stopContainer.AddChild(stopBtn);
|
||||
_infoTimer = new Timer { OneShot = true, WaitTime = InfoRefreshDelay };
|
||||
_infoTimer.Timeout += () => _infoPanel.Update(_editTabs.CurrentText);
|
||||
AddChild(_infoTimer);
|
||||
}
|
||||
|
||||
private void OnFileSelected(string filePath)
|
||||
@@ -180,15 +173,19 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_fileTreePanel.SelectFile(filePath);
|
||||
}
|
||||
|
||||
private void OnRescanPressed()
|
||||
{
|
||||
_fileTreePanel.Reload();
|
||||
_editTabs.RefreshGraph();
|
||||
SetStatus(ToolUi.Tr("tools/scenario_editor/rescanned"));
|
||||
}
|
||||
|
||||
private void OnTextTabSelected(string text)
|
||||
{
|
||||
_filePathLabel.Text = _editTabs.CurrentKey != null
|
||||
var hasFile = _editTabs.CurrentKey != null;
|
||||
_filePathLabel.Text = hasFile
|
||||
? ScenarioFileService.GetDisplayPath(_editTabs.CurrentModId, _editTabs.CurrentRelativePath) : "";
|
||||
_saveBtn.Disabled = false;
|
||||
_openFileBtn.Disabled = false;
|
||||
_playBtn.Disabled = false;
|
||||
_statusLabel.Text = _editTabs.IsCurrentDirty
|
||||
? TranslationServer.Translate("tools/scenario_editor/status_modified") : "";
|
||||
SetFileButtonsEnabled(hasFile);
|
||||
_infoPanel.Update(text);
|
||||
_thumbPanel.Load(text);
|
||||
}
|
||||
@@ -196,26 +193,38 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
private void OnGraphTabSelected()
|
||||
{
|
||||
_filePathLabel.Text = "";
|
||||
_saveBtn.Disabled = true;
|
||||
_openFileBtn.Disabled = true;
|
||||
_playBtn.Disabled = true;
|
||||
_statusLabel.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();
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/status_modified");
|
||||
}
|
||||
else
|
||||
{
|
||||
MarkClean();
|
||||
}
|
||||
|
||||
/// <summary>Ctrl+S.</summary>
|
||||
protected override void OnSaveShortcut() => OnSavePressed();
|
||||
|
||||
/// <summary>F5 play-tests the active scenario.</summary>
|
||||
protected override bool HandleKey(InputEventKey key)
|
||||
{
|
||||
if (key.Keycode != Key.F5 || !_editTabs.HasActiveFile)
|
||||
return false;
|
||||
|
||||
OnPlayPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSavePressed()
|
||||
@@ -230,22 +239,23 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
if (!ScenarioFileService.WriteAllText(modId, relativePath, text))
|
||||
{
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/readonly_zip");
|
||||
SetStatus(ToolUi.Tr("tools/scenario_editor/readonly_zip"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
_editTabs.SaveCurrentFile();
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/status_saved");
|
||||
// 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);
|
||||
_editTabs.RefreshGraph();
|
||||
GameLogger.Log("SCENARIO_EDITOR", $"Saved: {modId}/{relativePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GameLogger.Log("SCENARIO_EDITOR", $"Save failed: {ex.Message}", LogLevel.Error);
|
||||
_statusLabel.Text = string.Format(
|
||||
TranslationServer.Translate("tools/scenario_editor/save_error"), ex.Message);
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/scenario_editor/save_error"), ex.Message), ToolStatusKind.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,14 +267,16 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
var path = ScenarioFileService.GetAbsolutePath(_editTabs.CurrentModId, _editTabs.CurrentRelativePath);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/readonly_zip");
|
||||
SetStatus(ToolUi.Tr("tools/scenario_editor/readonly_zip"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
GD.Print($"[ScenarioEditor] Opening: {path}");
|
||||
var err = OS.ShellOpen(path);
|
||||
if (err != Error.Ok)
|
||||
GameLogger.Log("SCENARIO_EDITOR", $"ShellOpen error: {err}", LogLevel.Error);
|
||||
{
|
||||
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()
|
||||
@@ -277,7 +289,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
if (metadata.Scene == "-")
|
||||
{
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/no_scene_error");
|
||||
SetStatus(ToolUi.Tr("tools/scenario_editor/no_scene_error"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -291,7 +303,6 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
}
|
||||
else
|
||||
{
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/readonly_zip");
|
||||
GameLogger.Log("SCENARIO_EDITOR",
|
||||
$"Test play: '{modId}/{relativePath}' is read-only, playing from the editor buffer",
|
||||
LogLevel.Warning);
|
||||
@@ -299,14 +310,10 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_statusLabel.Text = string.Format(
|
||||
TranslationServer.Translate("tools/scenario_editor/save_error"), ex.Message);
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/scenario_editor/save_error"), ex.Message), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
_editLayout.Visible = false;
|
||||
_playLayout.Visible = true;
|
||||
|
||||
SceneManager.IsTestPlayActive = true;
|
||||
SceneManager.TestScenarioText = text;
|
||||
|
||||
@@ -314,18 +321,32 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
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<GodotSceneService>("/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<SceneManager>("/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);
|
||||
|
||||
var sceneManager = GetNodeOrNull<SceneManager>("/root/SceneManager");
|
||||
if (sceneManager != null)
|
||||
{
|
||||
sceneManager.Initialize();
|
||||
sceneManager.PendingSceneName = metadata.Scene;
|
||||
}
|
||||
|
||||
var sceneService = GetNodeOrNull<GodotSceneService>("/root/GodotSceneService");
|
||||
sceneService?.LoadShell();
|
||||
|
||||
_playActive = true;
|
||||
@@ -340,8 +361,6 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
private void ReturnFromPlay()
|
||||
{
|
||||
_playActive = false;
|
||||
_playLayout.Visible = false;
|
||||
_editLayout.Visible = true;
|
||||
Visible = true;
|
||||
|
||||
if (_stopOverlay != null)
|
||||
@@ -351,23 +370,38 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
}
|
||||
|
||||
var sceneService = GetNodeOrNull<GodotSceneService>("/root/GodotSceneService");
|
||||
var sceneManager = GetNodeOrNull<SceneManager>("/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"));
|
||||
}
|
||||
|
||||
/// <summary>Stop button shown over the running game while the scenario is play-tested.</summary>
|
||||
private CanvasLayer CreateStopOverlay()
|
||||
{
|
||||
var layer = new CanvasLayer { Layer = 128 };
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/stop"),
|
||||
CustomMinimumSize = new Vector2(120, 40)
|
||||
};
|
||||
btn.Pressed += OnStopPressed;
|
||||
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", 8);
|
||||
margin.AddThemeConstantOverride("margin_left", 8);
|
||||
margin.AddThemeConstantOverride("margin_top", UiTokens.Space3);
|
||||
margin.AddThemeConstantOverride("margin_left", UiTokens.Space3);
|
||||
margin.AddChild(btn);
|
||||
|
||||
layer.AddChild(margin);
|
||||
|
||||
@@ -6,80 +6,138 @@ using Godot;
|
||||
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="FileSelected"/> with the
|
||||
/// scenario key on picks.
|
||||
/// <para/>
|
||||
/// Built from <c>scenario_file_tree_panel.tscn</c>: the tscn builder creates
|
||||
/// the script node through its constructor before the child nodes exist,
|
||||
/// so children are looked up lazily (<see cref="Bind"/>) — not in the
|
||||
/// constructor, which is what used to crash the whole editor on open.
|
||||
/// </summary>
|
||||
public partial class ScenarioFileTreePanel : VBoxContainer
|
||||
{
|
||||
private Tree _fileTree;
|
||||
private readonly Dictionary<string, string> _fileItemToKey = new();
|
||||
private readonly Dictionary<string, TreeItem> _keyToTreeItem = new();
|
||||
private LineEdit _search;
|
||||
private List<ModScenarioTree> _mods = new();
|
||||
private readonly Dictionary<TreeItem, string> _itemToKey = new();
|
||||
private readonly Dictionary<string, TreeItem> _keyToItem = new();
|
||||
private bool _isProgrammaticSelection;
|
||||
private string _selectedKey;
|
||||
|
||||
public event Action<string> FileSelected;
|
||||
|
||||
public ScenarioFileTreePanel()
|
||||
/// <summary>Binds child nodes and fills the tree once the panel enters the tree.</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
Reload();
|
||||
}
|
||||
|
||||
/// <summary>Re-scans the mods' scenario files and rebuilds the tree.</summary>
|
||||
public void Reload()
|
||||
{
|
||||
Bind();
|
||||
_mods = ScenarioFileService.ScanScenarioFiles();
|
||||
Populate();
|
||||
}
|
||||
|
||||
private void Bind()
|
||||
{
|
||||
if (_fileTree != null)
|
||||
return;
|
||||
|
||||
_fileTree = GetNode<Tree>("FileTree");
|
||||
_fileTree.CellSelected += OnCellSelected;
|
||||
|
||||
Populate();
|
||||
_search = GetNode<LineEdit>("Search");
|
||||
_search.PlaceholderText = TranslationServer.Translate("tools/scenario_editor/search_files");
|
||||
_search.TextChanged += _ => Populate();
|
||||
}
|
||||
|
||||
private void Populate()
|
||||
{
|
||||
_fileTree.Clear();
|
||||
_fileItemToKey.Clear();
|
||||
_keyToTreeItem.Clear();
|
||||
_itemToKey.Clear();
|
||||
_keyToItem.Clear();
|
||||
|
||||
var filter = _search.Text.Trim();
|
||||
var root = _fileTree.CreateItem();
|
||||
var mods = ScenarioFileService.ScanScenarioFiles();
|
||||
|
||||
foreach (var mod in mods)
|
||||
foreach (var mod in _mods)
|
||||
{
|
||||
var modItem = _fileTree.CreateItem(root);
|
||||
modItem.SetText(0, $"[{mod.ModId}]");
|
||||
modItem.SetSelectable(0, false);
|
||||
modItem.SetCustomColor(0, UiTokens.Info);
|
||||
|
||||
TreeItem modItem = null;
|
||||
foreach (var group in mod.Groups)
|
||||
{
|
||||
TreeItem parentItem;
|
||||
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))
|
||||
{
|
||||
parentItem = modItem;
|
||||
groupItem = modItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = _fileTree.CreateItem(modItem);
|
||||
parentItem.SetText(0, group.DirectoryPath + "/");
|
||||
parentItem.SetSelectable(0, false);
|
||||
groupItem = _fileTree.CreateItem(modItem);
|
||||
groupItem.SetText(0, group.DirectoryPath + "/");
|
||||
groupItem.SetSelectable(0, false);
|
||||
groupItem.SetCustomColor(0, UiTokens.Muted);
|
||||
groupItem.Collapsed = filter.Length == 0;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var file in group.Files)
|
||||
{
|
||||
var fileItem = _fileTree.CreateItem(parentItem);
|
||||
var fileItem = _fileTree.CreateItem(groupItem);
|
||||
fileItem.SetText(0, file.Name);
|
||||
fileItem.SetMetadata(0, Variant.From("scenario"));
|
||||
_fileItemToKey[GetItemKey(fileItem)] = file.Key;
|
||||
_keyToTreeItem[file.Key] = fileItem;
|
||||
}
|
||||
fileItem.SetTooltipText(0, ScenarioFileService.GetDisplayPath(file.ModId, file.RelativePath));
|
||||
_itemToKey[fileItem] = file.Key;
|
||||
_keyToItem[file.Key] = fileItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetItemKey(TreeItem item)
|
||||
{
|
||||
return item.GetInstanceId().ToString();
|
||||
if (_selectedKey != null && _keyToItem.TryGetValue(_selectedKey, out var selected))
|
||||
SelectSilently(selected);
|
||||
}
|
||||
|
||||
/// <summary>Selects the tree row of a scenario key and raises <see cref="FileSelected"/>.</summary>
|
||||
public void SelectFile(string key)
|
||||
{
|
||||
if (!_keyToTreeItem.TryGetValue(key, out var item))
|
||||
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;
|
||||
|
||||
FileSelected?.Invoke(key);
|
||||
}
|
||||
|
||||
private void OnCellSelected()
|
||||
@@ -88,18 +146,11 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
return;
|
||||
|
||||
var item = _fileTree.GetSelected();
|
||||
if (item == null)
|
||||
if (item == null || !_itemToKey.TryGetValue(item, out var key))
|
||||
return;
|
||||
|
||||
var meta = item.GetMetadata(0);
|
||||
if (meta.VariantType != Variant.Type.String || meta.AsString() != "scenario")
|
||||
return;
|
||||
|
||||
var itemKey = GetItemKey(item);
|
||||
if (!_fileItemToKey.TryGetValue(itemKey, out var fileKey))
|
||||
return;
|
||||
|
||||
FileSelected?.Invoke(fileKey);
|
||||
_selectedKey = key;
|
||||
FileSelected?.Invoke(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Layered left-to-right layout of the scene graph: a scene's column is
|
||||
/// its distance (in scene switches) from the start scenes — the
|
||||
/// <c>start_scene</c> 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.
|
||||
/// </summary>
|
||||
public static class ScenarioGraphLayout
|
||||
{
|
||||
private const float ColumnWidth = 300f;
|
||||
private const float RowHeight = 130f;
|
||||
private const float Margin = 40f;
|
||||
|
||||
/// <summary>Canvas position per lower-cased scene name.</summary>
|
||||
public static Dictionary<string, Vector2> Compute(SceneGraphData data)
|
||||
{
|
||||
var names = data.Nodes.Select(n => n.SceneName.ToLowerInvariant()).Distinct().ToList();
|
||||
var outgoing = names.ToDictionary(n => n, _ => new List<string>());
|
||||
var incoming = new HashSet<string>();
|
||||
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<string, int>();
|
||||
var queue = new Queue<string>();
|
||||
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<string, Vector2>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
/// <summary>Cross-scene graph of all scenario files: a GraphEdit of scene nodes linked by switch targets, with a detail panel for the selected scenario.</summary>
|
||||
public partial class ScenarioGraphView : HSplitContainer
|
||||
{
|
||||
private GraphEdit _graph;
|
||||
@@ -39,27 +41,27 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_graph.NodeDeselected += OnNodeDeselected;
|
||||
AddChild(_graph);
|
||||
|
||||
var panel = new VBoxContainer { CustomMinimumSize = new Vector2(250, 0) };
|
||||
var panel = new VBoxContainer { CustomMinimumSize = new Vector2(260, 0) };
|
||||
panel.SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||
AddChild(panel);
|
||||
|
||||
panel.AddChild(new Label
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/graph_select_node"),
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart
|
||||
});
|
||||
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,
|
||||
FitContent = true,
|
||||
ScrollFollowing = 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);
|
||||
}
|
||||
|
||||
/// <summary>Rebuilds the graph nodes and edges, preserving the nodes' saved canvas positions.</summary>
|
||||
public void Populate(SceneGraphData data)
|
||||
{
|
||||
var savedPositions = SaveNodePositions();
|
||||
@@ -68,13 +70,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_nodes = new Dictionary<string, GraphNode>();
|
||||
_lookup = new Dictionary<string, SceneGraphNode>();
|
||||
|
||||
int x = 40, y = 40;
|
||||
foreach (var pos in savedPositions.Values)
|
||||
{
|
||||
int bottom = (int)pos.Y + 160 + 20;
|
||||
if (bottom > y)
|
||||
y = bottom;
|
||||
}
|
||||
var layout = ScenarioGraphLayout.Compute(data);
|
||||
|
||||
foreach (var nodeData in data.Nodes)
|
||||
{
|
||||
@@ -87,7 +83,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
var position = savedPositions.TryGetValue(key, out var saved)
|
||||
? saved
|
||||
: new Vector2(x, y);
|
||||
: layout[key];
|
||||
|
||||
var gn = new GraphNode
|
||||
{
|
||||
@@ -96,12 +92,15 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
PositionOffset = position
|
||||
};
|
||||
|
||||
gn.AddChild(new Label
|
||||
var nodeLabel = new Label
|
||||
{
|
||||
Text = $"Mod: {nodeData.ModId}\nScenarios: {nodeData.ScenarioCount}" +
|
||||
(bgCount > 0 ? $"\nBg: {firstBg}" +
|
||||
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)
|
||||
@@ -120,11 +119,6 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_nodes[key] = gn;
|
||||
_lookup[key] = nodeData;
|
||||
|
||||
if (!savedPositions.ContainsKey(key))
|
||||
{
|
||||
x += 280;
|
||||
if (x > 1000) { x = 40; y += 160; }
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var edge in data.Edges)
|
||||
@@ -135,6 +129,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
continue;
|
||||
_graph.ConnectNode(_nodes[srcK].Name, 0, _nodes[dstK].Name, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Dictionary<string, Vector2> SaveNodePositions()
|
||||
@@ -178,12 +173,12 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
return;
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"Scene: [b]{nodeData.SceneName}[/b]");
|
||||
sb.AppendLine($"Mod: {nodeData.ModId}");
|
||||
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($"Backgrounds: {string.Join(", ", nodeData.Backgrounds)}");
|
||||
sb.AppendLine(string.Format(Tr("tools/scenario_editor/graph_node_bg"), string.Join(", ", nodeData.Backgrounds)));
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"Scenarios ({nodeData.Scenarios.Count}):");
|
||||
sb.AppendLine(string.Format(Tr("tools/scenario_editor/graph_detail_scenarios"), nodeData.Scenarios.Count));
|
||||
|
||||
foreach (var sc in nodeData.Scenarios.OrderBy(s => s.Priority))
|
||||
{
|
||||
@@ -199,6 +194,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_infoLabel.Text = "";
|
||||
}
|
||||
|
||||
private static string Tr(string key) => TranslationServer.Translate(key);
|
||||
|
||||
private void OnLinkClicked(Variant meta)
|
||||
{
|
||||
var path = Uri.UnescapeDataString(meta.AsString());
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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
|
||||
{
|
||||
/// <summary>Read-only metadata panel for the scenario in the active text tab: scene, priority, condition, switch targets, locale keys and quest references.</summary>
|
||||
public partial class ScenarioInfoPanel : VBoxContainer
|
||||
{
|
||||
private Label _infoScene;
|
||||
@@ -13,25 +16,59 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
private Label _infoLocaleKeys;
|
||||
private Label _infoQuestRefs;
|
||||
|
||||
public ScenarioInfoPanel()
|
||||
/// <summary>Binds the rows once the panel enters the tree (see <see cref="Bind"/>).</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
GetNode<Label>("Scroll/VBox/HintScene").Text = TranslationServer.Translate("tools/scenario_editor/info_scene") + ":";
|
||||
GetNode<Label>("Scroll/VBox/HintPriority").Text = TranslationServer.Translate("tools/scenario_editor/info_priority") + ":";
|
||||
GetNode<Label>("Scroll/VBox/HintCondition").Text = TranslationServer.Translate("tools/scenario_editor/info_condition") + ":";
|
||||
GetNode<Label>("Scroll/VBox/HintSwitchTargets").Text = TranslationServer.Translate("tools/scenario_editor/info_switch_targets") + ":";
|
||||
GetNode<Label>("Scroll/VBox/HintLocaleKeys").Text = TranslationServer.Translate("tools/scenario_editor/info_locale_keys") + ":";
|
||||
GetNode<Label>("Scroll/VBox/HintQuestRefs").Text = TranslationServer.Translate("tools/scenario_editor/info_quest_refs") + ":";
|
||||
|
||||
_infoScene = GetNode<Label>("Scroll/VBox/InfoScene");
|
||||
_infoPriority = GetNode<Label>("Scroll/VBox/InfoPriority");
|
||||
_infoCondition = GetNode<Label>("Scroll/VBox/InfoCondition");
|
||||
_infoSwitchTargets = GetNode<Label>("Scroll/VBox/InfoSwitchTargets");
|
||||
_infoLocaleKeys = GetNode<Label>("Scroll/VBox/InfoLocaleKeys");
|
||||
_infoQuestRefs = GetNode<Label>("Scroll/VBox/InfoQuestRefs");
|
||||
Bind();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks the rows up lazily: the tscn builder runs the constructor before
|
||||
/// the child nodes exist, so lookups there would fail.
|
||||
/// </summary>
|
||||
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<Label>("Scroll/VBox/" + node);
|
||||
label.Text = TranslationServer.Translate(key).ToString().ToUpperInvariant();
|
||||
label.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
label.AddThemeFontSizeOverride("font_size", UiTokens.FontSmall);
|
||||
}
|
||||
|
||||
private Label Value(string node)
|
||||
{
|
||||
var label = GetNode<Label>("Scroll/VBox/" + node);
|
||||
label.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||
label.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
label.CustomMinimumSize = new Vector2(120, 0);
|
||||
label.AddThemeFontOverride("font", ToolWindow.GetMonospaceFont());
|
||||
label.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
|
||||
return label;
|
||||
}
|
||||
|
||||
/// <summary>Parses the given scenario text and refreshes every metadata row.</summary>
|
||||
public void Update(string text)
|
||||
{
|
||||
Bind();
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
Clear();
|
||||
@@ -49,8 +86,10 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
_infoQuestRefs.Text = refs.QuestRefs.Count > 0 ? string.Join(", ", refs.QuestRefs.Distinct()) : "-";
|
||||
}
|
||||
|
||||
/// <summary>Resets every metadata row to the "-" placeholder.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
Bind();
|
||||
_infoScene.Text = "-";
|
||||
_infoPriority.Text = "-";
|
||||
_infoCondition.Text = "-";
|
||||
|
||||
@@ -4,15 +4,18 @@ using Godot;
|
||||
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
/// <summary>Row of background thumbnails referenced by the scenario in the active text tab.</summary>
|
||||
public partial class ScenarioThumbnailPanel : ScrollContainer
|
||||
{
|
||||
private HBoxContainer _thumbContainer;
|
||||
|
||||
public ScenarioThumbnailPanel()
|
||||
{
|
||||
_thumbContainer = GetNode<HBoxContainer>("ThumbContainer");
|
||||
}
|
||||
/// <summary>
|
||||
/// Container lookup is lazy: the tscn builder runs the constructor
|
||||
/// before the child nodes exist.
|
||||
/// </summary>
|
||||
private HBoxContainer Thumbs => _thumbContainer ??= GetNode<HBoxContainer>("ThumbContainer");
|
||||
|
||||
/// <summary>Loads the background thumbnails referenced by the given scenario text (missing ones render as placeholders).</summary>
|
||||
public void Load(string text)
|
||||
{
|
||||
Clear();
|
||||
@@ -24,7 +27,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
if (refs.BackgroundRefs.Count == 0)
|
||||
{
|
||||
_thumbContainer.AddChild(new Label
|
||||
Thumbs.AddChild(new Label
|
||||
{
|
||||
Text = TranslationServer.Translate("tools/scenario_editor/no_backgrounds")
|
||||
});
|
||||
@@ -37,7 +40,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
if (tex != null)
|
||||
{
|
||||
_thumbContainer.AddChild(new TextureRect
|
||||
Thumbs.AddChild(new TextureRect
|
||||
{
|
||||
Texture = tex,
|
||||
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
|
||||
@@ -48,18 +51,19 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
}
|
||||
else
|
||||
{
|
||||
_thumbContainer.AddChild(new Label
|
||||
Thumbs.AddChild(new Label
|
||||
{
|
||||
Text = $"[? {id}]",
|
||||
TooltipText = $"Not found: {id}"
|
||||
TooltipText = string.Format(TranslationServer.Translate("tools/scenario_editor/bg_missing"), id)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes every thumbnail from the row.</summary>
|
||||
public new void Clear()
|
||||
{
|
||||
foreach (var c in _thumbContainer.GetChildren())
|
||||
foreach (var c in Thumbs.GetChildren())
|
||||
c.QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// State of one open scenario file tab: identity, the current text buffer
|
||||
/// (including unsaved edits while the tab is in the background) and the
|
||||
/// dirty flag.
|
||||
/// </summary>
|
||||
public sealed class TabData
|
||||
{
|
||||
public string Key;
|
||||
public string ModId;
|
||||
public string RelativePath;
|
||||
public string FileName;
|
||||
public string Buffer;
|
||||
public bool IsDirty;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
private string _key;
|
||||
|
||||
/// <summary>Binds the checkbox to the property key and seeds it with the current value.</summary>
|
||||
public void Setup(InspectableProperty property, bool value)
|
||||
{
|
||||
_key = property.Key;
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
private string _key;
|
||||
|
||||
/// <summary>Binds the picker to the property key and seeds it with the current color.</summary>
|
||||
public void Setup(InspectableProperty property, Color value)
|
||||
{
|
||||
_key = property.Key;
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
private InspectableProperty _property;
|
||||
|
||||
/// <summary>Populates the dropdown from the property's enum labels and selects the current value, if known.</summary>
|
||||
public void Setup(InspectableProperty property, long currentValue)
|
||||
{
|
||||
_property = property;
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
private string _key;
|
||||
private bool _integer;
|
||||
|
||||
/// <summary>Applies the property's min/max/step and integer mode, then seeds the initial (clamped) value.</summary>
|
||||
public void Setup(InspectableProperty property, double initialValue)
|
||||
{
|
||||
_key = property.Key;
|
||||
|
||||
@@ -4,6 +4,8 @@ using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
{
|
||||
/// <summary>
|
||||
@@ -49,24 +51,34 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
protected override void Construct()
|
||||
{
|
||||
// ModWidget is a plain Control: it neither sizes its child nor
|
||||
// reports the child's height. Anchor the column to full width and
|
||||
// mirror its height into the minimum size so the hosting
|
||||
// ScrollContainer can scroll (and wrapped hints get real width).
|
||||
var root = new VBoxContainer();
|
||||
root.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
root.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||
AddChild(root);
|
||||
root.SetAnchorsAndOffsetsPreset(LayoutPreset.TopWide);
|
||||
root.MinimumSizeChanged += () => CustomMinimumSize = new Vector2(0, root.GetCombinedMinimumSize().Y);
|
||||
|
||||
_hint = new Label();
|
||||
_hint.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||
_hint.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
_hint.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||
_hint.Visible = false;
|
||||
root.AddChild(_hint);
|
||||
|
||||
_header = new Label();
|
||||
_header.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||
_header.AddThemeFontSizeOverride("font_size", UiTokens.FontBodyLarge);
|
||||
_header.Visible = false;
|
||||
root.AddChild(_header);
|
||||
|
||||
_banner = new Label();
|
||||
_banner.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||
_banner.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
_banner.Modulate = UiTokens.Warning;
|
||||
_banner.AddThemeColorOverride("font_color", UiTokens.Warning);
|
||||
_banner.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
|
||||
_banner.Visible = false;
|
||||
root.AddChild(_banner);
|
||||
|
||||
@@ -79,7 +91,9 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
Text = TranslationServer.Translate("tools/scene_builder/inspector_preset"),
|
||||
TooltipText = "anchors_preset"
|
||||
};
|
||||
presetLabel.CustomMinimumSize = new Vector2(110f, 0f);
|
||||
presetLabel.CustomMinimumSize = new Vector2(120f, 0f);
|
||||
presetLabel.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
|
||||
presetLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
_presetRow.AddChild(presetLabel);
|
||||
|
||||
_presetDropdown = new OptionButton();
|
||||
@@ -112,7 +126,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
_hint.Visible = false;
|
||||
_header.Visible = true;
|
||||
_header.Text = node.Name + " (" + node.GetClass() + ")";
|
||||
_header.Text = node.Name + " · " + node.GetClass();
|
||||
|
||||
var insideContainer = node is Control && node.GetParent() is Container;
|
||||
_banner.Visible = insideContainer;
|
||||
@@ -125,8 +139,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
foreach (var entry in InspectorPropertyCatalog.Build(node, insideContainer))
|
||||
{
|
||||
var categoryLabel = new Label { Text = TranslationServer.Translate(entry.CategoryKey) };
|
||||
categoryLabel.Modulate = UiTokens.AccentBright;
|
||||
var categoryLabel = UiKit.SectionTitle(TranslationServer.Translate(entry.CategoryKey), UiTokens.FontSmall);
|
||||
_rows.AddChild(categoryLabel);
|
||||
|
||||
foreach (var property in entry.Properties)
|
||||
@@ -168,8 +181,10 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
var labelText = Humanize(property.Key);
|
||||
if (storedInFile)
|
||||
labelText += " *";
|
||||
var label = new Label { Text = labelText, TooltipText = BuildTooltip(property.Key, storedInFile) };
|
||||
label.CustomMinimumSize = new Vector2(110f, 0f);
|
||||
var label = new Label { Text = labelText, TooltipText = BuildTooltip(property.Key, storedInFile), MouseFilter = MouseFilterEnum.Pass, ClipText = true };
|
||||
label.CustomMinimumSize = new Vector2(120f, 0f);
|
||||
label.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
|
||||
label.AddThemeColorOverride("font_color", storedInFile ? UiTokens.Ink : UiTokens.Muted);
|
||||
label.SizeFlagsVertical = Control.SizeFlags.ShrinkCenter;
|
||||
row.AddChild(label);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
private string _key;
|
||||
|
||||
/// <summary>Binds the line edit to the property key and seeds it with the current text.</summary>
|
||||
public void Setup(InspectableProperty property, string value)
|
||||
{
|
||||
_key = property.Key;
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
private SpinBox _x;
|
||||
private SpinBox _y;
|
||||
|
||||
/// <summary>Builds the x/y spin pair bound to the property key and seeds both axes.</summary>
|
||||
public void Setup(InspectableProperty property, Vector2 initialValue)
|
||||
{
|
||||
_key = property.Key;
|
||||
|
||||
@@ -251,8 +251,9 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
/// <summary>
|
||||
/// Recursively walks the loaded scene tree and populates the given
|
||||
/// <see cref="Tree"/> control with nodes labelled <c>Name (Type)</c>.
|
||||
/// Skips the internal highlight rect.
|
||||
/// <see cref="Tree"/> control with nodes labelled <c>Name · Class</c>:
|
||||
/// the mod scene root in gold, editable mod nodes in ink, read-only
|
||||
/// base-scene nodes faint. Skips the internal highlight rect.
|
||||
/// </summary>
|
||||
public void BuildHierarchy(Tree tree)
|
||||
{
|
||||
@@ -271,9 +272,12 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
private void AddNodeToTree(Tree tree, TreeItem parent, Node node)
|
||||
{
|
||||
var item = tree.CreateItem(parent);
|
||||
var typeName = node.GetType().Name;
|
||||
item.SetText(0, $"{node.Name} ({typeName})");
|
||||
item.SetText(0, $"{node.Name} · {node.GetClass()}");
|
||||
item.SetSelectable(0, true);
|
||||
// Mod-scene nodes are the editable ones; base-scene nodes stay faint.
|
||||
item.SetCustomColor(0, IsEditableModNode(node) ? UiTokens.Ink : UiTokens.Faint);
|
||||
if (node == _modRoot)
|
||||
item.SetCustomColor(0, UiTokens.AccentBright);
|
||||
|
||||
_nodeToItem[node] = item;
|
||||
_itemToNode[item] = node;
|
||||
@@ -341,6 +345,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Hierarchy tree item for a live node; null when unmapped.</summary>
|
||||
public TreeItem GetItemForNode(Node node)
|
||||
{
|
||||
_nodeToItem.TryGetValue(node, out var item);
|
||||
@@ -392,6 +397,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
return string.Join("/", segments);
|
||||
}
|
||||
|
||||
/// <summary>Topmost loaded-scene node whose visual rect contains the viewport position; null when nothing matches.</summary>
|
||||
public Node FindNodeAtPosition(Vector2 position)
|
||||
{
|
||||
if (_loadedRoot == null)
|
||||
@@ -423,7 +429,8 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
/// <summary>Hides the node highlight overlay and resets its colour.</summary>
|
||||
public void HideHighlight()
|
||||
{
|
||||
if (_highlight != null)
|
||||
if (_highlight == null)
|
||||
return;
|
||||
_highlight.Visible = false;
|
||||
_highlight.Color = UiTokens.Alpha(UiTokens.Accent, 0.35f);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
{
|
||||
/// <summary>Registers the scene builder as a tool window. Creates <see cref="SceneBuilderWindow"/> instances.</summary>
|
||||
|
||||
@@ -5,6 +5,9 @@ using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
using Cthangover.Tools.Common;
|
||||
namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
{
|
||||
/// <summary>
|
||||
@@ -32,14 +35,15 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
private Timer _autosaveTimer;
|
||||
private Node _selectedNode;
|
||||
private TscnFileProps _fileProps = TscnFileProps.Build(null);
|
||||
private TextEdit _tscnEditor;
|
||||
private Button _reloadBtn;
|
||||
private Button _saveBtn;
|
||||
private Button _refreshBtn;
|
||||
private CodeEdit _tscnEditor;
|
||||
private OptionButton _newNodeTypeDropdown;
|
||||
private Button _addNodeBtn;
|
||||
private Button _deleteNodeBtn;
|
||||
private RichTextLabel _outputLabel;
|
||||
private int _shownBase;
|
||||
private int _shownMod;
|
||||
private int _shownScene;
|
||||
private bool _loadingText;
|
||||
|
||||
/// <summary>Logical game resolution (project.godot) the preview canvas is laid out at.</summary>
|
||||
private static readonly Vector2I GameResolution = new(1920, 1024);
|
||||
|
||||
/// <summary>Curated palette for node creation — common Control classes the runtime builder instantiates via ClassDB.</summary>
|
||||
private static readonly string[] CreatableTypes =
|
||||
@@ -63,6 +67,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
PopulateBaseList();
|
||||
PopulateModList();
|
||||
RebuildPreview();
|
||||
SetHint(ToolUi.Tr("tools/scene_builder/hint"));
|
||||
}
|
||||
|
||||
private void BuildUI()
|
||||
@@ -73,84 +78,121 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
var toolbar = CreateToolbar();
|
||||
outerVBox.AddChild(toolbar);
|
||||
|
||||
toolbar.AddChild(new Label { Text = TranslationServer.Translate("tools/scene_builder/base_scene") });
|
||||
_baseDropdown = Dropdown(toolbar, "tools/scene_builder/base_scene", OnBaseSelected);
|
||||
_modDropdown = Dropdown(toolbar, "tools/scene_builder/mod", OnModSelected);
|
||||
_sceneDropdown = Dropdown(toolbar, "tools/scene_builder/scene", OnSceneSelected);
|
||||
toolbar.AddChild(ToolUi.Divider());
|
||||
toolbar.AddChild(ToolUi.Button("tools/common/save", OnSavePressed, ButtonKind.Primary, "tools/scene_builder/save_tip"));
|
||||
toolbar.AddChild(ToolUi.Button("tools/scene_builder/reload", OnReloadPressed, ButtonKind.Secondary, "tools/scene_builder/reload_tip"));
|
||||
|
||||
_baseDropdown = new OptionButton();
|
||||
_baseDropdown.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
_baseDropdown.ItemSelected += OnBaseSelected;
|
||||
toolbar.AddChild(_baseDropdown);
|
||||
var mainSplit = new HSplitContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
outerVBox.AddChild(mainSplit);
|
||||
|
||||
toolbar.AddChild(new Label { Text = TranslationServer.Translate("tools/scene_builder/mod") });
|
||||
mainSplit.AddChild(BuildHierarchyPanel());
|
||||
|
||||
_modDropdown = new OptionButton();
|
||||
_modDropdown.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
_modDropdown.ItemSelected += OnModSelected;
|
||||
toolbar.AddChild(_modDropdown);
|
||||
var centerSplit = new HSplitContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
mainSplit.AddChild(centerSplit);
|
||||
centerSplit.AddChild(BuildPreview());
|
||||
centerSplit.AddChild(BuildSidePanel());
|
||||
|
||||
toolbar.AddChild(new Label { Text = TranslationServer.Translate("tools/scene_builder/scene") });
|
||||
_autosaveTimer = new Timer { OneShot = true, WaitTime = 0.4 };
|
||||
_autosaveTimer.Timeout += OnAutosaveTimeout;
|
||||
AddChild(_autosaveTimer);
|
||||
}
|
||||
|
||||
_sceneDropdown = new OptionButton();
|
||||
_sceneDropdown.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
_sceneDropdown.ItemSelected += OnSceneSelected;
|
||||
toolbar.AddChild(_sceneDropdown);
|
||||
private static OptionButton Dropdown(HBoxContainer toolbar, string tooltipKey, Action<long> selected)
|
||||
{
|
||||
var dropdown = new OptionButton
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
TooltipText = ToolUi.Tr(tooltipKey),
|
||||
FitToLongestItem = false
|
||||
};
|
||||
dropdown.ItemSelected += index => selected(index);
|
||||
toolbar.AddChild(dropdown);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
_reloadBtn = new Button { Text = TranslationServer.Translate("tools/scene_builder/reload") };
|
||||
_reloadBtn.Pressed += OnReloadPressed;
|
||||
toolbar.AddChild(_reloadBtn);
|
||||
private Control BuildHierarchyPanel()
|
||||
{
|
||||
var column = new VBoxContainer();
|
||||
column.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||
var panel = new PanelContainer { CustomMinimumSize = new Vector2(280, 0) };
|
||||
panel.AddThemeStyleboxOverride("panel", SidebarBox());
|
||||
panel.AddChild(column);
|
||||
|
||||
var mainHBox = new HBoxContainer();
|
||||
mainHBox.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
mainHBox.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
outerVBox.AddChild(mainHBox);
|
||||
column.AddChild(ToolUi.Section("tools/scene_builder/hierarchy"));
|
||||
|
||||
var leftVBox = new VBoxContainer();
|
||||
leftVBox.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
leftVBox.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
var leftPanel = CreateSidebar(leftVBox);
|
||||
mainHBox.AddChild(leftPanel);
|
||||
var actions = new HBoxContainer();
|
||||
actions.AddThemeConstantOverride("separation", UiTokens.Space1);
|
||||
column.AddChild(actions);
|
||||
|
||||
var hierarchyHeader = new HBoxContainer();
|
||||
var hierarchyLabel = new Label { Text = TranslationServer.Translate("tools/scene_builder/hierarchy") };
|
||||
hierarchyLabel.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
hierarchyHeader.AddChild(hierarchyLabel);
|
||||
|
||||
_newNodeTypeDropdown = new OptionButton();
|
||||
_newNodeTypeDropdown.TooltipText = TranslationServer.Translate("tools/scene_builder/add_node_tip");
|
||||
_newNodeTypeDropdown = new OptionButton
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
TooltipText = ToolUi.Tr("tools/scene_builder/node_type_tip")
|
||||
};
|
||||
foreach (var t in CreatableTypes)
|
||||
_newNodeTypeDropdown.AddItem(t);
|
||||
_newNodeTypeDropdown.Selected = 0;
|
||||
hierarchyHeader.AddChild(_newNodeTypeDropdown);
|
||||
actions.AddChild(_newNodeTypeDropdown);
|
||||
|
||||
_addNodeBtn = new Button { Text = "+" };
|
||||
_addNodeBtn.TooltipText = TranslationServer.Translate("tools/scene_builder/add_node_tip");
|
||||
_addNodeBtn.Pressed += OnAddNodePressed;
|
||||
hierarchyHeader.AddChild(_addNodeBtn);
|
||||
var addBtn = UiKit.IconButton("+", ToolUi.Tr("tools/scene_builder/add_node_tip"));
|
||||
addBtn.Pressed += OnAddNodePressed;
|
||||
actions.AddChild(addBtn);
|
||||
|
||||
_deleteNodeBtn = new Button { Text = "-" };
|
||||
_deleteNodeBtn.TooltipText = TranslationServer.Translate("tools/scene_builder/delete_node_tip");
|
||||
_deleteNodeBtn.Pressed += OnDeleteNodePressed;
|
||||
hierarchyHeader.AddChild(_deleteNodeBtn);
|
||||
var deleteBtn = UiKit.IconButton("\u2212", ToolUi.Tr("tools/scene_builder/delete_node_tip"));
|
||||
deleteBtn.Pressed += OnDeleteNodePressed;
|
||||
actions.AddChild(deleteBtn);
|
||||
|
||||
_refreshBtn = new Button();
|
||||
_refreshBtn.Text = TranslationServer.Translate("tools/scene_builder/refresh");
|
||||
_refreshBtn.Pressed += OnRefreshPressed;
|
||||
hierarchyHeader.AddChild(_refreshBtn);
|
||||
var refreshBtn = UiKit.IconButton("\u27F3", ToolUi.Tr("tools/scene_builder/refresh_tip"));
|
||||
refreshBtn.Pressed += OnRefreshPressed;
|
||||
actions.AddChild(refreshBtn);
|
||||
|
||||
leftVBox.AddChild(hierarchyHeader);
|
||||
|
||||
_hierarchyTree = new Tree();
|
||||
_hierarchyTree.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
_hierarchyTree.HideRoot = true;
|
||||
_hierarchyTree = new Tree
|
||||
{
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill,
|
||||
HideRoot = true
|
||||
};
|
||||
_hierarchyTree.CellSelected += OnTreeCellSelected;
|
||||
leftVBox.AddChild(_hierarchyTree);
|
||||
column.AddChild(_hierarchyTree);
|
||||
column.AddChild(ToolUi.Hint(ToolUi.Tr("tools/scene_builder/hierarchy_hint")));
|
||||
return panel;
|
||||
}
|
||||
|
||||
var previewContainer = new PanelContainer();
|
||||
previewContainer.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
previewContainer.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
mainHBox.AddChild(previewContainer);
|
||||
/// <summary>
|
||||
/// Preview canvas. The sub-viewport lays the scene out at the game
|
||||
/// resolution (2D size override) and is scaled to fit inside an
|
||||
/// aspect-locked box, so anchors and sizes look exactly as in the game
|
||||
/// whatever the window size. The selection overlay shares the box, so
|
||||
/// its coordinate mapping is a plain scale.
|
||||
/// </summary>
|
||||
private Control BuildPreview()
|
||||
{
|
||||
var previewContainer = new PanelContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
previewContainer.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusMd, UiTokens.Well, UiTokens.Line));
|
||||
|
||||
_viewport = new SubViewport();
|
||||
_viewport.Size = new Vector2I(1920, 1080);
|
||||
var aspect = new AspectRatioContainer
|
||||
{
|
||||
Ratio = (float)GameResolution.X / GameResolution.Y,
|
||||
StretchMode = AspectRatioContainer.StretchModeEnum.Fit
|
||||
};
|
||||
previewContainer.AddChild(aspect);
|
||||
|
||||
_viewport = new SubViewport
|
||||
{
|
||||
Size = GameResolution,
|
||||
Size2DOverride = GameResolution,
|
||||
Size2DOverrideStretch = true
|
||||
};
|
||||
|
||||
var clickCatcher = new Control();
|
||||
clickCatcher.MouseFilter = Control.MouseFilterEnum.Stop;
|
||||
@@ -160,97 +202,65 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
clickCatcher.GuiInput += OnViewportClick;
|
||||
_viewport.AddChild(clickCatcher);
|
||||
|
||||
var viewportContainer = new SubViewportContainer();
|
||||
viewportContainer.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
viewportContainer.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
viewportContainer.Stretch = true;
|
||||
viewportContainer.StretchShrink = 1;
|
||||
var viewportContainer = new SubViewportContainer { Stretch = true };
|
||||
viewportContainer.AddChild(_viewport);
|
||||
previewContainer.AddChild(viewportContainer);
|
||||
aspect.AddChild(viewportContainer);
|
||||
|
||||
_selectionOverlay = new SelectionOverlay();
|
||||
previewContainer.AddChild(_selectionOverlay);
|
||||
_selectionOverlay.Setup(_viewport, viewportContainer);
|
||||
aspect.AddChild(_selectionOverlay);
|
||||
_selectionOverlay.Setup(_viewport, viewportContainer, GameResolution);
|
||||
_selectionOverlay.PickRequested += OnOverlayPick;
|
||||
_selectionOverlay.BatchCommitted += OnOverlayBatchCommitted;
|
||||
_selectionOverlay.EnsureConstructed();
|
||||
|
||||
_autosaveTimer = new Timer { OneShot = true, WaitTime = 0.4 };
|
||||
_autosaveTimer.Timeout += OnAutosaveTimeout;
|
||||
AddChild(_autosaveTimer);
|
||||
return previewContainer;
|
||||
}
|
||||
|
||||
// Compact tabbed sidebar: inspector and tscn source share one column,
|
||||
// the output strip stays below the tabs so errors are always visible.
|
||||
var rightVBox = new VBoxContainer();
|
||||
rightVBox.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
rightVBox.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
var rightPanel = CreateSidebar(rightVBox, 360);
|
||||
mainHBox.AddChild(rightPanel);
|
||||
|
||||
_sideTabs = new TabContainer();
|
||||
_sideTabs.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
_sideTabs.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
rightVBox.AddChild(_sideTabs);
|
||||
|
||||
var inspectorTab = new VBoxContainer
|
||||
/// <summary>Inspector and tscn source share one tabbed column.</summary>
|
||||
private Control BuildSidePanel()
|
||||
{
|
||||
var panel = new PanelContainer { CustomMinimumSize = new Vector2(380, 0) };
|
||||
panel.AddThemeStyleboxOverride("panel", SidebarBox());
|
||||
|
||||
_sideTabs = new TabContainer
|
||||
{
|
||||
Name = TranslationServer.Translate("tools/scene_builder/tab_inspector"),
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
_sideTabs.AddChild(inspectorTab);
|
||||
panel.AddChild(_sideTabs);
|
||||
|
||||
_inspector = new InspectorPanel();
|
||||
inspectorTab.AddChild(_inspector);
|
||||
var inspectorScroll = new ScrollContainer { HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled };
|
||||
_sideTabs.AddChild(inspectorScroll);
|
||||
_sideTabs.SetTabTitle(0, ToolUi.Tr("tools/scene_builder/tab_inspector"));
|
||||
|
||||
_inspector = new InspectorPanel { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
inspectorScroll.AddChild(_inspector);
|
||||
_inspector.PropertyEdited += OnInspectorPropertyEdited;
|
||||
_inspector.EnsureConstructed();
|
||||
_inspector.ShowMessage(TranslationServer.Translate("tools/scene_builder/inspector_select"));
|
||||
_inspector.ShowMessage(ToolUi.Tr("tools/scene_builder/inspector_select"));
|
||||
|
||||
// Titles are set explicitly: a node name cannot hold "." (".tscn").
|
||||
var tscnTab = new VBoxContainer
|
||||
{
|
||||
Name = TranslationServer.Translate("tools/scene_builder/tab_tscn"),
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
_sideTabs.AddChild(tscnTab);
|
||||
_sideTabs.SetTabTitle(1, ToolUi.Tr("tools/scene_builder/tab_tscn"));
|
||||
|
||||
_tscnEditor = new TextEdit();
|
||||
_tscnEditor.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
_tscnEditor.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
_tscnEditor.WrapMode = TextEdit.LineWrappingMode.None;
|
||||
_tscnEditor.AddThemeFontOverride("font", GetMonospaceFont());
|
||||
_tscnEditor.TextChanged += () => SetDirty();
|
||||
_tscnEditor = ToolUi.Code();
|
||||
_tscnEditor.SyntaxHighlighter = SourceSyntax.CreateTscnHighlighter();
|
||||
_tscnEditor.TextChanged += OnTscnTextChanged;
|
||||
tscnTab.AddChild(_tscnEditor);
|
||||
tscnTab.AddChild(ToolUi.Hint(ToolUi.Tr("tools/scene_builder/tscn_hint")));
|
||||
|
||||
_saveBtn = new Button();
|
||||
_saveBtn.Text = TranslationServer.Translate("tools/scene_builder/save");
|
||||
_saveBtn.Pressed += OnSavePressed;
|
||||
tscnTab.AddChild(_saveBtn);
|
||||
|
||||
var outputHeader = new HBoxContainer();
|
||||
var outputLabel = new Label { Text = TranslationServer.Translate("tools/scene_builder/output") };
|
||||
outputLabel.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
outputHeader.AddChild(outputLabel);
|
||||
|
||||
var clearOutputBtn = new Button { Text = TranslationServer.Translate("tools/scene_builder/clear") };
|
||||
clearOutputBtn.Pressed += () => _outputLabel.Text = "";
|
||||
outputHeader.AddChild(clearOutputBtn);
|
||||
|
||||
rightVBox.AddChild(outputHeader);
|
||||
|
||||
_outputLabel = new RichTextLabel();
|
||||
_outputLabel.BbcodeEnabled = true;
|
||||
_outputLabel.FitContent = false;
|
||||
_outputLabel.ScrollActive = true;
|
||||
_outputLabel.ScrollFollowing = true;
|
||||
_outputLabel.CustomMinimumSize = new Vector2(0, 70);
|
||||
rightVBox.AddChild(_outputLabel);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void PopulateBaseList()
|
||||
{
|
||||
_baseDropdown.Clear();
|
||||
_baseDropdown.AddItem(TranslationServer.Translate("tools/scene_builder/empty_scene"));
|
||||
_baseDropdown.AddItem(ToolUi.Tr("tools/scene_builder/empty_scene"));
|
||||
_baseScenes = _controller.GetBaseSceneList();
|
||||
foreach (var s in _baseScenes)
|
||||
_baseDropdown.AddItem(s.Display);
|
||||
@@ -260,7 +270,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
private void PopulateModList()
|
||||
{
|
||||
_modDropdown.Clear();
|
||||
_modDropdown.AddItem(TranslationServer.Translate("tools/scene_builder/select_mod"));
|
||||
_modDropdown.AddItem(ToolUi.Tr("tools/scene_builder/select_mod"));
|
||||
_modScenes = _controller.GetModSceneIndex();
|
||||
_mods = _modScenes.Select(s => s.ModId).Distinct().ToList();
|
||||
foreach (var modId in _mods)
|
||||
@@ -268,7 +278,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
_modDropdown.Selected = 0;
|
||||
|
||||
_sceneDropdown.Clear();
|
||||
_sceneDropdown.AddItem(TranslationServer.Translate("tools/scene_builder/select_scene"));
|
||||
_sceneDropdown.AddItem(ToolUi.Tr("tools/scene_builder/select_scene"));
|
||||
_sceneDropdown.Selected = 0;
|
||||
}
|
||||
|
||||
@@ -283,7 +293,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
}
|
||||
|
||||
_sceneDropdown.Clear();
|
||||
_sceneDropdown.AddItem(TranslationServer.Translate("tools/scene_builder/select_scene"));
|
||||
_sceneDropdown.AddItem(ToolUi.Tr("tools/scene_builder/select_scene"));
|
||||
foreach (var s in _currentModScenes)
|
||||
_sceneDropdown.AddItem(s.Name);
|
||||
_sceneDropdown.Selected = 0;
|
||||
@@ -302,6 +312,10 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
private void RebuildPreview()
|
||||
{
|
||||
_shownBase = _baseDropdown.Selected;
|
||||
_shownMod = _modDropdown.Selected;
|
||||
_shownScene = _sceneDropdown.Selected;
|
||||
|
||||
var baseIdx = (int)_baseDropdown.Selected;
|
||||
if (baseIdx <= 0 || baseIdx - 1 >= _baseScenes.Count)
|
||||
_controller.LoadEmptyScene(_viewport);
|
||||
@@ -316,7 +330,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
RefreshFileSnapshot();
|
||||
_selectedNode = null;
|
||||
_selectionOverlay.Track(null);
|
||||
_inspector.ShowMessage(TranslationServer.Translate("tools/scene_builder/inspector_select"));
|
||||
_inspector.ShowMessage(ToolUi.Tr("tools/scene_builder/inspector_select"));
|
||||
}
|
||||
|
||||
/// <summary>Re-parses the current editor text into the file-truth property snapshot used by the inspector.</summary>
|
||||
@@ -339,43 +353,93 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
private void ShowTscnText()
|
||||
{
|
||||
_loadingText = true;
|
||||
_tscnEditor.Text = "";
|
||||
if (TryGetSelectedModScene(out var scene))
|
||||
{
|
||||
var text = _controller.ReadSceneText(scene.ModId, scene.RelativePath);
|
||||
_tscnEditor.Text = text ?? "";
|
||||
}
|
||||
_tscnEditor.ClearUndoHistory();
|
||||
_loadingText = false;
|
||||
MarkClean();
|
||||
}
|
||||
|
||||
/// <summary>Hand edits of the source mark the window dirty; they reach the preview on save.</summary>
|
||||
private void OnTscnTextChanged()
|
||||
{
|
||||
if (!_loadingText)
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
private void OnBaseSelected(long index)
|
||||
{
|
||||
RebuildPreview();
|
||||
RunDiscardingChanges(RebuildPreview, RestoreDropdowns);
|
||||
}
|
||||
|
||||
private void OnModSelected(long index)
|
||||
{
|
||||
RunDiscardingChanges(() =>
|
||||
{
|
||||
var modId = index == 0 ? null : _mods[(int)index - 1];
|
||||
PopulateSceneListForMod(modId);
|
||||
RebuildPreview();
|
||||
}, RestoreDropdowns);
|
||||
}
|
||||
|
||||
private void OnSceneSelected(long index)
|
||||
{
|
||||
RebuildPreview();
|
||||
RunDiscardingChanges(RebuildPreview, RestoreDropdowns);
|
||||
}
|
||||
|
||||
/// <summary>Rolls the selectors back to the scene still shown after a refused switch.</summary>
|
||||
private void RestoreDropdowns()
|
||||
{
|
||||
_baseDropdown.Select(_shownBase);
|
||||
_modDropdown.Select(_shownMod);
|
||||
PopulateSceneListForMod(_shownMod <= 0 ? null : _mods[_shownMod - 1]);
|
||||
_sceneDropdown.Select(_shownScene);
|
||||
}
|
||||
|
||||
private void OnReloadPressed()
|
||||
{
|
||||
RunDiscardingChanges(() =>
|
||||
{
|
||||
if (TryGetSelectedModScene(out var scene))
|
||||
_controller.InvalidateScene(scene.RelativePath);
|
||||
|
||||
RebuildPreview();
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/reloaded"), false);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/reloaded"), false);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Ctrl+S: save the source and rebuild the preview from it.</summary>
|
||||
protected override void OnSaveShortcut() => OnSavePressed();
|
||||
|
||||
/// <summary>Delete removes the selected mod node after a confirmation.</summary>
|
||||
protected override bool HandleKey(InputEventKey key)
|
||||
{
|
||||
if (key.Keycode != Key.Delete || _selectedNode == null || !_controller.IsEditableModNode(_selectedNode))
|
||||
return false;
|
||||
|
||||
ShowConfirm("tools/scene_builder/delete_node_title", "tools/scene_builder/delete_node_confirm", OnDeleteNodePressed);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the source and, when it was edited by hand, rebuilds the preview
|
||||
/// from the saved file — hand edits do not reach the live preview
|
||||
/// otherwise (inspector and overlay edits are applied live already).
|
||||
/// </summary>
|
||||
private void OnSavePressed()
|
||||
{
|
||||
SaveCurrentText(false);
|
||||
var handEdited = _dirty;
|
||||
if (!SaveCurrentText(false) || !handEdited || !TryGetSelectedModScene(out var scene))
|
||||
return;
|
||||
|
||||
_controller.InvalidateScene(scene.RelativePath);
|
||||
RebuildPreview();
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/saved_rebuilt"), false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -388,20 +452,20 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
if (!TryGetSelectedModScene(out var scene))
|
||||
{
|
||||
if (!auto)
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/error_no_scene"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/error_no_scene"), true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_controller.SaveSceneText(scene.ModId, scene.RelativePath, _tscnEditor.Text))
|
||||
{
|
||||
MarkClean();
|
||||
ShowOutput(TranslationServer.Translate(auto
|
||||
ShowOutput(ToolUi.Tr(auto
|
||||
? "tools/scene_builder/autosaved"
|
||||
: "tools/scene_builder/saved_ok"), false);
|
||||
return true;
|
||||
}
|
||||
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/error_save"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/error_save"), true);
|
||||
GameLogger.Log("SCENE_BUILDER",
|
||||
$"save failed: mod='{scene.ModId}' path='{scene.RelativePath}' (zip mods are read-only)",
|
||||
LogLevel.Error);
|
||||
@@ -465,7 +529,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
}
|
||||
else
|
||||
{
|
||||
_inspector.ShowMessage(TranslationServer.Translate("tools/scene_builder/inspector_readonly"));
|
||||
_inspector.ShowMessage(ToolUi.Tr("tools/scene_builder/inspector_readonly"));
|
||||
_selectionOverlay.Track(null);
|
||||
}
|
||||
}
|
||||
@@ -500,7 +564,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
{
|
||||
if (!TryGetSelectedModScene(out var scene))
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/error_no_scene"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/error_no_scene"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -508,14 +572,14 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
var parent = ResolveAddParent();
|
||||
if (parent == null)
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/error_no_scene"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/error_no_scene"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
var created = _controller.CreateUniqueChild(parent, typeName);
|
||||
if (created == null)
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/err_add_failed"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/err_add_failed"), true);
|
||||
GameLogger.Log("SCENE_BUILDER", $"ClassDB.Instantiate failed for '{typeName}'", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
@@ -526,7 +590,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
{
|
||||
created.GetParent().RemoveChild(created);
|
||||
created.QueueFree();
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/err_add_parent_missing"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/err_add_parent_missing"), true);
|
||||
GameLogger.Log("SCENE_BUILDER",
|
||||
$"node insert rejected: parent section '{parentPath}' absent from file", LogLevel.Warning);
|
||||
return;
|
||||
@@ -577,19 +641,19 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
var node = _selectedNode;
|
||||
if (node == null || !_controller.IsEditableModNode(node))
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/err_del_none"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/err_del_none"), true);
|
||||
return;
|
||||
}
|
||||
if (node == _controller.GetModRoot())
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/err_del_root"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/err_del_root"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
var path = _controller.GetModNodePath(node);
|
||||
if (!_controller.DeleteModNode(node))
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/err_del_none"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/err_del_none"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -601,14 +665,14 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/inspector_missing_section"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/inspector_missing_section"), true);
|
||||
GameLogger.Log("SCENE_BUILDER",
|
||||
$"subtree remove failed: section '{path}' absent from file", LogLevel.Warning);
|
||||
}
|
||||
|
||||
_selectedNode = null;
|
||||
_selectionOverlay.Track(null);
|
||||
_inspector.ShowMessage(TranslationServer.Translate("tools/scene_builder/inspector_select"));
|
||||
_inspector.ShowMessage(ToolUi.Tr("tools/scene_builder/inspector_select"));
|
||||
_controller.BuildHierarchy(_hierarchyTree);
|
||||
}
|
||||
|
||||
@@ -621,14 +685,14 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
{
|
||||
if (!TryGetSelectedModScene(out var scene))
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/error_no_scene"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/error_no_scene"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
var nodePath = _controller.GetModNodePath(node);
|
||||
if (nodePath == null)
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/inspector_readonly"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/inspector_readonly"), true);
|
||||
GameLogger.Log("SCENE_BUILDER", $"batch rejected: '{node.Name}' is not an editable mod node", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
@@ -639,14 +703,14 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
var serialized = TscnValueFormatter.Format(pair.Value);
|
||||
if (serialized == null)
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/inspector_bad_value"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/inspector_bad_value"), true);
|
||||
GameLogger.Log("SCENE_BUILDER", $"unserializable value for '{pair.Key}'", LogLevel.Warning);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TscnTextSurgery.TryUpsertProperty(text, nodePath, pair.Key, serialized, out text))
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/inspector_missing_section"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/inspector_missing_section"), true);
|
||||
GameLogger.Log("SCENE_BUILDER",
|
||||
$"section not found: path='{nodePath}' key='{pair.Key}'", LogLevel.Warning);
|
||||
}
|
||||
@@ -669,14 +733,14 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
{
|
||||
if (!TryGetSelectedModScene(out var scene))
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/error_no_scene"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/error_no_scene"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
var serialized = TscnValueFormatter.Format(value);
|
||||
if (serialized == null)
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/inspector_bad_value"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/inspector_bad_value"), true);
|
||||
GameLogger.Log("SCENE_BUILDER", $"unserializable value for '{key}'", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
@@ -684,14 +748,14 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
var nodePath = _controller.GetModNodePath(node);
|
||||
if (nodePath == null)
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/inspector_readonly"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/inspector_readonly"), true);
|
||||
GameLogger.Log("SCENE_BUILDER", $"edit rejected: '{node.Name}' is not an editable mod node", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TscnTextSurgery.TryUpsertProperty(_tscnEditor.Text, nodePath, key, serialized, out var updated))
|
||||
{
|
||||
ShowOutput(TranslationServer.Translate("tools/scene_builder/inspector_missing_section"), true);
|
||||
ShowOutput(ToolUi.Tr("tools/scene_builder/inspector_missing_section"), true);
|
||||
GameLogger.Log("SCENE_BUILDER",
|
||||
$"section not found: path='{nodePath}' key='{key}'", LogLevel.Warning);
|
||||
return;
|
||||
@@ -715,11 +779,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
private void ShowOutput(string text, bool isError)
|
||||
{
|
||||
if (_outputLabel == null)
|
||||
return;
|
||||
|
||||
var color = isError ? "red" : "green";
|
||||
_outputLabel.Text = $"[color={color}]{text}[/color]";
|
||||
SetStatus(text, isError ? ToolStatusKind.Error : ToolStatusKind.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
{
|
||||
/// <summary>
|
||||
@@ -15,11 +16,14 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
/// overlay itself hit-tests them in its <c>GuiInput</c> handler — because
|
||||
/// per-marker Controls cannot capture motion outside their small rects and
|
||||
/// virtual overrides (_Process/_GuiInput) do not dispatch in mod assemblies.
|
||||
/// Pressing inside the selected rectangle and dragging moves the node (all
|
||||
/// four offsets shift together); a press that does not move is a pick.
|
||||
/// Live edits go straight onto the node during the drag; the accumulated
|
||||
/// key/value batch is emitted once on mouse release via
|
||||
/// <see cref="BatchCommitted"/> so text write-back happens in one pass.
|
||||
/// Coordinates map between the fixed-size SubViewport canvas and this
|
||||
/// overlay through the stretch scale of the hosting SubViewportContainer.
|
||||
/// Coordinates map between the game-resolution canvas of the SubViewport
|
||||
/// and this overlay (which shares the viewport container's rectangle) by a
|
||||
/// plain scale.
|
||||
/// </summary>
|
||||
public partial class SelectionOverlay : ModWidget
|
||||
{
|
||||
@@ -42,7 +46,9 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
private const int ResizeMarkerCount = 8;
|
||||
private const int AnchorDotCount = 4;
|
||||
|
||||
private enum DragKind { None, Resize, Anchor }
|
||||
private const float MoveThreshold = 4f;
|
||||
|
||||
private enum DragKind { None, Resize, Anchor, PendingMove, Move }
|
||||
|
||||
private struct Marker
|
||||
{
|
||||
@@ -53,6 +59,9 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
private SubViewport _viewport;
|
||||
private Control _containerArea;
|
||||
private Vector2 _canvasSize;
|
||||
private Vector2 _pressPoint;
|
||||
private readonly float[] _moveStartOffsets = new float[4];
|
||||
private Control _selected;
|
||||
private readonly List<Marker> _markers = new();
|
||||
private readonly List<ColorRect> _borderRects = new();
|
||||
@@ -65,10 +74,11 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
private readonly Dictionary<string, double> _pendingValues = new();
|
||||
|
||||
/// <summary>Binds the overlay to the preview pipeline; call once before use.</summary>
|
||||
public void Setup(SubViewport viewport, Control containerArea)
|
||||
public void Setup(SubViewport viewport, Control containerArea, Vector2I canvasSize)
|
||||
{
|
||||
_viewport = viewport;
|
||||
_containerArea = containerArea;
|
||||
_canvasSize = canvasSize;
|
||||
_containerArea.Resized += Refresh;
|
||||
Refresh();
|
||||
}
|
||||
@@ -249,6 +259,11 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
{
|
||||
if (mb.Pressed)
|
||||
OnPress(mb.Position);
|
||||
else if (_drag == DragKind.PendingMove)
|
||||
{
|
||||
_drag = DragKind.None;
|
||||
PickRequested?.Invoke(LocalToCanvas(mb.Position));
|
||||
}
|
||||
else if (_drag != DragKind.None)
|
||||
FinishDrag();
|
||||
return;
|
||||
@@ -256,7 +271,10 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
|
||||
if (evt is InputEventMouseMotion motion)
|
||||
{
|
||||
if (_drag != DragKind.None)
|
||||
if (_drag == DragKind.PendingMove && (motion.Position - _pressPoint).Length() >= MoveThreshold)
|
||||
_drag = DragKind.Move;
|
||||
|
||||
if (_drag is DragKind.Resize or DragKind.Anchor or DragKind.Move)
|
||||
{
|
||||
ApplyDrag(motion.Position);
|
||||
Refresh();
|
||||
@@ -290,6 +308,19 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
return;
|
||||
}
|
||||
|
||||
if (_markers.Count > 0 && _selected is Control control
|
||||
&& CanvasRectToOverlay(control.GetGlobalRect()).HasPoint(localPoint))
|
||||
{
|
||||
BeginDrag(DragKind.PendingMove, 0);
|
||||
_pressPoint = localPoint;
|
||||
_moveStartOffsets[0] = control.OffsetLeft;
|
||||
_moveStartOffsets[1] = control.OffsetTop;
|
||||
_moveStartOffsets[2] = control.OffsetRight;
|
||||
_moveStartOffsets[3] = control.OffsetBottom;
|
||||
AcceptEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
PickRequested?.Invoke(LocalToCanvas(localPoint));
|
||||
}
|
||||
|
||||
@@ -314,6 +345,18 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
return;
|
||||
}
|
||||
|
||||
if (_drag == DragKind.Move)
|
||||
{
|
||||
var delta = LocalToCanvas(localPoint) - LocalToCanvas(_pressPoint);
|
||||
var dx = Mathf.Round(delta.X);
|
||||
var dy = Mathf.Round(delta.Y);
|
||||
StorePair(control, "offset_left", _moveStartOffsets[0] + dx);
|
||||
StorePair(control, "offset_top", _moveStartOffsets[1] + dy);
|
||||
StorePair(control, "offset_right", _moveStartOffsets[2] + dx);
|
||||
StorePair(control, "offset_bottom", _moveStartOffsets[3] + dy);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyAnchorDrag(control, parentSize, mouseInParent, _dragIndex);
|
||||
}
|
||||
|
||||
@@ -452,17 +495,19 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
}
|
||||
}
|
||||
|
||||
MouseDefaultCursorShape = Control.CursorShape.Arrow;
|
||||
MouseDefaultCursorShape = _markers.Count > 0 && _selected is Control control
|
||||
&& CanvasRectToOverlay(control.GetGlobalRect()).HasPoint(localPoint)
|
||||
? Control.CursorShape.Move
|
||||
: Control.CursorShape.Arrow;
|
||||
}
|
||||
|
||||
// ── coordinate mapping ──────────────────────────────────────────
|
||||
|
||||
private Vector2 ComputeStretchScale()
|
||||
{
|
||||
var viewSize = _viewport.Size;
|
||||
return new Vector2(
|
||||
viewSize.X > 0 ? _containerArea.Size.X / viewSize.X : 1f,
|
||||
viewSize.Y > 0 ? _containerArea.Size.Y / viewSize.Y : 1f);
|
||||
_canvasSize.X > 0 ? _containerArea.Size.X / _canvasSize.X : 1f,
|
||||
_canvasSize.Y > 0 ? _containerArea.Size.Y / _canvasSize.Y : 1f);
|
||||
}
|
||||
|
||||
private Vector2 CanvasToOverlay(Vector2 canvasPoint)
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
/// </summary>
|
||||
public static class TscnLiteralReader
|
||||
{
|
||||
/// <summary>Parses a bare .tscn float literal; constructors and references return false.</summary>
|
||||
public static bool TryReadFloat(string raw, out double value)
|
||||
{
|
||||
value = 0d;
|
||||
@@ -21,6 +22,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
return double.TryParse(raw.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
|
||||
/// <summary>Parses a bare .tscn integer literal; floats and constructors return false.</summary>
|
||||
public static bool TryReadLong(string raw, out long value)
|
||||
{
|
||||
value = 0;
|
||||
@@ -29,6 +31,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
return long.TryParse(raw.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
|
||||
/// <summary>Parses a .tscn boolean literal (true/false).</summary>
|
||||
public static bool TryReadBool(string raw, out bool value)
|
||||
{
|
||||
var t = raw?.Trim();
|
||||
@@ -56,6 +59,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
.Replace("\\\\", "\\");
|
||||
}
|
||||
|
||||
/// <summary>Parses a <c>Vector2(x, y)</c> constructor literal.</summary>
|
||||
public static bool TryReadVector2(string raw, out Vector2 value)
|
||||
{
|
||||
value = Vector2.Zero;
|
||||
@@ -68,6 +72,7 @@ namespace Cthangover.Core.UI.Tool.SceneBuilder
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Parses a <c>Color(r, g, b[, a])</c> constructor literal.</summary>
|
||||
public static bool TryReadColor(string raw, out Color value)
|
||||
{
|
||||
value = Colors.White;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
using Cthangover.Tools.Common;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.SceneEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// One background of the edited scene in <see cref="SceneEditorWindow"/>: a thumbnail
|
||||
/// (click shows it in the preview), the background id and the mod whose scene file
|
||||
/// adds it, and a remove button only for backgrounds of the file being edited —
|
||||
/// another mod's background can only be hidden by editing that mod.
|
||||
/// </summary>
|
||||
public partial class SceneBackgroundTile : PanelContainer
|
||||
{
|
||||
private readonly string _background;
|
||||
private readonly Action<string> _picked;
|
||||
private readonly Action<string> _removed;
|
||||
|
||||
/// <summary>Builds the tile; <paramref name="removable"/> shows the ✕ button, <paramref name="selected"/> the accent frame.</summary>
|
||||
public SceneBackgroundTile(string background, string modId, bool removable, bool selected,
|
||||
Action<string> picked, Action<string> removed)
|
||||
{
|
||||
_background = background;
|
||||
_picked = picked;
|
||||
_removed = removed;
|
||||
AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusSm, UiTokens.Surface1,
|
||||
selected ? UiTokens.Accent : UiTokens.Line));
|
||||
|
||||
var column = new VBoxContainer();
|
||||
column.AddThemeConstantOverride("separation", UiTokens.Space1);
|
||||
AddChild(column);
|
||||
|
||||
var thumb = new TextureButton
|
||||
{
|
||||
TextureNormal = ModResourceService.LoadBackgroundTexture(background),
|
||||
IgnoreTextureSize = true,
|
||||
StretchMode = TextureButton.StretchModeEnum.KeepAspectCentered,
|
||||
CustomMinimumSize = SceneEditorWindow.ThumbSize,
|
||||
TooltipText = background
|
||||
};
|
||||
thumb.Pressed += OnPicked;
|
||||
column.AddChild(thumb);
|
||||
|
||||
var row = new HBoxContainer();
|
||||
row.AddThemeConstantOverride("separation", UiTokens.Space1);
|
||||
column.AddChild(row);
|
||||
var caption = ToolUi.Hint(background + " · " + modId);
|
||||
caption.TooltipText = background + " (" + modId + ")";
|
||||
caption.MouseFilter = MouseFilterEnum.Pass;
|
||||
caption.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
caption.CustomMinimumSize = new Vector2(SceneEditorWindow.ThumbSize.X - 32, 0);
|
||||
caption.TextOverrunBehavior = TextServer.OverrunBehavior.TrimEllipsis;
|
||||
row.AddChild(caption);
|
||||
if (removable)
|
||||
{
|
||||
var remove = UiKit.IconButton("✕", ToolUi.Tr("tools/scene_editor/remove_background"));
|
||||
remove.Pressed += OnRemoved;
|
||||
row.AddChild(remove);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPicked() => _picked?.Invoke(_background);
|
||||
|
||||
private void OnRemoved() => _removed?.Invoke(_background);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Cthangover.Core.UI.Tool;
|
||||
|
||||
namespace Cthangover.Tools.SceneEditor
|
||||
{
|
||||
/// <summary>Toolbox button that opens the scene editor. Always visible.</summary>
|
||||
public class SceneEditorToolBoxButton : IToolBoxButton
|
||||
{
|
||||
/// <summary>Matches <see cref="SceneEditorToolProvider.Id"/>.</summary>
|
||||
public string ToolId => "scene_editor";
|
||||
/// <summary>No custom icon — uses text label.</summary>
|
||||
public string IconPath => "";
|
||||
/// <summary>Translation key for the button label.</summary>
|
||||
public string LocaleKey => "tools/scene_editor/title";
|
||||
/// <summary>Always visible in the toolbox.</summary>
|
||||
public bool IsVisible() => true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.SceneEditor
|
||||
{
|
||||
/// <summary>Registers the scene editor as a tool window (main menu tools, in-game toolbox).</summary>
|
||||
public class SceneEditorToolProvider : IToolProvider
|
||||
{
|
||||
/// <summary>Unique tool identifier.</summary>
|
||||
public string Id => "scene_editor";
|
||||
/// <summary>Translation key for the tool's display name.</summary>
|
||||
public string LocaleKey => "tools/scene_editor/title";
|
||||
/// <summary>Creates the editor window instance.</summary>
|
||||
public Window CreateWindow() => new SceneEditorWindow();
|
||||
/// <summary>Adds the launcher button to the main menu tool block.</summary>
|
||||
public void AddMenuButton(Container container) => ToolMenuButton.Attach(container, this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,743 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Audio;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Core.UI.Tool.ScenarioEditor;
|
||||
using Cthangover.Mods.Dialog;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
using Cthangover.Tools.Common;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.SceneEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor of logical scenes — the places of a novel: which backgrounds a scene shows
|
||||
/// (one of them at random on every entry), its ambient sound, the scenario played
|
||||
/// when nothing else starts, and the scenarios that happen there. Left: every scene
|
||||
/// of the loaded mods; centre: the background preview, the scene's backgrounds and a
|
||||
/// searchable list of all backgrounds to add; right: properties and the scene's
|
||||
/// scenarios (open one in the scenario editor, or start a new one).
|
||||
/// <para/>
|
||||
/// A scene is the merge of the <c>scenes/<name>.json</c> files of all mods
|
||||
/// (backgrounds add up, a later ambient or default scenario wins). Scenarios are optional:
|
||||
/// a place may hold only interactives, so a new scene starts without any. The editor writes
|
||||
/// one of them — the file of the mod chosen in "Save to": the scene's own file when
|
||||
/// that mod defines it, otherwise a new file that adds to a foreign scene without
|
||||
/// touching the foreign mod. Backgrounds of the other files are shown locked, with
|
||||
/// their mod. After saving the game re-reads scenes (<see cref="SceneManager.ReloadContent"/>),
|
||||
/// so a new scene can be entered at once with "Go to scene".
|
||||
/// </summary>
|
||||
public partial class SceneEditorWindow : ToolWindow
|
||||
{
|
||||
private const int ThumbWidth = 160;
|
||||
private const int ThumbHeight = 90;
|
||||
|
||||
private readonly List<SceneFileEntry> _files = new();
|
||||
private readonly List<string> _writableMods = new();
|
||||
private readonly List<string> _sounds = new();
|
||||
private readonly List<string> _scenarioKeys = new();
|
||||
private readonly List<string> _defaultChoices = new();
|
||||
|
||||
private string _sceneName;
|
||||
private bool _isNew;
|
||||
private SceneFileEntry _target;
|
||||
private string _preview;
|
||||
private bool _suppress;
|
||||
private bool _ambientPlaying;
|
||||
private string _pendingMod;
|
||||
private string _pendingScene;
|
||||
|
||||
private FilteredList _sceneList;
|
||||
private TextureRect _previewRect;
|
||||
private Label _previewCaption;
|
||||
private HFlowContainer _backgroundRow;
|
||||
private FilteredList _picker;
|
||||
private Button _addBackgroundBtn;
|
||||
private LineEdit _nameEdit;
|
||||
private OptionButton _modSelect;
|
||||
private Label _modHint;
|
||||
private OptionButton _ambientSelect;
|
||||
private Button _ambientPlayBtn;
|
||||
private Label _ambientHint;
|
||||
private OptionButton _defaultSelect;
|
||||
private Label _defaultHint;
|
||||
private ItemList _scenarioList;
|
||||
private Button _openScenarioBtn;
|
||||
private Button _newScenarioBtn;
|
||||
private Button _deleteBtn;
|
||||
private Button _goBtn;
|
||||
|
||||
public SceneEditorWindow() : base("tools/scene_editor/title")
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
BuildUI();
|
||||
LoadSources();
|
||||
SetHint(ToolUi.Tr("tools/scene_editor/hint"));
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
// ── UI construction ────────────────────────────────────────
|
||||
|
||||
private void BuildUI()
|
||||
{
|
||||
var outer = CreateFillContainer();
|
||||
AddChild(outer);
|
||||
|
||||
var toolbar = CreateToolbar();
|
||||
outer.AddChild(toolbar);
|
||||
toolbar.AddChild(ToolUi.Button("tools/common/save", Save, ButtonKind.Primary, "tools/scene_editor/save_tip"));
|
||||
toolbar.AddChild(ToolUi.Button("tools/scene_editor/new_scene", NewScene, ButtonKind.Secondary, "tools/scene_editor/new_scene_tip"));
|
||||
_deleteBtn = ToolUi.Button("tools/scene_editor/delete", AskDelete, ButtonKind.Danger, "tools/scene_editor/delete_tip");
|
||||
toolbar.AddChild(_deleteBtn);
|
||||
toolbar.AddChild(ToolUi.Divider());
|
||||
_goBtn = ToolUi.Button("tools/scene_editor/go", GoToScene, ButtonKind.Secondary, "tools/scene_editor/go_tip");
|
||||
toolbar.AddChild(_goBtn);
|
||||
toolbar.AddChild(UiKit.Spacer());
|
||||
toolbar.AddChild(ToolUi.Button("tools/scenario_editor/rescan", Rescan, ButtonKind.Secondary, "tools/scene_editor/rescan_tip"));
|
||||
|
||||
var main = new HSplitContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
outer.AddChild(main);
|
||||
|
||||
var left = new VBoxContainer();
|
||||
main.AddChild(CreateSidebar(left, 260));
|
||||
left.AddChild(ToolUi.Section("tools/scene_editor/scenes"));
|
||||
_sceneList = new FilteredList(200);
|
||||
_sceneList.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
_sceneList.Chosen += OnScenePicked;
|
||||
left.AddChild(_sceneList);
|
||||
|
||||
var rest = new HSplitContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
main.AddChild(rest);
|
||||
rest.AddChild(BuildCentre());
|
||||
|
||||
var right = new VBoxContainer();
|
||||
right.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||
rest.AddChild(CreateSidebar(right, 340));
|
||||
BuildProperties(right);
|
||||
}
|
||||
|
||||
private Control BuildCentre()
|
||||
{
|
||||
var column = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||
};
|
||||
column.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||
|
||||
var frame = new PanelContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill,
|
||||
CustomMinimumSize = new Vector2(0, 260)
|
||||
};
|
||||
frame.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusMd, UiTokens.Well, UiTokens.Line));
|
||||
column.AddChild(frame);
|
||||
_previewRect = new TextureRect
|
||||
{
|
||||
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
|
||||
StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered,
|
||||
MouseFilter = Control.MouseFilterEnum.Ignore
|
||||
};
|
||||
frame.AddChild(_previewRect);
|
||||
_previewCaption = ToolUi.Hint(string.Empty);
|
||||
column.AddChild(_previewCaption);
|
||||
|
||||
column.AddChild(ToolUi.Section("tools/scene_editor/backgrounds"));
|
||||
_backgroundRow = new HFlowContainer();
|
||||
_backgroundRow.AddThemeConstantOverride("h_separation", UiTokens.Space2);
|
||||
_backgroundRow.AddThemeConstantOverride("v_separation", UiTokens.Space2);
|
||||
column.AddChild(_backgroundRow);
|
||||
|
||||
column.AddChild(ToolUi.Section("tools/scene_editor/all_backgrounds"));
|
||||
_picker = new FilteredList(120);
|
||||
_picker.Chosen += ShowPreview;
|
||||
_picker.Activated += AddBackground;
|
||||
column.AddChild(_picker);
|
||||
_addBackgroundBtn = ToolUi.Button("tools/scene_editor/add_background", AddPreviewed, ButtonKind.Secondary, "tools/scene_editor/add_background_tip");
|
||||
column.AddChild(_addBackgroundBtn);
|
||||
return column;
|
||||
}
|
||||
|
||||
private void BuildProperties(VBoxContainer column)
|
||||
{
|
||||
column.AddChild(ToolUi.Section("tools/scene_editor/properties"));
|
||||
_nameEdit = ToolUi.Field(column, "tools/scene_editor/name", ToolUi.Text("my_scene", OnNameChanged), "tools/scene_editor/name_tip");
|
||||
_modSelect = ToolUi.Field(column, "tools/scene_editor/mod", new OptionButton(), "tools/scene_editor/mod_tip");
|
||||
_modSelect.ItemSelected += OnModPicked;
|
||||
_modHint = ToolUi.Hint(string.Empty);
|
||||
column.AddChild(_modHint);
|
||||
|
||||
var ambientRow = new HBoxContainer();
|
||||
ambientRow.AddThemeConstantOverride("separation", UiTokens.Space1);
|
||||
_ambientSelect = new OptionButton { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, FitToLongestItem = false };
|
||||
_ambientSelect.ItemSelected += OnAmbientPicked;
|
||||
ambientRow.AddChild(_ambientSelect);
|
||||
_ambientPlayBtn = ToolUi.Button("tools/scene_editor/listen", ToggleAmbientPreview, ButtonKind.Secondary, "tools/scene_editor/listen_tip");
|
||||
ambientRow.AddChild(_ambientPlayBtn);
|
||||
ToolUi.Field(column, "tools/scene_editor/ambient", ambientRow, "tools/scene_editor/ambient_tip");
|
||||
_ambientHint = ToolUi.Hint(string.Empty);
|
||||
column.AddChild(_ambientHint);
|
||||
|
||||
_defaultSelect = ToolUi.Field(column, "tools/scene_editor/default_scenario",
|
||||
new OptionButton { FitToLongestItem = false }, "tools/scene_editor/default_scenario_tip");
|
||||
_defaultSelect.ItemSelected += OnDefaultPicked;
|
||||
_defaultHint = ToolUi.Hint(string.Empty);
|
||||
column.AddChild(_defaultHint);
|
||||
|
||||
column.AddChild(ToolUi.Section("tools/scene_editor/scenarios"));
|
||||
_scenarioList = new ItemList { CustomMinimumSize = new Vector2(0, 140), SizeFlagsVertical = Control.SizeFlags.ExpandFill };
|
||||
_scenarioList.ItemActivated += OnScenarioActivated;
|
||||
column.AddChild(_scenarioList);
|
||||
|
||||
var row = new HBoxContainer();
|
||||
row.AddThemeConstantOverride("separation", UiTokens.Space1);
|
||||
column.AddChild(row);
|
||||
_openScenarioBtn = ToolUi.Button("tools/scene_editor/open_scenario", OpenSelectedScenario, ButtonKind.Secondary, "tools/scene_editor/open_scenario_tip");
|
||||
_openScenarioBtn.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
row.AddChild(_openScenarioBtn);
|
||||
_newScenarioBtn = ToolUi.Button("tools/scene_editor/new_scenario", NewScenario, ButtonKind.Secondary, "tools/scene_editor/new_scenario_tip");
|
||||
_newScenarioBtn.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
row.AddChild(_newScenarioBtn);
|
||||
}
|
||||
|
||||
// ── loading ────────────────────────────────────────────────
|
||||
|
||||
private void LoadSources()
|
||||
{
|
||||
_files.Clear();
|
||||
_files.AddRange(SceneFileService.Scan());
|
||||
_writableMods.Clear();
|
||||
_writableMods.AddRange(ModFileService.ListWritableMods());
|
||||
_sounds.Clear();
|
||||
_sounds.AddRange(ModResourceService.GetSoundIds());
|
||||
|
||||
_picker.SetItems(ModResourceService.GetBackgroundIds());
|
||||
_modSelect.Clear();
|
||||
foreach (var mod in _writableMods)
|
||||
_modSelect.AddItem(mod);
|
||||
_ambientSelect.Clear();
|
||||
_ambientSelect.AddItem(ToolUi.Tr("tools/scene_editor/none"));
|
||||
foreach (var sound in _sounds)
|
||||
_ambientSelect.AddItem(sound);
|
||||
RebuildSceneList();
|
||||
}
|
||||
|
||||
private void RebuildSceneList()
|
||||
{
|
||||
var items = new List<(string Id, string Text, Texture2D Icon)>();
|
||||
foreach (var group in _files.GroupBy(f => f.Name).OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase))
|
||||
items.Add((group.Key, group.Key + " · " + string.Join(", ", group.Select(f => f.ModId).Distinct()), null));
|
||||
_sceneList.SetItems(items);
|
||||
_sceneList.Select(_isNew ? null : _sceneName);
|
||||
}
|
||||
|
||||
private void OnScenePicked(string name)
|
||||
{
|
||||
if (name == _sceneName && !_isNew)
|
||||
return;
|
||||
_pendingScene = name;
|
||||
RunDiscardingChanges(OpenPendingScene, KeepSceneSelection);
|
||||
}
|
||||
|
||||
private void OpenPendingScene() => OpenScene(_pendingScene);
|
||||
|
||||
private void KeepSceneSelection() => _sceneList.Select(_isNew ? null : _sceneName);
|
||||
|
||||
private void OpenScene(string name)
|
||||
{
|
||||
_sceneName = name;
|
||||
_isNew = false;
|
||||
var target = FilesOf(name).LastOrDefault(f => _writableMods.Contains(f.ModId))?.ModId
|
||||
?? _writableMods.FirstOrDefault();
|
||||
LoadTarget(target);
|
||||
_preview = Merged().FirstOrDefault().Background;
|
||||
MarkClean();
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/opened"), name));
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
/// <summary>Takes the chosen mod's file of the current scene as the edited one (a copy), or a new empty file.</summary>
|
||||
private void LoadTarget(string modId)
|
||||
{
|
||||
if (modId == null)
|
||||
{
|
||||
_target = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var own = FilesOf(_sceneName).FirstOrDefault(f => f.ModId == modId);
|
||||
_target = new SceneFileEntry
|
||||
{
|
||||
ModId = modId,
|
||||
RelativePath = own?.RelativePath ?? SceneFileEntry.PathFor(_sceneName ?? string.Empty),
|
||||
Name = _sceneName,
|
||||
Backgrounds = new List<string>(own?.Backgrounds ?? new List<string>()),
|
||||
Ambient = own?.Ambient ?? string.Empty,
|
||||
DefaultScenario = own?.DefaultScenario ?? string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<SceneFileEntry> FilesOf(string name) => _files.Where(f => f.Name == name);
|
||||
|
||||
/// <summary>
|
||||
/// The scene as the game will merge it after saving: the other mods' files in load
|
||||
/// order with the edited file at its mod's place. Yields each background with the
|
||||
/// file it comes from.
|
||||
/// </summary>
|
||||
private IEnumerable<(string Background, SceneFileEntry File)> Merged()
|
||||
{
|
||||
foreach (var file in MergedFiles())
|
||||
{
|
||||
foreach (var background in file.Backgrounds)
|
||||
yield return (background, file);
|
||||
}
|
||||
}
|
||||
|
||||
private List<SceneFileEntry> MergedFiles()
|
||||
{
|
||||
var order = ModRegistry.Instance.GetOrderedModIds();
|
||||
var result = FilesOf(_sceneName).Where(f => _target == null || f.ModId != _target.ModId).ToList();
|
||||
if (_target != null)
|
||||
result.Add(_target);
|
||||
return result.OrderBy(f => Rank(order, f.ModId)).ToList();
|
||||
}
|
||||
|
||||
private static int Rank(IReadOnlyList<string> order, string modId)
|
||||
{
|
||||
for (var i = 0; i < order.Count; i++)
|
||||
{
|
||||
if (order[i] == modId)
|
||||
return i;
|
||||
}
|
||||
return order.Count;
|
||||
}
|
||||
|
||||
// ── editing ────────────────────────────────────────────────
|
||||
|
||||
private void NewScene()
|
||||
{
|
||||
RunDiscardingChanges(StartNewScene);
|
||||
}
|
||||
|
||||
private void StartNewScene()
|
||||
{
|
||||
_sceneName = string.Empty;
|
||||
_isNew = true;
|
||||
LoadTarget(_writableMods.FirstOrDefault());
|
||||
_preview = null;
|
||||
_sceneList.Select(null);
|
||||
SetDirty();
|
||||
SetStatus(ToolUi.Tr("tools/scene_editor/new_started"));
|
||||
RefreshAll();
|
||||
_nameEdit.GrabFocus();
|
||||
}
|
||||
|
||||
private void OnNameChanged(string text)
|
||||
{
|
||||
if (_suppress || !_isNew || _target == null)
|
||||
return;
|
||||
_sceneName = SceneFileService.ValidName(text) ?? string.Empty;
|
||||
_target.Name = _sceneName;
|
||||
_target.RelativePath = SceneFileEntry.PathFor(_sceneName);
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
private void OnModPicked(long index)
|
||||
{
|
||||
if (_suppress)
|
||||
return;
|
||||
var mod = index >= 0 && index < _writableMods.Count ? _writableMods[(int)index] : null;
|
||||
if (_isNew)
|
||||
{
|
||||
if (_target != null)
|
||||
_target.ModId = mod;
|
||||
RefreshAll();
|
||||
return;
|
||||
}
|
||||
_pendingMod = mod;
|
||||
RunDiscardingChanges(SwitchTargetMod, RefreshAll);
|
||||
}
|
||||
|
||||
private void SwitchTargetMod()
|
||||
{
|
||||
LoadTarget(_pendingMod);
|
||||
MarkClean();
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void OnAmbientPicked(long index)
|
||||
{
|
||||
if (_suppress || _target == null)
|
||||
return;
|
||||
_target.Ambient = index > 0 ? _sounds[(int)index - 1] : string.Empty;
|
||||
StopAmbientPreview();
|
||||
SetDirty();
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void OnDefaultPicked(long index)
|
||||
{
|
||||
if (_suppress || _target == null)
|
||||
return;
|
||||
_target.DefaultScenario = index > 0 ? _defaultChoices[(int)index - 1] : string.Empty;
|
||||
SetDirty();
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void ShowPreview(string background)
|
||||
{
|
||||
_preview = background;
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
private void AddPreviewed() => AddBackground(_preview);
|
||||
|
||||
private void AddBackground(string background)
|
||||
{
|
||||
if (_target == null || string.IsNullOrEmpty(background))
|
||||
return;
|
||||
if (Merged().Any(m => m.Background == background))
|
||||
{
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/already_there"), background));
|
||||
return;
|
||||
}
|
||||
_target.Backgrounds.Add(background);
|
||||
_preview = background;
|
||||
SetDirty();
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void RemoveBackground(string background)
|
||||
{
|
||||
if (_target == null || !_target.Backgrounds.Remove(background))
|
||||
return;
|
||||
if (_preview == background)
|
||||
_preview = Merged().FirstOrDefault().Background;
|
||||
SetDirty();
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
// ── saving, deleting, going ────────────────────────────────
|
||||
|
||||
/// <summary>Ctrl+S.</summary>
|
||||
protected override void OnSaveShortcut() => Save();
|
||||
|
||||
private void Save()
|
||||
{
|
||||
if (_target == null || !ModFileService.IsWritable(_target.ModId))
|
||||
{
|
||||
SetStatus(ToolUi.Tr("tools/scene_editor/no_writable_mod"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(_sceneName))
|
||||
{
|
||||
SetStatus(ToolUi.Tr("tools/scene_editor/bad_name"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
if (_isNew && _files.Any(f => f.Name == _sceneName))
|
||||
{
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/name_taken"), _sceneName), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
if (!Merged().Any())
|
||||
{
|
||||
SetStatus(ToolUi.Tr("tools/scene_editor/need_background"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SceneFileService.Write(_target))
|
||||
{
|
||||
SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var created = _isNew;
|
||||
SceneManager.ReloadContent();
|
||||
var name = _sceneName;
|
||||
var mod = _target.ModId;
|
||||
LoadSources();
|
||||
_sceneName = name;
|
||||
_isNew = false;
|
||||
LoadTarget(mod);
|
||||
MarkClean();
|
||||
_sceneList.Select(name);
|
||||
RefreshAll();
|
||||
SetStatus(string.Format(ToolUi.Tr(created ? "tools/scene_editor/created" : "tools/common/saved"),
|
||||
created ? name : mod + "/" + _target.RelativePath), ToolStatusKind.Ok);
|
||||
}
|
||||
|
||||
private void AskDelete()
|
||||
{
|
||||
if (_target == null || _isNew || !FilesOf(_sceneName).Any(f => f.ModId == _target.ModId))
|
||||
return;
|
||||
ShowConfirm("tools/scene_editor/delete", "tools/scene_editor/delete_confirm", Delete);
|
||||
}
|
||||
|
||||
private void Delete()
|
||||
{
|
||||
if (!SceneFileService.Delete(_target))
|
||||
{
|
||||
SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var path = _target.ModId + "/" + _target.RelativePath;
|
||||
SceneManager.ReloadContent();
|
||||
LoadSources();
|
||||
if (FilesOf(_sceneName).Any())
|
||||
{
|
||||
OpenScene(_sceneName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sceneName = null;
|
||||
_target = null;
|
||||
MarkClean();
|
||||
RefreshAll();
|
||||
}
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/deleted"), path), ToolStatusKind.Ok);
|
||||
}
|
||||
|
||||
private void GoToScene()
|
||||
{
|
||||
if (_isNew || string.IsNullOrEmpty(_sceneName))
|
||||
return;
|
||||
var manager = GetTree().Root.GetNodeOrNull<SceneManager>("SceneManager");
|
||||
if (manager == null || !manager.HasScene(_sceneName))
|
||||
{
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/go_failed"), _sceneName), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
StopAmbientPreview();
|
||||
// Like the studio's teleport: a dialog left running would keep the box, and the
|
||||
// entered scene's own scenario would wait behind it (or the old one would go on).
|
||||
SceneContextNode.FindNode<DialogBox>("DialogBox")?.Abort();
|
||||
manager.SwitchScene(_sceneName);
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/went"), _sceneName), ToolStatusKind.Ok);
|
||||
}
|
||||
|
||||
private void Rescan()
|
||||
{
|
||||
RunDiscardingChanges(RescanNow);
|
||||
}
|
||||
|
||||
private void RescanNow()
|
||||
{
|
||||
var name = _isNew ? null : _sceneName;
|
||||
LoadSources();
|
||||
if (name != null && FilesOf(name).Any())
|
||||
OpenScene(name);
|
||||
else
|
||||
RefreshAll();
|
||||
SetStatus(ToolUi.Tr("tools/scene_editor/rescanned"));
|
||||
}
|
||||
|
||||
// ── scenarios ──────────────────────────────────────────────
|
||||
|
||||
private void NewScenario()
|
||||
{
|
||||
if (_target == null || _isNew || string.IsNullOrEmpty(_sceneName))
|
||||
return;
|
||||
var key = ScenarioFileService.CreateForScene(_target.ModId, _sceneName, NewLine());
|
||||
if (key == null)
|
||||
{
|
||||
SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error);
|
||||
return;
|
||||
}
|
||||
SceneManager.ReloadContent();
|
||||
RefreshAll();
|
||||
OpenScenarioEditor(key);
|
||||
SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/scenario_created"), key), ToolStatusKind.Ok);
|
||||
}
|
||||
|
||||
private void OnScenarioActivated(long index) => OpenSelectedScenario();
|
||||
|
||||
private void OpenSelectedScenario()
|
||||
{
|
||||
var selected = _scenarioList.GetSelectedItems();
|
||||
if (selected.Length == 0 || selected[0] >= _scenarioKeys.Count)
|
||||
return;
|
||||
OpenScenarioEditor(_scenarioKeys[selected[0]]);
|
||||
}
|
||||
|
||||
private static void OpenScenarioEditor(string key)
|
||||
{
|
||||
var window = new ScenarioEditorWindow();
|
||||
window.OpenScenario(key);
|
||||
ShowWindow(window);
|
||||
}
|
||||
|
||||
private static string NewLine() => "text \"" + ToolUi.Tr("tools/scene_editor/new_line_text").Replace("\"", "\\\"") + "\"";
|
||||
|
||||
// ── ambient preview ────────────────────────────────────────
|
||||
|
||||
private void ToggleAmbientPreview()
|
||||
{
|
||||
if (_ambientPlaying)
|
||||
{
|
||||
StopAmbientPreview();
|
||||
RefreshAll();
|
||||
return;
|
||||
}
|
||||
|
||||
var ambient = EffectiveAmbient().Ambient;
|
||||
var audio = GetTree().Root.GetNodeOrNull<Node>("AudioService") as IAudioService;
|
||||
if (audio == null || string.IsNullOrEmpty(ambient))
|
||||
return;
|
||||
audio.PlayAmbient(ambient);
|
||||
_ambientPlaying = true;
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void StopAmbientPreview()
|
||||
{
|
||||
if (!_ambientPlaying)
|
||||
return;
|
||||
_ambientPlaying = false;
|
||||
(GetTree()?.Root.GetNodeOrNull<Node>("AudioService") as IAudioService)?.StopAmbient();
|
||||
}
|
||||
|
||||
/// <summary>Stops a playing ambient preview when the window closes.</summary>
|
||||
protected override void Cleanup() => StopAmbientPreview();
|
||||
|
||||
// ── refresh ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>The ambient the game will play: the last non-empty one in load order, and the mod it comes from.</summary>
|
||||
private (string Ambient, string ModId) EffectiveAmbient()
|
||||
{
|
||||
var file = MergedFiles().LastOrDefault(f => !string.IsNullOrEmpty(f.Ambient));
|
||||
return (file?.Ambient ?? string.Empty, file?.ModId);
|
||||
}
|
||||
|
||||
private (string Scenario, string ModId) EffectiveDefault()
|
||||
{
|
||||
var file = MergedFiles().LastOrDefault(f => !string.IsNullOrEmpty(f.DefaultScenario));
|
||||
return (file?.DefaultScenario ?? string.Empty, file?.ModId);
|
||||
}
|
||||
|
||||
private void RefreshAll()
|
||||
{
|
||||
_suppress = true;
|
||||
var hasScene = _target != null && (_isNew || !string.IsNullOrEmpty(_sceneName));
|
||||
|
||||
_nameEdit.Text = _sceneName ?? string.Empty;
|
||||
_nameEdit.Editable = _isNew;
|
||||
_modSelect.Disabled = !hasScene;
|
||||
_modSelect.Select(_target == null ? -1 : _writableMods.IndexOf(_target.ModId));
|
||||
_modHint.Text = ModHint();
|
||||
|
||||
_ambientSelect.Disabled = !hasScene;
|
||||
_ambientSelect.Select(_target == null ? 0 : _sounds.IndexOf(_target.Ambient) + 1);
|
||||
var ambient = EffectiveAmbient();
|
||||
_ambientHint.Text = ambient.ModId != null && ambient.ModId != _target?.ModId
|
||||
? string.Format(ToolUi.Tr("tools/scene_editor/effective"), ambient.Ambient, ambient.ModId)
|
||||
: string.Empty;
|
||||
_ambientPlayBtn.Text = ToolUi.Tr(_ambientPlaying ? "tools/scene_editor/stop" : "tools/scene_editor/listen");
|
||||
_ambientPlayBtn.Disabled = string.IsNullOrEmpty(ambient.Ambient) && !_ambientPlaying;
|
||||
|
||||
RefreshScenarios(hasScene);
|
||||
RefreshBackgrounds();
|
||||
RefreshPreview();
|
||||
|
||||
_deleteBtn.Disabled = _target == null || _isNew || !FilesOf(_sceneName).Any(f => f.ModId == _target.ModId);
|
||||
_goBtn.Disabled = _isNew || string.IsNullOrEmpty(_sceneName);
|
||||
_suppress = false;
|
||||
}
|
||||
|
||||
private string ModHint()
|
||||
{
|
||||
if (_writableMods.Count == 0)
|
||||
return ToolUi.Tr("tools/scene_editor/no_writable_mod");
|
||||
if (_target == null || _isNew)
|
||||
return string.Empty;
|
||||
var others = FilesOf(_sceneName).Select(f => f.ModId).Where(m => m != _target.ModId).Distinct().ToList();
|
||||
var own = FilesOf(_sceneName).Any(f => f.ModId == _target.ModId);
|
||||
if (!own)
|
||||
return string.Format(ToolUi.Tr("tools/scene_editor/adds_to"), string.Join(", ", others));
|
||||
return others.Count > 0 ? string.Format(ToolUi.Tr("tools/scene_editor/also_in"), string.Join(", ", others)) : string.Empty;
|
||||
}
|
||||
|
||||
private void RefreshScenarios(bool hasScene)
|
||||
{
|
||||
_scenarioKeys.Clear();
|
||||
_defaultChoices.Clear();
|
||||
_scenarioList.Clear();
|
||||
if (hasScene && !_isNew)
|
||||
{
|
||||
foreach (var file in ScenarioFileService.ScanScenarioFiles().SelectMany(m => m.Groups).SelectMany(g => g.Files))
|
||||
{
|
||||
var text = ScenarioFileService.ReadAllText(file.ModId, file.RelativePath);
|
||||
if (text == null || ScenarioFileService.ParseMetadata(text).Scene != _sceneName)
|
||||
continue;
|
||||
_scenarioKeys.Add(file.Key);
|
||||
_scenarioList.AddItem(file.ModId + ": " + file.RelativePath);
|
||||
if (!_defaultChoices.Contains(file.RelativePath))
|
||||
_defaultChoices.Add(file.RelativePath);
|
||||
}
|
||||
}
|
||||
if (_target != null && !string.IsNullOrEmpty(_target.DefaultScenario) && !_defaultChoices.Contains(_target.DefaultScenario))
|
||||
_defaultChoices.Add(_target.DefaultScenario);
|
||||
|
||||
_defaultSelect.Clear();
|
||||
_defaultSelect.AddItem(ToolUi.Tr("tools/scene_editor/none"));
|
||||
foreach (var path in _defaultChoices)
|
||||
_defaultSelect.AddItem(path);
|
||||
_defaultSelect.Select(_target == null ? 0 : _defaultChoices.IndexOf(_target.DefaultScenario) + 1);
|
||||
_defaultSelect.Disabled = !hasScene || _isNew;
|
||||
var effective = EffectiveDefault();
|
||||
_defaultHint.Text = effective.ModId != null && effective.ModId != _target?.ModId
|
||||
? string.Format(ToolUi.Tr("tools/scene_editor/effective"), effective.Scenario, effective.ModId)
|
||||
: string.Empty;
|
||||
|
||||
if (hasScene && !_isNew && _scenarioKeys.Count == 0)
|
||||
_scenarioList.AddItem(ToolUi.Tr("tools/scene_editor/no_scenarios"), null, false);
|
||||
_openScenarioBtn.Disabled = _scenarioKeys.Count == 0;
|
||||
_newScenarioBtn.Disabled = !hasScene || _isNew || !ModFileService.IsWritable(_target?.ModId);
|
||||
}
|
||||
|
||||
private void RefreshBackgrounds()
|
||||
{
|
||||
foreach (var child in _backgroundRow.GetChildren())
|
||||
{
|
||||
_backgroundRow.RemoveChild(child);
|
||||
child.QueueFree();
|
||||
}
|
||||
|
||||
var any = false;
|
||||
foreach (var (background, file) in Merged())
|
||||
{
|
||||
any = true;
|
||||
_backgroundRow.AddChild(new SceneBackgroundTile(background, file.ModId, file == _target, background == _preview,
|
||||
ShowPreview, RemoveBackground));
|
||||
}
|
||||
if (!any)
|
||||
_backgroundRow.AddChild(ToolUi.Hint(ToolUi.Tr(_target == null ? "tools/scene_editor/pick_scene" : "tools/scene_editor/no_backgrounds")));
|
||||
}
|
||||
|
||||
private void RefreshPreview()
|
||||
{
|
||||
_previewRect.Texture = string.IsNullOrEmpty(_preview) ? null : ModResourceService.LoadBackgroundTexture(_preview);
|
||||
_previewCaption.Text = string.IsNullOrEmpty(_preview)
|
||||
? ToolUi.Tr("tools/scene_editor/preview_hint")
|
||||
: _preview + (Merged().Any(m => m.Background == _preview) ? string.Empty : " " + ToolUi.Tr("tools/scene_editor/not_in_scene"));
|
||||
if (_addBackgroundBtn != null)
|
||||
_addBackgroundBtn.Disabled = _target == null || string.IsNullOrEmpty(_preview) || Merged().Any(m => m.Background == _preview);
|
||||
}
|
||||
|
||||
internal static Vector2 ThumbSize => new(ThumbWidth, ThumbHeight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Cthangover.Tools.Schemas
|
||||
{
|
||||
using Cthangover.Mods.Schemas;
|
||||
using SchemaFieldDefinition = Cthangover.Mods.Schemas.FieldSchema;
|
||||
using SchemaJsonValues = Cthangover.Mods.Schemas.SchemaJson;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
/// <summary>One field's editor in the data editor form: shows the field's JSON and gives it back edited.</summary>
|
||||
public interface ISchemaFieldEditor
|
||||
{
|
||||
/// <summary>The described field.</summary>
|
||||
SchemaFieldDefinition Field { get; }
|
||||
|
||||
/// <summary>Shows <paramref name="node"/> (null: the field is absent).</summary>
|
||||
void Load(JsonNode node);
|
||||
|
||||
/// <summary>The edited value, or null to remove the key.</summary>
|
||||
JsonNode Save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
namespace Cthangover.Tools.Schemas
|
||||
{
|
||||
using Cthangover.Mods.Schemas;
|
||||
using SchemaFieldDefinition = Cthangover.Mods.Schemas.FieldSchema;
|
||||
using SchemaJsonValues = Cthangover.Mods.Schemas.SchemaJson;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
using Cthangover.Tools.Common;
|
||||
using Godot;
|
||||
|
||||
/// <summary>
|
||||
/// Edits a <c>list</c> field — an array of objects described by <see cref="SchemaFieldDefinition.Of"/>
|
||||
/// (an item's traits, an archetype's learnings, a shop's goods) — as a stack of small
|
||||
/// cards with an add and a remove button, each card a form of the sub-fields.
|
||||
/// <para/>
|
||||
/// The <c>values</c> type is the same editor over the engine's older list of
|
||||
/// <c>"Key=Value"</c> strings (a battle action's <c>Values</c>): known keys become fields,
|
||||
/// entries the schema does not know are kept as they were.
|
||||
/// </summary>
|
||||
public class ListFieldEditor : ISchemaFieldEditor
|
||||
{
|
||||
private readonly SchemaFieldDefinition field;
|
||||
private readonly Action changed;
|
||||
private readonly VBoxContainer cards;
|
||||
private readonly List<List<SchemaFieldControl>> rows = new();
|
||||
private readonly List<string> unknownValues = new();
|
||||
private bool suppress;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SchemaFieldDefinition Field => field;
|
||||
|
||||
/// <summary>Adds the editor for <paramref name="field"/> to <paramref name="parent"/>.</summary>
|
||||
public ListFieldEditor(Container parent, SchemaFieldDefinition field, Action changed)
|
||||
{
|
||||
this.field = field;
|
||||
this.changed = changed;
|
||||
var box = new VBoxContainer { Name = "List_" + field.Key };
|
||||
box.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||
cards = new VBoxContainer();
|
||||
cards.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||
box.AddChild(cards);
|
||||
var add = ToolUi.Button("tools/schema/list_add", AddEmpty, ButtonKind.Secondary);
|
||||
add.Name = "Add";
|
||||
add.SizeFlagsHorizontal = Control.SizeFlags.ShrinkBegin;
|
||||
box.AddChild(add);
|
||||
ToolUi.Field(parent, string.IsNullOrEmpty(field.Name) ? field.Key : field.Name, box, field.Description);
|
||||
}
|
||||
|
||||
private bool IsValues => string.Equals(field.Type, "values", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Shows the array <paramref name="node"/> (null or not an array: empty).</summary>
|
||||
public void Load(JsonNode node)
|
||||
{
|
||||
suppress = true;
|
||||
Clear();
|
||||
unknownValues.Clear();
|
||||
if (node is JsonArray array)
|
||||
{
|
||||
if (IsValues)
|
||||
LoadValues(array);
|
||||
else
|
||||
foreach (var item in array.OfType<JsonObject>())
|
||||
AddCard(item);
|
||||
}
|
||||
suppress = false;
|
||||
}
|
||||
|
||||
/// <summary>The edited array, or null when it has no entries (the key is then removed).</summary>
|
||||
public JsonNode Save()
|
||||
{
|
||||
var array = new JsonArray();
|
||||
if (IsValues)
|
||||
{
|
||||
foreach (var card in rows)
|
||||
{
|
||||
foreach (var control in card.Where(c => c.Value.Length > 0))
|
||||
array.Add(control.Field.Key + "=" + control.Value);
|
||||
}
|
||||
foreach (var raw in unknownValues)
|
||||
array.Add(raw);
|
||||
return array.Count == 0 ? null : array;
|
||||
}
|
||||
foreach (var card in rows)
|
||||
{
|
||||
var item = new JsonObject();
|
||||
foreach (var control in card.Where(c => c.Value.Length > 0))
|
||||
item[control.Field.Key] = SchemaJsonValues.ToNode(control.Field, control.Value);
|
||||
if (item.Count > 0)
|
||||
array.Add(item);
|
||||
}
|
||||
return array.Count == 0 ? null : array;
|
||||
}
|
||||
|
||||
private void LoadValues(JsonArray array)
|
||||
{
|
||||
var known = new JsonObject();
|
||||
foreach (var raw in array.Select(n => n?.ToString()).Where(s => s != null))
|
||||
{
|
||||
var eq = raw.IndexOf('=');
|
||||
var key = eq > 0 ? raw.Substring(0, eq) : raw;
|
||||
var def = field.Of?.FirstOrDefault(f => string.Equals(f.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
if (def == null || eq <= 0)
|
||||
unknownValues.Add(raw);
|
||||
else
|
||||
known[def.Key] = raw.Substring(eq + 1);
|
||||
}
|
||||
AddCard(known);
|
||||
}
|
||||
|
||||
private void AddEmpty()
|
||||
{
|
||||
AddCard(new JsonObject());
|
||||
if (!suppress)
|
||||
changed?.Invoke();
|
||||
}
|
||||
|
||||
private void AddCard(JsonObject item)
|
||||
{
|
||||
if (IsValues && rows.Count > 0)
|
||||
return;
|
||||
var card = new PanelContainer { Name = "Card" + rows.Count };
|
||||
card.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusSm, UiTokens.Well, UiTokens.LineSoft));
|
||||
var column = new VBoxContainer();
|
||||
card.AddChild(column);
|
||||
var controls = new List<SchemaFieldControl>();
|
||||
foreach (var sub in field.Of ?? new List<SchemaFieldDefinition>())
|
||||
{
|
||||
var control = new SchemaFieldControl(column, sub, OnChanged);
|
||||
control.Value = SchemaJsonValues.ToText(Find(item, sub.Key));
|
||||
controls.Add(control);
|
||||
}
|
||||
if (!IsValues)
|
||||
{
|
||||
var remove = ToolUi.Button("tools/schema/list_remove", () => Remove(card, controls), ButtonKind.Secondary);
|
||||
remove.Name = "Remove";
|
||||
remove.SizeFlagsHorizontal = Control.SizeFlags.ShrinkEnd;
|
||||
column.AddChild(remove);
|
||||
}
|
||||
rows.Add(controls);
|
||||
cards.AddChild(card);
|
||||
}
|
||||
|
||||
private static JsonNode Find(JsonObject item, string key)
|
||||
=> item.FirstOrDefault(p => string.Equals(p.Key, key, StringComparison.OrdinalIgnoreCase)).Value;
|
||||
|
||||
private void Remove(Control card, List<SchemaFieldControl> controls)
|
||||
{
|
||||
rows.Remove(controls);
|
||||
cards.RemoveChild(card);
|
||||
card.QueueFree();
|
||||
changed?.Invoke();
|
||||
}
|
||||
|
||||
private void OnChanged()
|
||||
{
|
||||
if (!suppress)
|
||||
changed?.Invoke();
|
||||
}
|
||||
|
||||
private void Clear()
|
||||
{
|
||||
rows.Clear();
|
||||
foreach (var child in cards.GetChildren())
|
||||
{
|
||||
cards.RemoveChild(child);
|
||||
child.QueueFree();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
namespace Cthangover.Tools.Schemas
|
||||
{
|
||||
using Cthangover.Mods.Schemas;
|
||||
using SchemaFieldDefinition = Cthangover.Mods.Schemas.FieldSchema;
|
||||
using SchemaJsonValues = Cthangover.Mods.Schemas.SchemaJson;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Tools.Common;
|
||||
using Cthangover.Tools.EntityEditor;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
/// <summary>
|
||||
/// Edits a <c>locale</c> field: the locale key the JSON holds and, below it, the text of
|
||||
/// that key in every language of the mod — the way the item and character editors do,
|
||||
/// so a beginner types names and a professional can still rename the key. Texts go
|
||||
/// through the <see cref="EntityDocument"/> and are written by the window's Save.
|
||||
/// </summary>
|
||||
public class LocaleFieldEditor : ISchemaFieldEditor
|
||||
{
|
||||
private readonly SchemaFieldDefinition field;
|
||||
private readonly Action changed;
|
||||
private readonly LineEdit keyEdit;
|
||||
private readonly VBoxContainer texts;
|
||||
private readonly Dictionary<string, LineEdit> edits = new();
|
||||
private EntityDocument document;
|
||||
private string modId;
|
||||
private bool suppress;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SchemaFieldDefinition Field => field;
|
||||
|
||||
/// <summary>Adds the editor for <paramref name="field"/> to <paramref name="parent"/>.</summary>
|
||||
public LocaleFieldEditor(Container parent, SchemaFieldDefinition field, Action changed)
|
||||
{
|
||||
this.field = field;
|
||||
this.changed = changed;
|
||||
keyEdit = ToolUi.Field(parent, string.IsNullOrEmpty(field.Name) ? field.Key : field.Name, ToolUi.Text(string.Empty, OnTyped), "tools/entity_editor/locale_key_tip");
|
||||
keyEdit.Name = "Key_" + field.Key;
|
||||
keyEdit.AddThemeFontOverride("font", ToolUi.Mono());
|
||||
texts = new VBoxContainer();
|
||||
parent.AddChild(texts);
|
||||
}
|
||||
|
||||
/// <summary>Builds one text row per language of <paramref name="mod"/>.</summary>
|
||||
public void Open(EntityDocument doc, string mod, IEnumerable<string> languages)
|
||||
{
|
||||
document = doc;
|
||||
modId = mod;
|
||||
foreach (var child in texts.GetChildren())
|
||||
{
|
||||
texts.RemoveChild(child);
|
||||
child.QueueFree();
|
||||
}
|
||||
edits.Clear();
|
||||
foreach (var language in languages)
|
||||
{
|
||||
var row = new HBoxContainer();
|
||||
row.AddThemeConstantOverride("separation", UiTokens.Space3);
|
||||
row.AddChild(new Label { Text = string.Format(ToolUi.Tr("tools/entity_editor/text_in"), language), CustomMinimumSize = new Vector2(ToolUi.LabelWidth, 0), HorizontalAlignment = HorizontalAlignment.Right });
|
||||
var edit = ToolUi.Text(string.Empty, OnTyped);
|
||||
edit.Name = "Text_" + field.Key + "_" + language;
|
||||
edit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
row.AddChild(edit);
|
||||
texts.AddChild(row);
|
||||
edits[language] = edit;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shows the key in <paramref name="node"/> and its texts.</summary>
|
||||
public void Load(JsonNode node)
|
||||
{
|
||||
suppress = true;
|
||||
var key = node?.ToString() ?? string.Empty;
|
||||
keyEdit.Text = key;
|
||||
foreach (var pair in edits)
|
||||
pair.Value.Text = TextOf(pair.Key, key);
|
||||
suppress = false;
|
||||
}
|
||||
|
||||
/// <summary>The key for the JSON (null when empty); texts are put into the document.</summary>
|
||||
public JsonNode Save()
|
||||
{
|
||||
var key = keyEdit.Text.Trim();
|
||||
if (key.Length == 0)
|
||||
return null;
|
||||
foreach (var pair in edits)
|
||||
document?.SetText(pair.Key, key, pair.Value.Text);
|
||||
return JsonValue.Create(key);
|
||||
}
|
||||
|
||||
private string TextOf(string language, string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || document == null)
|
||||
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 void OnTyped(string text)
|
||||
{
|
||||
if (!suppress)
|
||||
changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
namespace Cthangover.Tools.Schemas
|
||||
{
|
||||
using Cthangover.Mods.Schemas;
|
||||
using SchemaFieldDefinition = Cthangover.Mods.Schemas.FieldSchema;
|
||||
using SchemaJsonValues = Cthangover.Mods.Schemas.SchemaJson;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
using Cthangover.Tools.Common;
|
||||
using Godot;
|
||||
|
||||
/// <summary>
|
||||
/// Edits a <c>map</c> field — an object whose keys are ids of <see cref="SchemaFieldDefinition.Kind"/>
|
||||
/// and whose values are numbers or formulas (a character's own <c>Stats</c>, an archetype's
|
||||
/// growth): one row per key with a key picker, a value box and a remove button. A number
|
||||
/// is written as a number, anything else as a formula text.
|
||||
/// </summary>
|
||||
public class MapFieldEditor : ISchemaFieldEditor
|
||||
{
|
||||
private readonly SchemaFieldDefinition field;
|
||||
private readonly Action changed;
|
||||
private readonly VBoxContainer lines;
|
||||
private readonly List<(OptionButton Key, List<string> Keys, LineEdit Value, Control Row)> rows = new();
|
||||
private bool suppress;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SchemaFieldDefinition Field => field;
|
||||
|
||||
/// <summary>Adds the editor for <paramref name="field"/> to <paramref name="parent"/>.</summary>
|
||||
public MapFieldEditor(Container parent, SchemaFieldDefinition field, Action changed)
|
||||
{
|
||||
this.field = field;
|
||||
this.changed = changed;
|
||||
var box = new VBoxContainer { Name = "Map_" + field.Key };
|
||||
lines = new VBoxContainer();
|
||||
lines.AddThemeConstantOverride("separation", UiTokens.Space1);
|
||||
box.AddChild(lines);
|
||||
var add = ToolUi.Button("tools/schema/map_add", AddEmpty, ButtonKind.Secondary);
|
||||
add.Name = "Add";
|
||||
add.SizeFlagsHorizontal = Control.SizeFlags.ShrinkBegin;
|
||||
box.AddChild(add);
|
||||
ToolUi.Field(parent, string.IsNullOrEmpty(field.Name) ? field.Key : field.Name, box, field.Description);
|
||||
}
|
||||
|
||||
/// <summary>Shows the object <paramref name="node"/>.</summary>
|
||||
public void Load(JsonNode node)
|
||||
{
|
||||
suppress = true;
|
||||
foreach (var row in rows)
|
||||
{
|
||||
lines.RemoveChild(row.Row);
|
||||
row.Row.QueueFree();
|
||||
}
|
||||
rows.Clear();
|
||||
if (node is JsonObject obj)
|
||||
{
|
||||
foreach (var pair in obj)
|
||||
AddRow(pair.Key, SchemaJsonValues.ToText(pair.Value));
|
||||
}
|
||||
suppress = false;
|
||||
}
|
||||
|
||||
/// <summary>The edited object, or null when empty.</summary>
|
||||
public JsonNode Save()
|
||||
{
|
||||
var obj = new JsonObject();
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var key = row.Key.Selected >= 0 && row.Key.Selected < row.Keys.Count ? row.Keys[row.Key.Selected] : null;
|
||||
if (!string.IsNullOrEmpty(key) && row.Value.Text.Trim().Length > 0)
|
||||
obj[key] = SchemaJsonValues.NumberOrText(row.Value.Text.Trim());
|
||||
}
|
||||
return obj.Count == 0 ? null : obj;
|
||||
}
|
||||
|
||||
private void AddEmpty()
|
||||
{
|
||||
AddRow(null, string.Empty);
|
||||
changed?.Invoke();
|
||||
}
|
||||
|
||||
private void AddRow(string key, string value)
|
||||
{
|
||||
var row = new HBoxContainer { Name = "Row" + rows.Count };
|
||||
row.AddThemeConstantOverride("separation", UiTokens.Space2);
|
||||
var keys = new List<string> { string.Empty };
|
||||
if (!string.IsNullOrEmpty(key) && !GameEntityIndex.Of(field.Kind).ContainsKey(key))
|
||||
keys.Add(key);
|
||||
keys.AddRange(GameEntityIndex.Of(field.Kind).Keys);
|
||||
var picker = new OptionButton { CustomMinimumSize = new Vector2(200, 0) };
|
||||
foreach (var k in keys)
|
||||
picker.AddItem(k.Length == 0 ? "—" : GameEntityIndex.Label(field.Kind, k));
|
||||
picker.Select(Math.Max(0, keys.FindIndex(k => string.Equals(k, key, StringComparison.OrdinalIgnoreCase))));
|
||||
picker.ItemSelected += OnPicked;
|
||||
row.AddChild(picker);
|
||||
var edit = ToolUi.Text(field.Default ?? string.Empty, OnTyped);
|
||||
edit.Text = value ?? string.Empty;
|
||||
edit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
edit.AddThemeFontOverride("font", ToolUi.Mono());
|
||||
row.AddChild(edit);
|
||||
var entry = (picker, keys, edit, (Control)row);
|
||||
row.AddChild(ToolUi.Button("tools/schema/list_remove", () => Remove(entry), ButtonKind.Secondary));
|
||||
rows.Add(entry);
|
||||
lines.AddChild(row);
|
||||
}
|
||||
|
||||
private void Remove((OptionButton Key, List<string> Keys, LineEdit Value, Control Row) entry)
|
||||
{
|
||||
rows.Remove(entry);
|
||||
lines.RemoveChild(entry.Row);
|
||||
entry.Row.QueueFree();
|
||||
changed?.Invoke();
|
||||
}
|
||||
|
||||
private void OnPicked(long index)
|
||||
{
|
||||
if (!suppress)
|
||||
changed?.Invoke();
|
||||
}
|
||||
|
||||
private void OnTyped(string text)
|
||||
{
|
||||
if (!suppress)
|
||||
changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace Cthangover.Tools.Schemas
|
||||
{
|
||||
using Cthangover.Mods.Schemas;
|
||||
using SchemaFieldDefinition = Cthangover.Mods.Schemas.FieldSchema;
|
||||
using SchemaJsonValues = Cthangover.Mods.Schemas.SchemaJson;
|
||||
using System;
|
||||
using System.Text.Json.Nodes;
|
||||
using Godot;
|
||||
|
||||
/// <summary>A data editor form field with a single value: a <see cref="SchemaFieldControl"/> plus the JSON conversion.</summary>
|
||||
public class ScalarFieldEditor : ISchemaFieldEditor
|
||||
{
|
||||
private readonly SchemaFieldControl control;
|
||||
|
||||
/// <summary>Adds the row for <paramref name="field"/> to <paramref name="parent"/>.</summary>
|
||||
public ScalarFieldEditor(Container parent, SchemaFieldDefinition field, Action changed)
|
||||
{
|
||||
control = new SchemaFieldControl(parent, field, changed);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SchemaFieldDefinition Field => control.Field;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Load(JsonNode node) => control.Value = SchemaJsonValues.ToText(node);
|
||||
|
||||
/// <inheritdoc />
|
||||
public JsonNode Save() => SchemaJsonValues.ToNode(control.Field, control.Value);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user