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:
parent
564d39dfea
commit
ab6d96d113
18 changed files with 1152 additions and 17 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,15 @@ public sealed class ClientObjectTable
|
|||
/// </summary>
|
||||
public event Action<ClientObject, uint, uint>? ObjectMoved;
|
||||
|
||||
/// <summary>
|
||||
/// Fires after an optimistic inventory/equipment move is rejected and
|
||||
/// <see cref="RollbackMove"/> has restored the exact pre-move state.
|
||||
/// Consumers with projections outside the item grid (for example a
|
||||
/// parented 3-D weapon) can restore their prior projection without
|
||||
/// treating every ordinary equip as a rollback.
|
||||
/// </summary>
|
||||
public event Action<ClientObject>? MoveRolledBack;
|
||||
|
||||
/// <summary>Fires when an object is removed from the session.</summary>
|
||||
public event Action<ClientObject>? ObjectRemoved;
|
||||
|
||||
|
|
@ -239,7 +248,10 @@ public sealed class ClientObjectTable
|
|||
{
|
||||
if (!_pendingMoves.TryGetValue(itemId, out var pre)) return false;
|
||||
_pendingMoves.Remove(itemId);
|
||||
return MoveItem(itemId, pre.container, pre.slot, pre.equip);
|
||||
if (!MoveItem(itemId, pre.container, pre.slot, pre.equip))
|
||||
return false;
|
||||
MoveRolledBack?.Invoke(_objects[itemId]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
85
src/AcDream.Core/Meshing/EquippedChildAttachment.cs
Normal file
85
src/AcDream.Core/Meshing/EquippedChildAttachment.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Meshing;
|
||||
|
||||
/// <summary>
|
||||
/// Retail held-object transform composition. A weapon is a separate child
|
||||
/// physics object: the parent's <see cref="Setup.HoldingLocations"/> selects
|
||||
/// a hand part and a frame relative to it, while the child's placement frame
|
||||
/// poses the weapon's own setup parts.
|
||||
///
|
||||
/// Sources: <c>CPhysicsObj::add_child @ 0x0050F870</c>,
|
||||
/// <c>CPhysicsObj::UpdateChild @ 0x00512D50</c>,
|
||||
/// <c>CPartArray::SetPlacementFrame @ 0x005193D0</c>, and
|
||||
/// <c>Frame::combine @ 0x005122E0</c>.
|
||||
/// </summary>
|
||||
public static class EquippedChildAttachment
|
||||
{
|
||||
public static bool TryCompose(
|
||||
Setup parentSetup,
|
||||
IReadOnlyList<MeshRef> currentParentPose,
|
||||
Setup childSetup,
|
||||
ParentLocation parentLocation,
|
||||
Placement placement,
|
||||
IReadOnlyList<MeshRef> childPartTemplate,
|
||||
float childScale,
|
||||
out IReadOnlyList<MeshRef> attachedParts)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(parentSetup);
|
||||
ArgumentNullException.ThrowIfNull(currentParentPose);
|
||||
ArgumentNullException.ThrowIfNull(childSetup);
|
||||
ArgumentNullException.ThrowIfNull(childPartTemplate);
|
||||
|
||||
if (!parentSetup.HoldingLocations.TryGetValue(parentLocation, out LocationType? holding))
|
||||
{
|
||||
attachedParts = Array.Empty<MeshRef>();
|
||||
return false;
|
||||
}
|
||||
|
||||
Matrix4x4 parentPart = holding.PartId >= 0
|
||||
&& holding.PartId < currentParentPose.Count
|
||||
? currentParentPose[holding.PartId].PartTransform
|
||||
: Matrix4x4.Identity;
|
||||
Matrix4x4 holdingFrame = ToMatrix(holding.Frame);
|
||||
Matrix4x4 childRoot = holdingFrame * parentPart;
|
||||
|
||||
// Retail CPartArray::SetPlacementFrame falls back specifically to
|
||||
// placement 0 (Default), then installs null if Default is absent.
|
||||
AnimationFrame? placementFrame = null;
|
||||
if (!childSetup.PlacementFrames.TryGetValue(placement, out placementFrame))
|
||||
childSetup.PlacementFrames.TryGetValue(Placement.Default, out placementFrame);
|
||||
|
||||
int partCount = Math.Min(childSetup.Parts.Count, childPartTemplate.Count);
|
||||
var result = new MeshRef[partCount];
|
||||
for (int i = 0; i < partCount; i++)
|
||||
{
|
||||
Frame partFrame = placementFrame is not null && i < placementFrame.Frames.Count
|
||||
? placementFrame.Frames[i]
|
||||
: new Frame { Orientation = Quaternion.Identity };
|
||||
Vector3 scale = i < childSetup.DefaultScale.Count
|
||||
? childSetup.DefaultScale[i]
|
||||
: Vector3.One;
|
||||
Matrix4x4 childPart = Matrix4x4.CreateScale(scale)
|
||||
* ToMatrix(partFrame);
|
||||
if (childScale != 1.0f)
|
||||
childPart *= Matrix4x4.CreateScale(childScale);
|
||||
|
||||
MeshRef template = childPartTemplate[i];
|
||||
result[i] = new MeshRef(template.GfxObjId, childPart * childRoot)
|
||||
{
|
||||
SurfaceOverrides = template.SurfaceOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
attachedParts = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Matrix4x4 ToMatrix(Frame frame) =>
|
||||
Matrix4x4.CreateFromQuaternion(frame.Orientation)
|
||||
* Matrix4x4.CreateTranslation(frame.Origin);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue