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() { }
}
}