修复技能输入并更新战斗资源
This commit is contained in:
@@ -70,6 +70,33 @@ public sealed class LobbyActorBehaviorRuntimeTests
|
||||
Assert.That(runtime.DrainEvents().Any(e => e.BehaviorId == LobbyActorBehaviorRuntime.SecondaryABehaviorId), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Skill1Tap_DoesNotRestartWhileSkillIsActive()
|
||||
{
|
||||
var runtime = new LobbyActorBehaviorRuntime();
|
||||
runtime.RegisterCatalog(CreateFighterCatalog());
|
||||
runtime.RegisterOrUpdateActor(10, Vector3.zero, Vector3.forward);
|
||||
|
||||
BehaviorStartResult first = runtime.HandleActionButton(
|
||||
CreateButtonEvent(ActionButtonId.Skill1, ActionButtonPhase.Tap, Vector2.zero),
|
||||
10,
|
||||
Vector3.forward);
|
||||
runtime.DrainEvents();
|
||||
|
||||
BehaviorStartResult second = runtime.HandleActionButton(
|
||||
CreateButtonEvent(ActionButtonId.Skill1, ActionButtonPhase.Tap, Vector2.zero),
|
||||
10,
|
||||
Vector3.forward);
|
||||
|
||||
BehaviorEvent[] events = runtime.DrainEvents().ToArray();
|
||||
Assert.That(first.Success, Is.True);
|
||||
Assert.That(second.Success, Is.False);
|
||||
Assert.That(second.Error, Is.EqualTo("actor already has active behavior"));
|
||||
Assert.That(events.Any(e => e.Kind == BehaviorEventKind.BehaviorStopped), Is.False);
|
||||
Assert.That(events.Any(e => e.Kind == BehaviorEventKind.BehaviorStarted), Is.False);
|
||||
Assert.That(events.Any(e => e.Kind == BehaviorEventKind.TimelineAnimation), Is.False);
|
||||
}
|
||||
|
||||
private static ActionButtonEvent CreateButtonEvent(ActionButtonId buttonId, ActionButtonPhase phase, Vector2 direction)
|
||||
{
|
||||
return new ActionButtonEvent(buttonId, phase, Vector2.zero, Vector2.zero, direction, direction.magnitude, 0f, false, 1);
|
||||
|
||||
@@ -2,7 +2,9 @@ using NUnit.Framework;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using XGame;
|
||||
using XGame.ActorBehavior;
|
||||
using XGame.VirtualInput;
|
||||
using XWorld.Framework.ActorBehavior;
|
||||
|
||||
public sealed class LobbyWorldControllerVirtualJoystickTests
|
||||
{
|
||||
@@ -60,6 +62,59 @@ public sealed class LobbyWorldControllerVirtualJoystickTests
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetMoveDirection_IgnoresJoystickWhileSelfBehaviorIsActive()
|
||||
{
|
||||
GameObject controllerObj = new GameObject("LobbyWorldControllerTest");
|
||||
try
|
||||
{
|
||||
LobbyWorldController controller = controllerObj.AddComponent<LobbyWorldController>();
|
||||
LobbyActorBehaviorRuntime runtime = CreateActiveSkillRuntime(10);
|
||||
|
||||
SetPrivateField(controller, "selfPlayerId", 10);
|
||||
SetPrivateField(controller, "actorBehaviorRuntime", runtime);
|
||||
SetPrivateField(controller, "joystick", Vector2.up);
|
||||
SetPrivateField(controller, "joystickActive", true);
|
||||
|
||||
Vector3 dir = (Vector3)InvokePrivate(controller, "GetMoveDirection");
|
||||
|
||||
Assert.That(dir, Is.EqualTo(Vector3.zero));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(controllerObj);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Skill1TapBeforeCatalogReady_StartsAfterCatalogBecomesReady()
|
||||
{
|
||||
GameObject controllerObj = new GameObject("LobbyWorldControllerTest");
|
||||
try
|
||||
{
|
||||
LobbyWorldController controller = controllerObj.AddComponent<LobbyWorldController>();
|
||||
LobbyActorBehaviorRuntime runtime = CreateReadySkillRuntime(10);
|
||||
|
||||
SetPrivateField(controller, "selfPlayerId", 10);
|
||||
SetPrivateField(controller, "actorBehaviorRuntime", runtime);
|
||||
SetPrivateField(controller, "actorBehaviorCatalogLoaded", false);
|
||||
|
||||
InvokePrivate(controller, "OnVirtualActionButtonEvent",
|
||||
new ActionButtonEvent(ActionButtonId.Skill1, ActionButtonPhase.Tap, Vector2.zero, Vector2.zero, Vector2.zero, 0f, 0f, false, 1));
|
||||
|
||||
Assert.That(runtime.HasActiveBehavior(10), Is.False);
|
||||
|
||||
SetPrivateField(controller, "actorBehaviorCatalogLoaded", true);
|
||||
InvokePrivate(controller, "ConsumePendingActionButtonEvent");
|
||||
|
||||
Assert.That(runtime.HasActiveBehavior(10), Is.True);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(controllerObj);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetPrivateField(object target, string name, object value)
|
||||
{
|
||||
FieldInfo field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
@@ -74,10 +129,55 @@ public sealed class LobbyWorldControllerVirtualJoystickTests
|
||||
return field.GetValue(target);
|
||||
}
|
||||
|
||||
private static void InvokePrivate(object target, string name, params object[] args)
|
||||
private static object InvokePrivate(object target, string name, params object[] args)
|
||||
{
|
||||
MethodInfo method = target.GetType().GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(method, Is.Not.Null);
|
||||
method.Invoke(target, args);
|
||||
return method.Invoke(target, args);
|
||||
}
|
||||
|
||||
private static LobbyActorBehaviorRuntime CreateActiveSkillRuntime(int actorId)
|
||||
{
|
||||
LobbyActorBehaviorRuntime runtime = CreateReadySkillRuntime(actorId);
|
||||
|
||||
BehaviorStartResult result = runtime.HandleActionButton(
|
||||
new ActionButtonEvent(ActionButtonId.Skill1, ActionButtonPhase.Tap, Vector2.zero, Vector2.zero, Vector2.zero, 0f, 0f, false, 1),
|
||||
actorId,
|
||||
Vector3.forward);
|
||||
|
||||
Assert.That(result.Success, Is.True);
|
||||
Assert.That(runtime.HasActiveBehavior(actorId), Is.True);
|
||||
return runtime;
|
||||
}
|
||||
|
||||
private static LobbyActorBehaviorRuntime CreateReadySkillRuntime(int actorId)
|
||||
{
|
||||
var runtime = new LobbyActorBehaviorRuntime();
|
||||
runtime.RegisterCatalog(CreateFighterCatalog());
|
||||
runtime.RegisterOrUpdateActor(actorId, Vector3.zero, Vector3.forward);
|
||||
Assert.That(runtime.SwitchActorCatalog(actorId, LobbyActorBehaviorRuntime.DefaultCatalogType), Is.True);
|
||||
return runtime;
|
||||
}
|
||||
|
||||
private static BehaviorConfigCatalog CreateFighterCatalog()
|
||||
{
|
||||
return BehaviorConfigCatalog.LoadFromJson(
|
||||
"fighter",
|
||||
@"{
|
||||
""type"": ""fighter"",
|
||||
""version"": 1,
|
||||
""behaviors"": [
|
||||
{
|
||||
""id"": ""quantao01"",
|
||||
""duration"": 1.0,
|
||||
""canBeInterrupted"": true,
|
||||
""input"": ""DirectionOrTarget"",
|
||||
""timeline"": [],
|
||||
""logicEffects"": []
|
||||
}
|
||||
]
|
||||
}",
|
||||
@"{ ""type"": ""fighter"", ""version"": 1, ""flyObjects"": [] }",
|
||||
@"{ ""type"": ""fighter"", ""version"": 1, ""buffs"": [] }");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,18 @@ public sealed class VirtualActionButtonTests
|
||||
Assert.That(events.Last().ButtonId, Is.EqualTo(ActionButtonId.Primary));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PointerUpInsideButtonWithoutMeaningfulDrag_EmitsTapEvenNearButtonEdge()
|
||||
{
|
||||
VirtualActionButton button = CreateButton(ActionButtonId.Skill1);
|
||||
List<ActionButtonEvent> events = Capture(button);
|
||||
|
||||
button.HandlePointerDownForTest(new Vector2(140f, 100f), 1, 0f);
|
||||
button.HandlePointerUpForTest(new Vector2(141f, 100f), 1, 0.1f);
|
||||
|
||||
Assert.That(events.Select(e => e.Phase), Is.EqualTo(new[] { ActionButtonPhase.Down, ActionButtonPhase.Tap }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PointerDragPastThreshold_EmitsReleaseWithDirection()
|
||||
{
|
||||
|
||||
@@ -46,9 +46,9 @@ namespace XGame
|
||||
private const float JoystickPadding = 100.0f;
|
||||
private const float JoystickDeadZone = 0.18f;
|
||||
private const float JoystickAcquireRadiusMultiplier = 1.6f;
|
||||
private const float CameraMinDistance = 2.0f;
|
||||
private const float CameraMaxDistance = 8f;
|
||||
private const float CameraDefaultDistance = 4.5f;
|
||||
private const float CameraMinDistance = 4.0f;
|
||||
private const float CameraMaxDistance = 15f;
|
||||
private const float CameraDefaultDistance = 6.0f;
|
||||
private const float CameraMinPitch = 20f;
|
||||
private const float CameraMaxPitch = 75f;
|
||||
private const float CameraMouseOrbitSensitivity = 0.2f;
|
||||
@@ -102,6 +102,9 @@ namespace XGame
|
||||
private bool actionPadLoading;
|
||||
private bool actorBehaviorCatalogLoading;
|
||||
private bool actorBehaviorCatalogLoaded;
|
||||
private bool hasPendingActionButtonEvent;
|
||||
private ActionButtonEvent pendingActionButtonEvent;
|
||||
private Vector3 pendingActionButtonForward;
|
||||
private static int materialLogCount;
|
||||
|
||||
public void Initialize(Action<FrameworkOpcode, byte[]> sendFramework, int playerId, string playerName, int figure)
|
||||
@@ -453,6 +456,11 @@ namespace XGame
|
||||
|
||||
private Vector3 GetMoveDirection()
|
||||
{
|
||||
if (IsSelfBehaviorActive())
|
||||
{
|
||||
return Vector3.zero;
|
||||
}
|
||||
|
||||
Camera cam = Camera.main;
|
||||
Vector3 forward = cam != null ? cam.transform.forward : Vector3.forward;
|
||||
Vector3 right = cam != null ? cam.transform.right : Vector3.right;
|
||||
@@ -466,6 +474,12 @@ namespace XGame
|
||||
Input.GetKey(KeyCode.D));
|
||||
}
|
||||
|
||||
private bool IsSelfBehaviorActive()
|
||||
{
|
||||
return actorBehaviorRuntime != null
|
||||
&& actorBehaviorRuntime.HasActiveBehavior(selfPlayerId);
|
||||
}
|
||||
|
||||
public static Vector3 MapMoveInputForTest(
|
||||
Vector2 joystickInput,
|
||||
Vector3 cameraForward,
|
||||
@@ -1008,6 +1022,7 @@ namespace XGame
|
||||
actorBehaviorRuntime.RegisterCatalog(catalog);
|
||||
actorBehaviorCatalogLoaded = true;
|
||||
UpdateActorBehaviorActors();
|
||||
ConsumePendingActionButtonEvent();
|
||||
}
|
||||
|
||||
private void RegisterActorBehavior(Avatar avatar)
|
||||
@@ -1164,19 +1179,52 @@ namespace XGame
|
||||
{
|
||||
if (LobbyActorBehaviorRuntime.IsSecondaryATrigger(evt))
|
||||
{
|
||||
QueuePendingActionButtonEvent(evt, GetSelfSkillForward());
|
||||
Debug.LogWarning("[LobbyWorldController] actor behavior catalog is not ready.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
BehaviorStartResult result = actorBehaviorRuntime.HandleActionButton(evt, selfPlayerId, GetSelfSkillForward());
|
||||
HandleReadyActionButtonEvent(evt, GetSelfSkillForward());
|
||||
}
|
||||
|
||||
private void QueuePendingActionButtonEvent(ActionButtonEvent evt, Vector3 fallbackForward)
|
||||
{
|
||||
if (hasPendingActionButtonEvent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
hasPendingActionButtonEvent = true;
|
||||
pendingActionButtonEvent = evt;
|
||||
pendingActionButtonForward = fallbackForward;
|
||||
}
|
||||
|
||||
private void ConsumePendingActionButtonEvent()
|
||||
{
|
||||
if (!hasPendingActionButtonEvent || !actorBehaviorCatalogLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActionButtonEvent evt = pendingActionButtonEvent;
|
||||
Vector3 fallbackForward = pendingActionButtonForward;
|
||||
hasPendingActionButtonEvent = false;
|
||||
pendingActionButtonEvent = default;
|
||||
pendingActionButtonForward = Vector3.zero;
|
||||
HandleReadyActionButtonEvent(evt, fallbackForward);
|
||||
}
|
||||
|
||||
private void HandleReadyActionButtonEvent(ActionButtonEvent evt, Vector3 fallbackForward)
|
||||
{
|
||||
BehaviorStartResult result = actorBehaviorRuntime.HandleActionButton(evt, selfPlayerId, fallbackForward);
|
||||
if (result.Success)
|
||||
{
|
||||
DrainActorBehaviorEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.Error != "button ignored")
|
||||
if (result.Error != "button ignored" && result.Error != "actor already has active behavior")
|
||||
{
|
||||
Debug.LogWarning("[LobbyWorldController] start behavior failed: " + result.Error);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ namespace XGame.VirtualInput
|
||||
private float pointerDownTime;
|
||||
private bool holdStarted;
|
||||
private float nextHoldTickTime;
|
||||
private Vector2 pointerDownScreenPosition;
|
||||
private Vector2 lastScreenPosition;
|
||||
private ActionButtonEvent lastDragEvent;
|
||||
|
||||
@@ -278,6 +279,7 @@ namespace XGame.VirtualInput
|
||||
pointerDownTime = now;
|
||||
nextHoldTickTime = now + holdThresholdSeconds + holdTickIntervalSeconds;
|
||||
holdStarted = false;
|
||||
pointerDownScreenPosition = screenPosition;
|
||||
lastScreenPosition = screenPosition;
|
||||
lastDragEvent = MakeEvent(ActionButtonPhase.Down, screenPosition, pointerId, now);
|
||||
SetAimVisible(false);
|
||||
@@ -344,9 +346,9 @@ namespace XGame.VirtualInput
|
||||
return ActionButtonPhase.Cancel;
|
||||
}
|
||||
|
||||
return MakeEvent(ActionButtonPhase.Release, screenPosition, activePointerId, Time.unscaledTime).Distance01 >= releaseDistanceThreshold01
|
||||
? ActionButtonPhase.Release
|
||||
: ActionButtonPhase.Tap;
|
||||
return IsInsideButton(screenPosition) && !HasMeaningfulDrag(screenPosition)
|
||||
? ActionButtonPhase.Tap
|
||||
: ActionButtonPhase.Release;
|
||||
}
|
||||
|
||||
private ActionButtonEvent MakeEvent(ActionButtonPhase phase, Vector2 screenPosition, int pointerId, float now)
|
||||
@@ -378,6 +380,18 @@ namespace XGame.VirtualInput
|
||||
return RectTransformUtility.RectangleContainsScreenPoint(cancelArea, screenPosition, GetUICamera(cancelArea));
|
||||
}
|
||||
|
||||
private bool IsInsideButton(Vector2 screenPosition)
|
||||
{
|
||||
RectTransform rect = rectTransform != null ? rectTransform : transform as RectTransform;
|
||||
return rect != null && RectTransformUtility.RectangleContainsScreenPoint(rect, screenPosition, GetUICamera(rect));
|
||||
}
|
||||
|
||||
private bool HasMeaningfulDrag(Vector2 screenPosition)
|
||||
{
|
||||
float threshold = GetScreenRadius() * maxDragRadiusMultiplier * releaseDistanceThreshold01;
|
||||
return (screenPosition - pointerDownScreenPosition).magnitude >= Mathf.Max(1f, threshold);
|
||||
}
|
||||
|
||||
private void RefreshVisuals()
|
||||
{
|
||||
bool interactable = IsInteractable;
|
||||
@@ -424,6 +438,7 @@ namespace XGame.VirtualInput
|
||||
pointerDown = false;
|
||||
activePointerId = int.MinValue;
|
||||
holdStarted = false;
|
||||
pointerDownScreenPosition = Vector2.zero;
|
||||
lastScreenPosition = Vector2.zero;
|
||||
lastDragEvent = default;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user