diff --git a/src/Game.Client/Assets/Programs/Editor/Tests/MVP/SurvivorWaveManagerTests.cs b/src/Game.Client/Assets/Programs/Editor/Tests/MVP/SurvivorWaveManagerTests.cs index 1da3291c..ff5fb4a5 100644 --- a/src/Game.Client/Assets/Programs/Editor/Tests/MVP/SurvivorWaveManagerTests.cs +++ b/src/Game.Client/Assets/Programs/Editor/Tests/MVP/SurvivorWaveManagerTests.cs @@ -3,7 +3,6 @@ using System.Reflection; using Game.MVP.Survivor.Services; using Game.Shared.Signals.Survivor; -using MessagePipe; using NUnit.Framework; using R3; @@ -13,36 +12,28 @@ namespace Game.Tests.MVP public class SurvivorWaveManagerTests { private SurvivorStageWaveManager _manager; - private TestPublisher _waveCompletedPublisher; + private List _waveCompletedEvents; private List _killCountedEvents; private IDisposable _killCountedSubscription; - - /// テスト用 IPublisher 実装 - private class TestPublisher : IPublisher - { - public List Published { get; } = new(); - public void Publish(T message) { Published.Add(message); } - } + private IDisposable _waveCompletedSubscription; [SetUp] public void Setup() { _manager = new SurvivorStageWaveManager(); _killCountedEvents = new List(); + _waveCompletedEvents = new List(); - // Wave.Completed のテスト用 Publisher を注入 - _waveCompletedPublisher = new TestPublisher(); - var pubField = typeof(SurvivorStageWaveManager).GetField("_waveCompletedPublisher", BindingFlags.NonPublic | BindingFlags.Instance); - pubField?.SetValue(_manager, _waveCompletedPublisher); - - // イベント購読 + // ローカル Observable を購読 _killCountedSubscription = _manager.OnKillCounted.Subscribe(unit => _killCountedEvents.Add(unit)); + _waveCompletedSubscription = _manager.OnWaveCompleted.Subscribe(e => _waveCompletedEvents.Add(e)); } [TearDown] public void TearDown() { _killCountedSubscription?.Dispose(); + _waveCompletedSubscription?.Dispose(); _manager?.Dispose(); } @@ -199,7 +190,7 @@ public void OnEnemyKilled_WhenTargetAndBossReached_TriggersWaveClear() _manager.OnEnemyKilled(isBoss: true); // Assert - Assert.That(_waveCompletedPublisher.Published.Count, Is.EqualTo(1)); + Assert.That(_waveCompletedEvents.Count, Is.EqualTo(1)); } [Test] @@ -212,7 +203,7 @@ public void OnEnemyKilled_WhenTargetReachedButBossNotReached_DoesNotTriggerWaveC _manager.OnEnemyKilled(isBoss: false); // Regular kill reaches target // Assert - Wave not cleared because boss requirement not met - Assert.That(_waveCompletedPublisher.Published.Count, Is.EqualTo(0)); + Assert.That(_waveCompletedEvents.Count, Is.EqualTo(0)); } [Test] @@ -225,7 +216,7 @@ public void OnEnemyKilled_WhenBossReachedButTargetNotReached_DoesNotTriggerWaveC _manager.OnEnemyKilled(isBoss: true); // Boss kill but target not reached // Assert - Assert.That(_waveCompletedPublisher.Published.Count, Is.EqualTo(0)); + Assert.That(_waveCompletedEvents.Count, Is.EqualTo(0)); } [Test] @@ -239,7 +230,7 @@ public void OnEnemyKilled_WithZeroBossRequirement_ClearsOnTargetReached() _manager.OnEnemyKilled(isBoss: false); // Assert - Assert.That(_waveCompletedPublisher.Published.Count, Is.EqualTo(1)); + Assert.That(_waveCompletedEvents.Count, Is.EqualTo(1)); } #endregion diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Enemy/SurvivorEnemyController.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Enemy/SurvivorEnemyController.cs index 81b4e1b8..2ce13218 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Enemy/SurvivorEnemyController.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Enemy/SurvivorEnemyController.cs @@ -43,6 +43,7 @@ public partial class SurvivorEnemyController : MonoBehaviour, ICombatTarget, IDe private int _currentHp; private Transform _target; private bool _isDead; + private int _networkId = -1; // Events private readonly Subject _onDeath = new(); @@ -63,6 +64,9 @@ public partial class SurvivorEnemyController : MonoBehaviour, ICombatTarget, IDe public int ExperienceValue => _experienceValue; public bool IsDead => _isDead; + /// ネットワーク同期用ID(SurvivorEnemySpawnerが設定) + public int NetworkId => _networkId; + /// 現在HP(ネットワーク同期用) public int CurrentHp => _currentHp; @@ -177,6 +181,8 @@ public void TakeDamage(int damage) } } + public void SetNetworkId(int id) => _networkId = id; + public void ApplyKnockback(Vector3 knockback) { if (_isDead || _navAgent == null || !_navAgent.enabled) return; @@ -192,6 +198,7 @@ public void ResetForPool() { _isDead = false; _currentHp = _maxHp; + _networkId = -1; _hasPendingDamage = false; _pendingDamageAmount = 0; _target = null; diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Enemy/SurvivorEnemySpawner.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Enemy/SurvivorEnemySpawner.cs index c0c054b6..0c8a0020 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Enemy/SurvivorEnemySpawner.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Enemy/SurvivorEnemySpawner.cs @@ -72,8 +72,6 @@ public class SurvivorEnemySpawner : MonoBehaviour private float _spawnTimer; private int _remainingSpawnCount; - private bool _isClient; - // ネットワーク敵同期 private ISurvivorNetworkBridge _networkBridge; private const float EnemySyncInterval = 0.1f; // 10Hz @@ -136,7 +134,6 @@ public void SetNetworkBridge(ISurvivorNetworkBridge bridge) public async UniTask InitializeAsync(SurvivorStageWaveManager waveManager) { _waveManager = waveManager; - _isClient = NetworkModeHelper.IsNetworkClient; // レイヤーマスクが未設定の場合、Structureレイヤーを使用 if (_obstacleLayerMask == 0) @@ -175,7 +172,7 @@ public async UniTask InitializeAsync(SurvivorStageWaveManager waveManager) } // MP Client: 敵はサーバーバッチ同期で表示、ローカルスポーン不要 - if (!_isClient) + if (!NetworkModeHelper.IsNetworkClient) { _waveManager.CurrentWave .Where(wave => wave > 0) @@ -240,7 +237,7 @@ private void Update() } // MP Client: ローカルスポーン無効 - if (_isClient) return; + if (NetworkModeHelper.IsNetworkClient) return; if (!_isSpawning) { @@ -375,6 +372,7 @@ private void SpawnNextEnemy() var networkId = _nextNetworkId++; _enemyNetworkIds[enemy] = networkId; _enemyByNetworkId[networkId] = enemy; + enemy.SetNetworkId(networkId); _activeEnemies.Add(enemy); _remainingSpawnCount--; _spawnTimer = spawnInfo.SpawnInterval; @@ -579,7 +577,7 @@ private void OnEnemyDeath(SurvivorEnemyController enemy) .AddTo(this); // ウェーブサービスに通知(ボスかどうかも伝える) - if (!_isClient) + if (!NetworkModeHelper.IsNetworkClient) { _waveManager.OnEnemyKilled(enemy.IsBoss); } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItem.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItem.cs index d1fda07d..0f369935 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItem.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItem.cs @@ -1,7 +1,5 @@ using System; using Game.Shared.Item; -using Game.Shared.Network; -using Game.Shared.Playmode; using UnityEngine; namespace Game.MVP.Survivor.Item @@ -98,7 +96,6 @@ private void ApplyTransform() private void Start() { - if (UnityPlaymodeHelper.IsServer()) return; _initialPosition = transform.position; _baseFloatAmplitude = _floatAmplitude * _scale; } @@ -122,9 +119,6 @@ private void Update() private void UpdateFloatAnimation() { - if (UnityPlaymodeHelper.IsServer()) - return; - _floatTimer += Time.deltaTime * _floatSpeed; float yOffset = Mathf.Sin(_floatTimer) * _baseFloatAmplitude; transform.position = _initialPosition + Vector3.up * yOffset; diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItemSpawner.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItemSpawner.cs index 860e3e5a..8a960ab9 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItemSpawner.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItemSpawner.cs @@ -206,7 +206,7 @@ public void SpawnItem(int itemId, Vector3 position) _activeItems[itemId].Add(item); // サーバー: クライアントにアイテムスポーンを通知 - _networkBridge?.NotifyItemSpawned(itemId, position.x, position.z); + _networkBridge?.NotifyItemSpawned(itemId, position.x, position.y, position.z); } } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItemView.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItemView.cs index fcfb5127..5c7fdf5d 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItemView.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Item/SurvivorItemView.cs @@ -1,5 +1,10 @@ using System; using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using Game.Shared.Constants; +using Game.Shared.Extensions; +using Game.Shared.Item; +using Game.Shared.Services; using Game.Shared.Signals.Survivor; using MessagePipe; using UnityEngine; @@ -7,58 +12,230 @@ namespace Game.MVP.Survivor.Item { /// - /// クライアントモード時、ClientRpc からプロキシアイテムオブジェクトを管理。 - /// Phase 5 は Sphere プロキシ。Phase 7 で正式モデルに置換。 + /// クライアントモード時、ClientRpc からアイテムオブジェクトを管理。 + /// サーバーからの ItemId で Addressable プレハブをロードし、正式モデルで表示する。 + /// プロキシは ICollectible を実装し、PlayerController の既存吸引ロジックで動作する。 /// public class SurvivorItemView : MonoBehaviour { - private readonly Dictionary _proxies = new(); + private const float FloatAmplitude = 0.2f; + private const float FloatSpeed = 2f; + + private readonly Dictionary _proxies = new(); + private readonly Dictionary _prefabs = new(); + private readonly Dictionary _scales = new(); private IDisposable _spawnSub; private IDisposable _despawnSub; + private IAddressableAssetService _assetService; + + private class ItemProxyData + { + public GameObject GameObject; + public ItemProxyCollectible Collectible; + public Vector3 InitialPosition; + public float FloatTimer; + public float Scale; + } - public void Initialize( + public async UniTask InitializeAsync( ISubscriber spawnSub, - ISubscriber despawnSub) + ISubscriber despawnSub, + IMasterDataService masterDataService, + IAddressableAssetService assetService) { - _spawnSub = spawnSub.Subscribe(s => OnSpawned(s.ItemId, s.PosX, s.PosZ)); + _assetService = assetService; + + // 全アイテムプレハブをプリロード + var allItems = masterDataService.MemoryDatabase.SurvivorItemMasterTable.All; + foreach (var item in allItems) + { + if (!_prefabs.ContainsKey(item.Id)) + { + try + { + var prefab = await assetService.LoadAssetAsync(item.AssetName); + _prefabs[item.Id] = prefab; + _scales[item.Id] = item.Scale.ToScale(); + } + catch + { + Debug.LogWarning($"[SurvivorItemView] Failed to load prefab: {item.AssetName}"); + } + } + } + + _spawnSub = spawnSub.Subscribe(s => OnSpawned(s.ItemId, s.PosX, s.PosY, s.PosZ)); _despawnSub = despawnSub.Subscribe(s => OnDespawned(s.ItemId)); + Debug.Log($"[SurvivorItemView] Initialized: prefabs={_prefabs.Count}"); } - private void OnSpawned(int itemId, float posX, float posZ) + private void OnSpawned(int itemId, float posX, float posY, float posZ) { + var position = new Vector3(posX, posY, posZ); + if (_proxies.TryGetValue(itemId, out var existing)) { - existing.transform.position = new Vector3(posX, 0.5f, posZ); + existing.GameObject.transform.position = position; + existing.InitialPosition = position; + existing.Collectible.Reset(); return; } - var proxy = GameObject.CreatePrimitive(PrimitiveType.Sphere); - proxy.name = $"ItemProxy_{itemId}"; - proxy.transform.position = new Vector3(posX, 0.5f, posZ); - proxy.transform.localScale = Vector3.one * 0.5f; - proxy.transform.SetParent(transform); - var col = proxy.GetComponent(); - if (col != null) Destroy(col); - _proxies[itemId] = proxy; + + float scale = 1f; + GameObject instance; + + if (_prefabs.TryGetValue(itemId, out var prefab) && prefab != null) + { + instance = Instantiate(prefab, transform); + + // サーバー専用 SurvivorItem を除去(ICollectible プロキシで置換する) + var itemComponent = instance.GetComponent(); + if (itemComponent != null) Destroy(itemComponent); + + // Collider をトリガーに変更(PlayerController の OverlapSphere/OnTriggerEnter で検出) + foreach (var col in instance.GetComponentsInChildren()) + { + col.isTrigger = true; + } + + if (_scales.TryGetValue(itemId, out var s)) + scale = s; + } + else + { + // フォールバック: プレハブ未ロード時 + instance = GameObject.CreatePrimitive(PrimitiveType.Sphere); + var col = instance.GetComponent(); + if (col != null) col.isTrigger = true; + scale = 0.5f; + Debug.LogWarning($"[SurvivorItemView] Prefab not found for item {itemId}, using fallback"); + } + + instance.name = $"ItemProxy_{itemId}"; + instance.transform.position = position; + instance.transform.localScale = Vector3.one * scale; + instance.transform.SetParent(transform); + + // Item レイヤー設定(PlayerController の OverlapSphere 検出用) + SetLayerRecursively(instance, LayerConstants.Item); + + // ICollectible プロキシ追加(PlayerController の吸引・収集ロジックで動作) + var collectible = instance.AddComponent(); + collectible.Initialize(scale); + + _proxies[itemId] = new ItemProxyData + { + GameObject = instance, + Collectible = collectible, + InitialPosition = position, + FloatTimer = 0f, + Scale = scale + }; + } + + private void Update() + { + float dt = Time.deltaTime; + + foreach (var kvp in _proxies) + { + var data = kvp.Value; + if (data.GameObject == null || data.Collectible.IsAttracting) continue; + + // 浮遊アニメーション(SurvivorItem.UpdateFloatAnimation と同等) + data.FloatTimer += dt * FloatSpeed; + float yOffset = Mathf.Sin(data.FloatTimer) * FloatAmplitude * data.Scale; + data.GameObject.transform.position = data.InitialPosition + Vector3.up * yOffset; + } } private void OnDespawned(int itemId) { - if (_proxies.TryGetValue(itemId, out var p)) + if (_proxies.TryGetValue(itemId, out var data)) { - Destroy(p); + if (data.GameObject != null) Destroy(data.GameObject); _proxies.Remove(itemId); } } + private static void SetLayerRecursively(GameObject go, int layer) + { + go.layer = layer; + foreach (Transform child in go.transform) + { + SetLayerRecursively(child.gameObject, layer); + } + } + private void OnDestroy() { _spawnSub?.Dispose(); _despawnSub?.Dispose(); - foreach (var p in _proxies.Values) + + foreach (var data in _proxies.Values) { - if (p != null) Destroy(p); + if (data.GameObject != null) Destroy(data.GameObject); } _proxies.Clear(); + + // プレハブリリース + foreach (var prefab in _prefabs.Values) + { + _assetService?.ReleaseAsset(prefab); + } + _prefabs.Clear(); + } + } + + /// + /// クライアントプロキシ用 ICollectible 実装。 + /// PlayerController の既存吸引ロジック(OverlapSphere → StartAttraction)で動作する。 + /// Collect は no-op(実際の回収はサーバーが管理、Despawn ClientRpc で削除)。 + /// + public class ItemProxyCollectible : MonoBehaviour, ICollectible + { + private Transform _attractTarget; + private float _attractSpeed; + private float _floatAmplitude; + private Vector3 _initialPosition; + + public bool IsCollected { get; private set; } + public bool IsAttracting => _attractTarget != null; + + public void Initialize(float scale) + { + _floatAmplitude = 0.2f * scale; + } + + public void StartAttraction(Transform target, float speed) + { + if (_attractTarget != null) return; + _attractTarget = target; + _attractSpeed = speed; + _initialPosition = transform.position; + } + + public void Collect() + { + // no-op: 実際の回収はサーバーが管理 + // Despawn ClientRpc で SurvivorItemView.OnDespawned が呼ばれ削除される + IsCollected = true; + } + + public void Reset() + { + _attractTarget = null; + _attractSpeed = 0f; + IsCollected = false; + } + + private void Update() + { + if (_attractTarget == null) return; + + // SurvivorItem.Update の吸引処理と同等 + var direction = (_attractTarget.position - transform.position).normalized; + transform.position += direction * _attractSpeed * Time.deltaTime; } } } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Player/SurvivorPlayerController.States.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Player/SurvivorPlayerController.States.cs index 898e2455..38f6cc27 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Player/SurvivorPlayerController.States.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Player/SurvivorPlayerController.States.cs @@ -79,7 +79,7 @@ private bool TryProcessDamage(out bool shouldDie) _hasPendingDamage = false; _currentHp.Value = Mathf.Max(0, _currentHp.Value - _pendingDamageAmount); - _damageReceivedPublisher?.Publish( + _onDamageReceived.OnNext( new SurvivorSignals.Player.DamageReceived(_pendingDamageAmount, _currentHp.Value)); // Server / Host: ClientRpc でダメージ通知 @@ -188,7 +188,8 @@ private class DeadState : State public override void Enter() { var ctx = Context; - ctx._diedPublisher?.Publish(new SurvivorSignals.Player.Died()); + Debug.Log("[SurvivorPlayerController] Player died"); + ctx._onDied.OnNext(new SurvivorSignals.Player.Died()); // Server / Host: ClientRpc で死亡通知 + 全滅判定 if (NetworkModeHelper.IsNetworkServer) diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Player/SurvivorPlayerController.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Player/SurvivorPlayerController.cs index 84c625eb..6ca81e70 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Player/SurvivorPlayerController.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Player/SurvivorPlayerController.cs @@ -32,8 +32,11 @@ public partial class SurvivorPlayerController : MonoBehaviour, IDamageable, ISur // VContainer Injection [Inject] private IPublisher _spawnedPublisher; - [Inject] private IPublisher _damageReceivedPublisher; - [Inject] private IPublisher _diedPublisher; + + private readonly Subject _onDamageReceived = new(); + private readonly Subject _onDied = new(); + public Observable OnDamageReceived => _onDamageReceived; + public Observable OnDied => _onDied; [Header("ジョギング速度")] [SerializeField] @@ -161,6 +164,8 @@ private void OnDestroy() _currentHp.Dispose(); _currentStamina.Dispose(); _isInvincible.Dispose(); + _onDamageReceived.Dispose(); + _onDied.Dispose(); } #endregion diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/ISurvivorStageSceneView.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/ISurvivorStageSceneView.cs deleted file mode 100644 index 14e04617..00000000 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/ISurvivorStageSceneView.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Game.MVP.Survivor.Scenes.Models; - -namespace Game.MVP.Survivor.Scenes -{ - /// - /// ステージシーンのHUD表示インターフェース - /// サーバーではNullStageViewを使用してno-op化 - /// - public interface ISurvivorStageSceneView - { - void Initialize(SurvivorStageModel model, int totalWaves); - void InitializeWeaponDisplay(); - void UpdateHp(int current, int max); - void UpdateStamina(int current, int max); - void UpdateExperience(int current, int max); - void UpdateLevel(int level); - void UpdateTime(float time); - void UpdateKills(int kills); - void UpdateWave(int wave, int totalWaves); - void UpdateEnemies(int killed, int total); - void ShowWaveBanner(int wave, int totalWaves, int enemyCount); - void ShowGameOver(); - void ShowVictory(); - void SetHudVisible(bool visible, bool immediate = false); - } -} diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/ISurvivorStageSceneView.cs.meta b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/ISurvivorStageSceneView.cs.meta deleted file mode 100644 index a2eea2b5..00000000 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/ISurvivorStageSceneView.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 9ccc793757b23274c9ae546d52664c0e \ No newline at end of file diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/NullSurvivorStageSceneView.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/NullSurvivorStageSceneView.cs deleted file mode 100644 index c3448473..00000000 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/NullSurvivorStageSceneView.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Game.MVP.Survivor.Scenes.Models; - -namespace Game.MVP.Survivor.Scenes -{ - /// - /// サーバー用ステージビュー(全メソッドno-op) - /// サーバーではHUD更新が不要なため、NullObjectパターンで安全にスキップ - /// - public class NullSurvivorStageSceneView : ISurvivorStageSceneView - { - public void Initialize(SurvivorStageModel model, int totalWaves) { } - public void InitializeWeaponDisplay() { } - public void UpdateHp(int current, int max) { } - public void UpdateStamina(int current, int max) { } - public void UpdateExperience(int current, int max) { } - public void UpdateLevel(int level) { } - public void UpdateTime(float time) { } - public void UpdateKills(int kills) { } - public void UpdateWave(int wave, int totalWaves) { } - public void UpdateEnemies(int killed, int total) { } - public void ShowWaveBanner(int wave, int totalWaves, int enemyCount) { } - public void ShowGameOver() { } - public void ShowVictory() { } - public void SetHudVisible(bool visible, bool immediate = false) { } - } -} diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/NullSurvivorStageSceneView.cs.meta b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/NullSurvivorStageSceneView.cs.meta deleted file mode 100644 index 4333ecde..00000000 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/NullSurvivorStageSceneView.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 9ef918c18ac13454bb6aa15dcaa21eb4 \ No newline at end of file diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.States.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.States.cs new file mode 100644 index 00000000..361fff06 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.States.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using Game.Library.Shared; +using Game.MVP.Survivor.Weapon; +using Game.Shared.Bootstrap; +using Game.Shared.Network; +using Game.Shared.Network.Survivor; +using MessagePipe; +using Unity.Collections; +using UnityEngine; + +namespace Game.MVP.Survivor.Scenes +{ + public partial class SurvivorNetworkStageScene + { + #region StateMachine + + private enum StageEvent + { + StartGame, + LevelUp, + LevelUpComplete, + Victory, + GameOver, + } + + private StateMachine _stateMachine; + private int _pendingLevelUpCount; + + private void BuildStateMachine() + { + _stateMachine = new StateMachine(this); + + _stateMachine.AddTransition(StageEvent.StartGame); + _stateMachine.AddTransition(StageEvent.LevelUp); + _stateMachine.AddTransition(StageEvent.Victory); + _stateMachine.AddTransition(StageEvent.GameOver); + _stateMachine.AddTransition(StageEvent.LevelUpComplete); + + _stateMachine.SetInitState(); + } + + #endregion + + #region StageStateBase + + private abstract class StageStateBase : State + { + protected Services.SurvivorStageWaveManager WaveManager => Context._waveManager; + protected Models.SurvivorStageModel StageModel => Context._stageModel; + protected SurvivorStageSceneComponent View => Context.SceneComponent; + protected Weapon.SurvivorNetworkWeaponManager WeaponManager => Context._weaponManager; + + protected void Transition(StageEvent evt) => StateMachine.Transition(evt); + } + + #endregion + + #region ReadyState + + private class ReadyState : StageStateBase + { + private bool _startComplete; + + public override void Enter() + { + Debug.Log("[SurvivorNetworkStageScene.ReadyState] Enter"); + _startComplete = false; + + // プレイヤー初期化(サーバーではカメラなし) + View.InitializePlayer(StageModel.CurrentLevelMaster, null); + + InitializeAndStartAsync().Forget(); + } + + private async UniTaskVoid InitializeAndStartAsync() + { + // ゲームコンポーネントの初期化(サーバーは SurvivorNetworkWeaponManager を使用、 + // SceneComponent.WeaponManager の初期化は不要) + await View.InitializeEnemySpawnerAsync(WaveManager); + await View.InitializeItemSpawnerAsync(); + + // サーバー側プレイヤーコントローラーにNetworkPlayerStateをバインド + InitializeServerViews(); + + Debug.Log("[SurvivorNetworkStageScene.ReadyState] Initialization complete, waiting for all clients scene ready..."); + + // 全クライアントのシーン準備完了を待機 + await WaitForAllClientsSceneReadyAsync(); + + Debug.Log("[SurvivorNetworkStageScene.ReadyState] All clients ready, starting game"); + _startComplete = true; + } + + public override void Update() + { + if (_startComplete) + { + Transition(StageEvent.StartGame); + } + } + + public override void Exit() => Debug.Log("[SurvivorNetworkStageScene.ReadyState] Exit"); + + /// + /// サーバー側: NetworkPlayerStateをPlayerControllerにバインドし、 + /// EnemySpawnerにプレイヤーTransformを登録する。 + /// + private void InitializeServerViews() + { + var playerController = View.PlayerController; + if (playerController != null) + { + foreach (var nps in NetworkModeHelper.GetNetworkPlayerComponents()) + { + if (nps != null) + { + Context._localPlayerState = nps; + playerController.BindNetworkPlayerState(nps); + View.EnemySpawner?.AddPlayer(playerController.transform); + break; // 現在は1プレイヤー対応 + } + } + } + + Debug.Log("[SurvivorNetworkStageScene.ReadyState] Server: network objects bound"); + } + + /// + /// サーバー: 全クライアントが NotifySceneReadyServerRpc を送信するまで待機。 + /// タイムアウト付き(30秒)で、クライアント切断に対応。 + /// + private async UniTask WaitForAllClientsSceneReadyAsync() + { + var gm = SurvivorNetworkGameManager.Instance; + if (gm == null) + { + Debug.LogWarning("[SurvivorNetworkStageScene.ReadyState] GameManager not found, skipping wait"); + return; + } + + gm.ResetSceneReadyTracking(); + + var tcs = new UniTaskCompletionSource(); + var subscription = Context._allClientsSceneReadySub.Subscribe(_ => tcs.TrySetResult()); + + try + { + var winIndex = await UniTask.WhenAny( + tcs.Task, + UniTask.Delay(TimeSpan.FromSeconds(30), DelayType.Realtime)); + if (winIndex == 1) + { + Debug.LogWarning("[SurvivorNetworkStageScene.ReadyState] Timeout waiting for clients, proceeding"); + } + } + finally + { + subscription.Dispose(); + } + } + } + + #endregion + + #region PlayingState + + private class PlayingState : StageStateBase + { + private bool _isFirstEntry = true; + + public override void Enter() + { + Debug.Log("[SurvivorNetworkStageScene.PlayingState] Enter"); + ApplicationEvents.ResumeTime(); + + // 初回のみWave開始(LevelUpからの復帰時は不要) + if (_isFirstEntry) + { + _isFirstEntry = false; + Debug.Log("[SurvivorNetworkStageScene.PlayingState] Starting first wave"); + WaveManager.StartWave(); + } + } + + public override void Update() + { + // レベルアップ処理 + if (Context._pendingLevelUpCount > 0) + { + Context._pendingLevelUpCount--; + Transition(StageEvent.LevelUp); + return; + } + + // ゲームタイマー更新 + StageModel.GameTime.Value += Time.deltaTime; + + // 勝利条件: 時間制限到達 or 全ウェーブクリア + if (StageModel.IsTimeUp || WaveManager.IsAllWavesCleared.CurrentValue) + { + Transition(StageEvent.Victory); + return; + } + + // 敗北条件: HP0 + if (StageModel.IsDead) + { + Transition(StageEvent.GameOver); + return; + } + } + + public override void Exit() => Debug.Log("[SurvivorNetworkStageScene.PlayingState] Exit"); + } + + #endregion + + #region LevelUpState + + private class LevelUpState : StageStateBase + { + public override void Enter() + { + Debug.Log($"[SurvivorNetworkStageScene.LevelUpState] Enter - Level {StageModel.Level.Value}"); + ApplicationEvents.PauseTime(); + + // プレイヤーステータス更新 + if (View.PlayerController != null && StageModel.CurrentLevelMaster != null) + { + View.PlayerController.UpdateLevelStats(StageModel.CurrentLevelMaster); + } + + // ダメージ倍率更新 + WeaponManager.UpdateDamageMultiplier(StageModel.GetDamageMultiplier()); + + // 武器選択肢を生成してクライアントに送信 + { + var serverOptions = WeaponManager.GetUpgradeOptions(StageModel.WeaponChoiceCount.Value); + if (serverOptions.Count > 0) + { + var networkOptions = ConvertToNetworkOptions(serverOptions); + var gm = SurvivorNetworkGameManager.Instance; + if (gm != null) + { + gm.SetPendingWeaponOptions(networkOptions); + gm.NotifyPlayerLevelUpClientRpc( + Context._localPlayerState?.PlayerUserId ?? default, + StageModel.Level.Value, + StageModel.Experience.Value, + StageModel.ExperienceToNextLevel.Value, + networkOptions); + Debug.Log($"[SurvivorNetworkStageScene.LevelUpState] Sent LevelUp RPC with {networkOptions.Length} options"); + } + } + } + + // サーバーは即座に完了(クライアントがUI選択を処理する) + Transition(StageEvent.LevelUpComplete); + } + + public override void Exit() + { + // サーバーはここでResumeTimeしない。 + // PlayingState.EnterがResumeTimeし、その後クライアントのRequestPauseServerRpcが + // GameManager経由で再度PauseTimeする(クライアントの武器選択中)。 + Debug.Log("[SurvivorNetworkStageScene.LevelUpState] Exit"); + } + + private static SurvivorNetworkWeaponUpgradeOption[] ConvertToNetworkOptions( + List options) + { + var result = new SurvivorNetworkWeaponUpgradeOption[options.Count]; + for (int i = 0; i < options.Count; i++) + { + var opt = options[i]; + result[i] = new SurvivorNetworkWeaponUpgradeOption + { + WeaponId = opt.WeaponId, + WeaponName = new FixedString128Bytes(opt.WeaponName ?? ""), + IsNewWeapon = opt.IsNewWeapon, + CurrentLevel = opt.CurrentLevel, + Description = new FixedString128Bytes(opt.Description ?? ""), + UpgradeEffect = new FixedString128Bytes(opt.UpgradeEffect ?? ""), + IconAssetName = new FixedString128Bytes(opt.IconAssetName ?? "") + }; + } + return result; + } + } + + #endregion + + #region VictoryState + + private class VictoryState : StageStateBase + { + public override void Enter() + { + Debug.Log("[SurvivorNetworkStageScene.VictoryState] Enter"); + ApplicationEvents.PauseTime(); + + // 残存敵を全クリア&スポーン停止 + View.EnemySpawner?.ClearAllEnemies(); + + SaveAndNotifyAsync().Forget(); + } + + private async UniTaskVoid SaveAndNotifyAsync() + { + var score = StageModel.Score.Value; + var kills = Context.GetCappedKills(); + var clearTime = StageModel.GameTime.Value; + var isTimeUp = StageModel.IsTimeUp; + var hpRatio = Context.GetHpRatio(); + + Debug.Log($"[SurvivorNetworkStageScene.VictoryState] Saving: score={score}, kills={kills}, time={clearTime:F2}s"); + + Context._saveService.CompleteCurrentStage(score, kills, clearTime, true, isTimeUp, hpRatio); + await Context._saveService.SaveAsync(); + + // クライアントに勝利を通知 + var gameResult = new SurvivorNetworkGameResult + { + IsVictory = true, + ClearTime = clearTime + }; + SurvivorNetworkGameManager.Instance?.NotifyGameEndedClientRpc(gameResult); + + Debug.Log("[SurvivorNetworkStageScene.VictoryState] Result saved, clients notified"); + ApplicationEvents.ResumeTime(); + } + + public override void Exit() => Debug.Log("[SurvivorNetworkStageScene.VictoryState] Exit"); + } + + #endregion + + #region GameOverState + + private class GameOverState : StageStateBase + { + public override void Enter() + { + Debug.Log("[SurvivorNetworkStageScene.GameOverState] Enter"); + ApplicationEvents.PauseTime(); + + // 残存敵を全クリア&スポーン停止 + View.EnemySpawner?.ClearAllEnemies(); + + SaveAndNotifyAsync().Forget(); + } + + private async UniTaskVoid SaveAndNotifyAsync() + { + var score = StageModel.Score.Value; + var kills = Context.GetCappedKills(); + var clearTime = StageModel.GameTime.Value; + + Debug.Log($"[SurvivorNetworkStageScene.GameOverState] Saving: score={score}, kills={kills}, time={clearTime:F2}s"); + + Context._saveService.CompleteCurrentStage(score, kills, clearTime, false, false, 0f); + await Context._saveService.SaveAsync(); + + // クライアントに敗北を通知 + var gameResult = new SurvivorNetworkGameResult + { + IsVictory = false, + ClearTime = clearTime + }; + SurvivorNetworkGameManager.Instance?.NotifyGameEndedClientRpc(gameResult); + + Debug.Log("[SurvivorNetworkStageScene.GameOverState] Result saved, clients notified"); + ApplicationEvents.ResumeTime(); + } + + public override void Exit() => Debug.Log("[SurvivorNetworkStageScene.GameOverState] Exit"); + } + + #endregion + } +} diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.States.cs.meta b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.States.cs.meta new file mode 100644 index 00000000..0c520d3b --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.States.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5492f618ae185084c9c6721e5200efc3 \ No newline at end of file diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.cs new file mode 100644 index 00000000..04fccb93 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.cs @@ -0,0 +1,334 @@ +using System; +using Cysharp.Threading.Tasks; +using Game.MVP.Core.Scenes; +using Game.MVP.Survivor.Enemy; +using Game.MVP.Survivor.Item; +using Game.MVP.Survivor.SaveData; +using Game.MVP.Survivor.Scenes.Models; +using Game.MVP.Survivor.Services; +using Game.MVP.Survivor.Weapon; +using Game.Shared.Bootstrap; +using Game.Shared.Network; +using Game.Shared.Network.Survivor; +using Game.Shared.Services; +using Game.Shared.Signals.Survivor; +using MessagePipe; +using R3; +using R3.Triggers; +using UnityEngine; +using UnityEngine.ResourceManagement.ResourceProviders; +using UnityEngine.SceneManagement; +using VContainer; + +namespace Game.MVP.Survivor.Scenes +{ + /// + /// Survivorメインステージシーン サーバー専用Presenter。 + /// ゲームロジック(ウェーブ管理・ダメージ・勝敗判定)のみを担当し、 + /// HUD/UI/ビジュアルは一切扱わない。 + /// + public partial class SurvivorNetworkStageScene : GamePrefabScene, IGameSceneScope + { + [Inject] private readonly IGameSceneService _sceneService; + [Inject] private readonly ISurvivorSaveService _saveService; + [Inject] private readonly IAddressableAssetService _addressableService; + + // Server signals + [Inject] private readonly ISubscriber _hitReportedSub; + [Inject] private readonly ISubscriber _weaponApplySub; + [Inject] private readonly ISubscriber _allPlayersDisconnectedSub; + [Inject] private readonly ISubscriber _allClientsSceneReadySub; + + private SurvivorStageModel _stageModel; + private SurvivorNetworkPlayerState _localPlayerState; + private SurvivorStageWaveManager _waveManager; + private SurvivorNetworkWeaponManager _weaponManager; + private SceneInstance? _stageSceneInstance; + + protected override string AssetPathOrAddress => "SurvivorStageScene"; + + #region IGameSceneScope + + public IObjectResolver ScopedResolver { get; set; } + + public void ConfigureScope(IContainerBuilder builder) + { + builder.Register(Lifetime.Scoped); + builder.Register(Lifetime.Scoped); + builder.Register(Lifetime.Scoped); + } + + #endregion + + public override async UniTask Startup() + { + await base.Startup(); + + Debug.Log($"[SurvivorNetworkStageScene] Startup: {NetworkModeHelper.GetDebugStatus()}"); + + var session = _saveService.CurrentSession; + if (session == null) + { + Debug.LogError("[SurvivorNetworkStageScene] No active session found!"); + return; + } + + _stageModel = ScopedResolver.Resolve(); + _stageModel.Initialize(session.PlayerId, session.StageId); + + _waveManager = ScopedResolver.Resolve(); + _waveManager.Initialize(session.StageId); + + _weaponManager = ScopedResolver.Resolve(); + _weaponManager.Initialize( + _stageModel.GetStartingWeaponId(), + _stageModel.GetDamageMultiplier()); + + await LoadUnitySceneAsync(); + await SpawnPlayerAsync(); + + BuildStateMachine(); + SubscribeEvents(); + SubscribeSignals(); + SetupServerNetworking(); + } + + private async UniTask LoadUnitySceneAsync() + { + var stageAssetName = _stageModel.StageMaster?.AssetName; + if (!string.IsNullOrEmpty(stageAssetName)) + { + _stageSceneInstance = await _addressableService.LoadSceneAsync(stageAssetName); + SceneManager.SetActiveScene(_stageSceneInstance.Value.Scene); + Debug.Log($"[SurvivorNetworkStageScene] Loaded stage environment: {stageAssetName}"); + } + } + + private async UniTask SpawnPlayerAsync() + { + if (!_stageSceneInstance.HasValue) + { + Debug.LogWarning("[SurvivorNetworkStageScene] Stage scene not loaded, skipping player spawn"); + return; + } + + var playerStart = SurvivorStageSceneHelper.GetPlayerStart(Resolver, _stageSceneInstance.Value.Scene); + if (playerStart == null) + { + Debug.LogWarning("[SurvivorNetworkStageScene] PlayerStart not found, player spawn skipped"); + return; + } + + var playerMaster = _stageModel.PlayerMaster; + var levelMaster = _stageModel.CurrentLevelMaster; + if (playerMaster == null || levelMaster == null) + { + Debug.LogError("[SurvivorNetworkStageScene] PlayerMaster or LevelMaster is null!"); + return; + } + + var playerController = await playerStart.LoadPlayerAsync(Resolver, playerMaster, levelMaster); + if (playerController != null) + { + SceneComponent.SetPlayerController(playerController); + Debug.Log("[SurvivorNetworkStageScene] Player spawned"); + } + } + + private void SubscribeEvents() + { + // キルカウントはWaveManagerのOnKillCountedを使用(目標数を超える加算を防ぐ) + _waveManager.OnKillCounted + .Subscribe(_ => _stageModel.AddKill()) + .AddTo(Disposables); + + // アイテム収集 → ClientRpc通知 + if (SceneComponent.SurvivorItemSpawner != null) + { + SceneComponent.SurvivorItemSpawner.OnItemCollected + .Subscribe(item => + { + _stageModel.CollectItem(item); + + var gm = SurvivorNetworkGameManager.Instance; + gm?.NotifyItemCollectedClientRpc( + _localPlayerState?.PlayerUserId ?? default, + item.ItemId, + (int)item.ItemType, + item.EffectValue, + _stageModel.Experience.Value, + _stageModel.ExperienceToNextLevel.Value); + }) + .AddTo(Disposables); + } + + // ローカルレベルアップ検知 + _stageModel.Level + .Skip(1) + .Subscribe(_ => _pendingLevelUpCount++) + .AddTo(Disposables); + + // StateMachine更新 + SceneComponent.UpdateAsObservable() + .Subscribe(_ => _stateMachine?.Update()) + .AddTo(Disposables); + + // 全クライアント切断 → 敵クリア + _allPlayersDisconnectedSub.Subscribe(_ => HandleAllPlayersDisconnected()).AddTo(Disposables); + } + + /// + /// サーバーシグナル購読(ローカルゲームロジック用) + /// + private void SubscribeSignals() + { + SceneComponent.PlayerController.OnDamageReceived + .Subscribe(s => _stageModel.TakeDamage(s.Damage)) + .AddTo(Disposables); + + SceneComponent.PlayerController.OnDied + .Subscribe(_ => _stageModel.ForceSetHp(0)) + .AddTo(Disposables); + + _waveManager.OnWaveStarted + .Subscribe(s => _stageModel.CurrentWave.Value = s.WaveNumber) + .AddTo(Disposables); + + _waveManager.OnWaveCompleted + .Subscribe(s => + { + var remainingTime = _stageModel.TimeLimit - _stageModel.GameTime.Value; + var spawnInfo = _waveManager.GetSpawnInfo(); + _stageModel.AddWaveClearScore( + s.WaveNumber, remainingTime, spawnInfo.ScoreMultiplier, + _stageModel.CurrentHp.Value, _stageModel.MaxHp.Value); + }).AddTo(Disposables); + } + + /// + /// サーバーネットワーキング: NetworkBridge + シグナル→ClientRpcブリッジ + /// + private void SetupServerNetworking() + { + var networkBridge = new SurvivorNetworkBridge(); + SceneComponent.EnemySpawner?.SetNetworkBridge(networkBridge); + SceneComponent.SurvivorItemSpawner?.SetNetworkBridge(networkBridge); + + // 武器適用・ヒット報告シグナル購読 + _weaponApplySub.Subscribe(s => OnServerWeaponApply(s.Request)).AddTo(Disposables); + _hitReportedSub.Subscribe(s => OnServerHitReported(s.EnemyNetworkId, s.WeaponId)).AddTo(Disposables); + + // シグナル→ClientRpcブリッジ + SubscribeNetworkSignals(); + } + + /// + /// Wave/ゲームイベントシグナル → ClientRpcブロードキャスト + /// + private void SubscribeNetworkSignals() + { + _waveManager.OnWaveStarted.Subscribe(s => + { + var gm = SurvivorNetworkGameManager.Instance; + gm?.NotifyWaveStartedClientRpc(s.WaveNumber, s.TargetKillCount, s.EnemyCount); + }).AddTo(Disposables); + + _waveManager.OnWaveCompleted.Subscribe(s => + { + // サーバー側でスコアを計算し、計算済みスコアをクライアントに送信 + var remainingTime = _stageModel.TimeLimit - _stageModel.GameTime.Value; + var spawnInfo = _waveManager.GetSpawnInfo(); + var hpRatio = _stageModel.MaxHp.Value > 0 + ? (float)_stageModel.CurrentHp.Value / _stageModel.MaxHp.Value : 1f; + var waveClearScore = remainingTime > 0 + ? (int)(remainingTime * spawnInfo.ScoreMultiplier * hpRatio) : 0; + var gm = SurvivorNetworkGameManager.Instance; + gm?.NotifyWaveClearedClientRpc(s.WaveNumber, _waveManager.CurrentWave.CurrentValue, waveClearScore); + }).AddTo(Disposables); + + // IsAllWavesCleared は変更なし(ReactiveProperty、IPublisher ではない) + _waveManager.IsAllWavesCleared + .Where(cleared => cleared) + .Subscribe(_ => SurvivorNetworkGameManager.Instance?.NotifyAllWavesClearedClientRpc()) + .AddTo(Disposables); + } + + private void HandleAllPlayersDisconnected() + { + Debug.Log("[SurvivorNetworkStageScene] All players disconnected, clearing enemies"); + SceneComponent.EnemySpawner?.ClearAllEnemies(); + } + + private void OnServerHitReported(int enemyNetworkId, int weaponId) + { + if (!SceneComponent.EnemySpawner.TryGetEnemyByNetworkId(enemyNetworkId, out var enemy)) + return; + if (enemy.IsDead) return; + + Vector3 playerPos = SceneComponent.PlayerController != null + ? SceneComponent.PlayerController.transform.position + : enemy.transform.position; + + _weaponManager.ProcessHitAuthority(enemy, weaponId, playerPos); + } + + private void OnServerWeaponApply(WeaponApplyRequest request) + { + switch (request.Type) + { + case WeaponApplyType.AddOrUpgrade: + if (request.IsNewWeapon) + _weaponManager.AddWeapon(request.WeaponId); + else + _weaponManager.UpgradeWeapon(request.WeaponId); + break; + + case WeaponApplyType.Replace: + _weaponManager.ReplaceWeapon(request.RemoveWeaponId, request.WeaponId); + break; + } + + _weaponManager.UpdateDamageMultiplier(_stageModel.GetDamageMultiplier()); + Debug.Log($"[SurvivorNetworkStageScene] Server weapon applied: type={request.Type}, weaponId={request.WeaponId}"); + } + + public override async UniTask Ready() + { + // ステートマシン開始(ReadyStateへ) + _stateMachine.Update(); + await UniTask.CompletedTask; + } + + public override async UniTask Terminate() + { + ApplicationEvents.ResumeTime(); + + // ステージ環境シーンをアンロード + if (_stageSceneInstance.HasValue) + { + await _addressableService.UnloadSceneAsync(_stageSceneInstance.Value); + _stageSceneInstance = null; + Debug.Log("[SurvivorNetworkStageScene] Unloaded stage environment"); + } + + await base.Terminate(); + } + + /// + /// HP割合を計算(0.0 ~ 1.0) + /// + private float GetHpRatio() + { + var maxHp = _stageModel.MaxHp.Value; + return maxHp > 0 ? (float)_stageModel.CurrentHp.Value / maxHp : 0f; + } + + /// + /// キル数をキャップして取得 + /// + private int GetCappedKills() + { + return Math.Min(_stageModel.TotalKills.Value, _waveManager.TotalTargetKills); + } + } +} diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.cs.meta b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.cs.meta new file mode 100644 index 00000000..010dfef1 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorNetworkStageScene.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2db15230b10aafd42807b6759d884750 \ No newline at end of file diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageScene.States.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageScene.States.cs index 10d564fd..a9a2f614 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageScene.States.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageScene.States.cs @@ -13,14 +13,11 @@ using Game.Shared.Bootstrap; using Game.Shared.Network; using Game.Shared.Network.Survivor; -using Game.Shared.Playmode; using Game.Shared.Services; using Game.Shared.Signals.Survivor; using MessagePipe; -using Unity.Collections; using UnityEngine; using VContainer; -using VContainer.Unity; namespace Game.MVP.Survivor.Scenes { @@ -77,7 +74,6 @@ private abstract class StageStateBase : State protected Services.SurvivorStageWaveManager WaveManager => Context._waveManager; protected Models.SurvivorStageModel StageModel => Context._stageModel; protected SurvivorStageSceneComponent View => Context.SceneComponent; - protected ISurvivorStageSceneView StageSceneView => Context._stageSceneView; protected void Transition(StageEvent evt) => StateMachine.Transition(evt); } @@ -93,9 +89,6 @@ private class ReadyState : StageStateBase public override void Enter() { Debug.Log("[ReadyState] Enter"); - - // 時間は動かしておく(Cinemachineカメラ追従のため) - // カウントダウン開始時に停止する _countdownComplete = false; // 暗転状態を維持(ステージ裏側が見えないように) @@ -116,71 +109,54 @@ await View.InitializeWeaponManagerAsync( StageModel.GetStartingWeaponId(), StageModel.GetDamageMultiplier() ); - StageSceneView.InitializeWeaponDisplay(); + + View.InitializeWeaponDisplay(); await View.InitializeEnemySpawnerAsync(WaveManager); await View.InitializeItemSpawnerAsync(); - // ネットワーク初期化 - // SurvivorStageConnectScene で接続確立済みのため、NetworkClient.localPlayer は利用可能 + // MP Client: ネットワーク初期化 if (NetworkModeHelper.IsNetworkClientConnected) { await InitializeClientViewsAsync(); } - else if (NetworkModeHelper.IsNetworkServer) - { - // Server-only: VContainer Inject でネットワークオブジェクトの IPublisher を解決 - InitializeServerViews(); - } Debug.Log("[ReadyState] Initialization complete, waiting for camera follow"); - if (UnityPlaymodeHelper.IsServer()) - { - // サーバー: 全クライアントのシーン準備完了を待機してからゲーム開始 - Debug.Log("[ReadyState] Server: waiting for all clients scene ready..."); - await WaitForAllClientsSceneReadyAsync(); - Debug.Log("[ReadyState] Server: all clients ready, starting game"); - _countdownComplete = true; - } - else - { - await UniTask.Yield(); + await UniTask.Yield(); - Debug.Log("[ReadyState] Camera ready, fading in"); + Debug.Log("[ReadyState] Camera ready, fading in"); - // フェードイン - var fadeTweener = GameRootController.FadeIn(0.5f); - if (fadeTweener != null) - { - await fadeTweener.ToUniTask(); - } + // フェードイン + var fadeTweener = GameRootController.FadeIn(0.5f); + if (fadeTweener != null) + { + await fadeTweener.ToUniTask(); + } - Debug.Log("[ReadyState] Showing countdown"); + Debug.Log("[ReadyState] Showing countdown"); - // カウントダウン中は時間を停止(敵スポーンやゲーム進行を防ぐ) - ApplicationEvents.PauseTime(); + // カウントダウン中は時間を停止(敵スポーンやゲーム進行を防ぐ) + ApplicationEvents.PauseTime(); - // カウントダウンダイアログを表示(3, 2, 1, GO!) - await SceneService.TransitionDialogAsync< - SurvivorCountdownDialog, - SurvivorCountdownDialogComponent, - SurvivorCountdownResult>(); + // カウントダウンダイアログを表示(3, 2, 1, GO!) + await SceneService.TransitionDialogAsync< + SurvivorCountdownDialog, + SurvivorCountdownDialogComponent, + SurvivorCountdownResult>(); - await readyAudioTask; - Debug.Log("[ReadyState] Countdown complete"); - AudioService.PlayRandomOneAsync(AudioPlayTag.StageStart).Forget(); + await readyAudioTask; + Debug.Log("[ReadyState] Countdown complete"); + AudioService.PlayRandomOneAsync(AudioPlayTag.StageStart).Forget(); - // サーバーに準備完了を通知(サーバーはこれを受けてゲーム開始) - Context._localPlayerState?.NotifySceneReadyServerRpc(); - Debug.Log("[ReadyState] Scene ready notification sent to server"); + // サーバーに準備完了を通知(サーバーはこれを受けてゲーム開始) + Context._localPlayerState?.NotifySceneReadyServerRpc(); + Debug.Log("[ReadyState] Scene ready notification sent to server"); - _countdownComplete = true; - } + _countdownComplete = true; } public override void Update() { - // カウントダウン完了後にゲーム開始 if (_countdownComplete) { Transition(StageEvent.StartGame); @@ -190,46 +166,12 @@ public override void Update() public override void Exit() => Debug.Log("[ReadyState] Exit"); /// - /// サーバー: 全クライアントが NotifySceneReadyServerRpc を送信するまで待機。 - /// タイムアウト付き(30秒)で、クライアント切断に対応。 + /// MP Client: ネットワークオブジェクトの初期化。 + /// ローカルプレイヤーの NetworkPlayerState をバインドし、 + /// EnemyView / ItemView を生成する。 /// - private async UniTask WaitForAllClientsSceneReadyAsync() - { - var gm = SurvivorNetworkGameManager.Instance; - if (gm == null) - { - Debug.LogWarning("[ReadyState] GameManager not found, skipping wait"); - return; - } - - gm.ResetSceneReadyTracking(); - - var tcs = new UniTaskCompletionSource(); - void OnReady() => tcs.TrySetResult(); - var subscription = Context._allClientsSceneReadySub.Subscribe(_ => OnReady()); - - try - { - var winIndex = await UniTask.WhenAny( - tcs.Task, - UniTask.Delay(TimeSpan.FromSeconds(30), DelayType.Realtime) - ); - if (winIndex == 1) - { - Debug.LogWarning("[ReadyState] Timeout waiting for clients scene ready, proceeding anyway"); - } - } - finally - { - subscription.Dispose(); - } - } - private async UniTask InitializeClientViewsAsync() { - // MP Client: NetworkBehaviour への DI 注入は不要 - // (NetworkMessage + RegisterHandler パターンにより VContainer/MessagePipe 依存を排除済み) - // ローカルプレイヤーの NetworkSurvivorPlayerState を取得 if (NetworkModeHelper.TryGetLocalPlayerComponent(out var localPlayerState)) { @@ -257,34 +199,11 @@ await enemyView.InitializeAsync( var itemViewGo = new GameObject("[SurvivorItemView]"); itemViewGo.transform.SetParent(View.transform); - itemViewGo.AddComponent().Initialize( - Context._itemSpawnedSub, Context._itemDespawnedSub); - } - - private void InitializeServerViews() - { - // サーバー側プレイヤーコントローラーに NetworkPlayerState をバインド - // → ServerInputProvider(ServerRpc 受信入力)+ StateSynchronizer(SyncVar 送信)が有効化 - var playerController = View.PlayerController; - if (playerController != null) - { - foreach (var nps in NetworkModeHelper.GetNetworkPlayerComponents()) - { - if (nps != null) - { - playerController.BindNetworkPlayerState(nps); - - // エネミースポーナーにプレイヤー Transform を登録 - // サーバーのプレイヤーコントローラーが物理演算を行うため、その Transform を使用 - View.EnemySpawner?.AddPlayer(playerController.transform); - break; // 現在は1プレイヤー対応 - } - } - } - - Debug.Log("[ReadyState] Server-only: network objects injected, player bound to NetworkPlayerState"); + await itemViewGo.AddComponent().InitializeAsync( + Context._itemSpawnedSub, Context._itemDespawnedSub, + Context.ScopedResolver.Resolve(), + Context._addressableService); } - } #endregion @@ -298,38 +217,18 @@ private class PlayingState : StageStateBase public override void Enter() { - Debug.Log($"[PlayingState] Enter (isClient={Context._isClient}, isServer={UnityPlaymodeHelper.IsServer()}, {NetworkModeHelper.GetDebugStatus()})"); + Debug.Log($"[PlayingState] Enter ({NetworkModeHelper.GetDebugStatus()})"); ApplicationEvents.ResumeTime(); ApplicationEvents.ShowCursor(); _disconnected = false; - // Mirror 切断検知(MP モード) - if (Context._isClient) - { - NetworkModeHelper.OnClientDisconnected += OnDisconnected; - } + NetworkModeHelper.OnClientDisconnected += OnDisconnected; - // 初回(ReadyStateからの遷移)のみWaveを開始 - // LevelUpStateやPausedStateからの復帰時はWaveを開始しない if (_isFirstEntry) { _isFirstEntry = false; - - // SP / Server: ローカルで Wave 開始 - // MP Client: サーバーが Wave 開始 → ClientRpc で通知 - if (!Context._isClient) - { - Debug.Log("[PlayingState] Starting first wave (server/SP)"); - WaveManager.StartWave(); - } - else - { - Debug.Log("[PlayingState] MP Client: wave start driven by server"); - } - - // HUDをフェードイン表示(カウントダウン後、初めてPlayingStateに入った時) - StageSceneView.SetHudVisible(true); + View.SetHudVisible(true); } Context._inputService.EnablePlayer(); @@ -362,37 +261,18 @@ public override void Update() return; } - // クライアントモード: サーバー権威 - if (Context._isClient) + // サーバー権威の勝敗結果 + if (StageModel.HasNetworkResult) { - // サーバーからの勝敗結果を確認 - if (StageModel.HasNetworkResult) - { - Transition(StageModel.NetworkResult.IsVictory - ? StageEvent.Victory : StageEvent.GameOver); - return; - } - - // HUDタイマー表示はローカル累積(サーバーと概ね同期) - StageModel.GameTime.Value += Time.deltaTime; - StageSceneView.UpdateTime(StageModel.GameTime.Value); - - // 勝敗判定はサーバーが Game.Ended ClientRpc で通知 + Transition(StageModel.NetworkResult.IsVictory + ? StageEvent.Victory : StageEvent.GameOver); return; } - // SP / Server: ローカルシミュレーション StageModel.GameTime.Value += Time.deltaTime; - StageSceneView.UpdateTime(StageModel.GameTime.Value); + View.UpdateTime(StageModel.GameTime.Value); - // 勝利条件: 時間制限到達 or 全ウェーブクリア - if (StageModel.IsTimeUp || WaveManager.IsAllWavesCleared.CurrentValue) - { - Transition(StageEvent.Victory); - return; - } - - // 敗北条件: HP0 + // 安全ネット: HP=0 で GameOver(サーバー Game.Ended が遅延した場合) if (StageModel.IsDead) { Transition(StageEvent.GameOver); @@ -425,11 +305,7 @@ public override void Enter() ApplicationEvents.PauseTime(); ApplicationEvents.ShowCursor(); - // MP Client: サーバーにもポーズを要求 - if (Context._isClient) - { - Context._localPlayerState?.RequestPauseServerRpc(); - } + Context._localPlayerState?.RequestPauseServerRpc(); ShowPauseDialogAsync().Forget(); } @@ -457,11 +333,7 @@ public override void Exit() { Debug.Log("[PausedState] Exit"); - // MP Client: サーバーにレジューム通知 - if (Context._isClient) - { - Context._localPlayerState?.RequestResumeServerRpc(); - } + Context._localPlayerState?.RequestResumeServerRpc(); ApplicationEvents.ResumeTime(); } @@ -479,12 +351,7 @@ public override void Enter() ApplicationEvents.PauseTime(); ApplicationEvents.ShowCursor(); - // MP Client: サーバーにポーズを要求 - if (Context._isClient) - { - Debug.Log("[LevelUpState] Client: sending RequestPauseServerRpc"); - Context._localPlayerState?.RequestPauseServerRpc(); - } + Context._localPlayerState?.RequestPauseServerRpc(); ShowLevelUpDialogAsync().Forget(); } @@ -494,54 +361,20 @@ private async UniTaskVoid ShowLevelUpDialogAsync() // プレイヤーのステータスを更新(移動速度、ピックアップ範囲など) UpdatePlayerStats(); - // サーバー: ステータス + ダメージ倍率を更新し、武器選択肢をクライアントに送信 - // ゲーム時間は PauseTime() のまま維持(Enter で停止済み) - // → クライアントの RequestResumeServerRpc でサーバー側 GameManager が ResumeTime() - if (UnityPlaymodeHelper.IsServer()) - { - View.WeaponManager?.UpdateDamageMultiplier(StageModel.GetDamageMultiplier()); - - // 武器選択肢を生成してクライアントに送信 - if (View.WeaponManager != null) - { - var serverOptions = View.WeaponManager.GetUpgradeOptions(StageModel.WeaponChoiceCount.Value); - if (serverOptions.Count > 0) - { - var networkOptions = ConvertToNetworkOptions(serverOptions); - var gm = SurvivorNetworkGameManager.Instance; - if (gm != null) - { - gm.SetPendingWeaponOptions(networkOptions); - gm.NotifyPlayerLevelUpClientRpc( - Context._localPlayerState?.PlayerUserId ?? default, - StageModel.Level.Value, - StageModel.Experience.Value, - StageModel.ExperienceToNextLevel.Value, - networkOptions); - Debug.Log($"[LevelUpState] Server: sent LevelUp RPC with {networkOptions.Length} options"); - } - } - } - - Transition(StageEvent.LevelUpComplete); - return; - } - if (View.WeaponManager == null) { Transition(StageEvent.LevelUpComplete); return; } - // クライアント: サーバー提供の選択肢を使用 / SP: ローカルで生成 + // 武器選択肢: サーバー提供 or ローカル生成 List options; - if (Context._isClient && Context._pendingLevelUps.Count > 0) + if (Context._pendingLevelUps.Count > 0) { var levelUpData = Context._pendingLevelUps.Dequeue(); options = (levelUpData.Options != null && levelUpData.Options.Length > 0) ? ConvertNetworkOptions(levelUpData.Options) : new List(); - Debug.Log($"[LevelUpState] Client: using server options ({options.Count} choices)"); } else { @@ -580,13 +413,12 @@ private async UniTaskVoid ShowLevelUpDialogAsync() if (removeWeaponId.HasValue) { - // 入れ替え実行 await View.WeaponManager.ReplaceWeaponAsync( removeWeaponId.Value, result.WeaponId); Context._localPlayerState?.SendWeaponReplaceServerRpc( removeWeaponId.Value, result.WeaponId); - break; // 成功したらループを抜ける + break; } // キャンセル時はループ継続(武器選択に戻る) @@ -594,22 +426,16 @@ await View.WeaponManager.ReplaceWeaponAsync( } else { - // 通常の武器追加/アップグレード await View.WeaponManager.ApplyUpgradeOptionAsync(result); Context._localPlayerState?.SendWeaponChoiceServerRpc( result.WeaponId, result.IsNewWeapon); - break; // 成功したらループを抜ける + break; } } View.WeaponManager.UpdateDamageMultiplier(StageModel.GetDamageMultiplier()); - // MP Client: サーバーにレジューム通知 - if (Context._isClient) - { - Debug.Log("[LevelUpState] Client: sending RequestResumeServerRpc"); - Context._localPlayerState?.RequestResumeServerRpc(); - } + Context._localPlayerState?.RequestResumeServerRpc(); Transition(StageEvent.LevelUpComplete); } @@ -625,12 +451,7 @@ private void UpdatePlayerStats() public override void Exit() { Debug.Log("[LevelUpState] Exit"); - - // サーバー: クライアントの RequestResumeServerRpc が ResumeTime する - if (UnityPlaymodeHelper.IsClient()) - { - ApplicationEvents.ResumeTime(); - } + ApplicationEvents.ResumeTime(); } private static List ConvertNetworkOptions( @@ -652,27 +473,6 @@ private static List ConvertNetworkOptions( } return result; } - - private static SurvivorNetworkWeaponUpgradeOption[] ConvertToNetworkOptions( - List options) - { - var result = new SurvivorNetworkWeaponUpgradeOption[options.Count]; - for (int i = 0; i < options.Count; i++) - { - var opt = options[i]; - result[i] = new SurvivorNetworkWeaponUpgradeOption - { - WeaponId = opt.WeaponId, - WeaponName = new FixedString128Bytes(opt.WeaponName ?? ""), - IsNewWeapon = opt.IsNewWeapon, - CurrentLevel = opt.CurrentLevel, - Description = new FixedString128Bytes(opt.Description ?? ""), - UpgradeEffect = new FixedString128Bytes(opt.UpgradeEffect ?? ""), - IconAssetName = new FixedString128Bytes(opt.IconAssetName ?? "") - }; - } - return result; - } } #endregion @@ -695,10 +495,10 @@ public override void Enter() View.EnemySpawner?.ClearAllEnemies(); // HUDを非表示 - StageSceneView.SetHudVisible(false); + View.SetHudVisible(false); ApplicationEvents.ShowCursor(); - StageSceneView.ShowVictory(); + View.ShowVictory(); // 保存完了を待機してからリザルト画面へ遷移 SaveAndTransitionToResultAsync().Forget(); @@ -719,26 +519,8 @@ private async UniTaskVoid SaveAndTransitionToResultAsync() await Context._saveService.SaveAsync(); Context._isResultSaved = true; - // サーバー: クライアントに勝利を通知(直接 ClientRpc) - if (!Context._isClient) - { - var gameResult = new SurvivorNetworkGameResult - { - IsVictory = true, - ClearTime = clearTime - }; - SurvivorNetworkGameManager.Instance?.NotifyGameEndedClientRpc(gameResult); - } - Debug.Log("[VictoryState] Result saved successfully"); - // サーバー: リザルト画面は不要 - if (UnityPlaymodeHelper.IsServer()) - { - ApplicationEvents.ResumeTime(); - return; - } - // Victory表示の待機(保存処理と並行して最低2秒は表示) await UniTask.Delay(ResultDisplayDuration, DelayType.Realtime); @@ -770,10 +552,10 @@ public override void Enter() View.EnemySpawner?.ClearAllEnemies(); // HUDを非表示 - StageSceneView.SetHudVisible(false); + View.SetHudVisible(false); ApplicationEvents.ShowCursor(); - StageSceneView.ShowGameOver(); + View.ShowGameOver(); // 保存完了を待機してからリザルト画面へ遷移 SaveAndTransitionToResultAsync().Forget(); @@ -794,26 +576,8 @@ private async UniTaskVoid SaveAndTransitionToResultAsync() Context._isResultSaved = true; - // サーバー: クライアントに敗北を通知(直接 ClientRpc) - if (!Context._isClient) - { - var gameResult = new SurvivorNetworkGameResult - { - IsVictory = false, - ClearTime = clearTime - }; - SurvivorNetworkGameManager.Instance?.NotifyGameEndedClientRpc(gameResult); - } - Debug.Log("[GameOverState] Result saved successfully"); - // サーバー: リザルト画面は不要 - if (UnityPlaymodeHelper.IsServer()) - { - ApplicationEvents.ResumeTime(); - return; - } - // GameOver表示の待機(保存処理と並行して最低2秒は表示) await UniTask.Delay(ResultDisplayDuration, DelayType.Realtime); @@ -835,11 +599,7 @@ public override void Enter() { Debug.Log("[RetryState] Enter"); - // クライアント: サーバーに退出を即時通知 - if (Context._isClient) - { - Context._localPlayerState?.RequestQuitServerRpc(); - } + Context._localPlayerState?.RequestQuitServerRpc(); // Retryフラグを設定(Terminate()でセーブデータ更新をスキップ) Context._retryOrQuit = true; @@ -883,11 +643,7 @@ public override void Enter() { Debug.Log("[QuitToTitleState] Enter"); - // クライアント: サーバーに退出を即時通知(DI スコープ解放で Connector が先に切断されるため、ここで送信) - if (Context._isClient) - { - Context._localPlayerState?.RequestQuitServerRpc(); - } + Context._localPlayerState?.RequestQuitServerRpc(); // Quitフラグを設定(Terminate()でセーブデータ更新をスキップ) Context._retryOrQuit = true; diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageScene.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageScene.cs index cee2f423..659ce084 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageScene.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageScene.cs @@ -13,13 +13,11 @@ using Game.Shared.Extensions; using Game.Shared.Network; using Game.Shared.Network.Survivor; -using Game.Shared.Playmode; using Game.Shared.Services; using Game.Shared.Signals.Survivor; using MessagePipe; using R3; using R3.Triggers; -using Unity.Collections; using UnityEngine; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.SceneManagement; @@ -29,8 +27,10 @@ namespace Game.MVP.Survivor.Scenes { /// - /// Survivorメインステージシーン(Presenter) - /// StateMachineでゲームループを管理 + /// Survivorメインステージシーン(クライアント/SP Presenter) + /// StateMachineでゲームループを管理。 + /// SP: ローカルサーバー+クライアント(ゲームロジック+ビジュアル) + /// MP Client: サーバー権威のクライアント(ビジュアル+サーバー同期) /// public partial class SurvivorStageScene : GamePrefabScene, IGameSceneScope { @@ -52,17 +52,11 @@ public partial class SurvivorStageScene : GamePrefabScene _itemDespawnedSub; [Inject] private readonly ISubscriber _leveledUpSub; [Inject] private readonly ISubscriber _itemCollectedSub; - [Inject] private readonly ISubscriber _hitReportedSub; - [Inject] private readonly ISubscriber _weaponApplySub; - [Inject] private readonly ISubscriber _allPlayersDisconnectedSub; - [Inject] private readonly ISubscriber _allClientsSceneReadySub; private SurvivorStageModel _stageModel; private SurvivorNetworkPlayerState _localPlayerState; private SurvivorStageWaveManager _waveManager; private SceneInstance? _stageSceneInstance; - private ISurvivorStageSceneView _stageSceneView; - private bool _isClient; protected override string AssetPathOrAddress => "SurvivorStageScene"; @@ -72,7 +66,6 @@ public partial class SurvivorStageScene : GamePrefabScene(Lifetime.Scoped); builder.Register(Lifetime.Scoped); } @@ -83,14 +76,7 @@ public override async UniTask Startup() { await base.Startup(); - // ネットワーククライアントモードを起動時に1回だけキャッシュ(SP: false, MP Client: true) - _isClient = NetworkModeHelper.IsNetworkClient; - Debug.Log($"[SurvivorStageScene] Startup: isClient={_isClient}, IsServer={UnityPlaymodeHelper.IsServer()}, {NetworkModeHelper.GetDebugStatus()}"); - - // サーバーではNullStageViewでHUD呼び出しをno-op化 - _stageSceneView = UnityPlaymodeHelper.IsServer() - ? new NullSurvivorStageSceneView() - : SceneComponent; + Debug.Log($"[SurvivorStageScene] Startup: {NetworkModeHelper.GetDebugStatus()}"); // セッションからステージ情報を取得 var session = _saveService.CurrentSession; @@ -107,26 +93,18 @@ public override async UniTask Startup() _waveManager = ScopedResolver.Resolve(); _waveManager.Initialize(session.StageId); - // MP Client: Wave進行をサーバー権威モードに設定 - if (_isClient) - { - _waveManager.SetClient(true); - } - // インゲームフィールドをロード await LoadUnitySceneAsync(); - // プレイヤーを動的生成(サーバーでも生成 — 物理・武器・ダメージ処理に必要) + // プレイヤーを動的生成 await SpawnPlayerAsync(); BuildStateMachine(); SubscribeEvents(); SubscribeSignals(); - - SetupServerNetworkingIfActive(); BindModelToView(); - _stageSceneView.Initialize(_stageModel, _waveManager.TotalWaves); + SceneComponent.Initialize(_stageModel, _waveManager.TotalWaves); // ReadyState開始前に暗転状態にしておく(ステージ裏側が見えないように) GameRootController?.SetFadeImmediate(1f); @@ -208,33 +186,7 @@ private void SubscribeEvents() if (SceneComponent.SurvivorItemSpawner != null) { SceneComponent.SurvivorItemSpawner.OnItemCollected - .Subscribe(item => - { - _stageModel.CollectItem(item); - - // Server / Host: アイテム収集をクライアントに通知(経験値状態含む) - if (NetworkModeHelper.IsNetworkServer) - { - var gm = SurvivorNetworkGameManager.Instance; - gm?.NotifyItemCollectedClientRpc( - _localPlayerState?.PlayerUserId ?? default, - item.ItemId, - (int)item.ItemType, - item.EffectValue, - _stageModel.Experience.Value, - _stageModel.ExperienceToNextLevel.Value); - } - }) - .AddTo(Disposables); - } - - // SP / Server: ローカルレベルアップ検知 - // MP Client: サーバーからの LeveledUp シグナルで _pendingLevelUpCount++ する(SubscribeSignals) - if (!_isClient) - { - _stageModel.Level - .Skip(1) - .Subscribe(_ => _pendingLevelUpCount++) + .Subscribe(item => _stageModel.CollectItem(item)) .AddTo(Disposables); } @@ -266,84 +218,59 @@ private void SubscribeEvents() }) .AddTo(Disposables); - // Client: ヒット報告コールバック設定(プロキシ命中 → ReportHitServerRpc) - // _localPlayerState は ReadyState.Enter() で設定されるため、ラムダキャプチャでフィールド参照 - if (_isClient) + // ヒットコールバック設定(武器サブクラスから Collider + WeaponId を受け取り、サーバーに委譲) + SceneComponent.WeaponManager.SetHitCallback((other, weaponId) => { - SceneComponent.WeaponManager?.SetHitCallback((enemyId, weaponId) => + if (_localPlayerState == null || !NetworkModeHelper.IsNetworkClientConnected) return; + + // Pure client: プロキシターゲット + var proxy = other.GetComponentInParent(); + if (proxy != null) { - if (NetworkModeHelper.IsNetworkClientConnected && _localPlayerState != null) - _localPlayerState.ReportHitServerRpc(enemyId, weaponId); - }); - } + _localPlayerState.ReportHitServerRpc(proxy.NetworkId, weaponId); + return; + } - // Server: 全クライアント切断時にスポーン停止 - if (NetworkModeHelper.IsNetworkServer) - { - _allPlayersDisconnectedSub.Subscribe(_ => HandleAllPlayersDisconnected()).AddTo(Disposables); - } + // Host mode: 実体エネミー(NetworkId はスポーン時に設定済み) + var enemy = other.GetComponentInParent(); + if (enemy != null && !enemy.IsDead && enemy.NetworkId >= 0) + { + _localPlayerState.ReportHitServerRpc(enemy.NetworkId, weaponId); + } + }); // 自動保存のセットアップ SetupAutoSave(); } /// - /// SurvivorSignals 購読(統一)。 - /// SP/Server: ゲームロジックが直接 Publish。 + /// SurvivorSignals 購読。 + /// SP: ゲームロジックが直接 Publish。 /// MP Client: ClientRpc → NetworkSurvivorGameManager が Publish。 /// private void SubscribeSignals() { - if (_isClient) - { - // Client: サーバーの権威的な残HPで同期(回復アイテムによるHP差分を補正) - _damageReceivedSub.Subscribe(s => _stageModel.ForceSetHp(s.RemainingHp)).AddTo(Disposables); - } - else - { - _damageReceivedSub.Subscribe(s => _stageModel.TakeDamage(s.Damage)).AddTo(Disposables); - } + // サーバー権威の残HPで同期(常にサーバーが正) + _damageReceivedSub.Subscribe(s => _stageModel.ForceSetHp(s.RemainingHp)).AddTo(Disposables); _playerDiedSub.Subscribe(_ => _stageModel.ForceSetHp(0)).AddTo(Disposables); _waveStartedSub.Subscribe(s => { _stageModel.CurrentWave.Value = s.WaveNumber; - _stageSceneView.UpdateWave(s.WaveNumber, _waveManager.TotalWaves); + SceneComponent.UpdateWave(s.WaveNumber, _waveManager.TotalWaves); + _waveManager.UpdateClientWaveDisplay(s.TargetKillCount, s.EnemyCount); - // クライアント: サーバーからの敵数情報でHUD表示を更新 - if (_isClient) + if (s.WaveNumber > 0 && _stageModel.GameTime.Value > 0) { - _waveManager.UpdateClientWaveDisplay(s.TargetKillCount, s.EnemyCount); - } - - // ウェーブバナーはゲーム開始後のみ表示(カウントダウン中は非表示) - if (s.WaveNumber > 0) - { - if (!_isClient || _stageModel.GameTime.Value > 0) - { - _stageSceneView.ShowWaveBanner(s.WaveNumber, _waveManager.TotalWaves, s.TargetKillCount); - } + SceneComponent.ShowWaveBanner(s.WaveNumber, _waveManager.TotalWaves, s.TargetKillCount); } }).AddTo(Disposables); _waveCompletedSub.Subscribe(s => { - if (_isClient) - { - // MP Client: サーバーが計算済みスコアをそのまま加算 - _stageModel.AddScore(s.WaveClearScore); - _waveManager.SetWaveFromServer(s.WaveNumber, s.WaveNumber + 1); - } - else - { - // SP/Server: ローカルで計算 - var remainingTime = _stageModel.TimeLimit - _stageModel.GameTime.Value; - var spawnInfo = _waveManager.GetSpawnInfo(); - _stageModel.AddWaveClearScore( - s.WaveNumber, remainingTime, spawnInfo.ScoreMultiplier, - _stageModel.CurrentHp.Value, _stageModel.MaxHp.Value); - } + _stageModel.AddScore(s.WaveClearScore); + _waveManager.SetWaveFromServer(s.WaveNumber, s.WaveNumber + 1); }).AddTo(Disposables); _gameEndedSub.Subscribe(s => _stageModel.SetNetworkResult(s.Result)).AddTo(Disposables); @@ -352,140 +279,35 @@ private void SubscribeSignals() { _stageModel.AddScore(s.ScoreGained); _stageModel.AddKill(); - _stageSceneView.UpdateKills(s.TotalKills); + SceneComponent.UpdateKills(s.TotalKills); }).AddTo(Disposables); - // クライアント: サーバーからの敵バッチ更新で死亡イベントをキルカウントに反映 - if (_isClient) + _enemyBatchSub.Subscribe(signal => { - _enemyBatchSub.Subscribe(signal => + foreach (var e in signal.Enemies) { - foreach (var e in signal.Enemies) + if (e.SyncType == EnemySyncType.Death) { - if (e.SyncType == EnemySyncType.Death) - { - _waveManager.IncrementClientKillCount(); - } + _waveManager.IncrementClientKillCount(); } - }).AddTo(Disposables); - - // MP Client: サーバーからのレベルアップ通知 - _leveledUpSub.Subscribe(s => - { - _stageModel.SetLevelFromServer(s.Level, s.Experience, s.ExperienceToNextLevel); - _pendingLevelUps.Enqueue(s); - _pendingLevelUpCount++; - Debug.Log($"[SurvivorStageScene] Client: LevelUp received from server: Lv.{s.Level}, options={s.Options?.Length ?? 0}"); - }).AddTo(Disposables); - - // MP Client: サーバーからのアイテム収集通知(経験値 + HP同期) - _itemCollectedSub.Subscribe(s => - { - _stageModel.SetExperienceFromServer(s.CurrentExperience, s.ExperienceToNextLevel); - - // 回復アイテム: クライアント側でもHP回復を反映 - if (s.ItemType == (int)SurvivorItemType.Recovery) - { - _stageModel.Heal(s.EffectValue); - } - }).AddTo(Disposables); - } - } - - /// - /// サーバーネットワーキングのセットアップ(ランタイム判定)。 - /// Dedicated Server: Startup 時に NetworkServer.active == true → 実行。 - /// Host mode: Startup 時は false → スキップ。ReadyState の StartHostAsync 後に再呼び出し。 - /// - internal void SetupServerNetworkingIfActive() - { - if (!NetworkModeHelper.IsNetworkServer) return; - - SubscribeNetworkSignals(); - var networkBridge = new SurvivorNetworkBridge(); - SceneComponent.EnemySpawner?.SetNetworkBridge(networkBridge); - SceneComponent.SurvivorItemSpawner?.SetNetworkBridge(networkBridge); - - // 武器適用・ヒット報告シグナル購読 - _weaponApplySub.Subscribe(s => OnServerWeaponApply(s.Request)).AddTo(Disposables); - _hitReportedSub.Subscribe(s => OnServerHitReported(s.EnemyNetworkId, s.WeaponId)).AddTo(Disposables); - - } - - private void HandleAllPlayersDisconnected() - { - Debug.Log("[SurvivorStageScene] All players disconnected, clearing enemies and stopping spawner"); - SceneComponent.EnemySpawner?.ClearAllEnemies(); - } - - private void OnServerHitReported(int enemyNetworkId, int weaponId) - { - if (!SceneComponent.EnemySpawner.TryGetEnemyByNetworkId(enemyNetworkId, out var enemy)) - return; - if (enemy.IsDead) return; - - if (!SceneComponent.WeaponManager.TryGetWeaponById(weaponId, out var weapon)) - return; - - Vector3 playerPos = SceneComponent.PlayerController != null - ? SceneComponent.PlayerController.transform.position - : enemy.transform.position; - - weapon.ProcessHitAuthority(enemy, playerPos); - } - - private void OnServerWeaponApply(WeaponApplyRequest request) - { - if (SceneComponent.WeaponManager == null) return; - - switch (request.Type) - { - case WeaponApplyType.AddOrUpgrade: - if (request.IsNewWeapon) - SceneComponent.WeaponManager.AddWeaponAsync(request.WeaponId).Forget(); - else - SceneComponent.WeaponManager.UpgradeWeapon(request.WeaponId); - break; - - case WeaponApplyType.Replace: - SceneComponent.WeaponManager.ReplaceWeaponAsync( - request.RemoveWeaponId, request.WeaponId).Forget(); - break; - } - - SceneComponent.WeaponManager.UpdateDamageMultiplier(_stageModel.GetDamageMultiplier()); - Debug.Log($"[SurvivorStageScene] Server weapon applied: type={request.Type}, weaponId={request.WeaponId}"); - } + } + }).AddTo(Disposables); - /// - /// Server 用: MessagePipe Signal → ClientRpc 転送。 - /// ダメージ・死亡は SurvivorPlayerController.States が直接 ClientRpc する。 - /// - private void SubscribeNetworkSignals() - { - _waveStartedSub.Subscribe(s => + _leveledUpSub.Subscribe(s => { - var gm = SurvivorNetworkGameManager.Instance; - gm?.NotifyWaveStartedClientRpc(s.WaveNumber, s.TargetKillCount, s.EnemyCount); + _stageModel.SetLevelFromServer(s.Level, s.Experience, s.ExperienceToNextLevel); + _pendingLevelUps.Enqueue(s); + _pendingLevelUpCount++; }).AddTo(Disposables); - _waveCompletedSub.Subscribe(s => + _itemCollectedSub.Subscribe(s => { - // サーバー側でスコアを計算し、計算済みスコアをクライアントに送信 - var remainingTime = _stageModel.TimeLimit - _stageModel.GameTime.Value; - var spawnInfo = _waveManager.GetSpawnInfo(); - var hpRatio = _stageModel.MaxHp.Value > 0 - ? (float)_stageModel.CurrentHp.Value / _stageModel.MaxHp.Value : 1f; - var waveClearScore = remainingTime > 0 - ? (int)(remainingTime * spawnInfo.ScoreMultiplier * hpRatio) : 0; - var gm = SurvivorNetworkGameManager.Instance; - gm?.NotifyWaveClearedClientRpc(s.WaveNumber, _waveManager.CurrentWave.CurrentValue, waveClearScore); + _stageModel.SetExperienceFromServer(s.CurrentExperience, s.ExperienceToNextLevel); + if (s.ItemType == (int)SurvivorItemType.Recovery) + { + _stageModel.Heal(s.EffectValue); + } }).AddTo(Disposables); - - _waveManager.IsAllWavesCleared - .Where(cleared => cleared) - .Subscribe(_ => SurvivorNetworkGameManager.Instance?.NotifyAllWavesClearedClientRpc()) - .AddTo(Disposables); } private void SetupAutoSave() @@ -500,8 +322,6 @@ private void SetupAutoSave() .Where(paused => paused) .Subscribe(_ => SaveCurrentSession()) .AddTo(Disposables); - - // OnApplicationQuit は削除(クリア記録はVictoryState/GameOverStateで保存済み) } private void SaveCurrentSession() @@ -541,7 +361,7 @@ private void BindModelToView() // HP(View更新) _stageModel.CurrentHp .CombineLatest(_stageModel.MaxHp, (current, max) => (current, max)) - .Subscribe(hp => _stageSceneView.UpdateHp(hp.current, hp.max)) + .Subscribe(hp => SceneComponent.UpdateHp(hp.current, hp.max)) .AddTo(Disposables); if (SceneComponent.PlayerController != null) @@ -553,7 +373,7 @@ private void BindModelToView() SceneComponent.PlayerController.CurrentStamina .Subscribe(stamina => { - _stageSceneView.UpdateStamina(stamina, SceneComponent.PlayerController.MaxStamina); + SceneComponent.UpdateStamina(stamina, SceneComponent.PlayerController.MaxStamina); if (_inputService.Player.enabled) { @@ -569,23 +389,23 @@ private void BindModelToView() // 経験値 _stageModel.Experience .CombineLatest(_stageModel.ExperienceToNextLevel, (current, max) => (current, max)) - .Subscribe(exp => _stageSceneView.UpdateExperience(exp.current, exp.max)) + .Subscribe(exp => SceneComponent.UpdateExperience(exp.current, exp.max)) .AddTo(Disposables); // レベル _stageModel.Level - .Subscribe(level => _stageSceneView.UpdateLevel(level)) + .Subscribe(level => SceneComponent.UpdateLevel(level)) .AddTo(Disposables); // キル数 _stageModel.TotalKills - .Subscribe(kills => _stageSceneView.UpdateKills(kills)) + .Subscribe(kills => SceneComponent.UpdateKills(kills)) .AddTo(Disposables); // 敵の撃破数(目標数に対する進捗を表示) _waveManager.EnemiesKilled .CombineLatest(_waveManager.TargetKillsThisWave, (killed, target) => (killed, target)) - .Subscribe(enemies => _stageSceneView.UpdateEnemies(enemies.killed, enemies.target)) + .Subscribe(enemies => SceneComponent.UpdateEnemies(enemies.killed, enemies.target)) .AddTo(Disposables); } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageSceneComponent.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageSceneComponent.cs index a20d209c..f85413f7 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageSceneComponent.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Scenes/SurvivorStageSceneComponent.cs @@ -25,7 +25,7 @@ namespace Game.MVP.Survivor.Scenes /// UI Toolkit(UXML/USS)使用、UI Builderで編集可能 /// HUD表示とゲームプレイUIを管理 /// - public class SurvivorStageSceneComponent : GameSceneComponent, ISurvivorStageSceneView + public class SurvivorStageSceneComponent : GameSceneComponent { [Header("UI Document")] [SerializeField] private UIDocument _uiDocument; diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Server/SurvivorServerGameLoop.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Server/SurvivorServerGameLoop.cs index 5624c6e4..43c2d01a 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Server/SurvivorServerGameLoop.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Server/SurvivorServerGameLoop.cs @@ -5,8 +5,7 @@ using Game.MVP.Survivor.Scenes; using Game.Shared.Network.Survivor; using Game.Shared.Services; -using Game.Shared.Signals.Survivor; -using MessagePipe; +using R3; using UnityEngine; using VContainer; using VContainer.Unity; @@ -15,7 +14,7 @@ namespace Game.MVP.Survivor.Server { /// /// MPPM / Dedicated Server 用エントリポイント。 - /// AllPlayersReady シグナル受信後にマスターデータ読み込み → SurvivorStageScene へ遷移し、 + /// AllPlayersReady シグナル受信後にマスターデータ読み込み → SurvivorNetworkStageScene へ遷移し、 /// サーバー権威のウェーブ管理・エネミースポーンを開始する。 /// public class SurvivorServerGameLoop : IAsyncStartable @@ -24,7 +23,6 @@ public class SurvivorServerGameLoop : IAsyncStartable [Inject] private readonly ISurvivorSaveService _saveService; [Inject] private readonly IMasterDataService _masterDataService; [Inject] private readonly SurvivorUnityServerSession _session; - [Inject] private readonly ISubscriber _allPlayersReadySub; public async UniTask StartAsync(CancellationToken cancellation) { @@ -35,7 +33,7 @@ public async UniTask StartAsync(CancellationToken cancellation) // AllPlayersReady シグナルを待機 var tcs = new UniTaskCompletionSource(); - var subscription = _allPlayersReadySub.Subscribe(_ => tcs.TrySetResult()); + var subscription = _session.OnAllPlayersReady.Subscribe(_ => tcs.TrySetResult()); try { await tcs.Task; @@ -50,10 +48,10 @@ public async UniTask StartAsync(CancellationToken cancellation) // セーブサービスにセッション情報を設定(SurvivorStageScene が参照する) _saveService.StartSession(_session.StageId, 1); - // SurvivorStageScene へ遷移 → サーバー側ウェーブ管理開始 - await _sceneService.TransitionAsync(); + // SurvivorNetworkStageScene へ遷移 → サーバー側ウェーブ管理開始 + await _sceneService.TransitionAsync(); - Debug.Log("[SurvivorServerGameLoop] SurvivorStageScene loaded on server"); + Debug.Log("[SurvivorServerGameLoop] SurvivorNetworkStageScene loaded on server"); } } } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Services/SurvivorStageWaveManager.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Services/SurvivorStageWaveManager.cs index 5cb7b897..017f7dbb 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Services/SurvivorStageWaveManager.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Services/SurvivorStageWaveManager.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using Game.Client.MasterData; +using Game.Shared.Network; using Game.Shared.Services; using Game.Shared.Signals.Survivor; -using MessagePipe; using R3; using UnityEngine; using VContainer; @@ -20,8 +20,11 @@ namespace Game.MVP.Survivor.Services public class SurvivorStageWaveManager : IDisposable { [Inject] private readonly IMasterDataService _masterDataService; - [Inject] private IPublisher _waveStartedPublisher; - [Inject] private IPublisher _waveCompletedPublisher; + + private readonly Subject _onWaveStarted = new(); + private readonly Subject _onWaveCompleted = new(); + public Observable OnWaveStarted => _onWaveStarted; + public Observable OnWaveCompleted => _onWaveCompleted; private readonly ReactiveProperty _currentWave = new(0); private readonly ReactiveProperty _enemiesThisWave = new(0); @@ -63,9 +66,6 @@ public int TotalTargetKills /// 現在が最終ウェーブかどうか public bool IsLastWave => _currentWaveIndex >= 0 && _currentWaveIndex >= _waves.Length - 1; - // サーバー権威: クライアントモードでは Wave ロジックを実行しない - private bool _isClient; - // ステージのウェーブ情報キャッシュ private int _stageId; private SurvivorStageWaveMaster[] _waves; @@ -73,12 +73,6 @@ public int TotalTargetKills private WaveSpawnInfo _currentSpawnInfo; private List _currentEnemySpawnList; - /// クライアントモードを設定(Wave ロジックをスキップ) - public void SetClient(bool isClient) - { - _isClient = isClient; - } - /// サーバーから通知された Wave 情報でクライアント状態を更新 public void SetWaveFromServer(int waveNumber, int nextWaveNumber) { @@ -102,7 +96,7 @@ public void UpdateClientWaveDisplay(int targetKillCount, int enemyCount) /// public void IncrementClientKillCount() { - if (!_isClient) return; + if (!NetworkModeHelper.IsNetworkClient) return; if (_enemiesKilled.Value < _targetKillsThisWave.Value) { _enemiesKilled.Value++; @@ -130,7 +124,7 @@ public void Initialize(int stageId) public void StartWave() { // Client-only: Wave進行はサーバーが駆動 - if (_isClient) return; + if (NetworkModeHelper.IsNetworkClient) return; _currentWaveIndex++; _enemiesKilled.Value = 0; @@ -200,14 +194,14 @@ public void StartWave() _currentWave.Value = wave.WaveNumber; // ローカルシグナル発行(CurrentWave 更新後) - _waveStartedPublisher?.Publish( + _onWaveStarted.OnNext( new SurvivorSignals.Wave.Started(wave.WaveNumber, targetKillCount, totalSpawnCount)); } public void OnEnemyKilled(bool isBoss = false) { // Client-only: キルトラッキングはサーバーが駆動 - if (_isClient) return; + if (NetworkModeHelper.IsNetworkClient) return; // 全ウェーブクリア後はキル処理しない(残存敵の死亡による再トリガー防止) if (_isAllWavesCleared.Value) return; @@ -238,7 +232,8 @@ public void OnEnemyKilled(bool isBoss = false) if (targetKillsReached && bossKillsReached) { var clearedWave = _currentWave.Value; - _waveCompletedPublisher?.Publish(new SurvivorSignals.Wave.Completed(clearedWave)); + Debug.Log($"[SurvivorStageWaveManager] Wave {clearedWave} completed (kills={_enemiesKilled.Value}/{_targetKillsThisWave.Value}, boss={_bossKills.Value}/{_requiredBossKills.Value})"); + _onWaveCompleted.OnNext(new SurvivorSignals.Wave.Completed(clearedWave)); StartWave(); } @@ -276,6 +271,8 @@ public void Dispose() _bossKills.Dispose(); _isAllWavesCleared.Dispose(); _onKillCounted.Dispose(); + _onWaveStarted.Dispose(); + _onWaveCompleted.Dispose(); } } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/NetworkWeaponSlot.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/NetworkWeaponSlot.cs new file mode 100644 index 00000000..0e625957 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/NetworkWeaponSlot.cs @@ -0,0 +1,36 @@ +using UnityEngine; + +namespace Game.MVP.Survivor.Weapon +{ + /// + /// サーバー用武器データスロット(純粋C#)。 + /// MonoBehaviour やプール/VFX を持たず、マスターデータから取得した + /// ダメージ計算に必要なパラメータのみを保持する。 + /// + public class NetworkWeaponSlot + { + // 基本情報 + public int WeaponId; + public string Name; + public string IconAssetName; + public int Level; + public int MaxLevel; + + // ダメージ計算パラメータ + public int Damage; + public int ProcRate; + public int CritChance; + public int CritMultiplier; + public int Pierce; + public float Knockback; + public float Range; + + // 倍率 + public float DamageMultiplier = 1f; + + /// + /// 最終ダメージ(Damage × DamageMultiplier) + /// + public int FinalDamage => Mathf.RoundToInt(Damage * DamageMultiplier); + } +} diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/NetworkWeaponSlot.cs.meta b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/NetworkWeaponSlot.cs.meta new file mode 100644 index 00000000..80995a81 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/NetworkWeaponSlot.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c010be521a3c8f84eb106983cc21660d \ No newline at end of file diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorAutoFireWeapon.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorAutoFireWeapon.cs index eb556f5a..d9a27364 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorAutoFireWeapon.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorAutoFireWeapon.cs @@ -157,58 +157,32 @@ private void ReturnToPool(SurvivorProjectile projectile) } /// - /// プロジェクタイル命中処理(SP/MP統一ロジック) - /// - /// SP/Host: プライマリヒット → SphereCastで貫通ターゲットを即時検出 → ダメージ適用 → 回収 - /// MP Client: プライマリヒット → RPC送信(サーバーが同じSphereCastロジックで貫通処理) → 回収 - /// - /// OnTriggerEnterによる物理的な貫通(敵を通り抜けて次の敵に当たる)は使用しない。 - /// 代わりにSphereCastで弾道上の敵を即時検出し、SP/MPで同一の結果を保証する。 + /// プロジェクタイル命中処理(SP/MP統一) + /// ヒット検出とVFX表示を行い、ダメージ処理はScene側のコールバックに委譲する。 /// private void OnProjectileHit(SurvivorProjectile projectile, Collider other) { using (s_processHitMarker.Auto()) { // プライマリヒット処理済み → 後続のOnTriggerEnterを無視 - // SphereCastで貫通処理済みのため、物理接触による二重ダメージを防止 if (projectile.HasPrimaryHitProcessed) return; - // クライアントモード: プロキシへの命中をサーバーに報告 - if (OnEnemyHitForServer != null) - { - var proxy = other.GetComponentInParent(); - if (proxy == null) return; - - projectile.MarkPrimaryHitProcessed(); - OnEnemyHitForServer.Invoke(proxy.NetworkId, WeaponId); - - // ヒットVFX(楽観的表示) - if (_vfxSpawner != null && !string.IsNullOrEmpty(_hitEffectAssetName)) - { - var hitPosition = other.ClosestPoint(projectile.transform.position); - _vfxSpawner.SpawnEffect(_hitEffectAssetName, hitPosition, _hitEffectScale); - } - - // サーバーがダメージ・貫通を処理するため、プロジェクタイルを即時回収 - ReturnToPool(projectile); + // ヒット対象チェック(SP: ICombatTarget, MP: EnemyProxyTarget) + if (other.GetComponentInParent() == null + && other.GetComponentInParent() == null) return; - } - - // SP/Host: ローカルダメージ処理(WeaponBaseの統一ロジックを使用) - var target = other.GetComponentInParent(); - if (target == null || target.IsDead) return; projectile.MarkPrimaryHitProcessed(); - // ヒットエフェクト(ダメージ計算前に表示 — ProcRate失敗でも弾は当たった) + // ヒットVFX(ダメージ計算前に表示 — ProcRate失敗でもプロジェクタイルは当たった) if (_vfxSpawner != null && !string.IsNullOrEmpty(_hitEffectAssetName)) { var hitPosition = other.ClosestPoint(projectile.transform.position); _vfxSpawner.SpawnEffect(_hitEffectAssetName, hitPosition, _hitEffectScale); } - // ダメージ計算 + 適用 + 貫通(全てWeaponBase内で完結) - ProcessHitLocal(target, _owner.position, projectile.transform.position, projectile.transform.forward); + // ダメージ処理をSceneに委譲(SP: ローカルダメージ, MP: RPC送信) + OnHitCallback?.Invoke(other, WeaponId); ReturnToPool(projectile); } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorGroundWeapon.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorGroundWeapon.cs index 77e37e82..8afbc22f 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorGroundWeapon.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorGroundWeapon.cs @@ -125,47 +125,26 @@ private void SpawnArea(Vector3 position) area.Activate(finalDamage, Duration, Interval, _knockback, hitboxRadius); } + /// + /// エリアダメージ命中処理(SP/MP統一) + /// VFX表示を行い、ダメージ処理はScene側のコールバックに委譲する。 + /// private void OnAreaHit(SurvivorGroundDamageArea area, Collider other) { - // クライアントモード: プロキシへの命中をサーバーに報告 - if (OnEnemyHitForServer != null) - { - var proxy = other.GetComponentInParent(); - if (proxy == null) return; - - OnEnemyHitForServer.Invoke(proxy.NetworkId, WeaponId); - - // ヒットVFX(楽観的表示) - if (_vfxSpawner != null && !string.IsNullOrEmpty(_hitEffectAssetName)) - { - var hitPos = other.ClosestPoint(area.transform.position); - _vfxSpawner.SpawnEffect(_hitEffectAssetName, hitPos, _hitEffectScale); - } + // ヒット対象チェック(SP: ICombatTarget, MP: EnemyProxyTarget) + if (other.GetComponentInParent() == null + && other.GetComponentInParent() == null) return; - } - // SP/Host: 既存のICombatTargetパス - var target = other.GetComponentInParent(); - if (target == null || target.IsDead) return; - - if (RollProcRate()) + // ヒットVFX + if (_vfxSpawner != null && !string.IsNullOrEmpty(_hitEffectAssetName)) { - target.TakeDamage(area.Damage); - - // ヒットエフェクト - if (_vfxSpawner != null && !string.IsNullOrEmpty(_hitEffectAssetName)) - { - var hitPos = other.ClosestPoint(area.transform.position); - _vfxSpawner.SpawnEffect(_hitEffectAssetName, hitPos, _hitEffectScale); - } - - // ノックバック - if (area.Knockback > 0) - { - Vector3 dir = (other.transform.position - area.transform.position).normalized; - target.ApplyKnockback(dir * area.Knockback); - } + var hitPos = other.ClosestPoint(area.transform.position); + _vfxSpawner.SpawnEffect(_hitEffectAssetName, hitPos, _hitEffectScale); } + + // ダメージ処理をSceneに委譲(ProcRate/Crit計算はSurvivorNetworkWeaponManagerが行う) + OnHitCallback?.Invoke(other, WeaponId); } private void OnAreaExpired(SurvivorGroundDamageArea area) diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorNetworkWeaponManager.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorNetworkWeaponManager.cs new file mode 100644 index 00000000..1a4ae970 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorNetworkWeaponManager.cs @@ -0,0 +1,311 @@ +using System.Collections.Generic; +using System.Linq; +using Game.Client.MasterData; +using Game.Shared.Combat; +using Game.Shared.Constants; +using Game.Shared.Extensions; +using Game.Shared.Services; +using UnityEngine; +using VContainer; + +namespace Game.MVP.Survivor.Weapon +{ + /// + /// サーバー用武器マネージャー(純粋C#クラス)。 + /// プレハブ/プール/VFX を一切持たず、マスターデータ駆動でダメージ計算を行う。 + /// SurvivorNetworkStageScene から DI で注入される。 + /// + public class SurvivorNetworkWeaponManager + { + [Inject] private readonly IMasterDataService _masterDataService; + private MemoryDatabase MemoryDatabase => _masterDataService.MemoryDatabase; + + private readonly List _weapons = new(); + private float _damageMultiplier = 1f; + + private const float PierceDetectionRadius = 0.5f; + + /// + /// 初期化。初期武器を追加する。 + /// + public void Initialize(int startingWeaponId, float damageMultiplier) + { + _damageMultiplier = damageMultiplier; + + if (startingWeaponId > 0) + { + AddWeapon(startingWeaponId); + } + } + + /// + /// 武器を追加。既に持っている場合はアップグレード。 + /// + public bool AddWeapon(int weaponId) + { + var existing = _weapons.Find(w => w.WeaponId == weaponId); + if (existing != null) + { + return UpgradeWeapon(weaponId); + } + + if (!MemoryDatabase.SurvivorWeaponMasterTable.TryFindById(weaponId, out var weaponMaster)) + { + Debug.LogError($"[SurvivorNetworkWeaponManager] Weapon master not found: {weaponId}"); + return false; + } + + var levelMasters = MemoryDatabase.SurvivorWeaponLevelMasterTable.FindByWeaponId(weaponId); + if (levelMasters.Count == 0) + { + Debug.LogError($"[SurvivorNetworkWeaponManager] No level masters for weapon: {weaponId}"); + return false; + } + + var slot = new NetworkWeaponSlot + { + WeaponId = weaponId, + Name = weaponMaster.Name, + IconAssetName = weaponMaster.IconAssetName, + MaxLevel = levelMasters.Max(l => l.Level), + DamageMultiplier = _damageMultiplier, + }; + + ApplyLevel(slot, levelMasters, 1); + _weapons.Add(slot); + + Debug.Log($"[SurvivorNetworkWeaponManager] Added weapon: {weaponMaster.Name} Lv.1"); + return true; + } + + /// + /// 武器をアップグレード。 + /// + public bool UpgradeWeapon(int weaponId) + { + var slot = _weapons.Find(w => w.WeaponId == weaponId); + if (slot == null) return false; + + int nextLevel = slot.Level + 1; + if (nextLevel > slot.MaxLevel) + { + Debug.LogWarning($"[SurvivorNetworkWeaponManager] Already max level: weaponId={weaponId}"); + return false; + } + + var levelMasters = MemoryDatabase.SurvivorWeaponLevelMasterTable.FindByWeaponId(weaponId); + if (!ApplyLevel(slot, levelMasters, nextLevel)) + { + return false; + } + + Debug.Log($"[SurvivorNetworkWeaponManager] Upgraded weapon: {weaponId} to Lv.{slot.Level}"); + return true; + } + + /// + /// 武器を入れ替え。 + /// + public bool ReplaceWeapon(int removeWeaponId, int newWeaponId) + { + var removeSlot = _weapons.Find(w => w.WeaponId == removeWeaponId); + if (removeSlot == null) + { + Debug.LogError($"[SurvivorNetworkWeaponManager] Weapon to remove not found: {removeWeaponId}"); + return false; + } + + _weapons.Remove(removeSlot); + Debug.Log($"[SurvivorNetworkWeaponManager] Removed weapon: {removeSlot.Name}"); + + return AddWeapon(newWeaponId); + } + + /// + /// WeaponId で武器スロットを検索。 + /// + public bool TryGetWeaponById(int weaponId, out NetworkWeaponSlot slot) + { + slot = _weapons.Find(w => w.WeaponId == weaponId); + return slot != null; + } + + /// + /// ダメージ倍率を更新。 + /// + public void UpdateDamageMultiplier(float multiplier) + { + _damageMultiplier = multiplier; + foreach (var slot in _weapons) + { + slot.DamageMultiplier = multiplier; + } + } + + public bool HasEmptySlot => _weapons.Count < 6; + + /// + /// サーバー権威ヒット処理。 + /// ProcRate判定 → ダメージ計算 → プライマリダメージ適用 → 貫通処理。 + /// + public void ProcessHitAuthority(ICombatTarget target, int weaponId, Vector3 playerPos) + { + if (!TryGetWeaponById(weaponId, out var slot)) return; + if (!CalculateHit(slot, out var damage)) return; + + // プライマリターゲット + ApplyDamageWithKnockback(target, damage, slot.Knockback, playerPos); + + // 貫通処理 + if (slot.Pierce > 0) + { + var targetPos = target.CenterPosition; + var direction = (targetPos - playerPos).normalized; + var origin = targetPos + direction * 0.1f; + ProcessPierce(slot, origin, direction, target, playerPos, damage); + } + } + + /// + /// レベルアップ時の選択肢を取得。 + /// + public List GetUpgradeOptions(int count = 3) + { + var options = new List(); + + // 既存武器のアップグレード(最大レベル未満のみ) + foreach (var slot in _weapons) + { + if (slot.Level >= slot.MaxLevel) continue; + + if (!MemoryDatabase.SurvivorWeaponLevelMasterTable.TryFindByWeaponIdAndLevel( + (slot.WeaponId, slot.Level + 1), out var nextLevelMaster)) + continue; + + MemoryDatabase.SurvivorWeaponMasterTable.TryFindById(slot.WeaponId, out var weaponMaster); + + options.Add(new SurvivorWeaponUpgradeOption + { + WeaponId = slot.WeaponId, + WeaponName = slot.Name, + IsNewWeapon = false, + CurrentLevel = slot.Level, + Description = weaponMaster?.Description, + UpgradeEffect = nextLevelMaster.Description, + IconAssetName = weaponMaster?.IconAssetName + }); + } + + // 新規武器 + var allWeapons = MemoryDatabase.SurvivorWeaponMasterTable.All; + foreach (var weaponMaster in allWeapons) + { + if (_weapons.Any(w => w.WeaponId == weaponMaster.Id)) continue; + + options.Add(new SurvivorWeaponUpgradeOption + { + WeaponId = weaponMaster.Id, + WeaponName = weaponMaster.Name, + IsNewWeapon = true, + CurrentLevel = 0, + Description = weaponMaster.Description, + UpgradeEffect = null, + IconAssetName = weaponMaster.IconAssetName + }); + } + + // ランダムに選択 + var result = new List(); + while (result.Count < count && options.Count > 0) + { + int index = Random.Range(0, options.Count); + result.Add(options[index]); + options.RemoveAt(index); + } + + return result; + } + + #region Private Helpers + + private static bool ApplyLevel(NetworkWeaponSlot slot, IReadOnlyList levelMasters, int level) + { + var levelMaster = levelMasters?.FirstOrDefault(l => l.Level == level); + if (levelMaster == null) + { + Debug.LogWarning($"[SurvivorNetworkWeaponManager] Level master not found: weaponId={slot.WeaponId}, level={level}"); + return false; + } + + slot.Level = level; + slot.Damage = levelMaster.Damage; + slot.ProcRate = levelMaster.ProcRate; + slot.CritChance = levelMaster.CritHitRate; + slot.CritMultiplier = levelMaster.CritHitMultiplier; + slot.Pierce = levelMaster.Penetration; + slot.Knockback = levelMaster.Knockback.ToUnit(); + slot.Range = levelMaster.Range.ToUnit(); + + return true; + } + + private static bool CalculateHit(NetworkWeaponSlot slot, out int damage) + { + damage = 0; + + if (!slot.ProcRate.RollChance()) return false; + + damage = slot.FinalDamage; + if (slot.CritChance.RollChance()) + { + damage = Mathf.RoundToInt(damage * slot.CritMultiplier.ToRate()); + } + + return true; + } + + private static void ApplyDamageWithKnockback(ICombatTarget target, int damage, float knockback, Vector3 playerPos) + { + target.TakeDamage(damage); + + if (knockback > 0) + { + var dir = (target.CenterPosition - playerPos).normalized; + target.ApplyKnockback(dir * knockback); + } + } + + private static void ProcessPierce( + NetworkWeaponSlot slot, + Vector3 origin, + Vector3 direction, + ICombatTarget primaryTarget, + Vector3 playerPos, + int damage) + { + var hits = Physics.SphereCastAll( + origin, PierceDetectionRadius, direction, slot.Range, + LayerMaskConstants.Enemy, QueryTriggerInteraction.Collide); + + System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance)); + + int pierceRemaining = slot.Pierce; + for (int i = 0; i < hits.Length && pierceRemaining > 0; i++) + { + var target = hits[i].collider.GetComponentInParent(); + if (target == null || target == primaryTarget || target.IsDead) continue; + + target.TakeDamage(damage); + pierceRemaining--; + + if (slot.Knockback > 0) + { + var dir = (hits[i].collider.transform.position - playerPos).normalized; + target.ApplyKnockback(dir * slot.Knockback); + } + } + } + + #endregion + } +} diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorNetworkWeaponManager.cs.meta b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorNetworkWeaponManager.cs.meta new file mode 100644 index 00000000..ecc90a58 --- /dev/null +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorNetworkWeaponManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6a5680b1cd7230840b782aef92bfa730 \ No newline at end of file diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorProjectile.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorProjectile.cs index 00967fe9..350f6fe7 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorProjectile.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorProjectile.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Game.Shared.Constants; using Game.Shared.Extensions; -using Game.Shared.Playmode; using UnityEngine; namespace Game.MVP.Survivor.Weapon @@ -108,8 +107,8 @@ public void Fire(Vector3 direction, float speed, int damage, float lifetime, int transform.rotation = Quaternion.LookRotation(_direction); } - // トレイルをリセット(サーバーでは不要) - if (!UnityPlaymodeHelper.IsServer() && _trailRenderer != null) + // トレイルをリセット + if (_trailRenderer != null) { _trailRenderer.Clear(); } @@ -250,7 +249,7 @@ public void Reset() _hitCountPerEnemy.Clear(); _hasPrimaryHitProcessed = false; - if (!UnityPlaymodeHelper.IsServer() && _trailRenderer != null) + if (_trailRenderer != null) { _trailRenderer.Clear(); } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorWeaponBase.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorWeaponBase.cs index 14358177..c169eea8 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorWeaponBase.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorWeaponBase.cs @@ -3,8 +3,6 @@ using System.Linq; using Cysharp.Threading.Tasks; using Game.Client.MasterData; -using Game.Shared.Combat; -using Game.Shared.Constants; using Game.Shared.Extensions; using Game.Shared.Services; using R3; @@ -145,10 +143,10 @@ public abstract class SurvivorWeaponBase : IDisposable #endregion /// - /// クライアントモード時のヒット報告コールバック (enemyNetworkId, weaponId)。 - /// null の場合はSP/Hostモード(ローカルTakeDamage)。 + /// ヒットコールバック (hitCollider, weaponId)。 + /// Scene側でモードに応じた処理を行う(SP: ローカルダメージ, MP: RPC送信)。 /// - public Action OnEnemyHitForServer; + public Action OnHitCallback; // Events protected readonly Subject _onAttack = new(); @@ -432,126 +430,6 @@ protected bool RollProcRate() return _procRate.RollChance(); } - #region 戦闘ロジック(SP/Server共通) - - /// - /// 貫通検出SphereCast半径(SP/Server共通) - /// - protected const float PierceDetectionRadius = 0.5f; - - /// - /// サーバー権威ヒット処理(RPC受信時のエントリポイント) - /// ProcRate判定 → ダメージ計算 → プライマリダメージ適用 → 貫通処理 - /// - /// ヒットした敵 - /// プレイヤー位置(貫通方向・ノックバック方向の算出用) - public void ProcessHitAuthority(ICombatTarget target, Vector3 playerPos) - { - if (!CalculateHit(out var damage, out _)) return; - - // プライマリターゲット - ApplyDamageWithKnockback(target, damage, playerPos); - - // 貫通処理 - if (_pierce > 0) - { - var targetPos = target.CenterPosition; - var direction = (targetPos - playerPos).normalized; - var origin = targetPos + direction * 0.1f; - ProcessPierce(origin, direction, target, playerPos, damage); - } - } - - /// - /// ローカルヒット処理(SP/Host用エントリポイント) - /// プロジェクタイルの弾道方向で貫通処理を行う - /// - /// ヒットした敵 - /// プレイヤー位置 - /// プロジェクタイル位置 - /// プロジェクタイル方向 - public void ProcessHitLocal(ICombatTarget target, Vector3 playerPos, Vector3 projectilePos, Vector3 projectileForward) - { - if (!CalculateHit(out var damage, out _)) return; - - // プライマリターゲット - ApplyDamageWithKnockback(target, damage, playerPos); - - // 貫通処理(弾道方向ベース) - if (_pierce > 0) - { - var origin = projectilePos + projectileForward * 0.1f; - ProcessPierce(origin, projectileForward, target, playerPos, damage); - } - } - - private bool CalculateHit(out int damage, out bool isCritical) - { - damage = 0; - isCritical = false; - - if (!RollProcRate()) return false; - - damage = Damage; - isCritical = RollCritical(); - if (isCritical) - damage = CalculateCriticalDamage(damage); - - return true; - } - - private void ApplyDamageWithKnockback(ICombatTarget target, int damage, Vector3 playerPos) - { - target.TakeDamage(damage); - - if (_knockback > 0) - { - var dir = (target.CenterPosition - playerPos).normalized; - target.ApplyKnockback(dir * _knockback); - } - } - - private void ProcessPierce( - Vector3 origin, - Vector3 direction, - ICombatTarget primaryTarget, - Vector3 playerPos, - int damage) - { - var hits = Physics.SphereCastAll( - origin, PierceDetectionRadius, direction, _range, - LayerMaskConstants.Enemy, QueryTriggerInteraction.Collide); - - System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance)); - - int pierceRemaining = _pierce; - for (int i = 0; i < hits.Length && pierceRemaining > 0; i++) - { - var target = hits[i].collider.GetComponentInParent(); - if (target == null || target == primaryTarget || target.IsDead) continue; - - target.TakeDamage(damage); - pierceRemaining--; - - // ヒットエフェクト - if (_vfxSpawner != null && !string.IsNullOrEmpty(_hitEffectAssetName)) - { - var hitPosition = hits[i].point != Vector3.zero - ? hits[i].point - : hits[i].collider.ClosestPoint(origin); - _vfxSpawner.SpawnEffect(_hitEffectAssetName, hitPosition, _hitEffectScale); - } - - // ノックバック - if (_knockback > 0) - { - var dir = (hits[i].collider.transform.position - playerPos).normalized; - target.ApplyKnockback(dir * _knockback); - } - } - } - - #endregion /// /// 武器リソースを解放する diff --git a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorWeaponManager.cs b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorWeaponManager.cs index ffe86b48..cebaedd1 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorWeaponManager.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVP/Survivor/Weapon/SurvivorWeaponManager.cs @@ -3,7 +3,6 @@ using System.Linq; using Cysharp.Threading.Tasks; using Game.Client.MasterData; -using Game.Shared.Playmode; using Game.Shared.Services; using R3; using UnityEngine; @@ -35,7 +34,7 @@ public class SurvivorWeaponManager : MonoBehaviour private Transform _owner; private float _damageMultiplier = 1f; private SurvivorWeaponVfxSpawner _vfxSpawner; - private Action _hitCallback; + private Action _hitCallback; // Events private readonly Subject _onWeaponAdded = new(); @@ -58,11 +57,7 @@ public async UniTask InitializeAsync(Transform owner, int startingWeaponId, floa _owner = owner; _damageMultiplier = damageMultiplier; - // サーバーではVFX不要(ParticleSystemRenderer がない環境でエラーになる) - if (!UnityPlaymodeHelper.IsServer()) - { - _vfxSpawner = new SurvivorWeaponVfxSpawner(transform, _assetService); - } + _vfxSpawner = new SurvivorWeaponVfxSpawner(transform, _assetService); // 初期武器を追加 if (startingWeaponId > 0) @@ -95,7 +90,7 @@ public async UniTask AddWeaponAsync(int weaponId) var weapon = SurvivorWeaponFactory.Create(_resolver, weaponMaster); await weapon.InitializeAsync(transform, _owner, _damageMultiplier, _vfxSpawner); - weapon.OnEnemyHitForServer = _hitCallback; + weapon.OnHitCallback = _hitCallback; _weapons.Add(weapon); _onWeaponAdded.OnNext(weapon); @@ -138,14 +133,14 @@ public void UpdateDamageMultiplier(float multiplier) } /// - /// ヒット報告コールバックを設定(クライアントモード用) + /// ヒットコールバックを設定(SP: ローカルダメージ, MP: RPC送信) /// - public void SetHitCallback(Action callback) + public void SetHitCallback(Action callback) { _hitCallback = callback; foreach (var weapon in _weapons) { - weapon.OnEnemyHitForServer = callback; + weapon.OnHitCallback = callback; } } @@ -285,9 +280,6 @@ public async UniTask ReplaceWeaponAsync(int removeWeaponId, int newWeaponI /// private void Update() { - // サーバーでは武器の自律発射を無効化(RPCのみ受付) - if (UnityPlaymodeHelper.IsServer()) return; - float deltaTime = Time.deltaTime; foreach (var weapon in _weapons) { diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/ISurvivorNetworkBridge.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/ISurvivorNetworkBridge.cs index ce91ac86..08571709 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/ISurvivorNetworkBridge.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/ISurvivorNetworkBridge.cs @@ -8,7 +8,7 @@ namespace Game.Shared.Network.Survivor public interface ISurvivorNetworkBridge { void BroadcastEnemyStates(SurvivorNetworkEnemyStateSnapshot[] snapshots); - void NotifyItemSpawned(int itemId, float posX, float posZ); + void NotifyItemSpawned(int itemId, float posX, float posY, float posZ); void NotifyItemDespawned(int itemId); } } diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkBridge.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkBridge.cs index e2624e09..73694502 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkBridge.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkBridge.cs @@ -11,9 +11,9 @@ public void BroadcastEnemyStates(SurvivorNetworkEnemyStateSnapshot[] snapshots) SurvivorNetworkEnemyState.Instance?.BroadcastEnemyStates(snapshots); } - public void NotifyItemSpawned(int itemId, float posX, float posZ) + public void NotifyItemSpawned(int itemId, float posX, float posY, float posZ) { - SurvivorNetworkItemSync.Instance?.SpawnItemClientRpc(itemId, posX, posZ); + SurvivorNetworkItemSync.Instance?.SpawnItemClientRpc(itemId, posX, posY, posZ); } public void NotifyItemDespawned(int itemId) diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkGameManager.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkGameManager.cs index 9587c99e..57663bad 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkGameManager.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkGameManager.cs @@ -60,10 +60,7 @@ public class SurvivorNetworkGameManager : NetworkBehaviour public void NotifyAllPlayersReadyClientRpc() { Debug.Log("[NetworkSurvivorGameManager] AllPlayersReady"); - if (!isServer) - { - _allPlayersReadyPub?.Publish(new SurvivorSignals.Session.AllPlayersReady()); - } + _allPlayersReadyPub?.Publish(new SurvivorSignals.Session.AllPlayersReady()); } /// @@ -74,10 +71,7 @@ public void NotifyAllPlayersReadyClientRpc() public void NotifyGameStartedClientRpc(float serverTime) { Debug.Log($"[NetworkSurvivorGameManager] GameStarted at serverTime={serverTime}"); - if (!isServer) - { - _gameStartedPub?.Publish(new SurvivorSignals.Session.GameStarted(serverTime)); - } + _gameStartedPub?.Publish(new SurvivorSignals.Session.GameStarted(serverTime)); } // ===================================================================== @@ -92,7 +86,7 @@ public void NotifyGameStartedClientRpc(float serverTime) [ClientRpc] public void NotifyPlayerDamagedClientRpc(FixedString64Bytes userId, int damage, int currentHp) { - if (!isServer && IsLocalPlayer(userId)) + if (IsLocalPlayer(userId)) { if (_playerDamagedPub == null) Debug.LogWarning("[NetworkSurvivorGameManager] _playerDamagedPub is NULL"); @@ -109,7 +103,8 @@ public void NotifyPlayerDamagedClientRpc(FixedString64Bytes userId, int damage, [ClientRpc] public void NotifyPlayerDiedClientRpc(FixedString64Bytes userId) { - if (!isServer && IsLocalPlayer(userId)) + Debug.Log($"[NetworkSurvivorGameManager] PlayerDied RPC received: userId={userId}"); + if (IsLocalPlayer(userId)) { _playerDiedPub?.Publish(new SurvivorSignals.Player.Died()); } @@ -124,13 +119,11 @@ public void NotifyItemCollectedClientRpc( FixedString64Bytes userId, int itemId, int itemType, int effectValue, int currentExperience, int experienceToNextLevel) { - if (!isServer) - { - _itemCollectedPub?.Publish( - new SurvivorSignals.Player.ItemCollected( - userId.ToString(), itemId, itemType, effectValue, - currentExperience, experienceToNextLevel)); - } + Debug.Log($"[NetworkSurvivorGameManager] ItemCollected RPC received: itemId={itemId}, type={itemType}, exp={currentExperience}"); + _itemCollectedPub?.Publish( + new SurvivorSignals.Player.ItemCollected( + userId.ToString(), itemId, itemType, effectValue, + currentExperience, experienceToNextLevel)); } /// @@ -143,12 +136,9 @@ public void NotifyPlayerLevelUpClientRpc( int experience, int experienceToNextLevel, SurvivorNetworkWeaponUpgradeOption[] options) { - if (!isServer) - { - _playerLeveledUpPub?.Publish( - new SurvivorSignals.Player.LeveledUp( - userId.ToString(), newLevel, experience, experienceToNextLevel, options)); - } + _playerLeveledUpPub?.Publish( + new SurvivorSignals.Player.LeveledUp( + userId.ToString(), newLevel, experience, experienceToNextLevel, options)); } /// @@ -158,11 +148,8 @@ public void NotifyPlayerLevelUpClientRpc( [ClientRpc] public void NotifyWeaponChangedClientRpc(FixedString64Bytes userId, int weaponId, int level, bool isNew) { - if (!isServer) - { - _weaponChangedPub?.Publish( - new SurvivorSignals.Player.WeaponChanged(userId.ToString(), weaponId, level, isNew)); - } + _weaponChangedPub?.Publish( + new SurvivorSignals.Player.WeaponChanged(userId.ToString(), weaponId, level, isNew)); } // ===================================================================== @@ -176,11 +163,8 @@ public void NotifyWeaponChangedClientRpc(FixedString64Bytes userId, int weaponId [ClientRpc] public void NotifyEnemyKilledClientRpc(FixedString64Bytes killerUserId, int enemyId, int scoreGained, int totalKills) { - if (!isServer) - { - _enemyKilledPub?.Publish( - new SurvivorSignals.Enemy.Killed(killerUserId.ToString(), enemyId, scoreGained, totalKills)); - } + _enemyKilledPub?.Publish( + new SurvivorSignals.Enemy.Killed(killerUserId.ToString(), enemyId, scoreGained, totalKills)); } // ===================================================================== @@ -194,14 +178,11 @@ public void NotifyEnemyKilledClientRpc(FixedString64Bytes killerUserId, int enem [ClientRpc] public void NotifyWaveClearedClientRpc(int waveNumber, int nextWaveNumber, int waveClearScore) { - Debug.Log($"[NetworkSurvivorGameManager] WaveCleared RPC received: wave={waveNumber}, next={nextWaveNumber}, isServer={isServer}"); - if (!isServer) - { - if (_waveClearedPub == null) - Debug.LogWarning("[NetworkSurvivorGameManager] _waveClearedPub is NULL"); - _waveClearedPub?.Publish( - new SurvivorSignals.Wave.Completed(waveNumber, waveClearScore)); - } + Debug.Log($"[NetworkSurvivorGameManager] WaveCleared RPC received: wave={waveNumber}, next={nextWaveNumber}"); + if (_waveClearedPub == null) + Debug.LogWarning("[NetworkSurvivorGameManager] _waveClearedPub is NULL"); + _waveClearedPub?.Publish( + new SurvivorSignals.Wave.Completed(waveNumber, waveClearScore)); } /// @@ -211,14 +192,11 @@ public void NotifyWaveClearedClientRpc(int waveNumber, int nextWaveNumber, int w [ClientRpc] public void NotifyWaveStartedClientRpc(int waveNumber, int targetKills, int totalEnemies) { - Debug.Log($"[NetworkSurvivorGameManager] WaveStarted RPC received: wave={waveNumber}, target={targetKills}, enemies={totalEnemies}, isServer={isServer}"); - if (!isServer) - { - if (_waveStartedPub == null) - Debug.LogWarning("[NetworkSurvivorGameManager] _waveStartedPub is NULL — VContainer injection failed"); - _waveStartedPub?.Publish( - new SurvivorSignals.Wave.Started(waveNumber, targetKills, totalEnemies)); - } + Debug.Log($"[NetworkSurvivorGameManager] WaveStarted RPC received: wave={waveNumber}, target={targetKills}, enemies={totalEnemies}"); + if (_waveStartedPub == null) + Debug.LogWarning("[NetworkSurvivorGameManager] _waveStartedPub is NULL — VContainer injection failed"); + _waveStartedPub?.Publish( + new SurvivorSignals.Wave.Started(waveNumber, targetKills, totalEnemies)); } /// @@ -228,10 +206,8 @@ public void NotifyWaveStartedClientRpc(int waveNumber, int targetKills, int tota [ClientRpc] public void NotifyAllWavesClearedClientRpc() { - if (!isServer) - { - _allWavesClearedPub?.Publish(new SurvivorSignals.Wave.AllCleared()); - } + Debug.Log("[NetworkSurvivorGameManager] AllWavesCleared RPC received"); + _allWavesClearedPub?.Publish(new SurvivorSignals.Wave.AllCleared()); } /// @@ -241,10 +217,7 @@ public void NotifyAllWavesClearedClientRpc() [ClientRpc] public void NotifyTimeUpClientRpc() { - if (!isServer) - { - _timeUpPub?.Publish(new SurvivorSignals.Wave.TimeUp()); - } + _timeUpPub?.Publish(new SurvivorSignals.Wave.TimeUp()); } // ===================================================================== @@ -260,13 +233,10 @@ public void NotifyTimeUpClientRpc() [ClientRpc] public void NotifyGameEndedClientRpc(SurvivorNetworkGameResult result) { - Debug.Log($"[NetworkSurvivorGameManager] GameEnded RPC received: victory={result.IsVictory}, isServer={isServer}"); - if (!isServer) - { - if (_gameEndedPub == null) - Debug.LogWarning("[NetworkSurvivorGameManager] _gameEndedPub is NULL"); - _gameEndedPub?.Publish(new SurvivorSignals.Game.Ended(result)); - } + Debug.Log($"[NetworkSurvivorGameManager] GameEnded RPC received: victory={result.IsVictory}"); + if (_gameEndedPub == null) + Debug.LogWarning("[NetworkSurvivorGameManager] _gameEndedPub is NULL"); + _gameEndedPub?.Publish(new SurvivorSignals.Game.Ended(result)); } // ===================================================================== @@ -280,11 +250,8 @@ public void NotifyGameEndedClientRpc(SurvivorNetworkGameResult result) [ClientRpc] public void NotifyGamePausedClientRpc(FixedString64Bytes requestedByUserId) { - if (!isServer) - { - _gamePausedPub?.Publish( - new SurvivorSignals.Game.Paused(requestedByUserId.ToString())); - } + _gamePausedPub?.Publish( + new SurvivorSignals.Game.Paused(requestedByUserId.ToString())); } /// @@ -294,10 +261,7 @@ public void NotifyGamePausedClientRpc(FixedString64Bytes requestedByUserId) [ClientRpc] public void NotifyGameResumedClientRpc() { - if (!isServer) - { - _gameResumedPub?.Publish(new SurvivorSignals.Game.Resumed()); - } + _gameResumedPub?.Publish(new SurvivorSignals.Game.Resumed()); } // ===================================================================== @@ -416,11 +380,8 @@ private void Update() [ClientRpc] public void NotifyPlayerConnectedClientRpc(FixedString64Bytes userId, FixedString64Bytes playerName) { - if (!isServer) - { - _playerConnectedPub?.Publish( - new SurvivorSignals.Connection.PlayerConnected(userId.ToString(), playerName.ToString())); - } + _playerConnectedPub?.Publish( + new SurvivorSignals.Connection.PlayerConnected(userId.ToString(), playerName.ToString())); } /// @@ -430,11 +391,8 @@ public void NotifyPlayerConnectedClientRpc(FixedString64Bytes userId, FixedStrin [ClientRpc] public void NotifyPlayerDisconnectedClientRpc(FixedString64Bytes userId, FixedString64Bytes playerName) { - if (!isServer) - { - _playerDisconnectedPub?.Publish( - new SurvivorSignals.Connection.PlayerDisconnected(userId.ToString(), playerName.ToString())); - } + _playerDisconnectedPub?.Publish( + new SurvivorSignals.Connection.PlayerDisconnected(userId.ToString(), playerName.ToString())); } // ===================================================================== @@ -549,23 +507,20 @@ public override void OnStartClient() Instance = this; // VContainer 注入診断: IPublisher が null の場合、ClientRpc → MessagePipe パスが機能しない - if (!isServer) - { - var nullPubs = new System.Text.StringBuilder(); - if (_allPlayersReadyPub == null) nullPubs.Append("AllPlayersReady,"); - if (_gameStartedPub == null) nullPubs.Append("GameStarted,"); - if (_gameEndedPub == null) nullPubs.Append("GameEnded,"); - if (_playerDamagedPub == null) nullPubs.Append("PlayerDamaged,"); - if (_playerDiedPub == null) nullPubs.Append("PlayerDied,"); - if (_waveStartedPub == null) nullPubs.Append("WaveStarted,"); - if (_waveClearedPub == null) nullPubs.Append("WaveCleared,"); - if (_enemyKilledPub == null) nullPubs.Append("EnemyKilled,"); - - if (nullPubs.Length > 0) - Debug.LogWarning($"[NetworkSurvivorGameManager] NULL IPublisher on client: {nullPubs}"); - else - Debug.Log("[NetworkSurvivorGameManager] All IPublisher fields injected OK"); - } + var nullPubs = new System.Text.StringBuilder(); + if (_allPlayersReadyPub == null) nullPubs.Append("AllPlayersReady,"); + if (_gameStartedPub == null) nullPubs.Append("GameStarted,"); + if (_gameEndedPub == null) nullPubs.Append("GameEnded,"); + if (_playerDamagedPub == null) nullPubs.Append("PlayerDamaged,"); + if (_playerDiedPub == null) nullPubs.Append("PlayerDied,"); + if (_waveStartedPub == null) nullPubs.Append("WaveStarted,"); + if (_waveClearedPub == null) nullPubs.Append("WaveCleared,"); + if (_enemyKilledPub == null) nullPubs.Append("EnemyKilled,"); + + if (nullPubs.Length > 0) + Debug.LogWarning($"[NetworkSurvivorGameManager] NULL IPublisher on client: {nullPubs}"); + else + Debug.Log("[NetworkSurvivorGameManager] All IPublisher fields injected OK"); Debug.Log("[NetworkSurvivorGameManager] Spawned on client"); } diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkItemSync.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkItemSync.cs index 1c0853d4..ce2a4cbf 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkItemSync.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorNetworkItemSync.cs @@ -20,12 +20,10 @@ public class SurvivorNetworkItemSync : NetworkBehaviour // --- アイテมスポーン --- [ClientRpc] - public void SpawnItemClientRpc(int itemId, float posX, float posZ) + public void SpawnItemClientRpc(int itemId, float posX, float posY, float posZ) { - if (!isServer) - { - _itemSpawnedPub?.Publish(new SurvivorSignals.Item.Spawned(itemId, posX, posZ)); - } + Debug.Log($"[NetworkSurvivorItemSync] SpawnItem RPC: itemId={itemId}, pos=({posX},{posY},{posZ})"); + _itemSpawnedPub?.Publish(new SurvivorSignals.Item.Spawned(itemId, posX, posY, posZ)); } // --- アイテム回収(NetworkSurvivorGameManager.NotifyItemCollectedClientRpc で通知) --- @@ -33,10 +31,8 @@ public void SpawnItemClientRpc(int itemId, float posX, float posZ) [ClientRpc] public void DespawnItemClientRpc(int itemId) { - if (!isServer) - { - _itemDespawnedPub?.Publish(new SurvivorSignals.Item.Despawned(itemId)); - } + Debug.Log($"[NetworkSurvivorItemSync] DespawnItem RPC: itemId={itemId}"); + _itemDespawnedPub?.Publish(new SurvivorSignals.Item.Despawned(itemId)); } // --- ライフサイクル --- @@ -51,12 +47,9 @@ public override void OnStartClient() { DontDestroyOnLoad(gameObject); Instance = this; - if (!isServer) - { - if (_itemSpawnedPub == null || _itemDespawnedPub == null) - Debug.LogWarning($"[NetworkSurvivorItemSync] NULL publishers on client: spawned={_itemSpawnedPub != null}, despawned={_itemDespawnedPub != null}"); - } - Debug.Log($"[NetworkSurvivorItemSync] Spawned on client (isServer={isServer})"); + if (_itemSpawnedPub == null || _itemDespawnedPub == null) + Debug.LogWarning($"[NetworkSurvivorItemSync] NULL publishers: spawned={_itemSpawnedPub != null}, despawned={_itemDespawnedPub != null}"); + Debug.Log("[NetworkSurvivorItemSync] Spawned on client"); } public override void OnStopServer() diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorUnityServerSession.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorUnityServerSession.cs index ec8b1be2..9f04a38c 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorUnityServerSession.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Network/Survivor/SurvivorUnityServerSession.cs @@ -4,6 +4,7 @@ using Game.Shared.Signals.Survivor; using MessagePipe; using Mirror; +using R3; using Unity.Collections; using UnityEngine; using VContainer; @@ -21,7 +22,12 @@ public class SurvivorUnityServerSession : MonoBehaviour public static SurvivorUnityServerSession Instance { get; private set; } private void Awake() { Instance = this; } - private void OnDestroy() { if (Instance == this) Instance = null; } + + private void OnDestroy() + { + if (Instance == this) Instance = null; + _onAllPlayersReady.Dispose(); + } /// /// クライアントが明示的に退出を通知した際に呼ばれる。 @@ -35,9 +41,11 @@ public static void NotifyPlayerQuit() } [Inject] private IObjectResolver _resolver; - [Inject] private IPublisher _allPlayersReadyPub; [Inject] private IPublisher _allPlayersDisconnectedPub; + private readonly Subject _onAllPlayersReady = new(); + public Observable OnAllPlayersReady => _onAllPlayersReady; + private int _stageId; private bool _stageLoaded; private bool _sessionStarted; @@ -232,9 +240,8 @@ private async UniTaskVoid NotifyPlayersReadyAsync() gm.NotifyGameStartedClientRpc(Time.time); } - // Server/Host: ClientRpc はサーバーローカルでは isServer ガードで Publish されないため、 - // MessagePipe 経由で直接シグナルを発火する - _allPlayersReadyPub?.Publish(new SurvivorSignals.Session.AllPlayersReady()); + // サーバー内部通知(ServerGameLoop が購読) + _onAllPlayersReady.OnNext(new SurvivorSignals.Session.AllPlayersReady()); Debug.Log("[SurvivorServerSession] AllPlayersReady + GameStarted sent"); } diff --git a/src/Game.Client/Assets/Programs/Runtime/Shared/Signals/SurvivorSignals.cs b/src/Game.Client/Assets/Programs/Runtime/Shared/Signals/SurvivorSignals.cs index c9481046..e7058dee 100644 --- a/src/Game.Client/Assets/Programs/Runtime/Shared/Signals/SurvivorSignals.cs +++ b/src/Game.Client/Assets/Programs/Runtime/Shared/Signals/SurvivorSignals.cs @@ -258,12 +258,14 @@ public readonly struct Spawned { public readonly int ItemId; public readonly float PosX; + public readonly float PosY; public readonly float PosZ; - public Spawned(int itemId, float posX, float posZ) + public Spawned(int itemId, float posX, float posY, float posZ) { ItemId = itemId; PosX = posX; + PosY = posY; PosZ = posZ; } } diff --git a/src/Game.Client/Packages/manifest.json b/src/Game.Client/Packages/manifest.json index 72bbc6b9..900fd34c 100644 --- a/src/Game.Client/Packages/manifest.json +++ b/src/Game.Client/Packages/manifest.json @@ -9,7 +9,7 @@ } ], "dependencies": { - "com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v9.4.7", + "com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v9.5.3", "com.cysharp.magiconion.client.unity": "https://github.com/Cysharp/MagicOnion.git?path=src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion.Client.Unity#7.0.9", "com.cysharp.memorypack": "https://github.com/Cysharp/MemoryPack.git?path=src/MemoryPack.Unity/Assets/MemoryPack.Unity", "com.cysharp.messagepipe": "https://github.com/Cysharp/MessagePipe.git?path=src/MessagePipe.Unity/Assets/Plugins/MessagePipe", diff --git a/src/Game.Client/Packages/packages-lock.json b/src/Game.Client/Packages/packages-lock.json index 76b8d4c2..ac4d989a 100644 --- a/src/Game.Client/Packages/packages-lock.json +++ b/src/Game.Client/Packages/packages-lock.json @@ -1,14 +1,14 @@ { "dependencies": { "com.coplaydev.unity-mcp": { - "version": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v9.4.7", + "version": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v9.5.3", "depth": 0, "source": "git", "dependencies": { "com.unity.nuget.newtonsoft-json": "3.0.2", "com.unity.test-framework": "1.1.31" }, - "hash": "d76a8df311c48263872be0cd3e6a8ef1a0fa8e59" + "hash": "a7c715fb1f2b7741b46f5ee48c70aa3bb1189bd2" }, "com.cysharp.magiconion.client.unity": { "version": "https://github.com/Cysharp/MagicOnion.git?path=src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion.Client.Unity#7.0.9",