using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Spells; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; namespace AcDream.Runtime.Tests.Gameplay; /// /// Slice 5.3 — the client-local vendor-panel distance watcher /// (). Mirrors the GameRuntime test /// fixture RuntimeHostileTargetQueryTests already established for /// exactly this class of "position + distance" Runtime query. /// public sealed class RuntimeVendorRangeQueryTests { private const uint Player = 0x50000001u; private const uint Vendor = 0x50000010u; private const uint Landblock = 0x01010001u; [Fact] public void EnforceRange_PlayerWithinVendorUseRadius_LeavesSessionOpen() { using GameRuntime runtime = Create(); runtime.PlayerIdentity.ServerGuid = Player; Add(runtime, Player, Landblock, 100f, 100f); Add(runtime, Vendor, Landblock, 102f, 100f, useRadius: 3f); // 2 m away, 3 m radius Open(runtime, Vendor); RuntimeVendorRangeQuery.EnforceRange(runtime); Assert.Equal(Vendor, runtime.InventoryOwner.Vendor.VendorId); } [Fact] public void EnforceRange_PlayerMovesBeyondVendorUseRadius_ClosesTheSession() { using GameRuntime runtime = Create(); runtime.PlayerIdentity.ServerGuid = Player; RuntimeEntityRecord playerRecord = Add(runtime, Player, Landblock, 100f, 100f); Add(runtime, Vendor, Landblock, 102f, 100f, useRadius: 3f); Open(runtime, Vendor); // Walk 20 m away — well beyond the vendor's own authored 3 m radius. SetPosition(playerRecord, Landblock, 122f, 100f); RuntimeVendorRangeQuery.EnforceRange(runtime); Assert.Equal(0u, runtime.InventoryOwner.Vendor.VendorId); } [Fact] public void EnforceRange_PlayerStaysWithinRadiusAfterSmallMove_LeavesSessionOpen() { using GameRuntime runtime = Create(); runtime.PlayerIdentity.ServerGuid = Player; RuntimeEntityRecord playerRecord = Add(runtime, Player, Landblock, 100f, 100f); Add(runtime, Vendor, Landblock, 102f, 100f, useRadius: 3f); Open(runtime, Vendor); // Shuffle 1 m closer — still well inside the 3 m radius. SetPosition(playerRecord, Landblock, 101f, 100f); RuntimeVendorRangeQuery.EnforceRange(runtime); Assert.Equal(Vendor, runtime.InventoryOwner.Vendor.VendorId); } [Fact] public void EnforceRange_NoVendorOpen_IsANoOpAndDoesNotThrow() { using GameRuntime runtime = Create(); runtime.PlayerIdentity.ServerGuid = Player; Add(runtime, Player, Landblock, 100f, 100f); RuntimeVendorRangeQuery.EnforceRange(runtime); Assert.Equal(0u, runtime.InventoryOwner.Vendor.VendorId); } [Fact] public void EnforceRange_VendorUseRadiusAbsent_FallsBackToTheAceDefault() { // ACE WorldObject_Use.cs:50 — `wo.UseRadius ?? 0.6f`. using GameRuntime runtime = Create(); runtime.PlayerIdentity.ServerGuid = Player; RuntimeEntityRecord playerRecord = Add(runtime, Player, Landblock, 100f, 100f); Add(runtime, Vendor, Landblock, 100.5f, 100f, useRadius: null); Open(runtime, Vendor); // 0.5 m: inside the 0.6 m fallback. RuntimeVendorRangeQuery.EnforceRange(runtime); Assert.Equal(Vendor, runtime.InventoryOwner.Vendor.VendorId); // Walk to 2 m: outside the 0.6 m fallback. SetPosition(playerRecord, Landblock, 102.5f, 100f); RuntimeVendorRangeQuery.EnforceRange(runtime); Assert.Equal(0u, runtime.InventoryOwner.Vendor.VendorId); } private static void Open(GameRuntime runtime, uint vendorGuid) => Assert.True(runtime.InventoryOwner.Vendor.Apply( vendorGuid, default, Array.Empty())); private static void SetPosition( RuntimeEntityRecord record, uint landblock, float x, float y, float z = 5f) { record.Snapshot = record.Snapshot with { Position = new CreateObject.ServerPosition( landblock, x, y, z, 1f, 0f, 0f, 0f), }; } private static GameRuntime Create() { var operations = new Operations(); return new GameRuntime(new GameRuntimeDependencies( operations, operations, operations, operations)); } private static RuntimeEntityRecord Add( GameRuntime runtime, uint guid, uint landblock, float x, float y, float? useRadius = null) { RuntimeEntityRecord record = runtime.EntityObjects .RegisterEntity(Spawn(guid, landblock, x, y, useRadius)) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); return record; } private static WorldSession.EntitySpawn Spawn( uint guid, uint landblock, float x, float y, float? useRadius) => new( guid, new CreateObject.ServerPosition(landblock, x, y, 5f, 1f, 0f, 0f, 0f), 0x02000001u, [], [], [], null, null, guid.ToString("X8"), null, null, null, UseRadius: useRadius); private sealed class Operations : IRuntimeCombatAttackOperations, IRuntimeCombatTargetOperations, IRuntimeCombatModeOperations, IRuntimeSpellCastOperations { public bool CanStartAttack() => false; public void PrepareAttackRequest() { } public bool SendAttack(AttackHeight height, float power) => false; public void SendCancelAttack() { } public bool IsDualWield => false; public bool PlayerReadyForAttack => false; public bool AutoRepeatAttack => false; public bool AutoTarget => false; public uint? SelectClosestTarget() => null; public bool IsInWorld => false; public IReadOnlyList GetOrderedEquipment() => []; public void NotifyExplicitCombatModeRequest() { } public void SendChangeCombatMode(CombatMode mode) { } public uint LocalPlayerId => 0u; public bool CanSend => false; public bool HasRequiredComponents(uint spellId) => false; public bool IsTargetCompatible( uint targetId, SpellMetadata spell, bool showMessage) => false; public void StopCompletely() { } public void SendUntargeted(uint spellId) { } public void SendTargeted(uint targetId, uint spellId) { } public void DisplayMessage(string message) { } public void IncrementBusy() { } } }