Update characters_editor implementation, assets and documentation
This commit is contained in:
1 parent
c975b5ae86
commit
d7250b26b2
28 files changed
+429
-73
No files matched your search
@@ -1,10 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Games;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Mods.Stats;
|
||||
using Cthangover.Tools.Common;
|
||||
using Cthangover.Tools.EntityEditor;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.CharactersEditor
|
||||
{
|
||||
@@ -17,6 +23,15 @@ namespace Cthangover.Tools.CharactersEditor
|
||||
/// 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>.
|
||||
/// <para>
|
||||
/// Plan 12.5: the panel under the form marks the party leader (the character
|
||||
/// whose <c>Charset</c> the player walks with on RPG maps: the game setting
|
||||
/// <c>stats.player</c>, else the first battle-set member, as <c>RpgPlayerLook</c>
|
||||
/// reads it) and shows the values a character's <c>Stats</c> compute. A character
|
||||
/// with <c>Stats</c> gets its health, attack and defence from them, so those three
|
||||
/// fields are read-only here (validator rule <c>STAT_FIELD_OVERRIDDEN</c>, plan 6.4).
|
||||
/// Setting the leader is the game settings' job (mod manager), not a button here.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public partial class CharacterEditorWindow : EntityEditorWindow
|
||||
{
|
||||
@@ -31,9 +46,9 @@ namespace Cthangover.Tools.CharactersEditor
|
||||
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 = "Health", LabelKey = "tools/characters_editor/health", Kind = EntityFieldKind.Integer, Locked = HasStats },
|
||||
new EntityField { Key = "Attack", LabelKey = "tools/characters_editor/attack", Kind = EntityFieldKind.Integer, Locked = HasStats },
|
||||
new EntityField { Key = "Defence", LabelKey = "tools/characters_editor/defence", Kind = EntityFieldKind.Integer, Locked = HasStats },
|
||||
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 },
|
||||
@@ -44,6 +59,114 @@ namespace Cthangover.Tools.CharactersEditor
|
||||
private static IEnumerable<string> Actions()
|
||||
=> (Entities.Collect<ActionCharacterInfo>()?.Keys ?? Enumerable.Empty<string>()).OrderBy(id => id, StringComparer.Ordinal);
|
||||
|
||||
/// <summary>A character with <c>Stats</c> gets its bound battle numbers from the stats (see the class summary).</summary>
|
||||
private static bool HasStats(JsonObject data) => data["Stats"] != null;
|
||||
|
||||
/// <summary>The party leader's id, as <c>RpgPlayerLook.LeaderId</c> reads it; null without one.</summary>
|
||||
public static string LeaderId()
|
||||
{
|
||||
var setting = ActiveGame.GetSetting(PlayerSetting);
|
||||
if (!string.IsNullOrWhiteSpace(setting))
|
||||
return setting.Trim();
|
||||
var roster = GameData.Instance?.Runtime?.CharacterData;
|
||||
return roster?.Characters.Keys.FirstOrDefault(id => roster.BattleSet.Contains(id));
|
||||
}
|
||||
|
||||
private const string PlayerSetting = "stats.player";
|
||||
|
||||
// Created by CreatePreviewBlock, which the base window calls while it builds (before this class's constructor body): no initialisers.
|
||||
private Label leaderLabel;
|
||||
private VBoxContainer computedBox;
|
||||
private CheckBox joinBox;
|
||||
private string shownId;
|
||||
|
||||
/// <summary>The party section and the computed battle numbers, below the fields.</summary>
|
||||
protected override Control CreatePreviewBlock()
|
||||
{
|
||||
var block = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
block.AddChild(ToolUi.Section("tools/characters_editor/section_party"));
|
||||
leaderLabel = ToolUi.Hint(string.Empty);
|
||||
leaderLabel.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||
block.AddChild(leaderLabel);
|
||||
joinBox = new CheckBox { Name = "JoinsParty", Text = ToolUi.Tr("tools/characters_editor/join_check"), TooltipText = ToolUi.Tr("tools/characters_editor/join_tip") };
|
||||
joinBox.Toggled += OnJoinToggled;
|
||||
block.AddChild(joinBox);
|
||||
var joinHint = ToolUi.Hint(ToolUi.Tr("tools/characters_editor/join_hint"));
|
||||
joinHint.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||
block.AddChild(joinHint);
|
||||
block.AddChild(ToolUi.Section("tools/characters_editor/section_computed"));
|
||||
block.AddChild(ToolUi.Hint(ToolUi.Tr("tools/characters_editor/computed_hint")));
|
||||
computedBox = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
block.AddChild(computedBox);
|
||||
return block;
|
||||
}
|
||||
|
||||
/// <summary>Refreshes the leader mark and the computed numbers for the shown character.</summary>
|
||||
protected override void OnSelectionChanged(EntityRecord record)
|
||||
{
|
||||
if (leaderLabel == null || computedBox == null)
|
||||
return;
|
||||
shownId = record?.Id;
|
||||
leaderLabel.Text = record == null ? string.Empty
|
||||
: ToolUi.Tr(record.Id == LeaderId() ? "tools/characters_editor/leader_yes" : "tools/characters_editor/leader_no");
|
||||
if (joinBox != null)
|
||||
{
|
||||
// The leader is the protagonist (stats.player): it never joins, and the check box says so.
|
||||
joinBox.SetPressedNoSignal(record != null && CompanionScenario.Exists(ModId, record.Id));
|
||||
joinBox.Disabled = record == null || record.Id == LeaderId();
|
||||
}
|
||||
foreach (var child in computedBox.GetChildren())
|
||||
{
|
||||
computedBox.RemoveChild(child);
|
||||
child.QueueFree();
|
||||
}
|
||||
if (record == null || !HasStats(record.Data))
|
||||
{
|
||||
computedBox.AddChild(ToolUi.Hint(ToolUi.Tr("tools/characters_editor/computed_none")));
|
||||
return;
|
||||
}
|
||||
foreach (var definition in StatsService.Definitions.Where(d => !string.IsNullOrEmpty(d.Binds)))
|
||||
{
|
||||
var row = string.Format(CultureInfo.InvariantCulture, ToolUi.Tr("tools/characters_editor/computed_row"),
|
||||
ToolUi.Tr(definition.Name), StatValue(record.Id, definition.ID), definition.Binds);
|
||||
computedBox.AddChild(ToolUi.Hint(row));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes or removes the join scenario of the shown character (plan 14.G7) at once: the file is the whole change, the
|
||||
/// map actor reads it the next time the character is chosen in the RPG map editor.
|
||||
/// </summary>
|
||||
private void OnJoinToggled(bool on)
|
||||
{
|
||||
if (string.IsNullOrEmpty(shownId) || shownId == LeaderId())
|
||||
return;
|
||||
var written = on ? CompanionScenario.Write(ModId, shownId) : CompanionScenario.Remove(ModId, shownId);
|
||||
if (!written)
|
||||
{
|
||||
SetStatus(ToolUi.Tr("tools/common/readonly"), Cthangover.Mods.Interface.Tool.ToolStatusKind.Error);
|
||||
joinBox.SetPressedNoSignal(!on);
|
||||
return;
|
||||
}
|
||||
Cthangover.Core.Mods.ModManager.Instance.GetResolver<ScenarioResolver>()?.ClearCache();
|
||||
SetStatus(string.Format(ToolUi.Tr(on ? "tools/characters_editor/join_on" : "tools/characters_editor/join_off"), shownId),
|
||||
Cthangover.Mods.Interface.Tool.ToolStatusKind.Ok);
|
||||
}
|
||||
|
||||
/// <summary>The character's sheet value of <paramref name="stat"/> (integer, invariant), or "-" when no sheet can be made.</summary>
|
||||
private static string StatValue(string characterId, string stat)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sheet = StatsService.Sheet(characterId);
|
||||
return sheet != null && sheet.Has(stat) ? sheet.Rounded(stat).ToString(CultureInfo.InvariantCulture) : "-";
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace Cthangover.Tools.CharactersEditor
|
||||
{
|
||||
using Cthangover.Tools.Services;
|
||||
|
||||
/// <summary>
|
||||
/// The join scenario of a character (plan 14.G7): <c>scenarios/<mod>/party/<id>.scenario</c>, which sets the join
|
||||
/// flag and runs <c>action party.add</c>. The "Joins the party" block of this editor writes or removes it. The rpg
|
||||
/// editor reads it when it makes the character's map actor (<c>RpgCharacterActors</c> in <c>rpg_editor</c>), which
|
||||
/// takes the scenario key and the flag from the same convention: change both together.
|
||||
/// </summary>
|
||||
public static class CompanionScenario
|
||||
{
|
||||
/// <summary>The file of the join scenario, relative to the mod.</summary>
|
||||
public static string Path(string mod, string characterId) => "scenarios/" + Key(mod, characterId) + ".scenario";
|
||||
|
||||
/// <summary>The scenario path in the <c>scenarios</c> group, without the extension.</summary>
|
||||
public static string Key(string mod, string characterId) => mod + "/party/" + characterId;
|
||||
|
||||
/// <summary>The story flag the join sets to 1; the map actor of the character hides then.</summary>
|
||||
public static string Flag(string characterId) => characterId + "_joined";
|
||||
|
||||
/// <summary>The text of the scenario: the flag, then the party change.</summary>
|
||||
public static string Text(string characterId)
|
||||
=> "flag " + Flag(characterId) + " 1\naction party.add id=" + characterId + "\nend\n";
|
||||
|
||||
/// <summary>True when the mod has the join scenario of the character.</summary>
|
||||
public static bool Exists(string mod, string characterId)
|
||||
=> !string.IsNullOrEmpty(mod) && !string.IsNullOrEmpty(characterId) && ModFileService.ReadText(mod, Path(mod, characterId)) != null;
|
||||
|
||||
/// <summary>Writes the join scenario; false when the mod is not writable.</summary>
|
||||
public static bool Write(string mod, string characterId) => ModFileService.WriteText(mod, Path(mod, characterId), Text(characterId));
|
||||
|
||||
/// <summary>Removes the join scenario; false when the mod is not writable.</summary>
|
||||
public static bool Remove(string mod, string characterId) => !Exists(mod, characterId) || ModFileService.DeleteFile(mod, Path(mod, characterId));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user