acdream/src/AcDream.App/UI/RetailSkillTrainingConfirmationController.cs
Erik 85980fe13f fix(ui): restore retail skill training confirmation
Route untrained-skill promotion through DialogFactory with retail's exact prompt and stored skill/credit properties. Defer the character-sheet refresh until accept or reject completes while leaving trained-skill XP raises immediate.

Co-Authored-By: Codex <codex@openai.com>
2026-07-13 17:57:29 +02:00

70 lines
2.8 KiB
C#

using System.Globalization;
using AcDream.App.UI.Layout;
namespace AcDream.App.UI;
/// <summary>
/// Ports the untrained-skill confirmation owned by
/// <c>gmSkillUI::TrainSkill @ 0x0049C5F0</c> and
/// <c>gmSkillUI::TrainSkillDialogCallback @ 0x0049C210</c>.
/// Already-trained skill XP raises deliberately bypass this controller, matching
/// retail's separate <c>gmSkillUI::RaiseSelection @ 0x0049C8C0</c> path.
/// </summary>
public sealed class RetailSkillTrainingConfirmationController
{
public const string MessageFormat =
"Are you sure you want to spend {0} credits to train {1}?";
private readonly RetailDialogFactory _dialogs;
public RetailSkillTrainingConfirmationController(RetailDialogFactory dialogs)
=> _dialogs = dialogs ?? throw new ArgumentNullException(nameof(dialogs));
public uint Request(
CharacterStatController.RaiseRequest request,
CharacterSheet sheet,
Action<CharacterStatController.RaiseRequest> accepted,
Action completed)
{
ArgumentNullException.ThrowIfNull(sheet);
ArgumentNullException.ThrowIfNull(accepted);
ArgumentNullException.ThrowIfNull(completed);
if (request.Kind != CharacterStatController.RaiseTargetKind.TrainSkill)
throw new ArgumentException("Only TrainSkill requests require this confirmation.", nameof(request));
if (request.Cost <= 0 || request.Cost > uint.MaxValue)
throw new ArgumentOutOfRangeException(nameof(request), "Training cost must fit retail's uint32 field.");
CharacterSkill skill = sheet.Skills.FirstOrDefault(candidate => candidate.Id == request.StatId)
?? throw new InvalidOperationException($"Skill {request.StatId} is absent from the character sheet.");
string message = string.Format(
CultureInfo.InvariantCulture,
MessageFormat,
request.Cost,
skill.Name);
RetailDialogData data = RetailDialogData.Confirmation(message)
.Set(RetailDialogProperty.TrainSkillId, request.StatId)
.Set(RetailDialogProperty.TrainSkillCredits, checked((uint)request.Cost));
return _dialogs.MakeDialog(data, result =>
{
if (result.GetBoolean(RetailDialogProperty.ConfirmationResult))
{
uint skillId = result.GetUInt32(RetailDialogProperty.TrainSkillId);
uint credits = result.GetUInt32(RetailDialogProperty.TrainSkillCredits);
if (skillId != 0u && credits != 0u)
{
accepted(new CharacterStatController.RaiseRequest(
CharacterStatController.RaiseTargetKind.TrainSkill,
skillId,
credits,
Amount: 1));
}
}
completed();
});
}
}