修复技能输入并更新战斗资源

This commit is contained in:
ud18010
2026-07-30 20:10:38 +08:00
parent d3c4bba5b2
commit 576aa6c14e
9077 changed files with 1322721 additions and 2648 deletions
@@ -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;
}