using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using AcDream.Core.Items;
using AcDream.Core.Spells;
namespace AcDream.App.UI.Layout;
///
/// Retained port of retail gmVitaeUI. It presents the active vitae
/// modifier and the authoritative experience pool needed to regain one percent.
///
public sealed class VitaeUiController : IRetainedPanelController
{
public const uint LayoutId = 0x21000020u;
public const uint RootId = 0x100001C1u;
public const uint MainTextId = 0x100001C3u;
public const uint CloseId = 0x100000FCu;
internal const uint VitaeCpPoolProperty = 0x81u;
internal const uint DeathLevelProperty = 0x8Bu;
internal const uint LevelProperty = 0x19u;
private readonly Spellbook _spellbook;
private readonly ClientObjectTable _objects;
private readonly Func _playerGuid;
private readonly VitaeStrings _strings;
private readonly UiText _mainText;
private readonly UiButton? _close;
private IReadOnlyList _lines = Array.Empty();
private bool _disposed;
private VitaeUiController(
ImportedLayout layout,
Spellbook spellbook,
ClientObjectTable objects,
Func playerGuid,
VitaeStrings strings,
Action? close)
{
_spellbook = spellbook;
_objects = objects;
_playerGuid = playerGuid;
_strings = strings;
_mainText = (UiText)layout.FindElement(MainTextId)!;
_close = layout.FindElement(CloseId) as UiButton;
_mainText.LinesProvider = () => _lines;
if (_close is not null) _close.OnClick = close;
_spellbook.EnchantmentsChanged += Update;
_objects.ObjectAdded += OnObjectChanged;
_objects.ObjectUpdated += OnObjectChanged;
_objects.Cleared += Update;
Update();
}
public static VitaeUiController? Bind(
ImportedLayout layout,
Spellbook spellbook,
ClientObjectTable objects,
Func playerGuid,
VitaeStrings strings,
Action? close = null)
{
ArgumentNullException.ThrowIfNull(layout);
ArgumentNullException.ThrowIfNull(spellbook);
ArgumentNullException.ThrowIfNull(objects);
ArgumentNullException.ThrowIfNull(playerGuid);
ArgumentNullException.ThrowIfNull(strings);
return layout.FindElement(MainTextId) is UiText
? new VitaeUiController(
layout, spellbook, objects, playerGuid, strings, close)
: null;
}
public void OnShown() => Update();
private void OnObjectChanged(ClientObject item)
{
if (item.ObjectId == _playerGuid()) Update();
}
private void Update()
{
float vitae = CurrentVitae();
int lostPercent = 100 - (int)(vitae * 100f);
string body;
if (lostPercent <= 0)
{
body = _strings.FullStrength;
}
else
{
ClientObject? player = _objects.Get(_playerGuid());
int pool = player?.Properties.GetInt(VitaeCpPoolProperty) ?? 0;
int level = 0;
if (player is not null)
{
level = player.Properties.Ints.TryGetValue(
DeathLevelProperty, out int deathLevel)
? deathLevel
: player.Properties.GetInt(LevelProperty);
}
int remaining = VitaeCpPoolThreshold(vitae, level) - pool;
body = _strings.LostPrefix
+ lostPercent
+ _strings.LostSuffix
+ _strings.SkillsPrefix
+ lostPercent
+ _strings.SkillsSuffix
+ _strings.RecoveryPrefix
// StringInfo::AddVariable_Int -> LInt_StringInfoData::ToString
// @ 0x0042F7E0 always enables localized digit grouping.
+ FormatExperience(remaining)
+ _strings.RecoverySuffix;
}
_lines = IndicatorDetailText.Shape(_mainText, body);
}
private float CurrentVitae()
=> _spellbook.ActiveEnchantmentSnapshot
.Where(record => record.Bucket == 4u)
.Select(record => record.StatModValue)
.OfType()
.Where(float.IsFinite)
.DefaultIfEmpty(1f)
.Last();
///
/// Retail VitaeSystem::VitaeCPPoolThreshold @ 0x005C8FD0, with
/// the x87 expression cross-checked against ACE's Player_Xp port.
///
internal static int VitaeCpPoolThreshold(float vitae, int level)
=> (int)(((Math.Pow(level, 2.5d) * 2.5d) + 20d)
* Math.Pow(vitae, 5d)
+ 0.5d);
internal static string FormatExperience(int experience)
=> experience.ToString("N0", CultureInfo.InvariantCulture);
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_spellbook.EnchantmentsChanged -= Update;
_objects.ObjectAdded -= OnObjectChanged;
_objects.ObjectUpdated -= OnObjectChanged;
_objects.Cleared -= Update;
if (_close is not null) _close.OnClick = null;
}
}
public sealed record VitaeStrings(
string FullStrength,
string LostPrefix,
string LostSuffix,
string SkillsPrefix,
string SkillsSuffix,
string RecoveryPrefix,
string RecoverySuffix)
{
public static VitaeStrings English { get; } = new(
"Your Vitae, or life force, is at full strength.",
"Due to your recent death, you have temporarily lost ",
"% of your Vitae, or life force.",
"\n\nThis means that your health, stamina, mana, and skills are temporarily reduced by ",
"%. A reduction of less than 15% will not hinder you much, but beware losing much more than that.",
"\n\nYou will regain 1% of your Vitae once you earn ",
" more experience.");
}