diff --git a/Assets/02_Scripts/Combat/ActionData.cs b/Assets/02_Scripts/Combat/ActionData.cs index b382dc2..d779c76 100644 --- a/Assets/02_Scripts/Combat/ActionData.cs +++ b/Assets/02_Scripts/Combat/ActionData.cs @@ -35,6 +35,12 @@ public class ActionData : ScriptableObject public float Cooldown = 0.3f; // 이 액션 발동 후 다음 공격 입력 받기까지 시간 public float ComboWindow = 0.25f; // 콤보 transition 받을 수 있는 시간 (ComboNode에서도 별도 설정 가능) + // ─── 무기 조건 (콤보 트랜지션 무기별 분기 필터) ─────────────────────── + // 같은 입력의 트랜지션을 무기별로 여러 개 깔아두면, 현재 장착 무기가 + // 이 조건을 만족하는 액션만 실행된다 (Any면 무기와 무관하게 항상 가능). + [Header("Weapon")] + public WeaponRequirement RequiredWeapon = WeaponRequirement.Any; + // ─── 이동(모션) 파라미터 (HasMotion=true일 때 적용) ────────────────── [Header("Motion")] public bool HasMotion; // 이 액션이 위치 이동을 동반하는지 diff --git a/Assets/02_Scripts/Combat/WeaponData.cs b/Assets/02_Scripts/Combat/WeaponData.cs index 2158573..fa8c066 100644 --- a/Assets/02_Scripts/Combat/WeaponData.cs +++ b/Assets/02_Scripts/Combat/WeaponData.cs @@ -11,6 +11,21 @@ public enum WeaponType Gun } +// ============================================================================ +// WeaponRequirement +// ---------------------------------------------------------------------------- +// 액션(ActionData)을 실행하기 위해 필요한 장착 무기 조건. +// 콤보 트랜지션을 무기별로 여러 개 깔아두고, 현재 장착 무기에 맞는 것만 +// 실행되도록 필터링하는 데 사용 (예: 구르기 후 검=베기 / 총=사격 / 맨손=펀치). +// ============================================================================ +public enum WeaponRequirement +{ + Any, // 무기 무관 (제약 없음) — 기존 액션의 기본값 + Unarmed, // 맨손일 때만 실행 가능 + Sword, // 검 장착 시만 실행 가능 + Gun // 총 장착 시만 실행 가능 +} + // ============================================================================ // WeaponData // ---------------------------------------------------------------------------- diff --git a/Assets/02_Scripts/Player/PlayerController.cs b/Assets/02_Scripts/Player/PlayerController.cs index 7f63518..8f2d244 100644 --- a/Assets/02_Scripts/Player/PlayerController.cs +++ b/Assets/02_Scripts/Player/PlayerController.cs @@ -21,6 +21,7 @@ public class PlayerController : MonoBehaviour,IDamageable [SerializeField] private float _moveSpeed = 5f; // 이동 속도 (units/sec) [SerializeField] private string _walkAnimationState = "Run"; // 걷기/달리기 애니메이션 State 이름 private float _moveInputX = 0f; // 현재 X 입력값 (-1/0/1) + private float _moveInputY = 0f; // 현재 Y 입력값 (-1/0/1) — 위/아래 방향 공격 판정용 private string _activeBaseState; // 현재 재생 중인 locomotion State (중복 Play 방지용) private bool _isInActionAnimation; // 액션 애니메이션 재생 중인지 (locomotion 잠시 양보) @@ -101,6 +102,8 @@ public class PlayerController : MonoBehaviour,IDamageable private CancellationTokenSource _actionVelocityCts; // 액션 속도 커브 토큰 private bool _isAttackActive; // 공격 진행 중 (이동/페이싱 보조 잠금용) private bool _isMotionActive; // 모션 진행 중 + private bool _actionAllowsMovement; // 현재 액션이 진행 중 좌우 이동을 허용하는지 (CanMoveDuringAction) + private bool _actionAllowsTurn; // 현재 액션이 진행 중 페이싱 변경을 허용하는지 (CanTurnDuringAction) private float _actionDirection = 1f; // 액션 시작 시 캐릭터 페이싱 (속도 X 부호) private ActionData _lastAttackGizmoData; // OnGUI 디버그 패널용 마지막 액션 참조 [SerializeField] private float _hitGizmoFadeDuration = 0.5f; // hit 영역 Gizmo 페이드 시간 @@ -247,11 +250,11 @@ private void FixedUpdate() ExecuteBufferedInputIfReady(); // 쿨다운 끝나면 저장된 입력 실행 TickComboWindow(); // 콤보 윈도우 카운트다운 - // 입력 잠금 + 액션 중이 아닐 때만 좌우 입력으로 velocity 갱신. - if (!IsMovementLocked() && !IsActionActive()) + // 입력 잠금이 없고, (액션 중이 아니거나 액션이 이동을 허용할 때) 좌우 입력으로 velocity 갱신. + if (!IsMovementLocked() && (!IsActionActive() || _actionAllowsMovement)) _rb.linearVelocity = new Vector2(_moveInputX * _moveSpeed, _rb.linearVelocity.y); - if (!IsFacingLocked() && !IsActionActive()) + if (!IsFacingLocked() && (!IsActionActive() || _actionAllowsTurn)) UpdateFacingFromMoveInput(); //중력적용 @@ -290,7 +293,8 @@ private void TickComboWindow() private void OnMoveInput(Vector2 value) { _moveInputX = value.x == 0f ? 0f : Mathf.Sign(value.x); - if (_facingLockTimer <= 0f && !IsActionActive()) + _moveInputY = value.y == 0f ? 0f : Mathf.Sign(value.y); + if (_facingLockTimer <= 0f && (!IsActionActive() || _actionAllowsTurn)) UpdateFacingFromMoveInput(); } @@ -372,11 +376,15 @@ private void ExecuteComboInput(ComboInputType input) foreach (var transition in _currentNode.Transitions) { if (transition.Trigger != input) continue; - if (transition.Next == null || transition.Next.Action == null) continue; - if (!CanPerformAction(transition.Next.Action)) continue; + if (transition.Next == null) continue; + + // 방향키 입력에 맞는 변형 액션을 고른다 (없으면 노드의 기본 Action). + ActionData nextAction = ResolveNodeAction(transition.Next); + if (nextAction == null) continue; + if (!CanPerformAction(nextAction)) continue; ApplyForwardStep(transition.ForwardStep, transition.ForwardStepDuration); - PerformAttack(transition.Next.Action, transition.ForwardStep > 0f); + PerformAttack(nextAction, transition.ForwardStep > 0f); _currentNode = transition.Next; _comboWindowTimer = transition.Next.ComboWindow; return; @@ -392,14 +400,49 @@ private void ExecuteComboInput(ComboInputType input) ComboInputType.Grab => _grabSmashRootNode, _ => null }; - if (root == null || root.Action == null) return; - if (!CanPerformAction(root.Action)) return; + if (root == null) return; - PerformAttack(root.Action); + ActionData rootAction = ResolveNodeAction(root); + if (rootAction == null) return; + if (!CanPerformAction(rootAction)) return; + + PerformAttack(rootAction); _currentNode = root; _comboWindowTimer = root.ComboWindow; } + // 현재 방향키 입력을 공격 방향(AttackDirection)으로 변환. + // 대각선 입력은 위/아래를 우선한다 (점프 견제·다운 어택을 우선). + // 좌우는 페이싱 기준으로 Forward(앞)/Back(뒤)로 구분. + private AttackDirection GetAttackDirection() + { + if (_moveInputY > 0f) return AttackDirection.Up; + if (_moveInputY < 0f) return AttackDirection.Down; + if (_moveInputX == 0f) return AttackDirection.Neutral; + + float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f; + return Mathf.Sign(_moveInputX) == facing ? AttackDirection.Forward : AttackDirection.Back; + } + + // 노드의 방향별 변형 중 현재 입력 방향과 일치하는 액션을 선택. + // 일치하는 변형이 없거나 방향 입력이 없으면 노드의 기본 Action을 사용. + private ActionData ResolveNodeAction(ComboNode node) + { + if (node == null) return null; + + AttackDirection direction = GetAttackDirection(); + if (direction != AttackDirection.Neutral && node.DirectionalVariants != null) + { + foreach (var variant in node.DirectionalVariants) + { + if (variant != null && variant.Direction == direction && variant.Action != null) + return variant.Action; + } + } + + return node.Action; + } + // 무장 시 무기 데이터의 AttackRootNode 사용 (비어있으면 기본 _punchRootNode 폴백). private ComboNode GetPunchRootNode() { @@ -412,11 +455,27 @@ private ComboNode GetPunchRootNode() private bool CanPerformAction(ActionData data) { if (data == null) return false; + // 무기 조건 미충족이면 실행 불가 (콤보 트랜지션 무기별 분기 필터링). + if (!IsWeaponRequirementMet(data)) return false; // 잡기 액션은 사정 거리 안에 적이 있어야만 실행. 없으면 입력 자체를 캔슬. if (data.IsGrab && FindGrabTarget(data) == null) return false; return true; } + // 현재 장착 무기가 액션의 RequiredWeapon 조건을 만족하는지. + // 같은 입력의 콤보 트랜지션을 무기별로 깔아두면 이 검사로 맞는 것만 통과한다. + private bool IsWeaponRequirementMet(ActionData data) + { + WeaponData equipped = _weaponInventory != null ? _weaponInventory.CurrentWeapon : null; + return data.RequiredWeapon switch + { + WeaponRequirement.Unarmed => equipped == null, + WeaponRequirement.Sword => equipped != null && equipped.Type == WeaponType.Sword, + WeaponRequirement.Gun => equipped != null && equipped.Type == WeaponType.Gun, + _ => true, // Any + }; + } + private void ExecuteMotionNode(ComboNode root) { if (root == null || root.Action == null) return; @@ -466,6 +525,8 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit _attackCooldownTimer = data.Cooldown; _isAttackActive = true; + _actionAllowsMovement = data.CanMoveDuringAction; + _actionAllowsTurn = data.CanTurnDuringAction; _lastAttackGizmoData = data; _attackStartTime = Time.time; _hitFired = false; @@ -476,7 +537,7 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit PlayActionVelocity(data); - LockMovementIfNeeded(data, preserveHorizontalVelocity, true); + LockMovementIfNeeded(data, preserveHorizontalVelocity); LockFacingIfNeeded(data); try @@ -492,7 +553,11 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit } if (_attackCts == currentAttackCts) + { _isAttackActive = false; + _actionAllowsMovement = false; + _actionAllowsTurn = false; + } } private async void PerformMotion(ActionData data) @@ -509,10 +574,12 @@ private async void PerformMotion(ActionData data) ClearActionLocks(); CaptureActionDirection(data); _isMotionActive = true; + _actionAllowsMovement = data.CanMoveDuringAction; + _actionAllowsTurn = data.CanTurnDuringAction; FaceMotionDirection(data); PlayActionAnimation(data); PlayActionVelocity(data); - LockMovementIfNeeded(data, false, true); + LockMovementIfNeeded(data, false); LockFacingIfNeeded(data); bool completed = false; @@ -524,12 +591,16 @@ private async void PerformMotion(ActionData data) catch (System.OperationCanceledException) { _isMotionActive = false; + _actionAllowsMovement = false; + _actionAllowsTurn = false; } if (completed) { StopActionVelocity(data); _isMotionActive = false; + _actionAllowsMovement = false; + _actionAllowsTurn = false; ClearActionLocks(); PlayIdleAnimation(); } @@ -646,6 +717,8 @@ private async void PerformGroundPound() private void CancelAttack() { _isAttackActive = false; + _actionAllowsMovement = false; + _actionAllowsTurn = false; CancelAndDispose(ref _attackCts); CancelAndDispose(ref _actionVelocityCts); _attackCooldownTimer = 0f; @@ -657,6 +730,8 @@ private void CancelAttack() private void CancelMotion() { _isMotionActive = false; + _actionAllowsMovement = false; + _actionAllowsTurn = false; CancelAndDispose(ref _motionCts); CancelAndDispose(ref _actionVelocityCts); } @@ -672,7 +747,11 @@ private bool CanTransitionFromCurrentNode(ComboInputType input) foreach (var transition in _currentNode.Transitions) { - if (transition.Trigger == input && transition.Next != null && transition.Next.Action != null) + if (transition.Trigger != input || transition.Next == null) continue; + + // 방향/무기 조건까지 반영해서 실제로 실행 가능한 트랜지션이 있는지 확인. + ActionData nextAction = ResolveNodeAction(transition.Next); + if (nextAction != null && CanPerformAction(nextAction)) return true; } @@ -898,25 +977,24 @@ private void StopActionVelocity(ActionData data) _rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y); } - private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVelocity = false, bool forceLock = false) + private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVelocity = false) { - if (!forceLock && data.CanMoveDuringAction) return; + // 액션 중 좌우 이동을 허용하는 액션(걸으면서 공격 등)은 입력 잠금을 걸지 않는다. + // 실제 이동 허용은 FixedUpdate의 _actionAllowsMovement 게이트가 담당. + if (data.CanMoveDuringAction) + { + _inputLockTimer = 0f; + _movementLockAction = null; + return; + } // 이동 없는 공격은 직전 프레임의 걷기 속도를 이어받지 않게 한다. if (!preserveHorizontalVelocity && data.Velocity == Vector2.zero) _rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y); - if (forceLock) - { - // 액션 길이 전체를 보장: 애니메이션 길이와 무관하게 MotionDuration만큼 잠금. - _inputLockTimer = Mathf.Max(data.MotionDuration, 0.02f); - _movementLockAction = null; - } - else - { - _inputLockTimer = GetActionLockDuration(data); - _movementLockAction = ShouldUseAnimationLength(data) ? data : null; - } + // 액션 길이 전체를 보장: 애니메이션 길이와 무관하게 MotionDuration만큼 잠금. + _inputLockTimer = Mathf.Max(data.MotionDuration, 0.02f); + _movementLockAction = null; } private void LockFacingIfNeeded(ActionData data) diff --git a/Assets/03_Character/WhiteMan/Animations/ComboAttackA.anim b/Assets/03_Character/WhiteMan/Animations/ComboAttackA.anim index db221a3..4ea85d1 100644 --- a/Assets/03_Character/WhiteMan/Animations/ComboAttackA.anim +++ b/Assets/03_Character/WhiteMan/Animations/ComboAttackA.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d4008c2856b8aa269613a258919b858935361d4fe4d023c413dc97e4a8837bb +oid sha256:b1b885062d25f344d87967f2edef238c92f087b85213f740d8c6e533b3fac462 size 2673 diff --git a/Assets/03_Character/WhiteMan/Animations/ComboAttackB.anim b/Assets/03_Character/WhiteMan/Animations/ComboAttackB.anim index 8c7b776..24d128a 100644 --- a/Assets/03_Character/WhiteMan/Animations/ComboAttackB.anim +++ b/Assets/03_Character/WhiteMan/Animations/ComboAttackB.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9375a38559dfccac18f54df288c123dedcc186ddbff82491dc805791608f836b +oid sha256:da414a6bb7ec53633cfef1e2cd91ceb410d9761ecbf0fdde24e0b691a71659d8 size 2501 diff --git a/Assets/03_Character/WhiteMan/Animations/ComboAttackC.anim b/Assets/03_Character/WhiteMan/Animations/ComboAttackC.anim index 338ce2c..80304b8 100644 --- a/Assets/03_Character/WhiteMan/Animations/ComboAttackC.anim +++ b/Assets/03_Character/WhiteMan/Animations/ComboAttackC.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:641d7ac3d3dd2156c721cce23383f7d66ae6d6fd71bb7017b86d608fa1630da9 +oid sha256:dda1c5a110dc2f8c55fceb62be3dca55c1aad691e565cdd5da466d77543d33a4 size 2501 diff --git a/Assets/03_Character/WhiteMan/Animations/ComboAttackD.anim b/Assets/03_Character/WhiteMan/Animations/ComboAttackD.anim index 65f3174..ffa859a 100644 --- a/Assets/03_Character/WhiteMan/Animations/ComboAttackD.anim +++ b/Assets/03_Character/WhiteMan/Animations/ComboAttackD.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a25882480d9d9d43a77fb6d3239790b5b416104872608feadf934263359a4ddb +oid sha256:e3401aec6c53dca10d351614545733b3d8de602a211df427814b4f6f5e23f465 size 3027 diff --git a/Assets/03_Character/WhiteMan/Animations/CrouchSlash.anim b/Assets/03_Character/WhiteMan/Animations/CrouchSlash.anim index 271d9ac..1041b2e 100644 --- a/Assets/03_Character/WhiteMan/Animations/CrouchSlash.anim +++ b/Assets/03_Character/WhiteMan/Animations/CrouchSlash.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22d18eb5e4786998b52d3c505e4b2e1e59aeadc99885396497b1e77a6e0be86d +oid sha256:153f2475b8af732a5a39e290d98882abbc81eed1369df37c5823e43ce6f14b38 size 2684 diff --git a/Assets/03_Character/WhiteMan/Animations/SwordCrouch.anim b/Assets/03_Character/WhiteMan/Animations/SwordCrouch.anim index 0a90435..cef5315 100644 --- a/Assets/03_Character/WhiteMan/Animations/SwordCrouch.anim +++ b/Assets/03_Character/WhiteMan/Animations/SwordCrouch.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f0c740ecbe0b706f92700052a793b6d0146ce206ba97b71b2cf2668b63cd645 +oid sha256:2479402529b784096656bf7b4928d3de196a2e93eaf84335340b047f32de3bd9 size 2642 diff --git a/Assets/05_Data/Attack/SwordAttack.asset b/Assets/05_Data/Attack/SwordAttack.asset index ccb7d74..a131b0c 100644 --- a/Assets/05_Data/Attack/SwordAttack.asset +++ b/Assets/05_Data/Attack/SwordAttack.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba2b26e371f27c9648d56089a4e90e73af2853abb74367fb877137bae910bd67 -size 2983 +oid sha256:ad4183c96c9190b1db5efd13187d6de52e74eae84c6e1e723f7efa2eb1e197dc +size 3181 diff --git a/Assets/05_Data/Attack/SwordAttackB.asset b/Assets/05_Data/Attack/SwordAttackB.asset new file mode 100644 index 0000000..673d342 --- /dev/null +++ b/Assets/05_Data/Attack/SwordAttackB.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60283e7870a6f2b1a15a3fcde6181f858a5e9409420c99b983614d3cb021aa8d +size 3184 diff --git a/Assets/05_Data/Attack/SwordAttackB.asset.meta b/Assets/05_Data/Attack/SwordAttackB.asset.meta new file mode 100644 index 0000000..22d2126 --- /dev/null +++ b/Assets/05_Data/Attack/SwordAttackB.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3da4b6a12487ffd4fb46fe4f3aa3bb2d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/05_Data/Attack/SwordCrouchSlash.asset b/Assets/05_Data/Attack/SwordCrouchSlash.asset new file mode 100644 index 0000000..64f2c26 --- /dev/null +++ b/Assets/05_Data/Attack/SwordCrouchSlash.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09e586abdb4117aaf32133fd381427bd445b75e8dd26357b348974a690121e7a +size 3190 diff --git a/Assets/05_Data/Attack/SwordCrouchSlash.asset.meta b/Assets/05_Data/Attack/SwordCrouchSlash.asset.meta new file mode 100644 index 0000000..0531ce0 --- /dev/null +++ b/Assets/05_Data/Attack/SwordCrouchSlash.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f4c2f40c6adfd1f40ae9fe453c76563c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/05_Data/Attack/SwordStandingSlash.asset b/Assets/05_Data/Attack/SwordStandingSlash.asset index 5b0844e..a8e5913 100644 --- a/Assets/05_Data/Attack/SwordStandingSlash.asset +++ b/Assets/05_Data/Attack/SwordStandingSlash.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ebc178d4f00bf3444f9be6b4d01db149385174de6fc5d3fdf3be2dd34455344 -size 3004 +oid sha256:03f64f08ccb75b196c066f0a3d44f69ba4b0ff9ca413904a81793f8e6dc9b29e +size 3202 diff --git a/Assets/05_Data/Combo/Combo_Roll.asset b/Assets/05_Data/Combo/Combo_Roll.asset index 42e7b4e..c9405f8 100644 --- a/Assets/05_Data/Combo/Combo_Roll.asset +++ b/Assets/05_Data/Combo/Combo_Roll.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:281b39a4ecb4e0e17b7872f293379b87aea41aa33b9a26936f52cfaf3621fcc3 -size 564 +oid sha256:caaf5b69efb2a96cd42d78251d67241a1056c6035267236e83a8e201508c5f50 +size 730 diff --git a/Assets/05_Data/Combo/Combo_SwordAttackB.asset b/Assets/05_Data/Combo/Combo_SwordAttackB.asset new file mode 100644 index 0000000..b68cb48 --- /dev/null +++ b/Assets/05_Data/Combo/Combo_SwordAttackB.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ac84d90db692e039b63bfcf538e564590c888214956e531ba79e9e94297a0ed +size 605 diff --git a/Assets/05_Data/Combo/Combo_SwordAttackB.asset.meta b/Assets/05_Data/Combo/Combo_SwordAttackB.asset.meta new file mode 100644 index 0000000..4978859 --- /dev/null +++ b/Assets/05_Data/Combo/Combo_SwordAttackB.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 65bb875904204db4caf0fafd932a566d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/05_Data/Combo/Combo_SwordCrouchAttack.asset b/Assets/05_Data/Combo/Combo_SwordCrouchAttack.asset new file mode 100644 index 0000000..e02e624 --- /dev/null +++ b/Assets/05_Data/Combo/Combo_SwordCrouchAttack.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c24c8976d460db731cc24892df38938a578d336e80c5e0317f5ac40d0525b18 +size 753 diff --git a/Assets/05_Data/Combo/Combo_SwordCrouchAttack.asset.meta b/Assets/05_Data/Combo/Combo_SwordCrouchAttack.asset.meta new file mode 100644 index 0000000..59632dc --- /dev/null +++ b/Assets/05_Data/Combo/Combo_SwordCrouchAttack.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c9859d998763ed7478c6c9e2c6b5b82a +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: