251 lines
11 KiB
C#
251 lines
11 KiB
C#
|
|
using Microsoft.Xna.Framework;
|
|||
|
|
using Microsoft.Xna.Framework.Graphics;
|
|||
|
|
using Theriapolis.Game.CodexUI.Core;
|
|||
|
|
using Theriapolis.Game.CodexUI.Screens;
|
|||
|
|
using Theriapolis.Game.CodexUI.Widgets;
|
|||
|
|
using Theriapolis.Game.UI;
|
|||
|
|
|
|||
|
|
namespace Theriapolis.Game.CodexUI.Steps;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Step VII — Sign. Name input + summary panels for lineage / calling /
|
|||
|
|
/// abilities / skills / starting kit. Each panel has an Edit › link that
|
|||
|
|
/// jumps the wizard back to the source step.
|
|||
|
|
/// </summary>
|
|||
|
|
public static class StepReview
|
|||
|
|
{
|
|||
|
|
public static CodexWidget Build(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover)
|
|||
|
|
{
|
|||
|
|
var col = new Column { Spacing = 14 };
|
|||
|
|
col.Add(StepCommon.PageIntro(
|
|||
|
|
"Folio VII — Of Names & Witness",
|
|||
|
|
"Sign the Codex",
|
|||
|
|
"Review your character. The name you sign here is the one the world will speak."));
|
|||
|
|
|
|||
|
|
// Name input
|
|||
|
|
var nameBlock = new Column { Spacing = 4 };
|
|||
|
|
nameBlock.Add(new CodexLabel("NAME", CodexFonts.MonoTag, CodexColors.InkMute));
|
|||
|
|
var nameInput = new CodexTextBox(s.Name, atlas, fixedWidth: 480, onChanged: t => s.Name = t)
|
|||
|
|
{
|
|||
|
|
Placeholder = "Wanderer",
|
|||
|
|
};
|
|||
|
|
nameBlock.Add(nameInput);
|
|||
|
|
col.Add(nameBlock);
|
|||
|
|
|
|||
|
|
// Lineage / Calling pair
|
|||
|
|
var pair = new Grid { Columns = 2 };
|
|||
|
|
pair.Add(BuildBlock(atlas, "Lineage", () => s.Step = 0, s.InvalidateLayout, BuildLineage(s)));
|
|||
|
|
pair.Add(BuildBlock(atlas, "Calling & History", () => s.Step = 2, s.InvalidateLayout, BuildCalling(s)));
|
|||
|
|
col.Add(pair);
|
|||
|
|
|
|||
|
|
// Final abilities
|
|||
|
|
col.Add(BuildBlock(atlas, "Final Abilities", () => s.Step = 4, s.InvalidateLayout, new StatStrip(s, atlas)));
|
|||
|
|
|
|||
|
|
// Skills — HoverableChip so the player can re-read each skill's
|
|||
|
|
// codex flavour text without bouncing back to the Skills folio.
|
|||
|
|
var skillsBlock = new WrapRow();
|
|||
|
|
if (s.Background is not null)
|
|||
|
|
foreach (var sk in s.Background.SkillProficiencies)
|
|||
|
|
skillsBlock.Add(new HoverableChip(atlas, popover,
|
|||
|
|
CodexCopy.SkillName(sk),
|
|||
|
|
CodexCopy.SkillName(sk),
|
|||
|
|
CodexCopy.SkillDescription(sk),
|
|||
|
|
"BACKGROUND",
|
|||
|
|
ChipKind.SkillFromBg));
|
|||
|
|
foreach (var sk in s.ChosenSkills.OrderBy(x => x.ToString()))
|
|||
|
|
{
|
|||
|
|
string id = CodexCopy.SkillIdToJson(sk);
|
|||
|
|
skillsBlock.Add(new HoverableChip(atlas, popover,
|
|||
|
|
CodexCopy.SkillName(id),
|
|||
|
|
CodexCopy.SkillName(id),
|
|||
|
|
CodexCopy.SkillDescription(id),
|
|||
|
|
"CLASS",
|
|||
|
|
ChipKind.SkillFromClass));
|
|||
|
|
}
|
|||
|
|
col.Add(BuildBlock(atlas, "Skills", () => s.Step = 5, s.InvalidateLayout, skillsBlock));
|
|||
|
|
|
|||
|
|
// Starting kit — each chip resolves the item def for description /
|
|||
|
|
// properties / damage etc. and shows them on hover.
|
|||
|
|
var kitBlock = new WrapRow();
|
|||
|
|
if (s.Class?.StartingKit is not null)
|
|||
|
|
foreach (var entry in s.Class.StartingKit)
|
|||
|
|
kitBlock.Add(new KitItemWidget(atlas, popover, entry, s.Content));
|
|||
|
|
col.Add(BuildBlock(atlas, "Starting Kit", () => s.Step = 2, s.InvalidateLayout, kitBlock));
|
|||
|
|
return col;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static CodexWidget BuildLineage(CodexCharacterCreationScreen s)
|
|||
|
|
{
|
|||
|
|
var col = new Column { Spacing = 4 };
|
|||
|
|
col.Add(new CodexLabel(s.Clade?.Name ?? "—", CodexFonts.DisplayMedium, CodexColors.Ink));
|
|||
|
|
col.Add(new CodexLabel(
|
|||
|
|
(s.Species?.Name ?? "—") + (s.Species is not null ? $" · {CodexCopy.SizeLabel(s.Species.Size).ToUpperInvariant()}" : ""),
|
|||
|
|
CodexFonts.SerifBody, CodexColors.InkSoft));
|
|||
|
|
return col;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static CodexWidget BuildCalling(CodexCharacterCreationScreen s)
|
|||
|
|
{
|
|||
|
|
var col = new Column { Spacing = 4 };
|
|||
|
|
col.Add(new CodexLabel(s.Class?.Name ?? "—", CodexFonts.DisplayMedium, CodexColors.Ink));
|
|||
|
|
col.Add(new CodexLabel(
|
|||
|
|
(s.Class is not null ? $"D{s.Class.HitDie} · {string.Join("/", s.Class.PrimaryAbility)}" : ""),
|
|||
|
|
CodexFonts.MonoTagSmall, CodexColors.InkMute));
|
|||
|
|
col.Add(new CodexLabel(s.Background?.Name ?? "—", CodexFonts.SerifItalic, CodexColors.InkSoft));
|
|||
|
|
return col;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static CodexWidget BuildBlock(CodexAtlas atlas, string title, System.Action onEdit, System.Action onAfterEdit, CodexWidget body)
|
|||
|
|
{
|
|||
|
|
var inner = new Column { Spacing = 8 };
|
|||
|
|
var head = new Row { Spacing = 8, VAlignChildren = VAlign.Middle };
|
|||
|
|
head.Add(new CodexLabel(title, CodexFonts.DisplaySmall, CodexColors.Ink));
|
|||
|
|
var spacerL = new HSpacer();
|
|||
|
|
head.Add(spacerL);
|
|||
|
|
head.Add(new EditLink(atlas, () => { onEdit(); onAfterEdit(); }));
|
|||
|
|
inner.Add(head);
|
|||
|
|
inner.Add(body);
|
|||
|
|
return new CodexPanel(atlas, inner);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal sealed class EditLink : CodexWidget
|
|||
|
|
{
|
|||
|
|
private readonly CodexAtlas _atlas;
|
|||
|
|
private readonly FontStashSharp.SpriteFontBase _font = CodexFonts.MonoTagSmall;
|
|||
|
|
private readonly System.Action _onClick;
|
|||
|
|
private bool _hovered;
|
|||
|
|
private const string Text = "EDIT ›";
|
|||
|
|
|
|||
|
|
public EditLink(CodexAtlas atlas, System.Action onClick) { _atlas = atlas; _onClick = onClick; }
|
|||
|
|
|
|||
|
|
protected override Point MeasureCore(Point available)
|
|||
|
|
{
|
|||
|
|
var s = _font.MeasureString(Text);
|
|||
|
|
return new Point((int)s.X + 6, (int)System.MathF.Ceiling(_font.LineHeight) + 4);
|
|||
|
|
}
|
|||
|
|
protected override void ArrangeCore(Rectangle bounds) { }
|
|||
|
|
|
|||
|
|
public override void Update(GameTime gt, CodexInput input)
|
|||
|
|
{
|
|||
|
|
_hovered = ContainsPoint(input.MousePosition);
|
|||
|
|
if (_hovered && input.LeftJustReleased) _onClick();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public override void Draw(SpriteBatch sb, GameTime gt)
|
|||
|
|
{
|
|||
|
|
var s = _font.MeasureString(Text);
|
|||
|
|
var color = _hovered ? CodexColors.GildBright : CodexColors.Gild;
|
|||
|
|
_font.DrawText(sb, Text, new Vector2(Bounds.X, Bounds.Y + (Bounds.Height - _font.LineHeight) / 2f), color);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal sealed class HSpacer : CodexWidget
|
|||
|
|
{
|
|||
|
|
protected override Point MeasureCore(Point available) => new(System.Math.Max(0, available.X - 80), 1);
|
|||
|
|
protected override void ArrangeCore(Rectangle bounds) { }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal sealed class KitItemWidget : CodexWidget
|
|||
|
|
{
|
|||
|
|
private readonly CodexAtlas _atlas;
|
|||
|
|
private readonly CodexHoverPopover _popover;
|
|||
|
|
private readonly Theriapolis.Core.Data.StartingKitItem _entry;
|
|||
|
|
private readonly Theriapolis.Core.Data.ItemDef? _itemDef;
|
|||
|
|
private readonly FontStashSharp.SpriteFontBase _font = CodexFonts.SerifBody;
|
|||
|
|
private readonly FontStashSharp.SpriteFontBase _qtyFont = CodexFonts.MonoTagSmall;
|
|||
|
|
private bool _hovered;
|
|||
|
|
|
|||
|
|
public KitItemWidget(CodexAtlas atlas, CodexHoverPopover popover,
|
|||
|
|
Theriapolis.Core.Data.StartingKitItem entry,
|
|||
|
|
Theriapolis.Core.Data.ContentResolver content)
|
|||
|
|
{
|
|||
|
|
_atlas = atlas;
|
|||
|
|
_popover = popover;
|
|||
|
|
_entry = entry;
|
|||
|
|
// Item id may not resolve (forward-compat starting kits, missing
|
|||
|
|
// entries) — gracefully fall back to a name-only popover.
|
|||
|
|
content.Items.TryGetValue(entry.ItemId, out _itemDef);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected override Point MeasureCore(Point available) => new(160, 48);
|
|||
|
|
protected override void ArrangeCore(Rectangle bounds) { }
|
|||
|
|
|
|||
|
|
public override void Update(GameTime gt, CodexInput input)
|
|||
|
|
{
|
|||
|
|
_hovered = ContainsPoint(input.MousePosition);
|
|||
|
|
if (_hovered) ShowPopover();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ShowPopover()
|
|||
|
|
{
|
|||
|
|
string title = CodexCopy.ItemName(_entry.ItemId);
|
|||
|
|
string body = ComposePopoverBody();
|
|||
|
|
string? tag = _itemDef?.Kind?.ToUpperInvariant();
|
|||
|
|
_popover.Show(Bounds, title, body, tag);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Compose a multi-line description from the item's stats and
|
|||
|
|
/// the auto-equip slot. Falls back to the codex flavor description
|
|||
|
|
/// when no kind-specific stats are available.</summary>
|
|||
|
|
private string ComposePopoverBody()
|
|||
|
|
{
|
|||
|
|
var sb = new System.Text.StringBuilder();
|
|||
|
|
if (_entry.Qty > 1) sb.Append($"Quantity: ×{_entry.Qty}\n");
|
|||
|
|
if (_entry.AutoEquip) sb.Append($"Equipped to: {_entry.EquipSlot.Replace('_', ' ')}\n");
|
|||
|
|
|
|||
|
|
if (_itemDef is not null)
|
|||
|
|
{
|
|||
|
|
switch (_itemDef.Kind)
|
|||
|
|
{
|
|||
|
|
case "weapon":
|
|||
|
|
sb.Append($"Damage: {_itemDef.Damage}");
|
|||
|
|
if (!string.IsNullOrEmpty(_itemDef.DamageType)) sb.Append(' ').Append(_itemDef.DamageType);
|
|||
|
|
if (!string.IsNullOrEmpty(_itemDef.DamageVersatile))
|
|||
|
|
sb.Append($" ({_itemDef.DamageVersatile} two-handed)");
|
|||
|
|
sb.Append('\n');
|
|||
|
|
break;
|
|||
|
|
case "armor":
|
|||
|
|
sb.Append($"AC {_itemDef.AcBase}");
|
|||
|
|
if (_itemDef.AcMaxDex >= 0) sb.Append($" + DEX (max +{_itemDef.AcMaxDex})");
|
|||
|
|
if (_itemDef.MinStr > 0) sb.Append($" · Min STR {_itemDef.MinStr}");
|
|||
|
|
sb.Append('\n');
|
|||
|
|
break;
|
|||
|
|
case "shield":
|
|||
|
|
sb.Append($"+{_itemDef.AcBase} AC\n");
|
|||
|
|
break;
|
|||
|
|
case "consumable":
|
|||
|
|
if (!string.IsNullOrEmpty(_itemDef.Healing)) sb.Append($"Heals {_itemDef.Healing}\n");
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
if (_itemDef.Properties.Length > 0)
|
|||
|
|
sb.Append("Properties: ").Append(string.Join(", ", _itemDef.Properties)).Append('\n');
|
|||
|
|
if (!string.IsNullOrEmpty(_itemDef.Description))
|
|||
|
|
sb.Append(_itemDef.Description);
|
|||
|
|
}
|
|||
|
|
if (sb.Length == 0) sb.Append("(no description)");
|
|||
|
|
return sb.ToString().TrimEnd('\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public override void Draw(SpriteBatch sb, GameTime gt)
|
|||
|
|
{
|
|||
|
|
Color border = _hovered ? CodexColors.Gild
|
|||
|
|
: _entry.AutoEquip ? CodexColors.Gild
|
|||
|
|
: CodexColors.Rule;
|
|||
|
|
sb.Draw(_atlas.Pixel, Bounds, CodexColors.Bg);
|
|||
|
|
sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Y, Bounds.Width, 1), border);
|
|||
|
|
sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Bottom - 1, Bounds.Width, 1), border);
|
|||
|
|
sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Y, 1, Bounds.Height), border);
|
|||
|
|
sb.Draw(_atlas.Pixel, new Rectangle(Bounds.Right - 1, Bounds.Y, 1, Bounds.Height), border);
|
|||
|
|
|
|||
|
|
_font.DrawText(sb, CodexCopy.ItemName(_entry.ItemId),
|
|||
|
|
new Vector2(Bounds.X + 8, Bounds.Y + 6), CodexColors.Ink);
|
|||
|
|
_qtyFont.DrawText(sb, "×" + _entry.Qty,
|
|||
|
|
new Vector2(Bounds.X + 8, Bounds.Y + 22), CodexColors.InkMute);
|
|||
|
|
if (_entry.AutoEquip)
|
|||
|
|
_qtyFont.DrawText(sb, _entry.EquipSlot.ToUpperInvariant(),
|
|||
|
|
new Vector2(Bounds.X + 8, Bounds.Bottom - _qtyFont.LineHeight - 4),
|
|||
|
|
CodexColors.Gild);
|
|||
|
|
}
|
|||
|
|
}
|