feat(combat): port retail held weapon parenting

Select the default combat mode from ordered equipped objects so bows request missile stance. Parse CreateObject parent metadata and ParentEvent, then render held objects as separate children composed from setup holding locations and placement frames each animation tick.
This commit is contained in:
Erik 2026-07-11 13:02:26 +02:00
parent 564d39dfea
commit ab6d96d113
18 changed files with 1152 additions and 17 deletions

View file

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using AcDream.Core.Items;
namespace AcDream.Core.Combat;
@ -48,6 +50,56 @@ public enum CombatAttackAction
/// </summary>
public static class CombatInputPlanner
{
private const EquipMask PrimaryWeaponLocations =
EquipMask.MeleeWeapon | EquipMask.MissileWeapon | EquipMask.TwoHanded;
/// <summary>
/// Port of retail <c>ClientCombatSystem::GetDefaultCombatMode</c>
/// (0x0056B310). <paramref name="orderedPlayerContents"/> must be in the
/// player's inventory-placement order; retail returns the first equipped
/// object intersecting the requested location mask.
/// </summary>
public static CombatMode GetDefaultCombatMode(
IReadOnlyList<ClientObject> orderedPlayerContents)
{
ArgumentNullException.ThrowIfNull(orderedPlayerContents);
ClientObject? weapon = GetObjectAtLocation(
orderedPlayerContents, PrimaryWeaponLocations);
if (weapon is not null)
{
// Retail COMBAT_USE_MISSILE = 2. Every other combat-use value in
// this primary weapon slot selects melee.
return weapon.CombatUse == 2
? CombatMode.Missile
: CombatMode.Melee;
}
ClientObject? held = GetObjectAtLocation(
orderedPlayerContents, EquipMask.Held);
if (held is null)
return CombatMode.Melee;
// The decomp's byte-1 sign test is ITEM_TYPE bit 15 (Caster).
return (held.Type & ItemType.Caster) != 0
? CombatMode.Magic
: CombatMode.NonCombat;
}
private static ClientObject? GetObjectAtLocation(
IReadOnlyList<ClientObject> orderedPlayerContents,
EquipMask locationMask)
{
for (int i = 0; i < orderedPlayerContents.Count; i++)
{
ClientObject candidate = orderedPlayerContents[i];
if ((candidate.CurrentlyEquippedLocation & locationMask) != 0)
return candidate;
}
return null;
}
public static CombatMode ToggleMode(
CombatMode currentMode,
CombatMode defaultCombatMode = CombatMode.Melee)