Character editor: characters of a mod in a searchable list, name in every language, portrait, RPG charset, stats, battle actions; GUI suite characters_editor_edit
This commit is contained in:
commit
c6627a8205
11 files changed
+461
No files matched your search
@@ -0,0 +1,30 @@
|
|||||||
|
# Line endings: LF everywhere, on every OS, whatever core.autocrlf says.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# cmd.exe misreads labels and goto in LF batch files, so they stay CRLF.
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# Binaries: never converted, never diffed as text.
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.webp binary
|
||||||
|
*.svg text
|
||||||
|
*.ogg binary
|
||||||
|
*.wav binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.otf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.dll binary
|
||||||
|
*.so binary
|
||||||
|
*.dylib binary
|
||||||
|
*.pdb binary
|
||||||
|
*.exe binary
|
||||||
|
*.zip binary
|
||||||
|
*.pck binary
|
||||||
|
*.moc3 binary
|
||||||
|
*.res binary
|
||||||
|
*.scn binary
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
mod_characters_editor_name = Character Editor
|
||||||
|
mod_characters_editor_desc = Editor of the game's characters — heroes, NPCs and enemies: a searchable list with portraits, the name in every language of the mod, portrait, the RPG map charset, battle stats and actions; a save shows in the game at once.
|
||||||
|
tools/characters_editor/title = Characters
|
||||||
|
tools/characters_editor/name = Name
|
||||||
|
tools/characters_editor/image = Portrait
|
||||||
|
tools/characters_editor/image_tip = Picture in the mod's characters/ folder: dialogs, battles and the character panel show it
|
||||||
|
tools/characters_editor/charset = Charset (RPG)
|
||||||
|
tools/characters_editor/charset_tip = How the character looks on an RPG map: a 3×4 frame sheet from the charsets/ folder. A novel does not need it
|
||||||
|
tools/characters_editor/level = Level
|
||||||
|
tools/characters_editor/health = Health
|
||||||
|
tools/characters_editor/attack = Attack
|
||||||
|
tools/characters_editor/defence = Defence
|
||||||
|
tools/characters_editor/point = Action points
|
||||||
|
tools/characters_editor/point_tip = Points to spend on actions in a battle
|
||||||
|
tools/characters_editor/exp = Experience
|
||||||
|
tools/characters_editor/exp_tip = The character's experience, read by the battle cores
|
||||||
|
tools/characters_editor/recruitment = Recruit chance
|
||||||
|
tools/characters_editor/recruitment_tip = Chance (percent) to take the beaten character into the party; 0 — never
|
||||||
|
tools/characters_editor/behaviour = Behaviour
|
||||||
|
tools/characters_editor/behaviour_tip = How the character acts in battle when the game controls it; base — the usual
|
||||||
|
tools/characters_editor/actions = Actions
|
||||||
|
tools/characters_editor/actions_tip = The character's battle actions from the loaded mods
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
mod_characters_editor_name = Редактор персонажей
|
||||||
|
mod_characters_editor_desc = Редактор персонажей игры — героев, NPC и врагов: список с поиском и портретами, имя на всех языках мода, портрет, чарсет для RPG-карты, боевые параметры и действия; сохранение сразу видно в игре.
|
||||||
|
tools/characters_editor/title = Персонажи
|
||||||
|
tools/characters_editor/name = Имя
|
||||||
|
tools/characters_editor/image = Портрет
|
||||||
|
tools/characters_editor/image_tip = Картинка из папки characters/ мода: её показывают диалоги, бои и панель персонажей
|
||||||
|
tools/characters_editor/charset = Чарсет (RPG)
|
||||||
|
tools/characters_editor/charset_tip = Как персонаж выглядит на RPG-карте: лист 3×4 кадра из папки charsets/. В новелле не нужен
|
||||||
|
tools/characters_editor/level = Уровень
|
||||||
|
tools/characters_editor/health = Здоровье
|
||||||
|
tools/characters_editor/attack = Атака
|
||||||
|
tools/characters_editor/defence = Защита
|
||||||
|
tools/characters_editor/point = Очки действий
|
||||||
|
tools/characters_editor/point_tip = Запас очков на действия в бою
|
||||||
|
tools/characters_editor/exp = Опыт
|
||||||
|
tools/characters_editor/exp_tip = Опыт персонажа, его читают боевые ядра
|
||||||
|
tools/characters_editor/recruitment = Шанс вербовки
|
||||||
|
tools/characters_editor/recruitment_tip = Шанс (в процентах) позвать побеждённого в отряд; 0 — нельзя
|
||||||
|
tools/characters_editor/behaviour = Поведение
|
||||||
|
tools/characters_editor/behaviour_tip = Как персонаж ведёт себя в бою под управлением игры; base — обычное
|
||||||
|
tools/characters_editor/actions = Действия
|
||||||
|
tools/characters_editor/actions_tip = Боевые действия персонажа из загруженных модов
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"id": "characters_editor",
|
||||||
|
"name": "Character Editor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "ct",
|
||||||
|
"description": "Editor of the game's characters (characters_base, \"Kind\": \"character\") in the style of the other editor windows: heroes, NPCs and enemies of a writable mod in a searchable list with their portraits, a form with the name in every language of the mod, portrait, the RPG map charset, battle stats and the battle actions picked from the loaded ones; loot and fields of other mods stay editable as JSON. New characters, copies, deletion, undo, and a save the running game sees at once. Opens from the main menu tools, the novel editor hub and the in-game toolbar.",
|
||||||
|
"category": "tools, editor",
|
||||||
|
"sources": [
|
||||||
|
"src/**/*.cs"
|
||||||
|
],
|
||||||
|
"depends": [
|
||||||
|
"editor_tools>=1",
|
||||||
|
"characters_base>=1",
|
||||||
|
"ui_base>=1"
|
||||||
|
],
|
||||||
|
"test_suites": [
|
||||||
|
{
|
||||||
|
"id": "edit",
|
||||||
|
"activate": [
|
||||||
|
"--itest=characters_editor_edit"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Cthangover.Core.UI.Tool;
|
||||||
|
|
||||||
|
namespace Cthangover.Tools.CharactersEditor
|
||||||
|
{
|
||||||
|
/// <summary>In-game toolbar button of the character editor, so a character can be fixed while playing.</summary>
|
||||||
|
public class CharacterEditorToolBoxButton : IToolBoxButton
|
||||||
|
{
|
||||||
|
/// <summary>Matches <see cref="CharacterEditorToolProvider.Id"/>.</summary>
|
||||||
|
public string ToolId => "characters_editor";
|
||||||
|
|
||||||
|
/// <summary>No icon: the button shows its label.</summary>
|
||||||
|
public string IconPath => "";
|
||||||
|
|
||||||
|
/// <summary>Button label key.</summary>
|
||||||
|
public string LocaleKey => "tools/characters_editor/title";
|
||||||
|
|
||||||
|
/// <summary>Always shown.</summary>
|
||||||
|
public bool IsVisible() => true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using Cthangover.Core.UI.Tool;
|
||||||
|
using Cthangover.Mods.Interface.Tool;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Tools.CharactersEditor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the character editor as a tool: it appears in the main menu's tool
|
||||||
|
/// block, the novel editor hub and (with <see cref="CharacterEditorToolBoxButton"/>)
|
||||||
|
/// the in-game toolbar.
|
||||||
|
/// </summary>
|
||||||
|
public class CharacterEditorToolProvider : IToolProvider
|
||||||
|
{
|
||||||
|
/// <summary>Tool id.</summary>
|
||||||
|
public string Id => "characters_editor";
|
||||||
|
|
||||||
|
/// <summary>Display name key.</summary>
|
||||||
|
public string LocaleKey => "tools/characters_editor/title";
|
||||||
|
|
||||||
|
/// <summary>Creates the window.</summary>
|
||||||
|
public Window CreateWindow() => new CharacterEditorWindow();
|
||||||
|
|
||||||
|
/// <summary>Adds the launcher button to the main menu's tool block.</summary>
|
||||||
|
public void AddMenuButton(Container container) => ToolMenuButton.Attach(container, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Cthangover.Core.Characters;
|
||||||
|
using Cthangover.Core.Mods.Resolvers;
|
||||||
|
using Cthangover.Tools.EntityEditor;
|
||||||
|
|
||||||
|
namespace Cthangover.Tools.CharactersEditor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Editor of characters (<c>"Kind": "character"</c>, <see cref="CharacterInfo"/>):
|
||||||
|
/// heroes, NPCs and enemies alike. Name in every language of the mod, the
|
||||||
|
/// portrait (<c>characters</c> group, shown in dialogs and battles), the RPG
|
||||||
|
/// map look (<c>Charset</c> in the <c>charsets</c> group: the RPG map editor
|
||||||
|
/// places the character with it; a novel leaves it empty), battle stats and the
|
||||||
|
/// battle actions offered from the loaded <c>action</c> entities. Loot and
|
||||||
|
/// other mods' fields are "other fields" of the base window. New characters go
|
||||||
|
/// to <c>characters/<mod>.json</c>.
|
||||||
|
/// </summary>
|
||||||
|
public partial class CharacterEditorWindow : EntityEditorWindow
|
||||||
|
{
|
||||||
|
/// <summary>Builds the window and opens the first mod that has characters.</summary>
|
||||||
|
public CharacterEditorWindow() : base("tools/characters_editor/title", "character", "characters", Fields())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<EntityField> Fields() => new()
|
||||||
|
{
|
||||||
|
new EntityField { Key = "Name", LabelKey = "tools/characters_editor/name", Kind = EntityFieldKind.LocaleText, KeyPattern = "characters/{0}/name" },
|
||||||
|
new EntityField { Key = "Image", LabelKey = "tools/characters_editor/image", TooltipKey = "tools/characters_editor/image_tip", Kind = EntityFieldKind.Picture, Group = "characters" },
|
||||||
|
new EntityField { Key = "Charset", LabelKey = "tools/characters_editor/charset", TooltipKey = "tools/characters_editor/charset_tip", Kind = EntityFieldKind.Picture, Group = "charsets" },
|
||||||
|
new EntityField { Key = "Level", LabelKey = "tools/characters_editor/level", Kind = EntityFieldKind.Integer },
|
||||||
|
new EntityField { Key = "Health", LabelKey = "tools/characters_editor/health", Kind = EntityFieldKind.Integer },
|
||||||
|
new EntityField { Key = "Attack", LabelKey = "tools/characters_editor/attack", Kind = EntityFieldKind.Integer },
|
||||||
|
new EntityField { Key = "Defence", LabelKey = "tools/characters_editor/defence", Kind = EntityFieldKind.Integer },
|
||||||
|
new EntityField { Key = "Point", LabelKey = "tools/characters_editor/point", TooltipKey = "tools/characters_editor/point_tip", Kind = EntityFieldKind.Integer },
|
||||||
|
new EntityField { Key = "Exp", LabelKey = "tools/characters_editor/exp", TooltipKey = "tools/characters_editor/exp_tip", Kind = EntityFieldKind.Integer },
|
||||||
|
new EntityField { Key = "RecruitmentChance", LabelKey = "tools/characters_editor/recruitment", TooltipKey = "tools/characters_editor/recruitment_tip", Kind = EntityFieldKind.Integer },
|
||||||
|
new EntityField { Key = "Behaviour", LabelKey = "tools/characters_editor/behaviour", TooltipKey = "tools/characters_editor/behaviour_tip", Kind = EntityFieldKind.Text },
|
||||||
|
new EntityField { Key = "Actions", LabelKey = "tools/characters_editor/actions", TooltipKey = "tools/characters_editor/actions_tip", Kind = EntityFieldKind.List, Options = Actions }
|
||||||
|
};
|
||||||
|
|
||||||
|
private static IEnumerable<string> Actions()
|
||||||
|
=> (Entities.Collect<ActionCharacterInfo>()?.Keys ?? Enumerable.Empty<string>()).OrderBy(id => id, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>A level 1 fighter with a plain attack; the portrait is the author's to pick.</summary>
|
||||||
|
protected override JsonObject CreateEntity(string id) => new()
|
||||||
|
{
|
||||||
|
["Id"] = id,
|
||||||
|
["Behaviour"] = "base",
|
||||||
|
["Level"] = 1,
|
||||||
|
["Point"] = 1,
|
||||||
|
["Health"] = 10,
|
||||||
|
["Defence"] = 0,
|
||||||
|
["Attack"] = 2,
|
||||||
|
["Exp"] = 1,
|
||||||
|
["RecruitmentChance"] = 0,
|
||||||
|
["Actions"] = new JsonArray("physics/attack")
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary><c><mod>_new_character</c>: character ids are flat.</summary>
|
||||||
|
protected override string NewIdBase(string mod) => mod + "_new_character";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "Character Editor Test",
|
||||||
|
"description": "GUI test of the character editor in rpg_demo: opens rpg_demo_game's characters, shows the slime's localized name, adds a character with the New button, types its name, health and RPG charset into the form, saves; the game must know the character with its stats and name at once, and the RPG map editor's characters tab must offer it with its charset. The new file and the locale edits are removed again. Activated by --itest=characters_editor_edit with --game=rpg_demo --new.",
|
||||||
|
"sources": [
|
||||||
|
"src/*.cs"
|
||||||
|
],
|
||||||
|
"depends": [
|
||||||
|
"rpg_demo_game>=1",
|
||||||
|
"rpg_editor>=1"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Cthangover.Core.Bootstrap;
|
||||||
|
using Cthangover.Core.Characters;
|
||||||
|
using Cthangover.Core.Localization;
|
||||||
|
using Cthangover.Core.Mods;
|
||||||
|
using Cthangover.Core.Mods.Resolvers;
|
||||||
|
using Cthangover.Core.Utils;
|
||||||
|
using Cthangover.Mods.Interface.Tool;
|
||||||
|
using Cthangover.Mods.Rpg;
|
||||||
|
using Cthangover.Tools.RpgEditor;
|
||||||
|
using Cthangover.Tools.Services;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Tools.CharactersEditor.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Drives the character editor like an author: the toolbar buttons and the
|
||||||
|
/// form's fields (their change signals), Save. Then the game itself is asked:
|
||||||
|
/// the entity cache must hold the new character with the typed health and its
|
||||||
|
/// action, the translation server its typed name, and the RPG map editor's
|
||||||
|
/// character list must offer it with the typed charset. The new characters
|
||||||
|
/// file is deleted and the mod's locale files are restored afterwards, so
|
||||||
|
/// rpg_demo_game stays as it was.
|
||||||
|
/// Result line: <c>[TEST_RESULT] name=characters_editor_edit passed=N failed=M</c>.
|
||||||
|
/// </summary>
|
||||||
|
public partial class CharacterEditorTestDriver : Node
|
||||||
|
{
|
||||||
|
private const float TickSeconds = 0.15f;
|
||||||
|
private const float TimeoutSeconds = 60f;
|
||||||
|
private const string Mod = "rpg_demo_game";
|
||||||
|
private const string NewFile = "characters/rpg_demo_game.json";
|
||||||
|
private const string NewId = "rpg_demo_game_new_character";
|
||||||
|
private const string TypedName = "Тестовый гоблин";
|
||||||
|
|
||||||
|
private readonly List<string> failures = new();
|
||||||
|
private readonly Dictionary<string, string> backups = new();
|
||||||
|
private readonly Dictionary<string, string> characterFiles = new();
|
||||||
|
private int passed;
|
||||||
|
private int step;
|
||||||
|
private double elapsed;
|
||||||
|
private CharacterEditorWindow window;
|
||||||
|
|
||||||
|
/// <summary>Creates the driver; <paramref name="shotsDir"/> is unused (no screenshots).</summary>
|
||||||
|
public CharacterEditorTestDriver(string shotsDir)
|
||||||
|
{
|
||||||
|
Name = "CharacterEditorTestDriver";
|
||||||
|
var timer = new Timer { WaitTime = TickSeconds, Autostart = true };
|
||||||
|
AddChild(timer);
|
||||||
|
timer.Timeout += Tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Tick()
|
||||||
|
{
|
||||||
|
if (step < 0)
|
||||||
|
return;
|
||||||
|
elapsed += TickSeconds;
|
||||||
|
if (elapsed > TimeoutSeconds)
|
||||||
|
{
|
||||||
|
Fail($"timeout at step {step}");
|
||||||
|
Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RunStep();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Fail($"step {step}: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
Finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunStep()
|
||||||
|
{
|
||||||
|
switch (step)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
if (!Boot.IsReady)
|
||||||
|
return;
|
||||||
|
foreach (var file in ModFileService.ListFiles(Mod, "locale", ".properties"))
|
||||||
|
backups[file] = ModFileService.ReadText(Mod, file);
|
||||||
|
foreach (var file in ModFileService.ListFiles(Mod, "characters", ".json"))
|
||||||
|
characterFiles[file] = ModFileService.ReadText(Mod, file);
|
||||||
|
Check(!characterFiles.ContainsKey(NewFile), "the mod has no editor characters file yet");
|
||||||
|
window = ToolWindow.Open<CharacterEditorWindow>();
|
||||||
|
step++;
|
||||||
|
return;
|
||||||
|
case 1:
|
||||||
|
if (window.Document == null)
|
||||||
|
return;
|
||||||
|
window.OpenMod(Mod);
|
||||||
|
var language = Language();
|
||||||
|
var slime = window.Document.Find("rpg_slime");
|
||||||
|
Check(slime != null, "the demo's characters are listed");
|
||||||
|
window.Select(slime);
|
||||||
|
Check(window.Document.GetText(language, "rpg_demo/character/slime") == TranslationServer.Translate("rpg_demo/character/slime").ToString(),
|
||||||
|
$"the form shows the character's name in '{language}'");
|
||||||
|
|
||||||
|
Press("tools/entity_editor/new");
|
||||||
|
Check(window.SelectedId == NewId, $"New adds and selects {NewId} (got {window.SelectedId})");
|
||||||
|
Type(NameEdit(), TypedName);
|
||||||
|
Row("tools/characters_editor/health").OfType<SpinBox>().First().Value = 33;
|
||||||
|
Type(Row("tools/characters_editor/charset").OfType<LineEdit>().First(), "village/slime");
|
||||||
|
var record = window.Document.Find(NewId);
|
||||||
|
Check(record.Data["Health"]?.ToString() == "33" && record.Data["Charset"]?.ToString() == "village/slime", "the form writes health and the charset");
|
||||||
|
Check(record.Data["Actions"]?.AsArray().Any(a => a?.ToString() == "physics/attack") == true, "a new character starts with the plain attack");
|
||||||
|
step++;
|
||||||
|
return;
|
||||||
|
case 2:
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Press("tools/common/save");
|
||||||
|
var characters = Entities.Collect<CharacterInfo>();
|
||||||
|
Check(characters.TryGetValue(NewId, out var saved), "the game knows the new character right after saving");
|
||||||
|
Check(saved?.Health == 33 && saved.Actions?.Contains("physics/attack") == true, "with the typed health and its action");
|
||||||
|
Check(TranslationServer.Translate(saved?.Name ?? "-").ToString() == TypedName, $"and the typed name ({saved?.Name})");
|
||||||
|
var offered = RpgCharacterCatalog.Load(new RpgTextures()).FirstOrDefault(e => e.Id == NewId);
|
||||||
|
Check(offered?.Look == "village/slime" && offered.Name == TypedName, $"the RPG map editor offers it with its charset ({offered?.Look})");
|
||||||
|
var written = ModFileService.ReadText(Mod, NewFile) ?? string.Empty;
|
||||||
|
Check(written.Contains("\"Kind\": \"character\"") && !written.Contains("\r"), "the new file declares its kind and has LF lines");
|
||||||
|
Check(characterFiles.All(f => ModFileService.ReadText(Mod, f.Key) == f.Value), "the demo's character files are untouched");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Cleanup();
|
||||||
|
}
|
||||||
|
Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The controls of the form row labelled <paramref name="labelKey"/>.</summary>
|
||||||
|
private IEnumerable<Node> Row(string labelKey)
|
||||||
|
{
|
||||||
|
var text = TranslationServer.Translate(labelKey).ToString();
|
||||||
|
return window.FindChildren("*", "", true, false).OfType<Label>().First(l => l.Text == text).GetParent().FindChildren("*", "", true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Language() => LanguageCode.Normalize(TranslationServer.GetLocale()) ?? "ru";
|
||||||
|
|
||||||
|
/// <summary>The first text row of the current language: the name (the first locale field).</summary>
|
||||||
|
private LineEdit NameEdit()
|
||||||
|
{
|
||||||
|
var label = string.Format(TranslationServer.Translate("tools/entity_editor/text_in"), Language());
|
||||||
|
return window.FindChildren("*", "", true, false).OfType<Label>().First(l => l.Text == label && l.IsVisibleInTree())
|
||||||
|
.GetParent().GetChildren().OfType<LineEdit>().First();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Type(LineEdit edit, string text)
|
||||||
|
{
|
||||||
|
edit.Text = text;
|
||||||
|
edit.EmitSignal(LineEdit.SignalName.TextChanged, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cleanup()
|
||||||
|
{
|
||||||
|
window?.QueueFree();
|
||||||
|
var path = ModFileService.GetWritablePath(Mod, NewFile);
|
||||||
|
if (path != null && File.Exists(path))
|
||||||
|
File.Delete(path);
|
||||||
|
foreach (var backup in backups)
|
||||||
|
ModFileService.WriteText(Mod, backup.Key, backup.Value);
|
||||||
|
ModManager.Instance.GetResolver<EntityResolver>()?.ClearCache();
|
||||||
|
LocaleFileService.ReloadGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Press(string key)
|
||||||
|
{
|
||||||
|
var text = TranslationServer.Translate(key);
|
||||||
|
var button = window.FindChildren("*", "", true, false).OfType<Button>().FirstOrDefault(b => b.Text == text);
|
||||||
|
if (button == null)
|
||||||
|
{
|
||||||
|
Fail($"button '{text}' not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.EmitSignal(BaseButton.SignalName.Pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Check(bool condition, string what)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
{
|
||||||
|
passed++;
|
||||||
|
GameLogger.Log("TEST", $"[PASS] {what}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Fail(what);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Fail(string what)
|
||||||
|
{
|
||||||
|
failures.Add(what);
|
||||||
|
GameLogger.Log("TEST", $"[FAIL] {what}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Finish()
|
||||||
|
{
|
||||||
|
step = -1;
|
||||||
|
GameLogger.Log("TEST", $"[TEST_RESULT] name=characters_editor_edit passed={passed} failed={failures.Count}");
|
||||||
|
GetTree().Quit(failures.Count == 0 ? 0 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Cthangover.Core.Mods;
|
||||||
|
using Cthangover.Core.Utils;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Tools.CharactersEditor.Tests
|
||||||
|
{
|
||||||
|
/// <summary>Adds the editor test driver when <c>--itest=characters_editor_edit</c> is given.</summary>
|
||||||
|
public class CharacterEditorTestInitializer : IModInitializer
|
||||||
|
{
|
||||||
|
private static bool driverQueued;
|
||||||
|
|
||||||
|
/// <summary>Nothing per mod.</summary>
|
||||||
|
public void OnModLoaded(string modId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Queues the driver once.</summary>
|
||||||
|
public void OnModResourcesReady()
|
||||||
|
{
|
||||||
|
var args = OS.GetCmdlineUserArgs();
|
||||||
|
if (driverQueued || !args.Any(a => a == "--itest=characters_editor_edit"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
driverQueued = true;
|
||||||
|
var shots = args.Where(a => a.StartsWith("--shots=", System.StringComparison.Ordinal))
|
||||||
|
.Select(a => a.Substring("--shots=".Length))
|
||||||
|
.FirstOrDefault();
|
||||||
|
GameLogger.Log("TEST", $"CharacterEditorTestInitializer: activating characters_editor_edit, shots={shots ?? "off"}");
|
||||||
|
(Godot.Engine.GetMainLoop() as SceneTree)?.Root?.CallDeferred("add_child", new CharacterEditorTestDriver(shots));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user