acdream/src/AcDream.App/Studio/FixtureProvider.cs
Erik 20df9d155d refactor(runtime): own combat and magic intent
Move attack build/repeat state, combat-mode policy, authoritative auto-target transitions, and spell-cast intent beneath RuntimeActionState. Keep App as the input, world-query, DAT-policy, transport, and presentation adapter while preserving retail request and busy ordering. Add direct/graphical parity, reset, failure, and instance-isolation coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-07-26 11:56:40 +02:00

225 lines
11 KiB
C#

using AcDream.App.Rendering;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Runtime.Gameplay;
using AcDream.UI.Abstractions.Panels.Settings;
using DatReaderWriter;
using AcDream.Content;
namespace AcDream.App.Studio;
/// <summary>
/// Populates a loaded panel with sample data by calling the production
/// controller Bind methods against <see cref="SampleData"/>.
///
/// <para>
/// The studio is intentionally thin — there is no live game session, no
/// server connection, and no network. FixtureProvider bridges that gap by
/// feeding static fixtures (vitals percentages, a fake inventory, empty
/// shortcut lists) so the bound widgets show plausible state instead of
/// empty zeroes.
/// </para>
///
/// <para>
/// <b>IconIds approach:</b> raw-resolve stub — resolve the base <c>iconId</c>
/// via <see cref="RenderStack.ResolveChrome"/> and return the GL handle
/// directly. This is intentionally simpler than GameWindow's full
/// <see cref="IconComposer"/> (5-layer composite). The raw icon is enough
/// to confirm the grid cells draw something in the studio; the full
/// compositor is the live-game concern, not the layout preview concern.
/// </para>
/// </summary>
public static class FixtureProvider
{
/// <summary>
/// Populate <paramref name="layout"/> with sample data appropriate for
/// <paramref name="layoutId"/>. Calls the production controller Bind
/// methods so the panel's widgets drive off the same code path as the
/// full game.
/// </summary>
/// <param name="layoutId">The LayoutDesc dat id used to import the layout.</param>
/// <param name="layout">The imported layout whose widgets to populate.</param>
/// <param name="stack">The live render stack (for <see cref="RenderStack.ResolveChrome"/>
/// and <see cref="RenderStack.VitalsDatFont"/>).</param>
/// <param name="objects">A <see cref="ClientObjectTable"/> already seeded by
/// <see cref="SampleData.BuildObjectTable"/>.</param>
/// <param name="dats">The live DAT collection used to resolve per-list empty-slot sprites
/// (same lookup GameWindow.OnLoad performs for the production binding).</param>
public static IRetainedPanelController? Populate(
uint layoutId,
ImportedLayout layout,
RenderStack stack,
ClientObjectTable objects,
IDatReaderWriter dats)
{
switch (layoutId)
{
case 0x2100006Cu: // vitals
VitalsController.Bind(layout,
healthPct: () => SampleData.HealthPct,
staminaPct: () => SampleData.StaminaPct,
manaPct: () => SampleData.ManaPct,
healthText: () => "80/100",
staminaText: () => "60/100",
manaText: () => "90/100");
return null;
case RadarController.LayoutId: // gmRadarUI
return RadarController.Bind(
layout,
() => new UiRadarSnapshot(
PlayerHeadingDegrees: 32f,
Blips:
[
new UiRadarBlip(0x50000001u, "Portal to Holtburg", 82f, 43f,
RadarColor(AcDream.Core.Ui.RadarBlipColors.Portal),
AcDream.Core.Ui.RadarBlipShape.Plus),
new UiRadarBlip(0x50000002u, "Drudge Prowler", 43f, 76f,
RadarColor(AcDream.Core.Ui.RadarBlipColors.Creature),
AcDream.Core.Ui.RadarBlipShape.Plus),
new UiRadarBlip(0x50000003u, "Town Crier", 68f, 87f,
RadarColor(AcDream.Core.Ui.RadarBlipColors.NPC),
AcDream.Core.Ui.RadarBlipShape.Box),
new UiRadarBlip(0x50000004u, "Selected PK", 39f, 39f,
RadarColor(AcDream.Core.Ui.RadarBlipColors.PlayerKiller),
AcDream.Core.Ui.RadarBlipShape.X,
Selected: true),
],
CoordinatesText: "42.1N,33.3E"),
datFont: stack.VitalsDatFont);
case CombatUiController.LayoutId:
{
var combat = new CombatState();
var attacks = new RuntimeCombatAttackState(
combat,
canStartAttack: () => true,
sendAttack: (_, _) => true,
autoRepeatAttack: () => false);
GameplaySettings gameplay = GameplaySettings.Default;
CombatUiController? controller = CombatUiController.Bind(
layout,
combat,
attacks,
() => gameplay,
value => gameplay = value,
new CombatUiLabels(
"Speed", "Power", "Repeat Attacks", "Auto Target", "Keep in View",
"High", "Medium", "Low"),
visible => layout.Root.Visible = visible);
combat.SetCombatMode(CombatMode.Melee);
return controller;
}
case 0x21000016u: // toolbar
return ToolbarController.Bind(
layout,
objects,
shortcuts: new ShortcutStore(),
iconIds: MakeIconIds(stack),
useItem: _ => { },
combatState: null,
regularDigits: null,
ghostedDigits: null,
emptyDigits: null,
sendAddShortcut: null,
sendRemoveShortcut: null);
case 0x21000023u: // inventory + paperdoll
{
// Resolve the per-list empty-slot art from the dat cell template, matching the
// exact lookup GameWindow.OnLoad performs (UIElement_ItemList::InternalCreateItem
// 0x004e3570 → attr 0x1000000e → catalog 0x21000037 → ItemSlot_Empty).
uint contentsEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000021u, 0x100001C6u);
uint sideBagEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001CAu);
uint mainPackEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001C9u);
var paperdollEmpty = PaperdollSlotBackgrounds.ResolveEmptySprites(dats);
var iconIds = MakeIconIds(stack);
var selection = new AcDream.Core.Selection.SelectionState();
var itemInteraction = new ItemInteractionController(
objects,
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(
new InventoryTransactionState(objects)),
new AcDream.Runtime.Gameplay.InteractionState(),
() => SampleData.PlayerGuid,
sendUse: null,
sendUseWithTarget: null,
sendWield: null,
sendDrop: null);
InventoryController inventory = InventoryController.Bind(
layout,
objects,
playerGuid: () => SampleData.PlayerGuid,
iconIds: iconIds,
strength: () => 100,
selection: selection,
datFont: stack.VitalsDatFont,
contentsEmptySprite: contentsEmpty,
sideBagEmptySprite: sideBagEmpty,
mainPackEmptySprite: mainPackEmpty);
// Bind the paperdoll equip slots with their authored per-location UIItem prototypes.
PaperdollController paperdoll = PaperdollController.Bind(
layout,
objects,
playerGuid: () => SampleData.PlayerGuid,
iconIds: iconIds,
selection: selection,
itemInteraction: itemInteraction,
emptySlotSprite: contentsEmpty,
datFont: stack.VitalsDatFont,
emptySlotSprites: paperdollEmpty,
ownsItemInteraction: true);
return new RetainedPanelControllerGroup(inventory, paperdoll);
}
case 0x2100002Eu: // gmStatManagementUI — Attributes/Skills/Titles window (LayoutDesc 0x2100002E)
// Bind the REAL importer-mounted header + list elements (name/heritage/PK/level/
// total-XP/XP-meter + the 9-row attribute list + footer State-A). NOT the text-report
// sub-panel (that is gmCharacterInfoUI 0x2100001A → CharacterController).
// LargeDatFont (0x40000001, MaxCharHeight=18) is used for the attribute row text;
// fallback to VitalsDatFont (0x40000000, 16px) if unavailable.
CharacterStatController.Bind(
layout,
data: SampleData.SampleCharacter,
datFont: stack.VitalsDatFont,
rowDatFont: stack.LargeDatFont ?? stack.VitalsDatFont,
spriteResolve: stack.ResolveChrome);
return null;
default:
// Unknown layout — no-op; the panel renders structurally.
return null;
}
}
// ── Helpers ─────────────────────────────────────────────────────────────
/// <summary>
/// Build the <c>iconIds</c> delegate for toolbar / inventory controllers.
///
/// <para>
/// Raw-resolve stub: resolve the base <paramref name="iconId"/> (arg 2)
/// via <see cref="RenderStack.ResolveChrome"/> and return its GL handle.
/// The remaining args (type, underlayId, overlayId, effects) are ignored
/// for the studio — a single-layer icon is sufficient for layout preview.
/// </para>
///
/// <para>This is what the task spec calls "v1 raw-resolve stub".</para>
/// </summary>
private static Func<ItemType, uint, uint, uint, uint, uint> MakeIconIds(RenderStack stack)
=> (_, iconId, _, _, _) =>
{
if (iconId == 0u) return 0u;
var (handle, _, _) = stack.ResolveChrome(iconId);
return handle;
};
private static System.Numerics.Vector4 RadarColor(AcDream.Core.Ui.RadarBlipColors.Rgba color)
=> new(color.Red, color.Green, color.Blue, color.Alpha);
}