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