namespace Cthangover.Tools.CharactersEditor.Tests { 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.Interface.View; using Cthangover.Tools.Services; using Cthangover.Mods.Rpg.Model; using Cthangover.Tools.RpgEditor; using Godot; /// /// 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, and the translation server its typed name. The new characters /// file is deleted and the mod's locale files are restored afterwards, so /// rpg_plus_game stays as it was. /// Result line: [TEST_RESULT] name=characters_editor_edit passed=N failed=M. /// public partial class CharacterEditorTestDriver : Node { private const float TickSeconds = 0.15f; private const float TimeoutSeconds = 60f; private const string Mod = "rpg_plus_game"; private const string NewFile = "characters/rpg_plus_game.json"; private const string NewId = "rpg_plus_game_new_character"; private const string TypedName = "Тестовый гоблин"; private readonly List failures = new(); private readonly Dictionary backups = new(); private readonly Dictionary characterFiles = new(); private int passed; private int step; private double elapsed; private readonly string shotsDir; private CharacterEditorWindow window; /// Creates the driver; with it saves a shot of the list and form. public CharacterEditorTestDriver(string shotsDir) { this.shotsDir = 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(); 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_plus/character/slime") == TranslationServer.Translate("rpg_plus/character/slime").ToString(), $"the form shows the character's name in '{language}'"); Check(Row("tools/characters_editor/health").OfType().First().Editable, "a character without Stats: health is edited"); var wolf = window.Document.Find("rpg_wolf"); Check(wolf?.Data["Stats"] != null, "the demo's wolf has Stats (its health comes from them)"); window.Select(wolf); Check(!Row("tools/characters_editor/health").OfType().First().Editable && !Row("tools/characters_editor/attack").OfType().First().Editable && !Row("tools/characters_editor/defence").OfType().First().Editable, "a character with Stats: health, attack and defence are computed, not edited"); window.Select(slime); 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().First().Value = 33; Type(Row("tools/characters_editor/charset").OfType().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: // The list's portraits are fitted into the panel (FilteredList.FitIcons). Shot("editor"); try { Press("tools/common/save"); var characters = Entities.Collect(); 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})"); // plan 14.G7: "Joins the party" writes the join scenario at once; unticking removes it again. var join = window.FindChildren("JoinsParty", "", true, false).OfType().FirstOrDefault(); Check(join != null && !join.Disabled && !join.ButtonPressed, "a new character can join the party (its check box is free and off)"); Check(!CompanionScenario.Exists(Mod, NewId), "no join scenario before the tick"); join.ButtonPressed = true; var joinText = ModFileService.ReadText(Mod, CompanionScenario.Path(Mod, NewId)) ?? string.Empty; Check(joinText.Contains("flag " + NewId + "_joined 1") && joinText.Contains("action party.add id=" + NewId) && !joinText.Contains("\r"), "the tick writes the join scenario (flag, party.add, LF lines)"); join.ButtonPressed = false; Check(!CompanionScenario.Exists(Mod, NewId), "the untick removes the join scenario"); // phase 8 (claude_plan.md E4): the RPG map editor opens beside the characters and its actor palette has rows. // It does not list characters yet (plan 12.4 leaves that to a later step), so the new character is not checked there. var rpgEditor = ToolWindow.Open(); Check(rpgEditor.Palette.Count(RpgObjectForm.Actor) > 0, "the RPG map editor opens and its palette lists actors"); rpgEditor.QueueFree(); 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; } } /// The controls of the form row labelled . private IEnumerable Row(string labelKey) { var text = TranslationServer.Translate(labelKey).ToString(); return window.FindChildren("*", "", true, false).OfType