Compare commits

...
2 Commits
20 changed files with 1387 additions and 69 deletions

No files matched your search

+26 -1
View File
@@ -1,7 +1,7 @@
mod_rpg_editor_name = RPG Map Editor mod_rpg_editor_name = RPG Map Editor
mod_rpg_editor_desc = Editor of tile maps for the RPG world: three tile layers, the collision layer and map events, with undo and a live preview in the running game. mod_rpg_editor_desc = Editor of tile maps for the RPG world: three tile layers, the collision layer and map events, with undo and a live preview in the running game.
tools/rpg_editor/title = RPG Map Editor tools/rpg_editor/title = RPG Map Editor
tools/rpg_editor/hint = LMB — paint · RMB — erase · Alt+LMB — pick tile · events: click — select/drag, double-click — add, Del — delete · 1–5 layers · B/E/G brush · Ctrl+wheel zoom · Ctrl+Z/Y · Ctrl+S tools/rpg_editor/hint = LMB — paint · RMB — erase · Alt+LMB — pick tile · events: click — select/drag, double-click — add, Del — delete · 1–5 layers · B/E/G brush · wheel/Shift+wheel, MMB drag — scroll · Ctrl+wheel zoom · Ctrl+Z/Y · Ctrl+S
tools/rpg_editor/layer_back = Back tools/rpg_editor/layer_back = Back
tools/rpg_editor/layer_main = Main tools/rpg_editor/layer_main = Main
tools/rpg_editor/layer_front = Front tools/rpg_editor/layer_front = Front
@@ -77,3 +77,28 @@ tools/rpg_editor/collision_auto = Auto — from the tiles
tools/rpg_editor/problem_tileset = Map "{0}" has no tileset. tools/rpg_editor/problem_tileset = Map "{0}" has no tileset.
tools/rpg_editor/problem_event_id = The event at {0} has no id. tools/rpg_editor/problem_event_id = The event at {0} has no id.
tools/rpg_editor/problem_event_duplicate = Two events share the id "{0}". tools/rpg_editor/problem_event_duplicate = Two events share the id "{0}".
tools/rpg_editor/tab_tiles = Tiles
tools/rpg_editor/tab_characters = Characters
tools/rpg_editor/character_search = Search by name or id
tools/rpg_editor/place_npc = Place as NPC
tools/rpg_editor/place_npc_tip = Put the character on the map as an event talking on E/F; write its lines in the scenario lines
tools/rpg_editor/place_enemy = Place as enemy
tools/rpg_editor/place_enemy_tip = Put the character on the map as an enemy: E/F starts a battle with it, it vanishes once beaten
tools/rpg_editor/characters_hint = Pick a character, press a button and click a cell on the map (RMB cancels). The charset is found by the character's Charset field or by its portrait path.
tools/rpg_editor/place_status = Click the cell to put “{0}” on (RMB cancels)
tools/rpg_editor/place_cancelled = Placing the character cancelled
tools/rpg_editor/placed_npc = “{0}” placed as an NPC: event {1}
tools/rpg_editor/placed_enemy = “{0}” placed as an enemy: event {1}
tools/rpg_editor/icon = Icon
tools/rpg_editor/icon_tip = Picture in the items group (like an item's Sprite) when there is neither a charset nor a tile
tools/rpg_editor/tab_items = Items
tools/rpg_editor/item_search = Search by name or id
tools/rpg_editor/items_hint = Pick an item, set the count, press "Place" and click a cell (RMB cancels). The player picks it up by stepping on the cell; then it is gone.
tools/rpg_editor/item_count = Count
tools/rpg_editor/item_count_tip = How many the player gets
tools/rpg_editor/place_item = Place
tools/rpg_editor/place_item_tip = Lay the item on the map: stepping on the cell picks it up
tools/rpg_editor/item_found_line = Found: {0} ×{1}
tools/rpg_editor/placed_item = “{0}” lies on the map: event {1}
tools/rpg_editor/picker_no_look = No map picture found, a marker is shown
tools/rpg_editor/problem_event_locked = Event id {0} is taken by an event of another file or mod — pick another one
+104
View File
@@ -0,0 +1,104 @@
mod_rpg_editor_name = Editor de mapas RPG
mod_rpg_editor_desc = Editor de mapas de casillas del mundo RPG: tres capas de casillas, la capa de colisiones y los eventos del mapa, con deshacer y vista previa en vivo en el juego en marcha.
tools/rpg_editor/title = Editor de mapas RPG
tools/rpg_editor/hint = Clic izq.: pintar · clic der.: borrar · Alt+clic izq.: tomar casilla · eventos: clic: seleccionar/arrastrar, doble clic: añadir, Supr: borrar · 1–5 capas · B/E/G pincel · rueda/Mayús+rueda, arrastrar con botón central: desplazar · Ctrl+rueda: zoom · Ctrl+Z/Y · Ctrl+S
tools/rpg_editor/layer_back = Fondo
tools/rpg_editor/layer_main = Principal
tools/rpg_editor/layer_front = Frente
tools/rpg_editor/layer_collision = Colisiones
tools/rpg_editor/layer_events = Eventos
tools/rpg_editor/tool_pencil = Lápiz
tools/rpg_editor/tool_eraser = Goma
tools/rpg_editor/tool_fill = Relleno
tools/rpg_editor/grid = Cuadrícula
tools/rpg_editor/zoom_in = Acercar
tools/rpg_editor/zoom_out = Alejar
tools/rpg_editor/undo = Deshacer
tools/rpg_editor/undo_tip = Deshacer el último cambio (Ctrl+Z)
tools/rpg_editor/redo = Rehacer
tools/rpg_editor/redo_tip = Rehacer (Ctrl+Y)
tools/rpg_editor/play = Jugar
tools/rpg_editor/play_tip = Guardar y recorrer el mapa desde su punto de inicio en el juego en marcha
tools/rpg_editor/play_no_scene = El juego no conoce la escena "{0}": revisa scenes/{0}.json en el mod del mapa.
tools/rpg_editor/scene_created = escena {0} creada
tools/rpg_editor/section_palette = Paleta
tools/rpg_editor/section_map = Mapa
tools/rpg_editor/section_spawn = Punto de inicio
tools/rpg_editor/section_encounters = Combates aleatorios
tools/rpg_editor/section_event = Evento
tools/rpg_editor/section_action = Qué hace
tools/rpg_editor/section_teleport = Teletransporte
tools/rpg_editor/map_id = Id
tools/rpg_editor/map_id_tip = El id del mapa es el nombre del archivo y el de la escena que muestra el mapa
tools/rpg_editor/map_name = Clave del nombre
tools/rpg_editor/map_name_tip = Clave de localización del nombre visible del mapa
tools/rpg_editor/tileset = Tileset
tools/rpg_editor/width = Ancho
tools/rpg_editor/height = Alto
tools/rpg_editor/resize = Aplicar tamaño
tools/rpg_editor/zoom = Zoom en el juego
tools/rpg_editor/zoom_tip = Píxeles de pantalla por píxel de casilla en el juego
tools/rpg_editor/x = X
tools/rpg_editor/y = Y
tools/rpg_editor/dir = Orientación
tools/rpg_editor/dir_down = Abajo
tools/rpg_editor/dir_left = Izquierda
tools/rpg_editor/dir_right = Derecha
tools/rpg_editor/dir_up = Arriba
tools/rpg_editor/encounter_steps = Cada ~N pasos
tools/rpg_editor/encounter_steps_tip = Número medio de pasos entre combates aleatorios; 0 los desactiva
tools/rpg_editor/encounter_groups = Grupos de enemigos
tools/rpg_editor/encounter_groups_tip = Ids de personajes, grupos separados por ";": por ejemplo slime; wolf,slime
tools/rpg_editor/no_event = Cambia a la capa Eventos y haz clic en un evento, o haz doble clic en una casilla vacía para añadir uno.
tools/rpg_editor/event_id = Id
tools/rpg_editor/sprite = Charset
tools/rpg_editor/sprite_tip = Ruta de la imagen en el grupo charsets: 3 fotogramas × 4 orientaciones
tools/rpg_editor/tile = Casilla
tools/rpg_editor/tile_tip = Índice del tileset que se dibuja cuando no hay charset; -1 para ninguno
tools/rpg_editor/trigger = Activación
tools/rpg_editor/trigger_action = Tecla de acción (E/F)
tools/rpg_editor/trigger_touch = Al pisarlo
tools/rpg_editor/solid = Sólido
tools/rpg_editor/solid_auto = Cuando se dibuja
tools/rpg_editor/solid_yes = Sí
tools/rpg_editor/solid_no = No
tools/rpg_editor/solid_tile = sólida
tools/rpg_editor/condition = Condición
tools/rpg_editor/condition_tip = Condición de escenario; el evento solo existe mientras se cumple
tools/rpg_editor/scenario = Escenario
tools/rpg_editor/scenario_tip = Ruta en el grupo scenarios, la extensión es opcional
tools/rpg_editor/commands = Líneas de escenario propias, se ejecutan después del escenario:
tools/rpg_editor/teleport_map = Mapa
tools/rpg_editor/teleport_map_tip = Id del mapa de destino; vacío con una casilla indicada significa este mapa
tools/rpg_editor/delete_event = Borrar evento
tools/rpg_editor/collision_block = Muro: siempre bloqueado
tools/rpg_editor/collision_open = Paso: siempre transitable
tools/rpg_editor/collision_auto = Auto: según las casillas
tools/rpg_editor/problem_tileset = El mapa "{0}" no tiene tileset.
tools/rpg_editor/problem_event_id = El evento en {0} no tiene id.
tools/rpg_editor/problem_event_duplicate = Dos eventos comparten el id "{0}".
tools/rpg_editor/tab_tiles = Casillas
tools/rpg_editor/tab_characters = Personajes
tools/rpg_editor/character_search = Buscar por nombre o id
tools/rpg_editor/place_npc = Colocar como PNJ
tools/rpg_editor/place_npc_tip = Pone al personaje en el mapa como un evento que habla con E/F; escribe sus frases en las líneas de escenario
tools/rpg_editor/place_enemy = Colocar como enemigo
tools/rpg_editor/place_enemy_tip = Pone al personaje en el mapa como enemigo: E/F inicia un combate con él y desaparece al ser vencido
tools/rpg_editor/characters_hint = Elige un personaje, pulsa un botón y haz clic en una casilla del mapa (clic derecho cancela). El charset se busca por el campo Charset del personaje o por la ruta de su retrato.
tools/rpg_editor/place_status = Haz clic en la casilla donde poner «{0}» (clic derecho cancela)
tools/rpg_editor/place_cancelled = Colocación del personaje cancelada
tools/rpg_editor/placed_npc = «{0}» colocado como PNJ: evento {1}
tools/rpg_editor/placed_enemy = «{0}» colocado como enemigo: evento {1}
tools/rpg_editor/icon = Icono
tools/rpg_editor/icon_tip = Imagen del grupo items (como el Sprite de un objeto) cuando no hay ni charset ni casilla
tools/rpg_editor/tab_items = Objetos
tools/rpg_editor/item_search = Buscar por nombre o id
tools/rpg_editor/items_hint = Elige un objeto, indica la cantidad, pulsa «Colocar» y haz clic en una casilla (clic derecho cancela). El jugador lo recoge al pisar la casilla; después desaparece.
tools/rpg_editor/item_count = Cantidad
tools/rpg_editor/item_count_tip = Cuántos recibe el jugador
tools/rpg_editor/place_item = Colocar
tools/rpg_editor/place_item_tip = Deja el objeto en el mapa: pisar la casilla lo recoge
tools/rpg_editor/item_found_line = Encontrado: {0} ×{1}
tools/rpg_editor/placed_item = «{0}» está en el mapa: evento {1}
tools/rpg_editor/picker_no_look = No se encontró imagen para el mapa, se muestra un marcador
tools/rpg_editor/problem_event_locked = El id de evento {0} ya lo usa un evento de otro archivo o mod: elige otro
+104
View File
@@ -0,0 +1,104 @@
mod_rpg_editor_name = RPG マップエディター
mod_rpg_editor_desc = RPG ワールドのタイルマップエディター:3 つのタイルレイヤー、衝突レイヤー、マップのイベント。元に戻す機能と、実行中のゲームでのライブプレビュー付き。
tools/rpg_editor/title = RPG マップエディター
tools/rpg_editor/hint = 左クリック——描く · 右クリック——消す · Alt+左クリック——タイルを拾う · イベント:クリック——選択/ドラッグ、ダブルクリック——追加、Del——削除 · 1–5 レイヤー · B/E/G ブラシ · ホイール/Shift+ホイール、中ボタンドラッグ——スクロール · Ctrl+ホイールでズーム · Ctrl+Z/Y · Ctrl+S
tools/rpg_editor/layer_back = 背景
tools/rpg_editor/layer_main = メイン
tools/rpg_editor/layer_front = 前景
tools/rpg_editor/layer_collision = 衝突
tools/rpg_editor/layer_events = イベント
tools/rpg_editor/tool_pencil = 鉛筆
tools/rpg_editor/tool_eraser = 消しゴム
tools/rpg_editor/tool_fill = 塗りつぶし
tools/rpg_editor/grid = グリッド
tools/rpg_editor/zoom_in = 拡大
tools/rpg_editor/zoom_out = 縮小
tools/rpg_editor/undo = 元に戻す
tools/rpg_editor/undo_tip = 最後の編集を元に戻す(Ctrl+Z)
tools/rpg_editor/redo = やり直す
tools/rpg_editor/redo_tip = やり直す(Ctrl+Y)
tools/rpg_editor/play = プレイ
tools/rpg_editor/play_tip = 保存して、実行中のゲームで開始地点からマップを歩く
tools/rpg_editor/play_no_scene = ゲームはシーン「{0}」を知りません:マップの MOD の scenes/{0}.json を確認してください。
tools/rpg_editor/scene_created = シーン {0} を作成しました
tools/rpg_editor/section_palette = パレット
tools/rpg_editor/section_map = マップ
tools/rpg_editor/section_spawn = 開始地点
tools/rpg_editor/section_encounters = ランダムバトル
tools/rpg_editor/section_event = イベント
tools/rpg_editor/section_action = 動作
tools/rpg_editor/section_teleport = テレポート
tools/rpg_editor/map_id = Id
tools/rpg_editor/map_id_tip = マップの id はファイル名であり、マップを表示するシーンの名前でもある
tools/rpg_editor/map_name = 名前のキー
tools/rpg_editor/map_name_tip = マップ表示名のローカライズキー
tools/rpg_editor/tileset = タイルセット
tools/rpg_editor/width = 幅
tools/rpg_editor/height = 高さ
tools/rpg_editor/resize = サイズを適用
tools/rpg_editor/zoom = ゲーム内の拡大率
tools/rpg_editor/zoom_tip = ゲームでタイル 1 ピクセルあたりの画面ピクセル数
tools/rpg_editor/x = X
tools/rpg_editor/y = Y
tools/rpg_editor/dir = 向き
tools/rpg_editor/dir_down = 下
tools/rpg_editor/dir_left = 左
tools/rpg_editor/dir_right = 右
tools/rpg_editor/dir_up = 上
tools/rpg_editor/encounter_steps = 約 N 歩ごと
tools/rpg_editor/encounter_steps_tip = ランダムバトルの間の平均歩数。0 で無効
tools/rpg_editor/encounter_groups = 敵のグループ
tools/rpg_editor/encounter_groups_tip = キャラクターの id。グループは「;」で区切る——例:slime; wolf,slime
tools/rpg_editor/no_event = イベントレイヤーに切り替えてイベントをクリックするか、空いたマスをダブルクリックして追加してください。
tools/rpg_editor/event_id = Id
tools/rpg_editor/sprite = キャラチップ
tools/rpg_editor/sprite_tip = charsets グループの画像パス:3 コマ × 4 方向
tools/rpg_editor/tile = タイル
tools/rpg_editor/tile_tip = キャラチップがないときに描くタイルセットの番号。-1 でなし
tools/rpg_editor/trigger = 起動
tools/rpg_editor/trigger_action = アクションキー(E/F)
tools/rpg_editor/trigger_touch = 踏んだとき
tools/rpg_editor/solid = 通れない
tools/rpg_editor/solid_auto = 描かれているとき
tools/rpg_editor/solid_yes = はい
tools/rpg_editor/solid_no = いいえ
tools/rpg_editor/solid_tile = 通れない
tools/rpg_editor/condition = 条件
tools/rpg_editor/condition_tip = シナリオの条件。真の間だけイベントが存在する
tools/rpg_editor/scenario = シナリオ
tools/rpg_editor/scenario_tip = scenarios グループ内のパス。拡張子は省略可
tools/rpg_editor/commands = シナリオの後に実行するインラインの行:
tools/rpg_editor/teleport_map = マップ
tools/rpg_editor/teleport_map_tip = 移動先マップの id。空でマスが指定されていればこのマップ
tools/rpg_editor/delete_event = イベントを削除
tools/rpg_editor/collision_block = 壁——常に通れない
tools/rpg_editor/collision_open = 通路——常に通れる
tools/rpg_editor/collision_auto = 自動——タイルに従う
tools/rpg_editor/problem_tileset = マップ「{0}」にタイルセットがありません。
tools/rpg_editor/problem_event_id = {0} にあるイベントに id がありません。
tools/rpg_editor/problem_event_duplicate = 2 つのイベントが同じ id「{0}」を使っています。
tools/rpg_editor/tab_tiles = タイル
tools/rpg_editor/tab_characters = キャラクター
tools/rpg_editor/character_search = 名前か id で検索
tools/rpg_editor/place_npc = NPC として配置
tools/rpg_editor/place_npc_tip = E/F で話すイベントとしてキャラクターをマップに置く。セリフはシナリオの行に書く
tools/rpg_editor/place_enemy = 敵として配置
tools/rpg_editor/place_enemy_tip = キャラクターを敵としてマップに置く:E/F でバトルが始まり、倒すと消える
tools/rpg_editor/characters_hint = キャラクターを選び、ボタンを押して、マップのマスをクリックします(右クリックでキャンセル)。キャラチップはキャラクターの Charset フィールドか肖像のパスで探します。
tools/rpg_editor/place_status = 「{0}」を置くマスをクリック(右クリックでキャンセル)
tools/rpg_editor/place_cancelled = キャラクターの配置をキャンセルしました
tools/rpg_editor/placed_npc = 「{0}」を NPC として配置:イベント {1}
tools/rpg_editor/placed_enemy = 「{0}」を敵として配置:イベント {1}
tools/rpg_editor/icon = アイコン
tools/rpg_editor/icon_tip = キャラチップもタイルもないときの items グループの画像(アイテムの Sprite と同じ)
tools/rpg_editor/tab_items = アイテム
tools/rpg_editor/item_search = 名前か id で検索
tools/rpg_editor/items_hint = アイテムを選び、数を決め、「配置」を押してマスをクリックします(右クリックでキャンセル)。プレイヤーはマスを踏むと拾い、その後は消えます。
tools/rpg_editor/item_count = 数
tools/rpg_editor/item_count_tip = プレイヤーが得る数
tools/rpg_editor/place_item = 配置
tools/rpg_editor/place_item_tip = アイテムをマップに置く:マスを踏むと拾える
tools/rpg_editor/item_found_line = 見つけた:{0} ×{1}
tools/rpg_editor/placed_item = 「{0}」をマップに置いた:イベント {1}
tools/rpg_editor/picker_no_look = マップ用の画像が見つからないため、マーカーを表示します
tools/rpg_editor/problem_event_locked = イベント id {0} は別のファイルか MOD のイベントが使っています——別の id にしてください
+104
View File
@@ -0,0 +1,104 @@
mod_rpg_editor_name = RPG 맵 에디터
mod_rpg_editor_desc = RPG 월드의 타일 맵 에디터: 타일 레이어 3개, 충돌 레이어, 맵 이벤트. 되돌리기와 실행 중인 게임에서의 실시간 미리 보기 지원.
tools/rpg_editor/title = RPG 맵 에디터
tools/rpg_editor/hint = 좌클릭 — 그리기 · 우클릭 — 지우기 · Alt+좌클릭 — 타일 집기 · 이벤트: 클릭 — 선택/드래그, 더블클릭 — 추가, Del — 삭제 · 1–5 레이어 · B/E/G 브러시 · 휠/Shift+휠, 가운데 버튼 드래그 — 스크롤 · Ctrl+휠 확대/축소 · Ctrl+Z/Y · Ctrl+S
tools/rpg_editor/layer_back = 배경
tools/rpg_editor/layer_main = 메인
tools/rpg_editor/layer_front = 전경
tools/rpg_editor/layer_collision = 충돌
tools/rpg_editor/layer_events = 이벤트
tools/rpg_editor/tool_pencil = 연필
tools/rpg_editor/tool_eraser = 지우개
tools/rpg_editor/tool_fill = 채우기
tools/rpg_editor/grid = 격자
tools/rpg_editor/zoom_in = 확대
tools/rpg_editor/zoom_out = 축소
tools/rpg_editor/undo = 되돌리기
tools/rpg_editor/undo_tip = 마지막 편집 되돌리기 (Ctrl+Z)
tools/rpg_editor/redo = 다시 실행
tools/rpg_editor/redo_tip = 다시 실행 (Ctrl+Y)
tools/rpg_editor/play = 플레이
tools/rpg_editor/play_tip = 저장하고 실행 중인 게임에서 시작 지점부터 맵을 걸어 봅니다
tools/rpg_editor/play_no_scene = 게임이 장면 "{0}"을(를) 모릅니다: 맵이 있는 모드의 scenes/{0}.json을 확인하세요.
tools/rpg_editor/scene_created = 장면 {0} 생성됨
tools/rpg_editor/section_palette = 팔레트
tools/rpg_editor/section_map = 맵
tools/rpg_editor/section_spawn = 시작 지점
tools/rpg_editor/section_encounters = 랜덤 전투
tools/rpg_editor/section_event = 이벤트
tools/rpg_editor/section_action = 동작
tools/rpg_editor/section_teleport = 순간 이동
tools/rpg_editor/map_id = Id
tools/rpg_editor/map_id_tip = 맵 id는 파일 이름이자 맵을 보여 주는 장면의 이름
tools/rpg_editor/map_name = 이름 키
tools/rpg_editor/map_name_tip = 맵 표시 이름의 로컬라이즈 키
tools/rpg_editor/tileset = 타일셋
tools/rpg_editor/width = 너비
tools/rpg_editor/height = 높이
tools/rpg_editor/resize = 크기 적용
tools/rpg_editor/zoom = 게임 내 확대 배율
tools/rpg_editor/zoom_tip = 게임에서 타일 픽셀 하나당 화면 픽셀 수
tools/rpg_editor/x = X
tools/rpg_editor/y = Y
tools/rpg_editor/dir = 방향
tools/rpg_editor/dir_down = 아래
tools/rpg_editor/dir_left = 왼쪽
tools/rpg_editor/dir_right = 오른쪽
tools/rpg_editor/dir_up = 위
tools/rpg_editor/encounter_steps = 약 N걸음마다
tools/rpg_editor/encounter_steps_tip = 랜덤 전투 사이의 평균 걸음 수. 0이면 끕니다
tools/rpg_editor/encounter_groups = 적 그룹
tools/rpg_editor/encounter_groups_tip = 캐릭터 id, 그룹은 ";"로 구분 — 예: slime; wolf,slime
tools/rpg_editor/no_event = 이벤트 레이어로 바꾸고 이벤트를 클릭하거나, 빈 칸을 더블클릭해 추가하세요.
tools/rpg_editor/event_id = Id
tools/rpg_editor/sprite = 캐릭터 시트
tools/rpg_editor/sprite_tip = charsets 그룹의 그림 경로: 3프레임 × 4방향
tools/rpg_editor/tile = 타일
tools/rpg_editor/tile_tip = 캐릭터 시트가 없을 때 그리는 타일셋 번호. -1이면 없음
tools/rpg_editor/trigger = 발동
tools/rpg_editor/trigger_action = 액션 키 (E/F)
tools/rpg_editor/trigger_touch = 밟았을 때
tools/rpg_editor/solid = 통과 불가
tools/rpg_editor/solid_auto = 그려져 있을 때
tools/rpg_editor/solid_yes = 예
tools/rpg_editor/solid_no = 아니요
tools/rpg_editor/solid_tile = 통과 불가
tools/rpg_editor/condition = 조건
tools/rpg_editor/condition_tip = 시나리오 조건. 참인 동안에만 이벤트가 존재한다
tools/rpg_editor/scenario = 시나리오
tools/rpg_editor/scenario_tip = scenarios 그룹의 경로, 확장자는 생략 가능
tools/rpg_editor/commands = 시나리오 뒤에 실행할 인라인 줄:
tools/rpg_editor/teleport_map = 맵
tools/rpg_editor/teleport_map_tip = 대상 맵 id. 비어 있고 칸이 지정되면 이 맵
tools/rpg_editor/delete_event = 이벤트 삭제
tools/rpg_editor/collision_block = 벽 — 항상 막힘
tools/rpg_editor/collision_open = 통로 — 항상 지나갈 수 있음
tools/rpg_editor/collision_auto = 자동 — 타일에 따름
tools/rpg_editor/problem_tileset = 맵 "{0}"에 타일셋이 없습니다.
tools/rpg_editor/problem_event_id = {0}에 있는 이벤트에 id가 없습니다.
tools/rpg_editor/problem_event_duplicate = 두 이벤트가 같은 id "{0}"을(를) 씁니다.
tools/rpg_editor/tab_tiles = 타일
tools/rpg_editor/tab_characters = 캐릭터
tools/rpg_editor/character_search = 이름 또는 id로 검색
tools/rpg_editor/place_npc = NPC로 배치
tools/rpg_editor/place_npc_tip = 캐릭터를 E/F로 대화하는 이벤트로 맵에 놓습니다. 대사는 시나리오 줄에 쓰세요
tools/rpg_editor/place_enemy = 적으로 배치
tools/rpg_editor/place_enemy_tip = 캐릭터를 적으로 맵에 놓습니다: E/F로 전투가 시작되고, 쓰러뜨리면 사라집니다
tools/rpg_editor/characters_hint = 캐릭터를 고르고 버튼을 누른 뒤 맵의 칸을 클릭하세요(우클릭으로 취소). 캐릭터 시트는 캐릭터의 Charset 필드나 초상화 경로로 찾습니다.
tools/rpg_editor/place_status = "{0}"을(를) 놓을 칸을 클릭하세요 (우클릭으로 취소)
tools/rpg_editor/place_cancelled = 캐릭터 배치를 취소했습니다
tools/rpg_editor/placed_npc = "{0}"을(를) NPC로 배치: 이벤트 {1}
tools/rpg_editor/placed_enemy = "{0}"을(를) 적으로 배치: 이벤트 {1}
tools/rpg_editor/icon = 아이콘
tools/rpg_editor/icon_tip = 캐릭터 시트도 타일도 없을 때 쓰는 items 그룹의 그림(아이템의 Sprite와 같음)
tools/rpg_editor/tab_items = 아이템
tools/rpg_editor/item_search = 이름 또는 id로 검색
tools/rpg_editor/items_hint = 아이템을 고르고 개수를 정한 뒤 "배치"를 누르고 칸을 클릭하세요(우클릭으로 취소). 플레이어가 칸을 밟으면 줍고, 그다음엔 사라집니다.
tools/rpg_editor/item_count = 개수
tools/rpg_editor/item_count_tip = 플레이어가 받는 개수
tools/rpg_editor/place_item = 배치
tools/rpg_editor/place_item_tip = 아이템을 맵에 놓습니다: 칸을 밟으면 줍습니다
tools/rpg_editor/item_found_line = 발견: {0} ×{1}
tools/rpg_editor/placed_item = "{0}"이(가) 맵에 놓였습니다: 이벤트 {1}
tools/rpg_editor/picker_no_look = 맵용 그림을 찾지 못해 표식을 표시합니다
tools/rpg_editor/problem_event_locked = 이벤트 id {0}은(는) 다른 파일이나 모드의 이벤트가 쓰고 있습니다 — 다른 것을 고르세요
+27 -2
View File
@@ -1,7 +1,7 @@
mod_rpg_editor_name = Редактор RPG-карт mod_rpg_editor_name = Редактор RPG-карт
mod_rpg_editor_desc = Редактор тайловых карт RPG-мира: три слоя тайлов, слой коллизий и события на карте, с отменой и живым предпросмотром в запущенной игре. mod_rpg_editor_desc = Редактор тайловых карт RPG-мира: три слоя тайлов, слой коллизий и события на карте, с отменой и живым предпросмотром в запущенной игре.
tools/rpg_editor/title = Редактор RPG-карт tools/rpg_editor/title = Редактор RPG-карт
tools/rpg_editor/hint = ЛКМ — рисовать · ПКМ — стирать · Alt+ЛКМ — взять тайл · события: клик — выбрать/тащить, двойной клик — добавить, Del — удалить · 1–5 слои · B/E/G кисть · Ctrl+колесо масштаб · Ctrl+Z/Y · Ctrl+S tools/rpg_editor/hint = ЛКМ — рисовать · ПКМ — стирать · Alt+ЛКМ — взять тайл · события: клик — выбрать/тащить, двойной клик — добавить, Del — удалить · 1–5 слои · B/E/G кисть · колесо/Shift+колесо, СКМ — прокрутка · Ctrl+колесо масштаб · Ctrl+Z/Y · Ctrl+S
tools/rpg_editor/layer_back = Фон tools/rpg_editor/layer_back = Фон
tools/rpg_editor/layer_main = Основной tools/rpg_editor/layer_main = Основной
tools/rpg_editor/layer_front = Передний tools/rpg_editor/layer_front = Передний
@@ -54,7 +54,7 @@ tools/rpg_editor/event_id = Id
tools/rpg_editor/sprite = Чарсет tools/rpg_editor/sprite = Чарсет
tools/rpg_editor/sprite_tip = Путь к картинке в группе charsets: 3 кадра × 4 направления tools/rpg_editor/sprite_tip = Путь к картинке в группе charsets: 3 кадра × 4 направления
tools/rpg_editor/tile = Тайл tools/rpg_editor/tile = Тайл
tools/rpg_editor/tile_tip = Номер тайла, если чарсета нет; -1 — ничего tools/rpg_editor/tile_tip = Номер тайла, если чарсета нет; -1 — нет тайла
tools/rpg_editor/trigger = Запуск tools/rpg_editor/trigger = Запуск
tools/rpg_editor/trigger_action = Клавиша действия (E/F) tools/rpg_editor/trigger_action = Клавиша действия (E/F)
tools/rpg_editor/trigger_touch = Шаг на клетку tools/rpg_editor/trigger_touch = Шаг на клетку
@@ -77,3 +77,28 @@ tools/rpg_editor/collision_auto = Авто — по тайлам
tools/rpg_editor/problem_tileset = У карты «{0}» не выбран тайлсет. tools/rpg_editor/problem_tileset = У карты «{0}» не выбран тайлсет.
tools/rpg_editor/problem_event_id = У события в клетке {0} нет id. tools/rpg_editor/problem_event_id = У события в клетке {0} нет id.
tools/rpg_editor/problem_event_duplicate = Два события с одинаковым id «{0}». tools/rpg_editor/problem_event_duplicate = Два события с одинаковым id «{0}».
tools/rpg_editor/tab_tiles = Тайлы
tools/rpg_editor/tab_characters = Персонажи
tools/rpg_editor/character_search = Поиск по имени или id
tools/rpg_editor/place_npc = На карту: NPC
tools/rpg_editor/place_npc_tip = Поставить персонажа как событие с разговором на E/F; реплики пишутся в «Строках сценария»
tools/rpg_editor/place_enemy = На карту: враг
tools/rpg_editor/place_enemy_tip = Поставить персонажа как врага: E/F начинает с ним бой, после победы он исчезает
tools/rpg_editor/characters_hint = Выберите персонажа, нажмите кнопку и кликните клетку на карте (ПКМ — отмена). Чарсет ищется по полю Charset персонажа или по пути его портрета.
tools/rpg_editor/place_status = Кликните клетку, куда поставить «{0}» (ПКМ — отмена)
tools/rpg_editor/place_cancelled = Постановка персонажа отменена
tools/rpg_editor/placed_npc = «{0}» поставлен как NPC: событие {1}
tools/rpg_editor/placed_enemy = «{0}» поставлен как враг: событие {1}
tools/rpg_editor/icon = Иконка
tools/rpg_editor/icon_tip = Картинка из группы items (как Sprite предмета), если нет ни чарсета, ни тайла
tools/rpg_editor/tab_items = Предметы
tools/rpg_editor/item_search = Поиск по названию или id
tools/rpg_editor/items_hint = Выберите предмет, задайте количество, нажмите «На карту» и кликните клетку (ПКМ — отмена). Игрок подбирает предмет, наступив на клетку; потом предмет исчезает.
tools/rpg_editor/item_count = Количество
tools/rpg_editor/item_count_tip = Сколько штук получит игрок
tools/rpg_editor/place_item = На карту
tools/rpg_editor/place_item_tip = Положить предмет на карту: шаг на клетку подбирает его
tools/rpg_editor/item_found_line = Найдено: {0} ×{1}
tools/rpg_editor/placed_item = «{0}» лежит на карте: событие {1}
tools/rpg_editor/picker_no_look = Картинка для карты не найдена, будет маркер
tools/rpg_editor/problem_event_locked = Id события {0} уже занят событием из другого файла или мода — выберите другой
+104
View File
@@ -0,0 +1,104 @@
mod_rpg_editor_name = Trình sửa bản đồ RPG
mod_rpg_editor_desc = Trình sửa bản đồ ô cho thế giới RPG: ba lớp ô, lớp va chạm và sự kiện trên bản đồ, có hoàn tác và xem trước trực tiếp trong game đang chạy.
tools/rpg_editor/title = Trình sửa bản đồ RPG
tools/rpg_editor/hint = Chuột trái — vẽ · chuột phải — xóa · Alt+chuột trái — lấy ô · sự kiện: nhấp — chọn/kéo, nhấp đúp — thêm, Del — xóa · 1–5 lớp · B/E/G bút · cuộn/Shift+cuộn, kéo chuột giữa — di chuyển · Ctrl+cuộn thu phóng · Ctrl+Z/Y · Ctrl+S
tools/rpg_editor/layer_back = Nền
tools/rpg_editor/layer_main = Chính
tools/rpg_editor/layer_front = Phía trước
tools/rpg_editor/layer_collision = Va chạm
tools/rpg_editor/layer_events = Sự kiện
tools/rpg_editor/tool_pencil = Bút chì
tools/rpg_editor/tool_eraser = Tẩy
tools/rpg_editor/tool_fill = Tô
tools/rpg_editor/grid = Lưới
tools/rpg_editor/zoom_in = Phóng to
tools/rpg_editor/zoom_out = Thu nhỏ
tools/rpg_editor/undo = Hoàn tác
tools/rpg_editor/undo_tip = Hoàn tác thay đổi cuối (Ctrl+Z)
tools/rpg_editor/redo = Làm lại
tools/rpg_editor/redo_tip = Làm lại (Ctrl+Y)
tools/rpg_editor/play = Chơi
tools/rpg_editor/play_tip = Lưu và đi trên bản đồ từ điểm xuất phát trong game đang chạy
tools/rpg_editor/play_no_scene = Game không biết cảnh "{0}": hãy kiểm tra scenes/{0}.json trong mod của bản đồ.
tools/rpg_editor/scene_created = đã tạo cảnh {0}
tools/rpg_editor/section_palette = Bảng màu
tools/rpg_editor/section_map = Bản đồ
tools/rpg_editor/section_spawn = Điểm xuất phát
tools/rpg_editor/section_encounters = Trận đấu ngẫu nhiên
tools/rpg_editor/section_event = Sự kiện
tools/rpg_editor/section_action = Tác dụng
tools/rpg_editor/section_teleport = Dịch chuyển
tools/rpg_editor/map_id = Id
tools/rpg_editor/map_id_tip = Id của bản đồ là tên tệp và cũng là tên cảnh hiển thị bản đồ
tools/rpg_editor/map_name = Khóa tên
tools/rpg_editor/map_name_tip = Khóa bản địa hóa của tên hiển thị bản đồ
tools/rpg_editor/tileset = Tileset
tools/rpg_editor/width = Chiều rộng
tools/rpg_editor/height = Chiều cao
tools/rpg_editor/resize = Áp dụng kích thước
tools/rpg_editor/zoom = Thu phóng trong game
tools/rpg_editor/zoom_tip = Số điểm ảnh màn hình cho mỗi điểm ảnh ô trong game
tools/rpg_editor/x = X
tools/rpg_editor/y = Y
tools/rpg_editor/dir = Hướng nhìn
tools/rpg_editor/dir_down = Xuống
tools/rpg_editor/dir_left = Trái
tools/rpg_editor/dir_right = Phải
tools/rpg_editor/dir_up = Lên
tools/rpg_editor/encounter_steps = Khoảng mỗi N bước
tools/rpg_editor/encounter_steps_tip = Số bước trung bình giữa các trận đấu ngẫu nhiên; 0 để tắt
tools/rpg_editor/encounter_groups = Nhóm kẻ địch
tools/rpg_editor/encounter_groups_tip = Id nhân vật, các nhóm tách bằng ";" — ví dụ slime; wolf,slime
tools/rpg_editor/no_event = Chuyển sang lớp Sự kiện và nhấp vào một sự kiện, hoặc nhấp đúp vào ô trống để thêm.
tools/rpg_editor/event_id = Id
tools/rpg_editor/sprite = Charset
tools/rpg_editor/sprite_tip = Đường dẫn hình trong nhóm charsets: 3 khung × 4 hướng
tools/rpg_editor/tile = Ô
tools/rpg_editor/tile_tip = Số ô của tileset được vẽ khi không có charset; -1 là không có
tools/rpg_editor/trigger = Kích hoạt
tools/rpg_editor/trigger_action = Phím hành động (E/F)
tools/rpg_editor/trigger_touch = Khi bước lên
tools/rpg_editor/solid = Chặn đường
tools/rpg_editor/solid_auto = Khi được vẽ
tools/rpg_editor/solid_yes = Có
tools/rpg_editor/solid_no = Không
tools/rpg_editor/solid_tile = chặn đường
tools/rpg_editor/condition = Điều kiện
tools/rpg_editor/condition_tip = Điều kiện kịch bản; sự kiện chỉ tồn tại khi điều kiện đúng
tools/rpg_editor/scenario = Kịch bản
tools/rpg_editor/scenario_tip = Đường dẫn trong nhóm scenarios, có thể bỏ phần mở rộng
tools/rpg_editor/commands = Các dòng kịch bản riêng, chạy sau kịch bản:
tools/rpg_editor/teleport_map = Bản đồ
tools/rpg_editor/teleport_map_tip = Id bản đồ đích; để trống mà có ô thì là bản đồ này
tools/rpg_editor/delete_event = Xóa sự kiện
tools/rpg_editor/collision_block = Tường — luôn bị chặn
tools/rpg_editor/collision_open = Lối đi — luôn đi được
tools/rpg_editor/collision_auto = Tự động — theo các ô
tools/rpg_editor/problem_tileset = Bản đồ "{0}" không có tileset.
tools/rpg_editor/problem_event_id = Sự kiện ở {0} không có id.
tools/rpg_editor/problem_event_duplicate = Hai sự kiện dùng chung id "{0}".
tools/rpg_editor/tab_tiles = Ô
tools/rpg_editor/tab_characters = Nhân vật
tools/rpg_editor/character_search = Tìm theo tên hoặc id
tools/rpg_editor/place_npc = Đặt làm NPC
tools/rpg_editor/place_npc_tip = Đặt nhân vật lên bản đồ như một sự kiện nói chuyện bằng E/F; viết lời thoại trong các dòng kịch bản
tools/rpg_editor/place_enemy = Đặt làm kẻ địch
tools/rpg_editor/place_enemy_tip = Đặt nhân vật lên bản đồ làm kẻ địch: E/F bắt đầu trận đấu với nó, nó biến mất khi bị đánh bại
tools/rpg_editor/characters_hint = Chọn một nhân vật, nhấn nút rồi nhấp vào một ô trên bản đồ (chuột phải để hủy). Charset được tìm theo trường Charset của nhân vật hoặc theo đường dẫn chân dung.
tools/rpg_editor/place_status = Nhấp vào ô để đặt "{0}" (chuột phải để hủy)
tools/rpg_editor/place_cancelled = Đã hủy đặt nhân vật
tools/rpg_editor/placed_npc = "{0}" đã đặt làm NPC: sự kiện {1}
tools/rpg_editor/placed_enemy = "{0}" đã đặt làm kẻ địch: sự kiện {1}
tools/rpg_editor/icon = Biểu tượng
tools/rpg_editor/icon_tip = Hình trong nhóm items (như Sprite của vật phẩm) khi không có charset lẫn ô
tools/rpg_editor/tab_items = Vật phẩm
tools/rpg_editor/item_search = Tìm theo tên hoặc id
tools/rpg_editor/items_hint = Chọn một vật phẩm, đặt số lượng, nhấn "Đặt" và nhấp vào một ô (chuột phải để hủy). Người chơi nhặt nó khi bước lên ô; sau đó nó biến mất.
tools/rpg_editor/item_count = Số lượng
tools/rpg_editor/item_count_tip = Người chơi nhận được bao nhiêu
tools/rpg_editor/place_item = Đặt
tools/rpg_editor/place_item_tip = Đặt vật phẩm lên bản đồ: bước lên ô sẽ nhặt được
tools/rpg_editor/item_found_line = Tìm thấy: {0} ×{1}
tools/rpg_editor/placed_item = "{0}" nằm trên bản đồ: sự kiện {1}
tools/rpg_editor/picker_no_look = Không tìm thấy hình cho bản đồ, hiển thị một điểm đánh dấu
tools/rpg_editor/problem_event_locked = Id sự kiện {0} đã được một sự kiện ở tệp hoặc mod khác dùng — hãy chọn id khác
+104
View File
@@ -0,0 +1,104 @@
mod_rpg_editor_name = RPG 地图编辑器
mod_rpg_editor_desc = RPG 世界的瓦片地图编辑器:三层瓦片、碰撞层和地图事件,支持撤销,并在运行中的游戏里实时预览。
tools/rpg_editor/title = RPG 地图编辑器
tools/rpg_editor/hint = 左键——绘制 · 右键——擦除 · Alt+左键——吸取瓦片 · 事件:点击——选择/拖动,双击——添加,Del——删除 · 1–5 切换图层 · B/E/G 画笔 · 滚轮/Shift+滚轮、中键拖动——滚动 · Ctrl+滚轮缩放 · Ctrl+Z/Y · Ctrl+S
tools/rpg_editor/layer_back = 背景层
tools/rpg_editor/layer_main = 主层
tools/rpg_editor/layer_front = 前景层
tools/rpg_editor/layer_collision = 碰撞
tools/rpg_editor/layer_events = 事件
tools/rpg_editor/tool_pencil = 铅笔
tools/rpg_editor/tool_eraser = 橡皮
tools/rpg_editor/tool_fill = 填充
tools/rpg_editor/grid = 网格
tools/rpg_editor/zoom_in = 放大
tools/rpg_editor/zoom_out = 缩小
tools/rpg_editor/undo = 撤销
tools/rpg_editor/undo_tip = 撤销上一次编辑(Ctrl+Z)
tools/rpg_editor/redo = 重做
tools/rpg_editor/redo_tip = 重做(Ctrl+Y)
tools/rpg_editor/play = 试玩
tools/rpg_editor/play_tip = 保存,并在运行中的游戏里从出生点开始走这张地图
tools/rpg_editor/play_no_scene = 游戏不认识场景"{0}":请检查地图所在模组中的 scenes/{0}.json。
tools/rpg_editor/scene_created = 已创建场景 {0}
tools/rpg_editor/section_palette = 调色板
tools/rpg_editor/section_map = 地图
tools/rpg_editor/section_spawn = 出生点
tools/rpg_editor/section_encounters = 随机战斗
tools/rpg_editor/section_event = 事件
tools/rpg_editor/section_action = 效果
tools/rpg_editor/section_teleport = 传送
tools/rpg_editor/map_id = Id
tools/rpg_editor/map_id_tip = 地图 id 就是文件名,也是显示该地图的场景名
tools/rpg_editor/map_name = 名称键
tools/rpg_editor/map_name_tip = 地图显示名称的本地化键
tools/rpg_editor/tileset = 瓦片集
tools/rpg_editor/width = 宽度
tools/rpg_editor/height = 高度
tools/rpg_editor/resize = 应用尺寸
tools/rpg_editor/zoom = 游戏内缩放
tools/rpg_editor/zoom_tip = 游戏中每个瓦片像素对应的屏幕像素
tools/rpg_editor/x = X
tools/rpg_editor/y = Y
tools/rpg_editor/dir = 朝向
tools/rpg_editor/dir_down = 下
tools/rpg_editor/dir_left = 左
tools/rpg_editor/dir_right = 右
tools/rpg_editor/dir_up = 上
tools/rpg_editor/encounter_steps = 约每 N 步
tools/rpg_editor/encounter_steps_tip = 两次随机战斗之间的平均步数;0 表示关闭
tools/rpg_editor/encounter_groups = 敌人组
tools/rpg_editor/encounter_groups_tip = 角色 id,用";"分隔各组——例如 slime; wolf,slime
tools/rpg_editor/no_event = 切换到事件层并点击一个事件,或双击空格子来添加一个。
tools/rpg_editor/event_id = Id
tools/rpg_editor/sprite = 行走图
tools/rpg_editor/sprite_tip = charsets 组中的图片路径:3 帧 × 4 个朝向
tools/rpg_editor/tile = 瓦片
tools/rpg_editor/tile_tip = 没有行走图时绘制的瓦片集编号;-1 表示无
tools/rpg_editor/trigger = 触发
tools/rpg_editor/trigger_action = 动作键(E/F)
tools/rpg_editor/trigger_touch = 踩上去
tools/rpg_editor/solid = 实体
tools/rpg_editor/solid_auto = 绘制时
tools/rpg_editor/solid_yes = 是
tools/rpg_editor/solid_no = 否
tools/rpg_editor/solid_tile = 实体
tools/rpg_editor/condition = 条件
tools/rpg_editor/condition_tip = 剧本条件;只有条件成立时事件才存在
tools/rpg_editor/scenario = 剧本
tools/rpg_editor/scenario_tip = scenarios 组中的路径,扩展名可省略
tools/rpg_editor/commands = 内联剧本行,在剧本之后执行:
tools/rpg_editor/teleport_map = 地图
tools/rpg_editor/teleport_map_tip = 目标地图 id;为空且设置了格子表示本地图
tools/rpg_editor/delete_event = 删除事件
tools/rpg_editor/collision_block = 墙——始终阻挡
tools/rpg_editor/collision_open = 通道——始终可走
tools/rpg_editor/collision_auto = 自动——取决于瓦片
tools/rpg_editor/problem_tileset = 地图"{0}"没有瓦片集。
tools/rpg_editor/problem_event_id = 位于 {0} 的事件没有 id。
tools/rpg_editor/problem_event_duplicate = 两个事件使用了相同的 id"{0}"。
tools/rpg_editor/tab_tiles = 瓦片
tools/rpg_editor/tab_characters = 角色
tools/rpg_editor/character_search = 按名称或 id 搜索
tools/rpg_editor/place_npc = 放置为 NPC
tools/rpg_editor/place_npc_tip = 把角色作为按 E/F 对话的事件放到地图上;它的台词写在剧本行中
tools/rpg_editor/place_enemy = 放置为敌人
tools/rpg_editor/place_enemy_tip = 把角色作为敌人放到地图上:按 E/F 与它战斗,被击败后消失
tools/rpg_editor/characters_hint = 选择一个角色,按下按钮,再点击地图上的格子(右键取消)。行走图按角色的 Charset 字段或其立绘路径查找。
tools/rpg_editor/place_status = 点击要放置"{0}"的格子(右键取消)
tools/rpg_editor/place_cancelled = 已取消放置角色
tools/rpg_editor/placed_npc = "{0}"已作为 NPC 放置:事件 {1}
tools/rpg_editor/placed_enemy = "{0}"已作为敌人放置:事件 {1}
tools/rpg_editor/icon = 图标
tools/rpg_editor/icon_tip = items 组中的图片(与物品的 Sprite 相同),在既没有行走图也没有瓦片时使用
tools/rpg_editor/tab_items = 物品
tools/rpg_editor/item_search = 按名称或 id 搜索
tools/rpg_editor/items_hint = 选择物品,设置数量,按"放置"并点击格子(右键取消)。玩家踩上格子即可拾取,之后它就消失了。
tools/rpg_editor/item_count = 数量
tools/rpg_editor/item_count_tip = 玩家获得的数量
tools/rpg_editor/place_item = 放置
tools/rpg_editor/place_item_tip = 把物品放到地图上:踩上格子即可拾取
tools/rpg_editor/item_found_line = 找到:{0} ×{1}
tools/rpg_editor/placed_item = "{0}"放在地图上:事件 {1}
tools/rpg_editor/picker_no_look = 没有找到地图用的图片,显示为标记
tools/rpg_editor/problem_event_locked = 事件 id {0} 已被其他文件或模组的事件占用——请换一个
+3 -1
View File
@@ -3,7 +3,7 @@
"name": "RPG Map Editor", "name": "RPG Map Editor",
"version": "1.0.0", "version": "1.0.0",
"author": "ct", "author": "ct",
"description": "Editor of the rpg mod's tile maps (maps/*.json): paints the back, main and front tile layers and the collision layer with a pencil, an eraser and a flood fill from the tileset palette, places and edits map events (look, trigger, scenario, inline commands, teleport, condition), sets the map size, spawn and random encounters. Undo/redo, live update of the running world after saving, play from the spawn. Opens from the main menu tools, the novel editor hub and the in-game toolbar.", "description": "Editor of the rpg mod's tile maps (maps/*.json): paints the back, main and front tile layers and the collision layer with a pencil, an eraser and a flood fill from the tileset palette, places and edits map events (look, trigger, scenario, inline commands, teleport, condition), sets the map size, spawn and random encounters. Undo/redo, live update of the running world after saving, play from the spawn. Opens from the main menu tools and the in-game toolbar.",
"category": "tools, editor", "category": "tools, editor",
"sources": [ "sources": [
"src/**/*.cs" "src/**/*.cs"
@@ -13,6 +13,8 @@
], ],
"depends": [ "depends": [
"rpg>=1", "rpg>=1",
"characters_base>=1",
"items_base>=1",
"editor_tools>=1", "editor_tools>=1",
"ui_base>=1" "ui_base>=1"
], ],
+65
View File
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Cthangover.Core.Characters;
using Cthangover.Core.Mods.Resolvers;
using Cthangover.Mods.Rpg;
using Godot;
namespace Cthangover.Tools.RpgEditor
{
/// <summary>
/// The characters the running game knows (<c>Kind: character</c> of every
/// loaded mod), sorted by display name, for placing them on a map.
/// <para/>
/// A character has no map look of its own, so its charset is found by name:
/// an explicit <c>"Charset"</c> field of the character (kept by the
/// character's extra properties; the character editor sets it) wins, otherwise the
/// portrait path in the <c>characters</c> group (<c>characters/village/slime</c> or
/// <c>village/slime</c> → <c>village/slime</c>), as village_pack names both. Only charsets that exist are used: the texture
/// loader logs an error for a missing one. The row picture is the middle frame
/// of the "down" row, how the character stands on the map.
/// </summary>
public static class RpgCharacterCatalog
{
private const string ImageGroup = "characters";
/// <summary>Reads the character list anew.</summary>
public static List<RpgPickerEntry> Load(RpgTextures textures)
{
var charsets = RpgItemCatalog.GroupFiles(RpgTextures.CharsetGroup);
return (Entities.Collect<CharacterInfo>()?.Values ?? Enumerable.Empty<CharacterInfo>())
.Where(info => !string.IsNullOrEmpty(info.ID))
.Select(info => Entry(info, CharsetOf(info, charsets), textures))
.OrderBy(entry => entry.Name, StringComparer.CurrentCultureIgnoreCase)
.ToList();
}
private static RpgPickerEntry Entry(CharacterInfo info, string charset, RpgTextures textures) => new()
{
Id = info.ID,
Name = string.IsNullOrEmpty(info.Name) ? info.ID : TranslationServer.Translate(info.Name).ToString(),
Look = charset,
Icon = FrontFrame(charset == null ? null : textures.Charset(charset))
};
private static string CharsetOf(CharacterInfo info, HashSet<string> charsets)
{
object explicitCharset = null;
info.ExtraProperties?.TryGetValue("Charset", out explicitCharset);
var image = info.Image ?? string.Empty;
if (image.StartsWith(ImageGroup + "/", StringComparison.OrdinalIgnoreCase))
image = image.Substring(ImageGroup.Length + 1);
var candidates = new[] { explicitCharset?.ToString(), image };
return candidates.FirstOrDefault(path => !string.IsNullOrEmpty(path) && charsets.Contains(path));
}
private static Texture2D FrontFrame(Texture2D image)
{
if (image == null)
return null;
var frame = new Vector2(image.GetWidth() / 3f, image.GetHeight() / 4f);
return new AtlasTexture { Atlas = image, Region = new Rect2(new Vector2(frame.X, 0f), frame) };
}
}
}
+123
View File
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Cthangover.Mods.Interface.Theming;
using Cthangover.Tools.Common;
using Godot;
namespace Cthangover.Tools.RpgEditor
{
/// <summary>
/// A tab of the editor's left column listing entities of the game (the
/// characters, the items) with a picture, a search over name and id and
/// buttons that act on the selected row. The window decides what the
/// entries are (<see cref="Source"/>) and what the buttons do
/// (<see cref="AddAction"/>); placing on the map is then a click on a cell.
/// <para/>
/// The list is read again on every <see cref="Reload"/> (the window calls it
/// whenever the tab is opened), so entities added to the game's data while
/// the editor is open show up.
/// </summary>
public partial class RpgEntityPicker : VBoxContainer
{
private readonly List<RpgPickerEntry> shown = new();
private readonly List<Button> actions = new();
private readonly LineEdit search;
private readonly ItemList list;
private readonly HBoxContainer options;
private readonly HBoxContainer buttons;
private List<RpgPickerEntry> all = new();
/// <summary>Builds the search field, the list, the option and button rows and the hint.</summary>
public RpgEntityPicker(string searchKey, string hintKey)
{
search = ToolUi.Text(ToolUi.Tr(searchKey), OnSearchChanged);
AddChild(search);
list = new ItemList
{
AutoHeight = true,
FixedIconSize = new Vector2I(32, 48),
TextureFilter = TextureFilterEnum.Nearest,
SizeFlagsHorizontal = SizeFlags.ExpandFill
};
list.ItemSelected += OnItemSelected;
AddChild(list);
options = new HBoxContainer();
AddChild(options);
buttons = new HBoxContainer();
AddChild(buttons);
AddChild(ToolUi.Hint(ToolUi.Tr(hintKey)));
}
/// <summary>Reads the entries; called by <see cref="Reload"/>.</summary>
public Func<List<RpgPickerEntry>> Source { get; set; }
/// <summary>The selected entry, or null.</summary>
public RpgPickerEntry Selected
{
get
{
var picked = list.GetSelectedItems();
return picked.Length == 0 ? null : shown[picked[0]];
}
}
/// <summary>Adds a button acting on the selected entry; it is disabled while nothing is selected.</summary>
public void AddAction(string textKey, string tooltipKey, Action<RpgPickerEntry> action)
{
var button = ToolUi.Button(textKey, null, ButtonKind.Secondary, tooltipKey);
button.Pressed += () => Run(action);
button.Disabled = true;
actions.Add(button);
buttons.AddChild(button);
}
/// <summary>Adds a control (a count, a mode) to the row above the buttons.</summary>
public void AddOption(Control control) => options.AddChild(control);
/// <summary>Reads the entries again and reapplies the search.</summary>
public void Reload()
{
all = Source?.Invoke() ?? new List<RpgPickerEntry>();
Filter();
}
private void Filter()
{
var selectedId = Selected?.Id;
var query = search.Text.Trim();
shown.Clear();
shown.AddRange(all.Where(entry => query.Length == 0
|| entry.Name.Contains(query, StringComparison.CurrentCultureIgnoreCase)
|| entry.Id.Contains(query, StringComparison.OrdinalIgnoreCase)));
list.Clear();
foreach (var entry in shown)
{
var index = list.AddItem($"{entry.Name} · {entry.Id}", entry.Icon);
list.SetItemTooltip(index, entry.Look ?? ToolUi.Tr("tools/rpg_editor/picker_no_look"));
if (entry.Id == selectedId)
list.Select(index);
}
UpdateButtons();
}
private void Run(Action<RpgPickerEntry> action)
{
if (Selected != null)
action(Selected);
}
private void UpdateButtons()
{
foreach (var button in actions)
button.Disabled = Selected == null;
}
private void OnSearchChanged(string text) => Filter();
private void OnItemSelected(long index) => UpdateButtons();
}
}
+6 -2
View File
@@ -8,8 +8,8 @@ using Godot;
namespace Cthangover.Tools.RpgEditor namespace Cthangover.Tools.RpgEditor
{ {
/// <summary> /// <summary>
/// Properties of the selected map event: id, cell, look (charset path or /// Properties of the selected map event: id, cell, look (charset path, tileset index
/// tileset index), facing, trigger, solidity, what it runs (scenario path, /// or item picture), facing, trigger, solidity, what it runs (scenario path,
/// inline DSL lines, or a teleport) and the condition that shows it. /// inline DSL lines, or a teleport) and the condition that shows it.
/// <para/> /// <para/>
/// A teleport is on when its map or cell is filled in; clearing the teleport /// A teleport is on when its map or cell is filled in; clearing the teleport
@@ -29,6 +29,7 @@ namespace Cthangover.Tools.RpgEditor
private readonly SpinBox ySpin; private readonly SpinBox ySpin;
private readonly LineEdit spriteEdit; private readonly LineEdit spriteEdit;
private readonly SpinBox tileSpin; private readonly SpinBox tileSpin;
private readonly LineEdit iconEdit;
private readonly OptionButton dirPicker; private readonly OptionButton dirPicker;
private readonly OptionButton triggerPicker; private readonly OptionButton triggerPicker;
private readonly OptionButton solidPicker; private readonly OptionButton solidPicker;
@@ -66,6 +67,7 @@ namespace Cthangover.Tools.RpgEditor
ySpin = ToolUi.Field(body, "tools/rpg_editor/y", ToolUi.Number(0, 255, 1, OnNumberChanged)); ySpin = ToolUi.Field(body, "tools/rpg_editor/y", ToolUi.Number(0, 255, 1, OnNumberChanged));
spriteEdit = ToolUi.Field(body, "tools/rpg_editor/sprite", ToolUi.Text("village/elder", OnTextChanged), "tools/rpg_editor/sprite_tip"); spriteEdit = ToolUi.Field(body, "tools/rpg_editor/sprite", ToolUi.Text("village/elder", OnTextChanged), "tools/rpg_editor/sprite_tip");
tileSpin = ToolUi.Field(body, "tools/rpg_editor/tile", ToolUi.Number(-1, 4095, 1, OnNumberChanged), "tools/rpg_editor/tile_tip"); tileSpin = ToolUi.Field(body, "tools/rpg_editor/tile", ToolUi.Number(-1, 4095, 1, OnNumberChanged), "tools/rpg_editor/tile_tip");
iconEdit = ToolUi.Field(body, "tools/rpg_editor/icon", ToolUi.Text("rpg_demo/branch_bundle", OnTextChanged), "tools/rpg_editor/icon_tip");
dirPicker = ToolUi.Field(body, "tools/rpg_editor/dir", RpgEditorFields.Direction(OnDirectionPicked)); dirPicker = ToolUi.Field(body, "tools/rpg_editor/dir", RpgEditorFields.Direction(OnDirectionPicked));
triggerPicker = ToolUi.Field(body, "tools/rpg_editor/trigger", RpgEditorFields.Choice(TriggerKeys, OnChoicePicked)); triggerPicker = ToolUi.Field(body, "tools/rpg_editor/trigger", RpgEditorFields.Choice(TriggerKeys, OnChoicePicked));
solidPicker = ToolUi.Field(body, "tools/rpg_editor/solid", RpgEditorFields.Choice(SolidKeys, OnChoicePicked)); solidPicker = ToolUi.Field(body, "tools/rpg_editor/solid", RpgEditorFields.Choice(SolidKeys, OnChoicePicked));
@@ -106,6 +108,7 @@ namespace Cthangover.Tools.RpgEditor
ySpin.Value = value.Y; ySpin.Value = value.Y;
spriteEdit.Text = value.Sprite ?? string.Empty; spriteEdit.Text = value.Sprite ?? string.Empty;
tileSpin.Value = value.Tile; tileSpin.Value = value.Tile;
iconEdit.Text = value.Icon ?? string.Empty;
dirPicker.Select((int)value.Dir); dirPicker.Select((int)value.Dir);
triggerPicker.Select((int)value.Trigger); triggerPicker.Select((int)value.Trigger);
solidPicker.Select(value.Solid == null ? 0 : value.Solid.Value ? 1 : 2); solidPicker.Select(value.Solid == null ? 0 : value.Solid.Value ? 1 : 2);
@@ -161,6 +164,7 @@ namespace Cthangover.Tools.RpgEditor
mapEvent.Y = (int)ySpin.Value; mapEvent.Y = (int)ySpin.Value;
mapEvent.Sprite = NullIfEmpty(spriteEdit.Text); mapEvent.Sprite = NullIfEmpty(spriteEdit.Text);
mapEvent.Tile = (int)tileSpin.Value; mapEvent.Tile = (int)tileSpin.Value;
mapEvent.Icon = NullIfEmpty(iconEdit.Text);
mapEvent.Dir = (RpgDirection)dirPicker.Selected; mapEvent.Dir = (RpgDirection)dirPicker.Selected;
mapEvent.Trigger = (RpgEventTrigger)triggerPicker.Selected; mapEvent.Trigger = (RpgEventTrigger)triggerPicker.Selected;
mapEvent.Solid = solidPicker.Selected switch { 1 => true, 2 => false, _ => null }; mapEvent.Solid = solidPicker.Selected switch { 1 => true, 2 => false, _ => null };
+53
View File
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Cthangover.Core.Items;
using Cthangover.Core.Mods.Resolvers;
using Cthangover.Mods.Rpg;
using Godot;
namespace Cthangover.Tools.RpgEditor
{
/// <summary>
/// The items the running game knows (<c>Kind: item</c> of every loaded mod),
/// sorted by display name, for laying them on a map. The look is the item's
/// own <c>Sprite</c> in the <c>items</c> group, the picture the inventory
/// shows; an item whose picture is missing gets none (the texture loader
/// would log an error for it).
/// </summary>
public static class RpgItemCatalog
{
/// <summary>Reads the item list anew.</summary>
public static List<RpgPickerEntry> Load(RpgTextures textures)
{
var pictures = GroupFiles(RpgTextures.IconGroup);
return (Entities.Collect<ItemInfo>()?.Values ?? Enumerable.Empty<ItemInfo>())
.Where(info => !string.IsNullOrEmpty(info.ID))
.Select(info => Entry(info, pictures.Contains(info.Sprite ?? string.Empty) ? info.Sprite : null, textures))
.OrderBy(entry => entry.Name, StringComparer.CurrentCultureIgnoreCase)
.ToList();
}
/// <summary>Paths of a file group without extensions, case-insensitive: what exists to be loaded.</summary>
public static HashSet<string> GroupFiles(string group)
{
var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var files = Files.Collect(group);
if (files != null)
{
foreach (var key in files.Keys)
paths.Add(Path.ChangeExtension(key, null));
}
return paths;
}
private static RpgPickerEntry Entry(ItemInfo info, string picture, RpgTextures textures) => new()
{
Id = info.ID,
Name = string.IsNullOrEmpty(info.Name) ? info.ID : TranslationServer.Translate(info.Name).ToString(),
Look = picture,
Icon = picture == null ? null : textures.Icon(picture)
};
}
}
+122 -20
View File
@@ -10,12 +10,19 @@ namespace Cthangover.Tools.RpgEditor
{ {
/// <summary> /// <summary>
/// The map being edited, free of any UI: the <see cref="RpgMap"/> with its /// The map being edited, free of any UI: the <see cref="RpgMap"/> with its
/// cells parsed into an <see cref="RpgMapGrid"/>, the edit operations of the /// cells parsed into an <see cref="RpgMapGrid"/>, its events, the edit
/// window and an undo history. /// operations of the window and an undo history.
/// <para/>
/// Events come in two parts. <see cref="Events"/> are the editor's own file of
/// the map's mod, <see cref="EventsPath"/>; the editor changes and saves only
/// them, so a beginner never deals with event files. <see cref="LockedEvents"/>
/// are all the others the game shows on this map: events other mods add, and
/// hand-written files of the same mod (an author may split events over files).
/// They are drawn, never edited, and no new event may take their ids.
/// <para/> /// <para/>
/// Undo works on whole states: before an edit the window calls /// Undo works on whole states: before an edit the window calls
/// <see cref="BeginChange"/>, which stores the grid and the map's other data /// <see cref="BeginChange"/>, which stores the grid and the map's other data
/// (events, spawn, size…) as JSON. A paint stroke is one change, however many /// (own events, spawn, size…) as JSON. A paint stroke is one change, however many
/// cells it touches. The grid is the only source of the cells while editing; /// cells it touches. The grid is the only source of the cells while editing;
/// <see cref="ToJson"/> writes it back into the map rows. /// <see cref="ToJson"/> writes it back into the map rows.
/// </summary> /// </summary>
@@ -26,9 +33,15 @@ namespace Cthangover.Tools.RpgEditor
private readonly List<RpgMapSnapshot> undo = new(); private readonly List<RpgMapSnapshot> undo = new();
private readonly List<RpgMapSnapshot> redo = new(); private readonly List<RpgMapSnapshot> redo = new();
/// <summary>Map data (events, spawn, encounters…); its layer rows are stale until <see cref="ToJson"/>.</summary> /// <summary>Map data (spawn, encounters…); its layer rows are stale until <see cref="ToJson"/>.</summary>
public RpgMap Map { get; private set; } public RpgMap Map { get; private set; }
/// <summary>Events of the editor's own file (<see cref="EventsPath"/>), in drawing order.</summary>
public List<RpgMapEvent> Events { get; private set; } = new();
/// <summary>Events of the map from other files and mods: shown, not edited (<see cref="SetLockedEvents"/>).</summary>
public IReadOnlyList<RpgMapEvent> LockedEvents { get; private set; } = new List<RpgMapEvent>();
/// <summary>The cells.</summary> /// <summary>The cells.</summary>
public RpgMapGrid Grid { get; private set; } public RpgMapGrid Grid { get; private set; }
@@ -41,7 +54,6 @@ namespace Cthangover.Tools.RpgEditor
private RpgMapDocument(RpgMap map) private RpgMapDocument(RpgMap map)
{ {
Map = map; Map = map;
Map.Events ??= new List<RpgMapEvent>();
Map.Spawn ??= new RpgTeleport(); Map.Spawn ??= new RpgTeleport();
Grid = RpgMapGrid.Parse(map); Grid = RpgMapGrid.Parse(map);
} }
@@ -64,24 +76,41 @@ namespace Cthangover.Tools.RpgEditor
} }
/// <summary> /// <summary>
/// Reads a map file (<c>{"Kind":"rpg_map","Items":[…]}</c>, the first item). /// Reads a map file (<c>{"Kind":"rpg_map","Items":[…]}</c>, the first item),
/// and the editor's events file (<c>{"Events":[…]}</c>; null or empty for none).
/// Throws <see cref="JsonException"/> on broken JSON and /// Throws <see cref="JsonException"/> on broken JSON and
/// <see cref="InvalidOperationException"/> on a file without maps, so the /// <see cref="InvalidOperationException"/> on a file without maps, so the
/// window can show the reason instead of opening an empty map. /// window can show the reason instead of opening an empty map.
/// </summary> /// </summary>
public static RpgMapDocument Load(string json) public static RpgMapDocument Load(string json, string eventsJson = null)
{ {
var data = JsonSerializer.Deserialize<FileData<RpgMap>>(json, JsonOptionsProvider.ReadOptions); var data = JsonSerializer.Deserialize<FileData<RpgMap>>(json, JsonOptionsProvider.ReadOptions);
var map = data?.Items?.FirstOrDefault() ?? throw new InvalidOperationException("the file has no rpg_map item"); var map = data?.Items?.FirstOrDefault() ?? throw new InvalidOperationException("the file has no rpg_map item");
return new RpgMapDocument(map); var document = new RpgMapDocument(map);
if (!string.IsNullOrWhiteSpace(eventsJson))
document.Events = JsonSerializer.Deserialize<RpgMapEventFile>(eventsJson, JsonOptionsProvider.ReadOptions)?.Events ?? new List<RpgMapEvent>();
return document;
} }
/// <summary>The map file text: cells written into rows, nulls left out, indented.</summary> /// <summary>Sets the map's events from other files and mods (shown, not edited, not undone).</summary>
public void SetLockedEvents(IReadOnlyList<RpgMapEvent> events) => LockedEvents = events ?? new List<RpgMapEvent>();
/// <summary>Mod-relative path of the editor's events file of a map: <c>map_events/&lt;id&gt;/events.json</c>.</summary>
public static string EventsPath(string mapId) => $"{RpgMapEventResolver.Directory}/{mapId}/events.json";
/// <summary>The map file text: cells written into rows, nulls left out, indented, LF line breaks on every OS, a final one.</summary>
public string ToJson() public string ToJson()
{ {
Grid.WriteTo(Map); Grid.WriteTo(Map);
var data = new FileData<RpgMap> { Kind = "rpg_map", Items = new List<RpgMap> { Map } }; var data = new FileData<RpgMap> { Kind = "rpg_map", Items = new List<RpgMap> { Map } };
return JsonSerializer.Serialize(data, JsonOptionsProvider.WriteIndentedIgnoreNullOptions); return JsonSerializer.Serialize(data, JsonOptionsProvider.WriteIndentedIgnoreNullOptions).Replace("\r\n", "\n") + "\n";
}
/// <summary>The events file text (<see cref="EventsPath"/>): the own events, nulls left out, indented.</summary>
public string EventsToJson()
{
var data = new RpgMapEventFile { Events = Events };
return JsonSerializer.Serialize(data, JsonOptionsProvider.WriteIndentedIgnoreNullOptions).Replace("\r\n", "\n") + "\n";
} }
/// <summary>Remembers the current state as one undo step and forgets the redo branch.</summary> /// <summary>Remembers the current state as one undo step and forgets the redo branch.</summary>
@@ -152,7 +181,7 @@ namespace Cthangover.Tools.RpgEditor
/// <summary>The events on a cell, top-most (last) first.</summary> /// <summary>The events on a cell, top-most (last) first.</summary>
public List<RpgMapEvent> EventsAt(int x, int y) public List<RpgMapEvent> EventsAt(int x, int y)
{ {
var found = Map.Events.Where(e => e.X == x && e.Y == y).ToList(); var found = Events.Where(e => e.X == x && e.Y == y).ToList();
found.Reverse(); found.Reverse();
return found; return found;
} }
@@ -161,21 +190,90 @@ namespace Cthangover.Tools.RpgEditor
public RpgMapEvent AddEvent(int x, int y) public RpgMapEvent AddEvent(int x, int y)
{ {
var number = 1; var number = 1;
while (Map.Events.Any(e => string.Equals(e.Id, "event_" + number, StringComparison.OrdinalIgnoreCase))) while (IdTaken("event_" + number))
number++; number++;
var mapEvent = new RpgMapEvent { Id = "event_" + number, X = x, Y = y }; var mapEvent = new RpgMapEvent { Id = "event_" + number, X = x, Y = y };
Map.Events.Add(mapEvent); Events.Add(mapEvent);
return mapEvent; return mapEvent;
} }
/// <summary>Removes an event; false when it is not on this map.</summary> /// <summary>
public bool RemoveEvent(RpgMapEvent mapEvent) => Map.Events.Remove(mapEvent); /// Puts a character of the game (<c>Kind: character</c>) on a cell as an
/// action event looking like <paramref name="charset"/> (null: the editor's
/// marker). The event id is the character id, numbered (<c>_2</c>, <c>_3</c>…)
/// when taken. An enemy starts <c>rpg.battle</c> with that character on E/F
/// and vanishes once beaten: its win flag <c>&lt;map&gt;_&lt;event&gt;_won</c> is
/// also its hiding condition, so every placed enemy is beaten separately.
/// A non-enemy gets no lines; the author writes its dialog.
/// </summary>
public RpgMapEvent AddCharacter(int x, int y, string characterId, string charset, bool enemy)
{
var id = FreeId(characterId);
var mapEvent = new RpgMapEvent { Id = id, X = x, Y = y, Sprite = charset };
if (enemy)
{
var flag = $"{Map.ID}_{id}_won";
mapEvent.Commands = new List<string> { $"action rpg.battle enemies={characterId} win_flag={flag}", "end" };
mapEvent.Condition = flag + " != 1";
}
Events.Add(mapEvent);
return mapEvent;
}
/// <summary>Changes the map size; events left outside are moved to the nearest edge cell.</summary> /// <summary>
/// Lays an item of the game (<c>Kind: item</c>) on a cell to be picked up by
/// stepping on it, the way the demo's forest pickups work: the event shows
/// <paramref name="icon"/> (the item's picture), is not solid, says
/// <paramref name="line"/>, adds <paramref name="count"/> of the item and sets
/// its own flag <c>&lt;map&gt;_&lt;event&gt;_taken</c>, which hides it for good.
/// The event id is the item id with every character a flag name cannot hold
/// turned into <c>_</c> (<c>rpg_demo/branches</c> → <c>rpg_demo_branches</c>),
/// numbered when taken.
/// </summary>
public RpgMapEvent AddItem(int x, int y, string itemId, string icon, int count, string line)
{
var id = FreeId(new string(itemId.Select(c => char.IsLetterOrDigit(c) || c == '_' ? c : '_').ToArray()));
var flag = $"{Map.ID}_{id}_taken";
var mapEvent = new RpgMapEvent
{
Id = id,
X = x,
Y = y,
Icon = icon,
Trigger = RpgEventTrigger.Touch,
Solid = false,
Commands = new List<string>
{
$"ptext \"{line.Replace("\"", "'")}\"",
$"action item.add id={itemId} count={Math.Max(1, count)}",
$"flag {flag} 1",
"end"
},
Condition = flag + " != 1"
};
Events.Add(mapEvent);
return mapEvent;
}
/// <summary><paramref name="wanted"/> when no event has it (ignoring case), else the first free <c>wanted_N</c> from 2.</summary>
private string FreeId(string wanted)
{
var id = wanted;
for (var number = 2; IdTaken(id); number++)
id = wanted + "_" + number;
return id;
}
private bool IdTaken(string id) => Events.Concat(LockedEvents).Any(e => string.Equals(e.Id, id, StringComparison.OrdinalIgnoreCase));
/// <summary>Removes an event; false when it is not on this map.</summary>
public bool RemoveEvent(RpgMapEvent mapEvent) => Events.Remove(mapEvent);
/// <summary>Changes the map size; own events left outside are moved to the nearest edge cell.</summary>
public void Resize(int width, int height) public void Resize(int width, int height)
{ {
Grid.Resize(width, height); Grid.Resize(width, height);
foreach (var mapEvent in Map.Events) foreach (var mapEvent in Events)
{ {
mapEvent.X = Math.Clamp(mapEvent.X, 0, Grid.Width - 1); mapEvent.X = Math.Clamp(mapEvent.X, 0, Grid.Width - 1);
mapEvent.Y = Math.Clamp(mapEvent.Y, 0, Grid.Height - 1); mapEvent.Y = Math.Clamp(mapEvent.Y, 0, Grid.Height - 1);
@@ -193,12 +291,15 @@ namespace Cthangover.Tools.RpgEditor
var problems = new List<(string, string)>(); var problems = new List<(string, string)>();
if (string.IsNullOrWhiteSpace(Map.Tileset)) if (string.IsNullOrWhiteSpace(Map.Tileset))
problems.Add(("tools/rpg_editor/problem_tileset", Map.ID)); problems.Add(("tools/rpg_editor/problem_tileset", Map.ID));
foreach (var mapEvent in Map.Events) foreach (var mapEvent in Events)
{ {
if (string.IsNullOrWhiteSpace(mapEvent.Id)) if (string.IsNullOrWhiteSpace(mapEvent.Id))
problems.Add(("tools/rpg_editor/problem_event_id", $"{mapEvent.X},{mapEvent.Y}")); problems.Add(("tools/rpg_editor/problem_event_id", $"{mapEvent.X},{mapEvent.Y}"));
var locked = LockedEvents.FirstOrDefault(e => string.Equals(e.Id, mapEvent.Id, StringComparison.OrdinalIgnoreCase));
if (locked != null)
problems.Add(("tools/rpg_editor/problem_event_locked", $"{mapEvent.Id} ({locked.ModId}/{locked.File})"));
} }
foreach (var group in Map.Events.Where(e => !string.IsNullOrWhiteSpace(e.Id)).GroupBy(e => e.Id, StringComparer.OrdinalIgnoreCase)) foreach (var group in Events.Where(e => !string.IsNullOrWhiteSpace(e.Id)).GroupBy(e => e.Id, StringComparer.OrdinalIgnoreCase))
{ {
if (group.Count() > 1) if (group.Count() > 1)
problems.Add(("tools/rpg_editor/problem_event_duplicate", group.Key)); problems.Add(("tools/rpg_editor/problem_event_duplicate", group.Key));
@@ -213,7 +314,7 @@ namespace Cthangover.Tools.RpgEditor
private RpgMapSnapshot Snapshot() private RpgMapSnapshot Snapshot()
{ {
return new RpgMapSnapshot(Grid.Clone(), JsonSerializer.Serialize(Map)); return new RpgMapSnapshot(Grid.Clone(), JsonSerializer.Serialize(Map), JsonSerializer.Serialize(Events));
} }
private bool Swap(List<RpgMapSnapshot> from, List<RpgMapSnapshot> to) private bool Swap(List<RpgMapSnapshot> from, List<RpgMapSnapshot> to)
@@ -225,6 +326,7 @@ namespace Cthangover.Tools.RpgEditor
from.RemoveAt(from.Count - 1); from.RemoveAt(from.Count - 1);
Grid = state.Grid.Clone(); Grid = state.Grid.Clone();
Map = JsonSerializer.Deserialize<RpgMap>(state.MapJson); Map = JsonSerializer.Deserialize<RpgMap>(state.MapJson);
Events = JsonSerializer.Deserialize<List<RpgMapEvent>>(state.EventsJson);
return true; return true;
} }
} }
+120 -23
View File
@@ -16,7 +16,12 @@ namespace Cthangover.Tools.RpgEditor
/// a drag paints a stroke, RMB erases, Alt+LMB picks the tile under the /// a drag paints a stroke, RMB erases, Alt+LMB picks the tile under the
/// pointer. On the events layer: LMB selects the top event of a cell and drags /// pointer. On the events layer: LMB selects the top event of a cell and drags
/// it, a double click on an empty cell adds an event, RMB clears the selection. /// it, a double click on an empty cell adds an event, RMB clears the selection.
/// Ctrl+wheel zooms. /// Locked events (other mods' and other files') are drawn faded with a dashed
/// frame and ignore the mouse.
/// While <see cref="Placing"/>, a click only chooses a cell for the window.
/// Ctrl+wheel zooms. The plain wheel (Shift+wheel horizontally) is left to
/// the surrounding ScrollContainer, and a middle-button drag pans it, so a
/// map larger than the view is reachable on any layer and with any tool.
/// <para/> /// <para/>
/// The canvas owns no data. It edits the window's /// The canvas owns no data. It edits the window's
/// <see cref="RpgMapDocument"/> and reports through events; every stroke or /// <see cref="RpgMapDocument"/> and reports through events; every stroke or
@@ -29,12 +34,16 @@ namespace Cthangover.Tools.RpgEditor
private static readonly Color OpenColor = new(0.2f, 0.85f, 0.35f, 0.4f); private static readonly Color OpenColor = new(0.2f, 0.85f, 0.35f, 0.4f);
private static readonly Color GridColor = new(1f, 1f, 1f, 0.12f); private static readonly Color GridColor = new(1f, 1f, 1f, 0.12f);
private static readonly Color MarkerColor = new(0.35f, 0.55f, 1f, 0.55f); private static readonly Color MarkerColor = new(0.35f, 0.55f, 1f, 0.55f);
private static readonly Color LockedModulate = new(1f, 1f, 1f, 0.55f);
private readonly RpgTextures textures = new(); private readonly RpgTextures textures = new();
private Vector2I? hover; private Vector2I? hover;
private bool stroking; private bool stroking;
private RpgMapEvent dragged; private RpgMapEvent dragged;
private bool dragMoved; private bool dragMoved;
private bool panning;
private Vector2 panStartMouse;
private Vector2 panStartScroll;
/// <summary>The edited map; null shows an empty canvas.</summary> /// <summary>The edited map; null shows an empty canvas.</summary>
public RpgMapDocument Document { get; set; } public RpgMapDocument Document { get; set; }
@@ -78,6 +87,15 @@ namespace Cthangover.Tools.RpgEditor
/// <summary>Alt+click picked a tile.</summary> /// <summary>Alt+click picked a tile.</summary>
public event Action<int> TilePicked; public event Action<int> TilePicked;
/// <summary>
/// The next LMB on a cell is not an edit but a choice reported through
/// <see cref="CellChosen"/> (placing a character); RMB cancels. Either ends it.
/// </summary>
public bool Placing { get; set; }
/// <summary>The cell chosen while <see cref="Placing"/>, or null when cancelled.</summary>
public event Action<Vector2I?> CellChosen;
/// <summary>Creates the canvas; input comes through the GuiInput event.</summary> /// <summary>Creates the canvas; input comes through the GuiInput event.</summary>
public RpgMapEditorCanvas() public RpgMapEditorCanvas()
{ {
@@ -147,30 +165,55 @@ namespace Cthangover.Tools.RpgEditor
private void DrawEvents() private void DrawEvents()
{ {
foreach (var mapEvent in Document.Map.Events) foreach (var mapEvent in Document.LockedEvents)
{ DrawEvent(mapEvent, true);
var rect = CellRect(mapEvent.X, mapEvent.Y); foreach (var mapEvent in Document.Events)
var charset = string.IsNullOrEmpty(mapEvent.Sprite) ? null : textures.Charset(mapEvent.Sprite); DrawEvent(mapEvent, false);
if (charset != null) }
{
var frame = new Vector2(charset.GetWidth() / 3f, charset.GetHeight() / 4f);
var source = new Rect2(new Vector2(frame.X, frame.Y * (int)mapEvent.Dir), frame);
var size = frame * Zoom;
var target = new Rect2(rect.Position + new Vector2((rect.Size.X - size.X) / 2f, rect.Size.Y - size.Y), size);
DrawTextureRectRegion(charset, target, source);
}
else if (mapEvent.Tile >= 0 && Atlas != null && Tileset != null)
{
DrawTextureRectRegion(Atlas, rect, TileRegion(mapEvent.Tile));
}
else
{
DrawRect(rect.Grow(-CellSize * 0.15f), MarkerColor);
}
if (ActiveLayer == RpgEditLayer.Events) /// <summary>One event: its look, then on the events layer a frame (dashed grey for a locked one).</summary>
DrawRect(rect, mapEvent == SelectedEvent ? UiTokens.AccentBright : UiTokens.Alpha(UiTokens.White, 0.5f), false, mapEvent == SelectedEvent ? 3f : 1f); private void DrawEvent(RpgMapEvent mapEvent, bool locked)
{
var modulate = locked ? LockedModulate : Colors.White;
var rect = CellRect(mapEvent.X, mapEvent.Y);
var charset = string.IsNullOrEmpty(mapEvent.Sprite) ? null : textures.Charset(mapEvent.Sprite);
if (charset != null)
{
var frame = new Vector2(charset.GetWidth() / 3f, charset.GetHeight() / 4f);
var source = new Rect2(new Vector2(frame.X, frame.Y * (int)mapEvent.Dir), frame);
var size = frame * Zoom;
var target = new Rect2(rect.Position + new Vector2((rect.Size.X - size.X) / 2f, rect.Size.Y - size.Y), size);
DrawTextureRectRegion(charset, target, source, modulate);
} }
else if (mapEvent.Tile >= 0 && Atlas != null && Tileset != null)
{
DrawTextureRectRegion(Atlas, rect, TileRegion(mapEvent.Tile), modulate);
}
else if (!string.IsNullOrEmpty(mapEvent.Icon) && textures.Icon(mapEvent.Icon) is { } icon)
{
// Fitted like RpgActor.UseIcon: three quarters of the cell, centred.
var fit = CellSize * 0.75f / Mathf.Max(1, Mathf.Max(icon.GetWidth(), icon.GetHeight()));
var size = icon.GetSize() * fit;
DrawTextureRect(icon, new Rect2(rect.GetCenter() - size / 2f, size), false, modulate);
}
else
{
DrawRect(rect.Grow(-CellSize * 0.15f), MarkerColor * modulate);
}
if (ActiveLayer == RpgEditLayer.Events && locked)
DrawDashedRect(rect.Grow(-1f));
else if (ActiveLayer == RpgEditLayer.Events)
DrawRect(rect, mapEvent == SelectedEvent ? UiTokens.AccentBright : UiTokens.Alpha(UiTokens.White, 0.5f), false, mapEvent == SelectedEvent ? 3f : 1f);
}
private void DrawDashedRect(Rect2 rect)
{
var color = UiTokens.Alpha(UiTokens.White, 0.6f);
DrawDashedLine(rect.Position, new Vector2(rect.End.X, rect.Position.Y), color, 1f, 4f);
DrawDashedLine(new Vector2(rect.End.X, rect.Position.Y), rect.End, color, 1f, 4f);
DrawDashedLine(rect.End, new Vector2(rect.Position.X, rect.End.Y), color, 1f, 4f);
DrawDashedLine(new Vector2(rect.Position.X, rect.End.Y), rect.Position, color, 1f, 4f);
} }
private void DrawSpawn() private void DrawSpawn()
@@ -228,6 +271,12 @@ namespace Cthangover.Tools.RpgEditor
if (@event is InputEventMouseMotion motion) if (@event is InputEventMouseMotion motion)
{ {
if (panning)
{
Pan(motion);
AcceptEvent();
return;
}
hover = CellAt(motion.Position); hover = CellAt(motion.Position);
if (stroking && (motion.ButtonMask & (MouseButtonMask.Left | MouseButtonMask.Right)) != 0) if (stroking && (motion.ButtonMask & (MouseButtonMask.Left | MouseButtonMask.Right)) != 0)
Apply(hover.Value, (motion.ButtonMask & MouseButtonMask.Right) != 0); Apply(hover.Value, (motion.ButtonMask & MouseButtonMask.Right) != 0);
@@ -246,6 +295,15 @@ namespace Cthangover.Tools.RpgEditor
AcceptEvent(); AcceptEvent();
return; return;
} }
// Not accepted: the event bubbles up and the ScrollContainer scrolls.
if (button.ButtonIndex is MouseButton.WheelUp or MouseButton.WheelDown or MouseButton.WheelLeft or MouseButton.WheelRight)
return;
if (button.ButtonIndex == MouseButton.Middle)
{
StartOrEndPan(button);
AcceptEvent();
return;
}
var cell = CellAt(button.Position); var cell = CellAt(button.Position);
if (!button.Pressed) if (!button.Pressed)
@@ -258,6 +316,15 @@ namespace Cthangover.Tools.RpgEditor
return; return;
} }
if (Placing && (button.ButtonIndex == MouseButton.Left || button.ButtonIndex == MouseButton.Right))
{
Placing = false;
var chosen = button.ButtonIndex == MouseButton.Left && Document.Grid.InBounds(cell.X, cell.Y);
CellChosen?.Invoke(chosen ? cell : null);
AcceptEvent();
return;
}
if (ActiveLayer == RpgEditLayer.Events) if (ActiveLayer == RpgEditLayer.Events)
PressOnEvents(button, cell); PressOnEvents(button, cell);
else if (button.ButtonIndex == MouseButton.Left || button.ButtonIndex == MouseButton.Right) else if (button.ButtonIndex == MouseButton.Left || button.ButtonIndex == MouseButton.Right)
@@ -343,6 +410,36 @@ namespace Cthangover.Tools.RpgEditor
QueueRedraw(); QueueRedraw();
} }
/// <summary>
/// Middle button down starts panning the parent ScrollContainer, up ends
/// it. The scroll offset is anchored to the press point rather than
/// accumulated from deltas, so hitting a scroll edge builds no drift.
/// </summary>
private void StartOrEndPan(InputEventMouseButton button)
{
var scroll = GetParentOrNull<ScrollContainer>();
panning = button.Pressed && scroll != null;
MouseDefaultCursorShape = panning ? CursorShape.Drag : CursorShape.Arrow;
if (!panning)
return;
panStartMouse = button.GlobalPosition;
panStartScroll = new Vector2(scroll.ScrollHorizontal, scroll.ScrollVertical);
}
private void Pan(InputEventMouseMotion motion)
{
var scroll = GetParentOrNull<ScrollContainer>();
if (scroll == null || (motion.ButtonMask & MouseButtonMask.Middle) == 0)
{
panning = false;
MouseDefaultCursorShape = CursorShape.Arrow;
return;
}
var delta = motion.GlobalPosition - panStartMouse;
scroll.ScrollHorizontal = Mathf.RoundToInt(panStartScroll.X - delta.X);
scroll.ScrollVertical = Mathf.RoundToInt(panStartScroll.Y - delta.Y);
}
private void OnMouseExited() private void OnMouseExited()
{ {
hover = null; hover = null;
+1 -1
View File
@@ -7,7 +7,7 @@ namespace Cthangover.Tools.RpgEditor
/// <summary> /// <summary>
/// Registers the RPG map editor as a tool: the kernel's tool factory finds /// Registers the RPG map editor as a tool: the kernel's tool factory finds
/// every <see cref="IToolProvider"/>, so the editor appears in the main /// every <see cref="IToolProvider"/>, so the editor appears in the main
/// menu's tool block, the novel editor hub and (with /// menu's tool block and (with
/// <see cref="RpgMapEditorToolBoxButton"/>) the in-game toolbar. /// <see cref="RpgMapEditorToolBoxButton"/>) the in-game toolbar.
/// </summary> /// </summary>
public class RpgMapEditorToolProvider : IToolProvider public class RpgMapEditorToolProvider : IToolProvider
+118 -6
View File
@@ -19,8 +19,11 @@ namespace Cthangover.Tools.RpgEditor
/// <summary> /// <summary>
/// Editor of RPG tile maps (<c>maps/*.json</c>, kind <c>rpg_map</c>). /// Editor of RPG tile maps (<c>maps/*.json</c>, kind <c>rpg_map</c>).
/// Layout: toolbar (file, save, layer, brush, grid, zoom, undo/redo, play), /// Layout: toolbar (file, save, layer, brush, grid, zoom, undo/redo, play),
/// the tile palette on the left, the map in the middle, the map's and the /// the tile palette, the game's characters and items as tabs on the left, the
/// selected event's properties on the right. /// map in the middle, the map's and the selected event's properties on the right.
/// A character is put on the map as an NPC or an enemy, an item as a pickup:
/// pick it, press the button, click a cell (<see cref="RpgMapDocument.AddCharacter"/>,
/// <see cref="RpgMapDocument.AddItem"/>).
/// <para/> /// <para/>
/// Layers are the game's: back, main, front, the collision layer (codes /// Layers are the game's: back, main, front, the collision layer (codes
/// over the tiles' own solidity) and events. Every edit is undoable /// over the tiles' own solidity) and events. Every edit is undoable
@@ -31,6 +34,10 @@ namespace Cthangover.Tools.RpgEditor
/// (<see cref="SceneManager.ReloadContent"/>), so "Play" works on a new map /// (<see cref="SceneManager.ReloadContent"/>), so "Play" works on a new map
/// without a mod reload. /// without a mod reload.
/// <para/> /// <para/>
/// Saving writes the map file and the mod's events file of the map,
/// <c>map_events/&lt;id&gt;/events.json</c> (<see cref="RpgMapDocument"/> explains
/// own and locked events).
/// <para/>
/// After a save the map is put into the running game's map cache, and a /// After a save the map is put into the running game's map cache, and a
/// world showing it is remounted, so the edit is visible at once; "Play" /// world showing it is remounted, so the edit is visible at once; "Play"
/// saves and moves the player onto the map at its spawn. /// saves and moves the player onto the map at its spawn.
@@ -44,7 +51,12 @@ namespace Cthangover.Tools.RpgEditor
private readonly Dictionary<RpgEditTool, Button> toolButtons = new(); private readonly Dictionary<RpgEditTool, Button> toolButtons = new();
private ModFileSelector fileSelector; private ModFileSelector fileSelector;
private TabContainer sideTabs;
private RpgTilePalette palette; private RpgTilePalette palette;
private SpinBox itemCount;
private Func<Vector2I, RpgMapEvent> placeAt;
private string placedKey;
private string placedName;
private RpgMapEditorCanvas canvas; private RpgMapEditorCanvas canvas;
private RpgMapPropertiesPanel mapPanel; private RpgMapPropertiesPanel mapPanel;
private RpgEventPropertiesPanel eventPanel; private RpgEventPropertiesPanel eventPanel;
@@ -78,13 +90,38 @@ namespace Cthangover.Tools.RpgEditor
var main = new HSplitContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, SizeFlagsVertical = Control.SizeFlags.ExpandFill }; var main = new HSplitContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, SizeFlagsVertical = Control.SizeFlags.ExpandFill };
outer.AddChild(main); outer.AddChild(main);
var leftColumn = new VBoxContainer();
sideTabs = new TabContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
leftColumn.AddChild(sideTabs);
var paletteColumn = new VBoxContainer(); var paletteColumn = new VBoxContainer();
paletteColumn.AddChild(ToolUi.Section("tools/rpg_editor/section_palette")); paletteColumn.AddChild(ToolUi.Section("tools/rpg_editor/section_palette"));
palette = new RpgTilePalette(); palette = new RpgTilePalette();
palette.TileSelected += OnPaletteTile; palette.TileSelected += OnPaletteTile;
palette.CollisionSelected += OnPaletteCollision; palette.CollisionSelected += OnPaletteCollision;
paletteColumn.AddChild(palette); paletteColumn.AddChild(palette);
main.AddChild(CreateSidebar(paletteColumn, 400)); sideTabs.AddChild(paletteColumn);
sideTabs.SetTabTitle(0, ToolUi.Tr("tools/rpg_editor/tab_tiles"));
var characters = new RpgEntityPicker("tools/rpg_editor/character_search", "tools/rpg_editor/characters_hint") { Name = "Characters" };
characters.Source = LoadCharacters;
characters.AddAction("tools/rpg_editor/place_npc", "tools/rpg_editor/place_npc_tip", PlaceNpc);
characters.AddAction("tools/rpg_editor/place_enemy", "tools/rpg_editor/place_enemy_tip", PlaceEnemy);
sideTabs.AddChild(characters);
sideTabs.SetTabTitle(1, ToolUi.Tr("tools/rpg_editor/tab_characters"));
var items = new RpgEntityPicker("tools/rpg_editor/item_search", "tools/rpg_editor/items_hint") { Name = "Items" };
items.Source = LoadItems;
itemCount = ToolUi.Number(1, 999, 1, null);
itemCount.Value = 1;
itemCount.TooltipText = ToolUi.Tr("tools/rpg_editor/item_count_tip");
items.AddOption(new Label { Text = ToolUi.Tr("tools/rpg_editor/item_count") });
items.AddOption(itemCount);
items.AddAction("tools/rpg_editor/place_item", "tools/rpg_editor/place_item_tip", PlaceItem);
sideTabs.AddChild(items);
sideTabs.SetTabTitle(2, ToolUi.Tr("tools/rpg_editor/tab_items"));
sideTabs.TabChanged += OnSideTabChanged;
main.AddChild(CreateSidebar(leftColumn, 400));
var center = new HSplitContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; var center = new HSplitContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
main.AddChild(center); main.AddChild(center);
@@ -94,6 +131,7 @@ namespace Cthangover.Tools.RpgEditor
canvas.Changed += OnDocumentChanged; canvas.Changed += OnDocumentChanged;
canvas.EventSelected += OnEventSelected; canvas.EventSelected += OnEventSelected;
canvas.TilePicked += OnTilePicked; canvas.TilePicked += OnTilePicked;
canvas.CellChosen += OnCellChosen;
center.AddChild(Scroll(canvas)); center.AddChild(Scroll(canvas));
var properties = new VBoxContainer(); var properties = new VBoxContainer();
@@ -206,7 +244,7 @@ namespace Cthangover.Tools.RpgEditor
{ {
var text = ModFileService.ReadText(mod, file); var text = ModFileService.ReadText(mod, file);
isNew = string.IsNullOrEmpty(text); isNew = string.IsNullOrEmpty(text);
document = isNew ? CreateDocument(file) : RpgMapDocument.Load(text); document = isNew ? CreateDocument(file) : LoadDocument(mod, text);
SetStatus(string.Format(ToolUi.Tr(isNew ? "tools/common/new_file_status" : "tools/common/loaded"), mod + "/" + file)); SetStatus(string.Format(ToolUi.Tr(isNew ? "tools/common/new_file_status" : "tools/common/loaded"), mod + "/" + file));
} }
catch (Exception ex) catch (Exception ex)
@@ -221,6 +259,23 @@ namespace Cthangover.Tools.RpgEditor
BindDocument(); BindDocument();
} }
/// <summary>
/// The map with the editor's events file of the same mod; every other event
/// the game shows on the map is locked. An own event hides a locked one of the
/// same id (another mod replacing it), so the author sees and edits their own.
/// </summary>
private static RpgMapDocument LoadDocument(string mod, string text)
{
var id = RpgMapDocument.Load(text).Map.ID;
var eventsPath = RpgMapDocument.EventsPath(id);
var document = RpgMapDocument.Load(text, ModFileService.ReadText(mod, eventsPath));
var own = new HashSet<string>(document.Events.Select(e => e.Id), StringComparer.OrdinalIgnoreCase);
document.SetLockedEvents(RpgMapEvents.Of(id)
.Where(e => !(e.ModId == mod && e.File == eventsPath) && !own.Contains(e.Id))
.ToList());
return document;
}
private static RpgMapDocument CreateDocument(string file) private static RpgMapDocument CreateDocument(string file)
{ {
var tileset = Entities.Collect<RpgTileset>().Keys.OrderBy(k => k).FirstOrDefault(); var tileset = Entities.Collect<RpgTileset>().Keys.OrderBy(k => k).FirstOrDefault();
@@ -248,11 +303,15 @@ namespace Cthangover.Tools.RpgEditor
} }
var json = document.ToJson(); var json = document.ToJson();
if (!ModFileService.WriteText(modId, path, json)) var eventsPath = RpgMapDocument.EventsPath(document.Map.ID);
var writeEvents = document.Events.Count > 0 || !string.IsNullOrEmpty(ModFileService.ReadText(modId, eventsPath));
if (!ModFileService.WriteText(modId, path, json)
|| (writeEvents && !ModFileService.WriteText(modId, eventsPath, document.EventsToJson())))
{ {
SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error); SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error);
return false; return false;
} }
RpgMapEvents.Invalidate();
var sceneNote = isNew ? EnsureScene() : null; var sceneNote = isNew ? EnsureScene() : null;
if (sceneNote != null) if (sceneNote != null)
@@ -342,7 +401,7 @@ namespace Cthangover.Tools.RpgEditor
return; return;
// The document holds a new map object now: find the selection again by id. // The document holds a new map object now: find the selection again by id.
var selectedId = canvas.SelectedEvent?.Id; var selectedId = canvas.SelectedEvent?.Id;
canvas.SelectedEvent = document.Map.Events.FirstOrDefault(e => e.Id == selectedId); canvas.SelectedEvent = document.Events.FirstOrDefault(e => e.Id == selectedId);
SetDirty(); SetDirty();
BindDocument(); BindDocument();
} }
@@ -379,6 +438,57 @@ namespace Cthangover.Tools.RpgEditor
private void OnPaletteCollision(char code) => canvas.SelectedCollision = code; private void OnPaletteCollision(char code) => canvas.SelectedCollision = code;
private void OnSideTabChanged(long tab)
{
if (sideTabs.GetCurrentTabControl() is RpgEntityPicker picker)
picker.Reload();
}
private List<RpgPickerEntry> LoadCharacters() => RpgCharacterCatalog.Load(textures);
private List<RpgPickerEntry> LoadItems() => RpgItemCatalog.Load(textures);
private void PlaceNpc(RpgPickerEntry entry)
=> BeginPlacing(entry, "tools/rpg_editor/placed_npc", cell => document.AddCharacter(cell.X, cell.Y, entry.Id, entry.Look, false));
private void PlaceEnemy(RpgPickerEntry entry)
=> BeginPlacing(entry, "tools/rpg_editor/placed_enemy", cell => document.AddCharacter(cell.X, cell.Y, entry.Id, entry.Look, true));
private void PlaceItem(RpgPickerEntry entry)
{
var count = (int)itemCount.Value;
var line = string.Format(ToolUi.Tr("tools/rpg_editor/item_found_line"), entry.Name, count);
BeginPlacing(entry, "tools/rpg_editor/placed_item", cell => document.AddItem(cell.X, cell.Y, entry.Id, entry.Look, count, line));
}
/// <summary>Something is to be placed: switch to the events layer and wait for a cell; <paramref name="place"/> makes the event.</summary>
private void BeginPlacing(RpgPickerEntry entry, string doneKey, Func<Vector2I, RpgMapEvent> place)
{
if (document == null)
return;
placeAt = place;
placedKey = doneKey;
placedName = entry.Name;
SelectLayer(RpgEditLayer.Events);
canvas.Placing = true;
SetStatus(string.Format(ToolUi.Tr("tools/rpg_editor/place_status"), entry.Name));
}
private void OnCellChosen(Vector2I? cell)
{
if (cell == null || placeAt == null || document == null)
{
SetStatus(ToolUi.Tr("tools/rpg_editor/place_cancelled"));
return;
}
RecordUndo();
var mapEvent = placeAt(cell.Value);
canvas.SelectedEvent = mapEvent;
eventPanel.Bind(mapEvent);
OnDocumentChanged();
SetStatus(string.Format(ToolUi.Tr(placedKey), placedName, mapEvent.Id), ToolStatusKind.Ok);
}
private void OnTilePicked(int index) private void OnTilePicked(int index)
{ {
canvas.SelectedTile = index; canvas.SelectedTile = index;
@@ -388,6 +498,8 @@ namespace Cthangover.Tools.RpgEditor
private void SelectLayer(RpgEditLayer layer) private void SelectLayer(RpgEditLayer layer)
{ {
canvas.ActiveLayer = layer; canvas.ActiveLayer = layer;
if (layer != RpgEditLayer.Events)
canvas.Placing = false;
foreach (var pair in layerButtons) foreach (var pair in layerButtons)
pair.Value.SetPressedNoSignal(pair.Key == layer); pair.Value.SetPressedNoSignal(pair.Key == layer);
palette.SetCollisionMode(layer == RpgEditLayer.Collision); palette.SetCollisionMode(layer == RpgEditLayer.Collision);
+6 -2
View File
@@ -2,7 +2,7 @@ using Cthangover.Mods.Rpg;
namespace Cthangover.Tools.RpgEditor namespace Cthangover.Tools.RpgEditor
{ {
/// <summary>One undo step of <see cref="RpgMapDocument"/>: the cells and the rest of the map as JSON.</summary> /// <summary>One undo step of <see cref="RpgMapDocument"/>: the cells, the rest of the map and the own events as JSON.</summary>
public class RpgMapSnapshot public class RpgMapSnapshot
{ {
/// <summary>Copy of the cells.</summary> /// <summary>Copy of the cells.</summary>
@@ -11,11 +11,15 @@ namespace Cthangover.Tools.RpgEditor
/// <summary>The map without its cells (rows may be stale), serialized.</summary> /// <summary>The map without its cells (rows may be stale), serialized.</summary>
public string MapJson { get; } public string MapJson { get; }
/// <summary>The document's own events, serialized.</summary>
public string EventsJson { get; }
/// <summary>Creates a step.</summary> /// <summary>Creates a step.</summary>
public RpgMapSnapshot(RpgMapGrid grid, string mapJson) public RpgMapSnapshot(RpgMapGrid grid, string mapJson, string eventsJson)
{ {
Grid = grid; Grid = grid;
MapJson = mapJson; MapJson = mapJson;
EventsJson = eventsJson;
} }
} }
} }
+20
View File
@@ -0,0 +1,20 @@
using Godot;
namespace Cthangover.Tools.RpgEditor
{
/// <summary>One row of an <see cref="RpgEntityPicker"/>: a character or an item of the game.</summary>
public class RpgPickerEntry
{
/// <summary>Entity id (what <c>rpg.battle enemies=</c> or <c>item.add id=</c> takes).</summary>
public string Id { get; init; }
/// <summary>Display name in the current language (the id when the entity has none).</summary>
public string Name { get; init; }
/// <summary>How it looks on the map: a charset path for a character, an item picture path for an item; null for none.</summary>
public string Look { get; init; }
/// <summary>Picture of the row, or null.</summary>
public Texture2D Icon { get; init; }
}
}
+75 -5
View File
@@ -16,10 +16,13 @@ namespace Cthangover.Tools.RpgEditor.Tests
/// <summary> /// <summary>
/// Drives the RPG map editor through its controls like an author: layer /// Drives the RPG map editor through its controls like an author: layer
/// buttons are pressed, the palette and the canvas get mouse events through /// buttons are pressed, the palette and the canvas get mouse events through
/// their <c>gui_input</c> signal, Ctrl+Z goes into the window. That window is /// their <c>gui_input</c> signal, Ctrl+Z goes into the window, a slime found in
/// the characters tab is placed as an enemy and branches from the items tab as
/// a pickup. That window is
/// freed without saving, so the demo maps never change. A second window then /// freed without saving, so the demo maps never change. A second window then
/// creates a new map through the file selector's "+" and saves it: the game must /// creates a new map through the file selector's "+" and saves it: the game must
/// know the map's scene at once (no mod reload); its files are deleted again. /// know the map's scene and its event (saved apart, into map_events) at once,
/// with no mod reload; its files are deleted again.
/// Result line: <c>[TEST_RESULT] name=rpg_editor_edit passed=N failed=M</c>. /// Result line: <c>[TEST_RESULT] name=rpg_editor_edit passed=N failed=M</c>.
/// </summary> /// </summary>
public partial class RpgEditorTestDriver : Node public partial class RpgEditorTestDriver : Node
@@ -31,6 +34,8 @@ namespace Cthangover.Tools.RpgEditor.Tests
private static readonly Vector2I PaintCell = new(3, 5); private static readonly Vector2I PaintCell = new(3, 5);
private static readonly Vector2I EventCell = new(2, 6); private static readonly Vector2I EventCell = new(2, 6);
private static readonly Vector2I CharacterCell = new(4, 6);
private static readonly Vector2I ItemCell = new(5, 6);
private readonly string shotsDir; private readonly string shotsDir;
private readonly List<string> failures = new(); private readonly List<string> failures = new();
@@ -95,6 +100,8 @@ namespace Cthangover.Tools.RpgEditor.Tests
if (canvas?.Document == null || canvas.Tileset == null) if (canvas?.Document == null || canvas.Tileset == null)
return; return;
Check(canvas.Document.Map.ID.StartsWith("rpg_"), $"the editor opened a demo map ({canvas.Document.Map.ID})"); Check(canvas.Document.Map.ID.StartsWith("rpg_"), $"the editor opened a demo map ({canvas.Document.Map.ID})");
Check(canvas.Document.Events.Count > 0 && canvas.Document.LockedEvents.Count == 0,
$"its events come from the mod's map_events file ({canvas.Document.Events.Count} own, {canvas.Document.LockedEvents.Count} locked)");
Shot("editor"); Shot("editor");
Press("tools/rpg_editor/layer_main"); Press("tools/rpg_editor/layer_main");
Check(canvas.ActiveLayer == RpgEditLayer.Main, "the layer button switches the brush to the main layer"); Check(canvas.ActiveLayer == RpgEditLayer.Main, "the layer button switches the brush to the main layer");
@@ -110,14 +117,15 @@ namespace Cthangover.Tools.RpgEditor.Tests
window.PushInput(new InputEventKey { Keycode = Key.Z, PhysicalKeycode = Key.Z, CtrlPressed = true, Pressed = true }); window.PushInput(new InputEventKey { Keycode = Key.Z, PhysicalKeycode = Key.Z, CtrlPressed = true, Pressed = true });
Check(canvas.Document.Grid.GetTile(RpgLayer.Main, PaintCell.X, PaintCell.Y) == before, "Ctrl+Z undoes the stroke"); Check(canvas.Document.Grid.GetTile(RpgLayer.Main, PaintCell.X, PaintCell.Y) == before, "Ctrl+Z undoes the stroke");
Press("tools/rpg_editor/layer_events"); Press("tools/rpg_editor/layer_events");
eventsBefore = canvas.Document.Map.Events.Count; eventsBefore = canvas.Document.Events.Count;
ClickCanvas(EventCell, true); ClickCanvas(EventCell, true);
Check(canvas.Document.Map.Events.Count == eventsBefore + 1 && canvas.SelectedEvent != null, "a double click adds and selects an event"); Check(canvas.Document.Events.Count == eventsBefore + 1 && canvas.SelectedEvent != null, "a double click adds and selects an event");
step++; step++;
return; return;
case 3: case 3:
// Screenshots one tick after the action, when the frame shows it. // Screenshots one tick after the action, when the frame shows it.
Shot("events"); Shot("events");
PlaceSlime();
Press("tools/rpg_editor/layer_collision"); Press("tools/rpg_editor/layer_collision");
step++; step++;
return; return;
@@ -149,8 +157,12 @@ namespace Cthangover.Tools.RpgEditor.Tests
return; return;
try try
{ {
canvas.Document.AddEvent(1, 1).Sprite = "village/elder";
Press("tools/common/save"); Press("tools/common/save");
Check(File.Exists(ModFileService.GetWritablePath(newMapMod, "scenes/" + NewMap + ".json")), "the first save of a new map writes its scene"); Check(File.Exists(ModFileService.GetWritablePath(newMapMod, "scenes/" + NewMap + ".json")), "the first save of a new map writes its scene");
Check(!ModFileService.ReadText(newMapMod, "maps/" + NewMap + ".json").Contains("\"Events\""), "the map file holds no events");
Check(RpgMapEvents.Of(NewMap).Any(e => e.Id == "event_1" && e.ModId == newMapMod && e.File == RpgMapDocument.EventsPath(NewMap)),
"the event is saved into map_events and the game reads it at once");
Check(GetTree().Root.GetNodeOrNull<SceneManager>("SceneManager")?.HasScene(NewMap) == true, "the game knows the new map's scene without a mod reload"); Check(GetTree().Root.GetNodeOrNull<SceneManager>("SceneManager")?.HasScene(NewMap) == true, "the game knows the new map's scene without a mod reload");
} }
finally finally
@@ -164,16 +176,74 @@ namespace Cthangover.Tools.RpgEditor.Tests
} }
} }
/// <summary>Characters tab: search "slime", pick it, "place as enemy", click a cell; items tab: two bundles of branches.</summary>
private void PlaceSlime()
{
var picker = Pick("Characters", 1, "slime", "rpg_slime");
if (picker == null)
return;
Check(picker.Selected.Look == "village/slime" && picker.Selected.Icon != null, $"the slime is picked with its charset ({picker.Selected.Look})");
var count = canvas.Document.Events.Count;
Press("tools/rpg_editor/place_enemy");
Check(canvas.Placing && canvas.ActiveLayer == RpgEditLayer.Events, "placing waits for a cell on the events layer");
ClickCanvas(CharacterCell, false);
var placed = canvas.SelectedEvent;
Check(canvas.Document.Events.Count == count + 1 && placed?.Sprite == "village/slime" && placed.X == CharacterCell.X && placed.Y == CharacterCell.Y,
"a click on the map puts the slime there");
Check(placed?.Commands?.FirstOrDefault()?.StartsWith("action rpg.battle enemies=rpg_slime ") == true && !canvas.Placing, "the placed enemy starts a battle with it");
picker = Pick("Items", 2, "branches", "rpg_demo/branches");
if (picker == null)
return;
Check(picker.Selected.Look == "rpg_demo/branch_bundle" && picker.Selected.Icon != null, $"the branches are picked with their picture ({picker.Selected.Look})");
var countSpin = picker.FindChildren("*", "", true, false).OfType<SpinBox>().First();
countSpin.Value = 2;
Press("tools/rpg_editor/place_item");
ClickCanvas(ItemCell, false);
placed = canvas.SelectedEvent;
Check(placed?.Icon == "rpg_demo/branch_bundle" && placed.X == ItemCell.X && placed.Trigger == RpgEventTrigger.Touch && placed.Solid == false,
"a click on the map lays the branches there, picked up by stepping on them");
Check(placed?.Commands?.Contains("action item.add id=rpg_demo/branches count=2") == true, "the pickup gives the chosen count");
}
/// <summary>Opens the side tab, types the query, checks every row matches it and selects <paramref name="id"/>; null when it is not listed.</summary>
private RpgEntityPicker Pick(string name, int tab, string query, string id)
{
window.FindChildren("*", "", true, false).OfType<TabContainer>().First(t => t.GetTabCount() == 3).CurrentTab = tab;
var picker = window.FindChildren("*", "", true, false).OfType<RpgEntityPicker>().First(p => p.Name == name);
var search = picker.FindChildren("*", "", true, false).OfType<LineEdit>().First(e => e.GetParent() == picker);
search.Text = query;
search.EmitSignal(LineEdit.SignalName.TextChanged, query);
var list = picker.FindChildren("*", "", true, false).OfType<ItemList>().First();
Check(list.ItemCount >= 1 && Enumerable.Range(0, list.ItemCount).All(i => list.GetItemText(i).Contains(query, System.StringComparison.OrdinalIgnoreCase)),
$"the {name} search keeps only '{query}' ({list.ItemCount} rows)");
for (var i = 0; i < list.ItemCount; i++)
{
if (!list.GetItemText(i).EndsWith("· " + id))
continue;
list.Select(i);
list.EmitSignal(ItemList.SignalName.ItemSelected, i);
return picker;
}
Fail($"'{id}' is not in the {name} list");
return null;
}
private ModFileSelector Selector() => window.FindChildren("*", "", true, false).OfType<ModFileSelector>().First(); private ModFileSelector Selector() => window.FindChildren("*", "", true, false).OfType<ModFileSelector>().First();
private void DeleteNewMapFiles() private void DeleteNewMapFiles()
{ {
foreach (var file in new[] { "maps/" + NewMap + ".json", "scenes/" + NewMap + ".json" }) foreach (var file in new[] { "maps/" + NewMap + ".json", "scenes/" + NewMap + ".json", RpgMapDocument.EventsPath(NewMap) })
{ {
var path = ModFileService.GetWritablePath(newMapMod, file); var path = ModFileService.GetWritablePath(newMapMod, file);
if (path != null && File.Exists(path)) if (path != null && File.Exists(path))
File.Delete(path); File.Delete(path);
} }
var eventsDir = Path.GetDirectoryName(ModFileService.GetWritablePath(newMapMod, RpgMapDocument.EventsPath(NewMap)) ?? "");
if (!string.IsNullOrEmpty(eventsDir) && Directory.Exists(eventsDir) && !Directory.EnumerateFileSystemEntries(eventsDir).Any())
Directory.Delete(eventsDir);
RpgMapEvents.Invalidate();
} }
private void Press(string key) private void Press(string key)
+102 -6
View File
@@ -58,7 +58,7 @@ namespace Cthangover.Tools.RpgEditor.Tests
document.AddEvent(3, 1).Sprite = "village/elder"; document.AddEvent(3, 1).Sprite = "village/elder";
ModAssert.True(document.Undo(), "undo the event"); ModAssert.True(document.Undo(), "undo the event");
ModAssert.Equal(0, document.Map.Events.Count, "event gone"); ModAssert.Equal(0, document.Events.Count, "event gone");
ModAssert.Equal(9, document.Grid.GetTile(RpgLayer.Front, 0, 0), "the earlier paint stays"); ModAssert.Equal(9, document.Grid.GetTile(RpgLayer.Front, 0, 0), "the earlier paint stays");
ModAssert.True(document.Undo(), "undo the paint"); ModAssert.True(document.Undo(), "undo the paint");
ModAssert.Equal(-1, document.Grid.GetTile(RpgLayer.Front, 0, 0), "paint gone"); ModAssert.Equal(-1, document.Grid.GetTile(RpgLayer.Front, 0, 0), "paint gone");
@@ -66,7 +66,7 @@ namespace Cthangover.Tools.RpgEditor.Tests
ModAssert.True(document.Redo(), "redo the paint"); ModAssert.True(document.Redo(), "redo the paint");
ModAssert.True(document.Redo(), "redo the event"); ModAssert.True(document.Redo(), "redo the event");
ModAssert.Equal("village/elder", document.Map.Events[0].Sprite, "event back with its data"); ModAssert.Equal("village/elder", document.Events[0].Sprite, "event back with its data");
document.Undo(); document.Undo();
document.BeginChange(); document.BeginChange();
@@ -87,6 +87,95 @@ namespace Cthangover.Tools.RpgEditor.Tests
ModAssert.Equal("event_1", document.AddEvent(0, 0).Id, "a freed id is used again"); ModAssert.Equal("event_1", document.AddEvent(0, 0).Id, "a freed id is used again");
} }
[ModTest("document/characters_become_npc_and_enemy_events")]
static void CharactersBecomeNpcAndEnemyEvents(ModTestContext ctx)
{
var document = NewMap();
var npc = document.AddCharacter(1, 2, "rpg_elder", "village/elder", false);
var enemy = document.AddCharacter(3, 1, "rpg_slime", "village/slime", true);
var second = document.AddCharacter(4, 1, "rpg_slime", null, true);
ModAssert.Equal("rpg_elder", npc.Id, "the character id is the event id");
ModAssert.Equal("village/elder", npc.Sprite, "the charset is the look");
ModAssert.Equal(1, npc.X, "x");
ModAssert.Equal(2, npc.Y, "y");
ModAssert.True(npc.Commands == null && npc.Condition == null, "an NPC gets no lines and no condition");
ModAssert.Equal("Action", npc.Trigger.ToString(), "talked to on E/F");
ModAssert.Equal("action rpg.battle enemies=rpg_slime win_flag=test_map_rpg_slime_won", enemy.Commands[0], "an enemy starts a battle with itself");
ModAssert.Equal("end", enemy.Commands[1], "and ends the dialog");
ModAssert.Equal("test_map_rpg_slime_won != 1", enemy.Condition, "hidden once beaten");
ModAssert.Equal("rpg_slime_2", second.Id, "a taken id gets a number");
ModAssert.Null(second.Sprite, "no charset: no look");
ModAssert.Equal("test_map_rpg_slime_2_won != 1", second.Condition, "each placed enemy is beaten separately");
ModAssert.Equal(0, document.Validate().Count, "the ids are valid");
var loaded = RpgMapDocument.Load(document.ToJson(), document.EventsToJson());
var restored = loaded.Events[1];
ModAssert.Equal("rpg_slime", restored.Id, "id survives the file");
ModAssert.Equal("village/slime", restored.Sprite, "sprite survives the file");
ModAssert.Equal(2, restored.Commands.Count, "commands survive the file");
ModAssert.Equal(enemy.Commands[0], restored.Commands[0], "battle line survives the file");
ModAssert.Equal(enemy.Condition, restored.Condition, "condition survives the file");
}
[ModTest("document/items_become_pickups")]
static void ItemsBecomePickups(ModTestContext ctx)
{
var document = NewMap();
var first = document.AddItem(2, 1, "rpg_demo/branches", "rpg_demo/branch_bundle", 3, "Found \"branches\"");
var second = document.AddItem(3, 1, "rpg_demo/branches", null, 0, "x");
ModAssert.Equal("rpg_demo_branches", first.Id, "the id holds no slash");
ModAssert.Equal("rpg_demo/branch_bundle", first.Icon, "the item picture is the look");
ModAssert.Equal("Touch", first.Trigger.ToString(), "picked up by stepping on it");
ModAssert.False(first.IsSolid, "the player can step on it");
ModAssert.Equal("ptext \"Found 'branches'\"", first.Commands[0], "the line, quotes made safe");
ModAssert.Equal("action item.add id=rpg_demo/branches count=3", first.Commands[1], "adds the stack");
ModAssert.Equal("flag test_map_rpg_demo_branches_taken 1", first.Commands[2], "remembers it was taken");
ModAssert.Equal("end", first.Commands[3], "ends the dialog");
ModAssert.Equal("test_map_rpg_demo_branches_taken != 1", first.Condition, "gone once taken");
ModAssert.Equal("rpg_demo_branches_2", second.Id, "a taken id gets a number");
ModAssert.Equal("action item.add id=rpg_demo/branches count=1", second.Commands[1], "at least one item");
ModAssert.Equal("test_map_rpg_demo_branches_2_taken != 1", second.Condition, "each pickup has its own flag");
var restored = RpgMapDocument.Load(document.ToJson(), document.EventsToJson()).Events[0];
ModAssert.Equal("rpg_demo/branch_bundle", restored.Icon, "icon survives the file");
ModAssert.Equal("Touch", restored.Trigger.ToString(), "trigger survives the file");
ModAssert.False(restored.IsSolid, "solidity survives the file");
ModAssert.Equal(4, restored.Commands.Count, "commands survive the file");
ModAssert.Equal(first.Condition, restored.Condition, "condition survives the file");
}
[ModTest("document/locked_events_are_shown_not_edited")]
static void LockedEventsAreShownNotEdited(ModTestContext ctx)
{
var document = NewMap();
document.SetLockedEvents(new[]
{
new RpgMapEvent { Id = "event_1", X = 1, Y = 1, ModId = "quest_mod", File = "map_events/test_map/quest.json" },
new RpgMapEvent { Id = "rpg_slime", X = 2, Y = 2, ModId = "quest_mod", File = "map_events/test_map/quest.json" }
});
ModAssert.Equal(0, document.EventsAt(1, 1).Count, "a locked event is not picked on its cell");
ModAssert.Equal("event_2", document.AddEvent(1, 1).Id, "a new event does not take a locked id");
ModAssert.Equal("rpg_slime_2", document.AddCharacter(0, 0, "rpg_slime", null, true).Id, "nor does a placed character");
document.BeginChange();
document.Events[0].Id = "EVENT_1";
var problems = document.Validate();
ModAssert.Equal(1, problems.Count, "renaming onto a locked id is a problem");
ModAssert.Equal("tools/rpg_editor/problem_event_locked", problems[0].Key, "the problem says why");
ModAssert.True(problems[0].Argument.Contains("quest_mod"), "and names the mod");
document.Undo();
ModAssert.Equal(2, document.LockedEvents.Count, "undo keeps the locked events");
ModAssert.Equal("event_2", document.Events[0].Id, "and restores the own ones");
ModAssert.False(document.EventsToJson().Contains("quest"), "locked events are not written");
}
[ModTest("document/resize_pulls_events_inside")] [ModTest("document/resize_pulls_events_inside")]
static void ResizePullsEventsInside(ModTestContext ctx) static void ResizePullsEventsInside(ModTestContext ctx)
{ {
@@ -128,15 +217,22 @@ namespace Cthangover.Tools.RpgEditor.Tests
mapEvent.Teleport = new RpgTeleport { Map = "other", X = 4, Y = 5, Dir = RpgDirection.Left }; mapEvent.Teleport = new RpgTeleport { Map = "other", X = 4, Y = 5, Dir = RpgDirection.Left };
var json = document.ToJson(); var json = document.ToJson();
var loaded = RpgMapDocument.Load(json); var eventsJson = document.EventsToJson();
var loaded = RpgMapDocument.Load(json, eventsJson);
ModAssert.True(json.Contains("\"Kind\": \"rpg_map\""), "the file declares its kind"); ModAssert.True(json.Contains("\"Kind\": \"rpg_map\""), "the file declares its kind");
ModAssert.False(json.Contains("\"Scenario\""), "empty fields are left out"); ModAssert.False(json.Contains("\"Events\""), "the map file holds no events");
ModAssert.True(eventsJson.Contains("\"Events\""), "the events file holds them");
ModAssert.False(eventsJson.Contains("IsSolid"), "the computed solidity is not written");
ModAssert.True(json.EndsWith("}\n") && eventsJson.EndsWith("}\n"), "both files end with a line break");
ModAssert.False(eventsJson.Contains("\"Scenario\""), "empty fields are left out");
ModAssert.Equal("map_events/test_map/events.json", RpgMapDocument.EventsPath("test_map"), "the editor's events file of the map");
ModAssert.Equal(0, RpgMapDocument.Load(json).Events.Count, "a map without an events file has no events");
ModAssert.Equal("test_map", loaded.Map.ID, "id"); ModAssert.Equal("test_map", loaded.Map.ID, "id");
ModAssert.Equal(16, loaded.Grid.GetTile(RpgLayer.Main, 3, 2), "main tile"); ModAssert.Equal(16, loaded.Grid.GetTile(RpgLayer.Main, 3, 2), "main tile");
ModAssert.Equal("o", loaded.Grid.GetCollision(0, 3).ToString(), "collision"); ModAssert.Equal("o", loaded.Grid.GetCollision(0, 3).ToString(), "collision");
ModAssert.Equal("Touch", loaded.Map.Events[0].Trigger.ToString(), "trigger"); ModAssert.Equal("Touch", loaded.Events[0].Trigger.ToString(), "trigger");
ModAssert.Equal("other", loaded.Map.Events[0].Teleport.Map, "teleport"); ModAssert.Equal("other", loaded.Events[0].Teleport.Map, "teleport");
ModAssert.Throws(() => RpgMapDocument.Load("{\"Kind\":\"rpg_map\",\"Items\":[]}"), "a file without maps", anyException: true); ModAssert.Throws(() => RpgMapDocument.Load("{\"Kind\":\"rpg_map\",\"Items\":[]}"), "a file without maps", anyException: true);
} }
} }