From d1e0b55cf7e78b0db7aba77f23861f63a3c746c8 Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Thu, 20 Aug 2026 19:32:16 +0900 Subject: [PATCH 1/2] =?UTF-8?q?else=E5=90=88=E6=88=90=E5=95=8F=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Runtime/MVC/Horror/Player/HorrorPlayerController.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs index 6f0feb7a7..0e5c8034a 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Player/HorrorPlayerController.cs @@ -900,7 +900,10 @@ private void UpdateCrouchInput() { if (_isCrouching) { - if (CanStandUp()) _isCrouching = false; + if (CanStandUp()) + { + _isCrouching = false; + } } else { From e542dddec9005c649353510989cc44f2b96c00ed Mon Sep 17 00:00:00 2001 From: "Rei.K" Date: Thu, 20 Aug 2026 19:54:38 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=99=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=83=AA/=E8=A3=85=E5=82=99=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E3=82=A4=E3=83=99=E3=83=B3=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MVC/Horror/HorrorEquipmentServiceTests.cs | 184 ++++++++++++++++++ .../MVC/Horror/HorrorInventoryServiceTests.cs | 128 ++++++++++++ .../HorrorEquipmentShortcutDialogComponent.cs | 19 +- .../Horror/Dialogs/HorrorInventoryDialog.cs | 2 - .../Dialogs/HorrorInventoryDialogComponent.cs | 27 ++- .../MVC/Horror/Inventory/HorrorCraftView.cs | 29 ++- .../Inventory/HorrorInventorySlotView.cs | 7 - .../Horror/Services/HorrorEquipmentService.cs | 26 ++- .../Horror/Services/HorrorInventoryService.cs | 20 +- .../Interfaces/IHorrorEquipmentService.cs | 4 + .../Interfaces/IHorrorInventoryService.cs | 4 + 11 files changed, 386 insertions(+), 64 deletions(-) diff --git a/src/Game.Client/Assets/Programs/Editor/Tests/MVC/Horror/HorrorEquipmentServiceTests.cs b/src/Game.Client/Assets/Programs/Editor/Tests/MVC/Horror/HorrorEquipmentServiceTests.cs index 6663d74d4..fbf83be13 100644 --- a/src/Game.Client/Assets/Programs/Editor/Tests/MVC/Horror/HorrorEquipmentServiceTests.cs +++ b/src/Game.Client/Assets/Programs/Editor/Tests/MVC/Horror/HorrorEquipmentServiceTests.cs @@ -12,6 +12,7 @@ using Game.Shared.Services; using NSubstitute; using NUnit.Framework; +using R3; using UnityEditor; using UnityEngine; using UnityEngine.TestTools; @@ -692,5 +693,188 @@ public async Task GetEquippableWeaponMasters_AppendsEquippedNotInSlots() Assert.That(masters[0].Id, Is.EqualTo(5)); Assert.That(masters[1].Id, Is.EqualTo(7), "装備中は末尾に合流する"); } + + [Test] + public async Task TryEquip_Success_EmitsEquipmentChangedOnce() + { + await LoadDefaultData(); + SetupRealDatabase(5); + _mockInventory.HasObject(ObjectCategory.Weapon, 5).Returns(true); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + var ok = _service.TryEquip(ObjectCategory.Weapon, 5); + + Assert.That(ok, Is.True); + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task TryEquip_NotPossessed_DoesNotEmitEquipmentChanged() + { + await LoadDefaultData(); + _mockInventory.HasObject(ObjectCategory.Weapon, 5).Returns(false); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + var ok = _service.TryEquip(ObjectCategory.Weapon, 5); + + Assert.That(ok, Is.False); + Assert.That(emitted, Is.EqualTo(0)); + } + + [Test] + public async Task Unequip_WhenEquipped_EmitsEquipmentChangedOnce() + { + await LoadDefaultData(); + SetupRealDatabase(5); + _repository.Data.Equipment.ObjectCategory = ObjectCategory.Weapon; + _repository.Data.Equipment.Id = 5; + _service.ResolveEquippedWeaponMaster(); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + _service.Unequip(); + + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task Unequip_WhenNotEquipped_DoesNotEmitEquipmentChanged() + { + await LoadDefaultData(); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + _service.Unequip(); + + Assert.That(emitted, Is.EqualTo(0)); + } + + [Test] + public async Task ResolveEquippedWeaponMaster_MasterNotFound_EmitsEquipmentChangedOnce() + { + await LoadDefaultData(); + SetupRealDatabase(7); // 記録された Id=5 はテーブル未登録 → 未装備へ戻す修復パス + _repository.Data.Equipment.ObjectCategory = ObjectCategory.Weapon; + _repository.Data.Equipment.Id = 5; + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + LogAssert.Expect(LogType.Error, "装備中の武器マスターが見つかりません Id=5。未装備へ戻します"); + _service.ResolveEquippedWeaponMaster(); + + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task ResolveEquippedWeaponMaster_Unequipped_DoesNotEmitEquipmentChanged() + { + await LoadDefaultData(); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + _service.ResolveEquippedWeaponMaster(); + + Assert.That(emitted, Is.EqualTo(0)); + } + + [Test] + public async Task TrySetSlot_Success_EmitsEquipmentChangedOnce() + { + await LoadDefaultData(); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + var ok = _service.TrySetSlot(1, ObjectCategory.Weapon, 5); + + Assert.That(ok, Is.True); + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task TryAssignSlot_MoveBetweenSlots_EmitsEquipmentChangedOnce() + { + await LoadDefaultData(); + _service.TrySetSlot(0, ObjectCategory.Weapon, 5); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + // 2 スロット(移動元・移動先)が変わっても発行は操作単位で 1 回 + var ok = _service.TryAssignSlot(1, ObjectCategory.Weapon, 5); + + Assert.That(ok, Is.True); + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task TryAssignSlot_SameSlot_DoesNotEmitEquipmentChanged() + { + await LoadDefaultData(); + _service.TrySetSlot(2, ObjectCategory.Weapon, 5); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + var ok = _service.TryAssignSlot(2, ObjectCategory.Weapon, 5); + + Assert.That(ok, Is.False); + Assert.That(emitted, Is.EqualTo(0)); + } + + [Test] + public async Task TryAutoAssignSlot_Success_EmitsEquipmentChangedOnce() + { + await LoadDefaultData(); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + // TrySetSlot 経由の登録でも二重発行しない + var ok = _service.TryAutoAssignSlot(ObjectCategory.Weapon, 5); + + Assert.That(ok, Is.True); + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task ClearSlot_Success_EmitsEquipmentChangedOnce() + { + await LoadDefaultData(); + _service.TrySetSlot(2, ObjectCategory.Item, 3); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + var ok = _service.ClearSlot(2); + + Assert.That(ok, Is.True); + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task TryClearSlotOf_Registered_EmitsEquipmentChangedOnce() + { + await LoadDefaultData(); + _service.TrySetSlot(1, ObjectCategory.Weapon, 2); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + var ok = _service.TryClearSlotOf(ObjectCategory.Weapon, 2); + + Assert.That(ok, Is.True); + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task SetMagazineCount_Always_DoesNotEmitEquipmentChanged() + { + await LoadDefaultData(); + int emitted = 0; + using var sub = _service.EquipmentChanged.Subscribe(_ => emitted++); + + // 弾倉残弾は発行対象外(発砲ごとに変わるため、装備変更の購読者に通知しない) + _service.SetMagazineCount(5, 12); + + Assert.That(emitted, Is.EqualTo(0)); + Assert.That(_repository.IsDirty, Is.True, "Dirty にはなる(保存対象ではある)"); + } } } diff --git a/src/Game.Client/Assets/Programs/Editor/Tests/MVC/Horror/HorrorInventoryServiceTests.cs b/src/Game.Client/Assets/Programs/Editor/Tests/MVC/Horror/HorrorInventoryServiceTests.cs index 3c6e1af5b..a07858830 100644 --- a/src/Game.Client/Assets/Programs/Editor/Tests/MVC/Horror/HorrorInventoryServiceTests.cs +++ b/src/Game.Client/Assets/Programs/Editor/Tests/MVC/Horror/HorrorInventoryServiceTests.cs @@ -11,6 +11,7 @@ using Game.Shared.Services; using NSubstitute; using NUnit.Framework; +using R3; namespace Game.Tests.MVC.Horror { @@ -720,5 +721,132 @@ public async Task CanAddAfterConsume_NoConsumption_JudgesCapacityOnly() _service.CanAddAfterConsume(null, ObjectCategory.Item, 4, 1, 1), Is.True, "空きが生まれた"); } + + [Test] + public async Task TryAdd_Success_EmitsSlotsChangedOnce() + { + await LoadDefaultData(); + int emitted = 0; + using var sub = _service.SlotsChanged.Subscribe(_ => emitted++); + + // 分割配置(3 スロットに跨る)でも発行は操作単位で 1 回 + var ok = _service.TryAdd(ObjectCategory.Item, 3, 25, 10); + + Assert.That(ok, Is.True); + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task TryAdd_InsufficientCapacity_DoesNotEmitSlotsChanged() + { + await LoadDefaultData(); + FillSlotsWithDummies(HorrorInventoryConstants.MaxSlotCount); + int emitted = 0; + using var sub = _service.SlotsChanged.Subscribe(_ => emitted++); + + var ok = _service.TryAdd(ObjectCategory.Item, 3, 1, 10); + + Assert.That(ok, Is.False); + Assert.That(emitted, Is.EqualTo(0)); + } + + [Test] + public async Task TryConsume_Success_EmitsSlotsChangedOnce() + { + await LoadDefaultData(); + AddSlotDirect(ObjectCategory.Item, 3, 10, 0); + AddSlotDirect(ObjectCategory.Item, 3, 5, 1); + int emitted = 0; + using var sub = _service.SlotsChanged.Subscribe(_ => emitted++); + + // 複数スロットに跨る消費でも発行は操作単位で 1 回 + var ok = _service.TryConsume(ObjectCategory.Item, 3, 12); + + Assert.That(ok, Is.True); + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task TryConsume_InsufficientCount_DoesNotEmitSlotsChanged() + { + await LoadDefaultData(); + AddSlotDirect(ObjectCategory.Item, 3, 2, 0); + int emitted = 0; + using var sub = _service.SlotsChanged.Subscribe(_ => emitted++); + + var ok = _service.TryConsume(ObjectCategory.Item, 3, 5); + + Assert.That(ok, Is.False); + Assert.That(emitted, Is.EqualTo(0)); + } + + [Test] + public async Task TryConsumeAt_Success_EmitsSlotsChangedOnce() + { + await LoadDefaultData(); + AddSlotDirect(ObjectCategory.Item, 3, 5, 0); + int emitted = 0; + using var sub = _service.SlotsChanged.Subscribe(_ => emitted++); + + var ok = _service.TryConsumeAt(ObjectCategory.Item, 3, 0, 1); + + Assert.That(ok, Is.True); + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task TryConsumeAt_EmptyPosition_DoesNotEmitSlotsChanged() + { + await LoadDefaultData(); + AddSlotDirect(ObjectCategory.Item, 3, 5, 0); + int emitted = 0; + using var sub = _service.SlotsChanged.Subscribe(_ => emitted++); + + var ok = _service.TryConsumeAt(ObjectCategory.Item, 3, 1, 1); + + Assert.That(ok, Is.False); + Assert.That(emitted, Is.EqualTo(0)); + } + + [Test] + public async Task DiscardSlot_Success_EmitsSlotsChangedOnce() + { + await LoadDefaultData(); + AddSlotDirect(ObjectCategory.Item, 3, 1); + int emitted = 0; + using var sub = _service.SlotsChanged.Subscribe(_ => emitted++); + + var ok = _service.DiscardSlot(0); + + Assert.That(ok, Is.True); + Assert.That(emitted, Is.EqualTo(1)); + } + + [Test] + public async Task DiscardSlot_EmptyPosition_DoesNotEmitSlotsChanged() + { + await LoadDefaultData(); + int emitted = 0; + using var sub = _service.SlotsChanged.Subscribe(_ => emitted++); + + var ok = _service.DiscardSlot(0); + + Assert.That(ok, Is.False); + Assert.That(emitted, Is.EqualTo(0)); + } + + [Test] + public async Task CanAddAfterConsume_DoesNotEmitSlotsChanged() + { + await LoadDefaultData(); + AddSlotDirect(ObjectCategory.Item, 3, 2, 0); + int emitted = 0; + using var sub = _service.SlotsChanged.Subscribe(_ => emitted++); + + // 判定系はインベントリを変更しないため発行しない + _service.CanAddAfterConsume(Amounts((3, 2)), ObjectCategory.Item, 4, 1, 1); + + Assert.That(emitted, Is.EqualTo(0)); + } } } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorEquipmentShortcutDialogComponent.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorEquipmentShortcutDialogComponent.cs index 257912cc8..77d8e92fa 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorEquipmentShortcutDialogComponent.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorEquipmentShortcutDialogComponent.cs @@ -3,7 +3,6 @@ using Game.Horror.Services.Interfaces; using Game.MVC.Core.Scenes; using Game.Shared.Interfaces; -using Game.Shared.Services; using R3; using UnityEngine; @@ -47,22 +46,22 @@ public void Initialize(IObjectInfo target) // 初期フォーカスを先頭スロットへ if (_slots.Length > 0) _inputService.SetSelectedGameObject(_slots[0].gameObject); + + _equipmentService.EquipmentChanged + .Subscribe(_ => RefreshAllSlots()) + .AddTo(Disposables); } - /// 現在選択中スロットの登録を外す(Dialog の Remove 入力から呼ぶ)。 + /// 現在選択中スロットの登録を外す(Dialog の Remove 入力から呼ぶ)。表示は EquipmentChanged の購読で追従する。 public void RemoveCurrent() - { - if (_equipmentService.ClearSlot(_currentIndex)) - _slots[_currentIndex].SetEmpty(); - } + => _equipmentService.ClearSlot(_currentIndex); - // 対象アイテムを指定スロットへ登録し、表示を更新する。 - // Assign は既登録なら移動/入替、未登録なら上書きするため、影響が2スロットに及ぶ。全スロット再表示する。 + // 対象アイテムを指定スロットへ登録する。Assign は既登録なら移動/入替、未登録なら上書きで + // 影響が2スロットに及ぶが、表示は EquipmentChanged の購読(全スロット再表示)で追従する。 private void Register(int index) { if (_target == null) return; - if (_equipmentService.TryAssignSlot(index, _target.ObjectCategory, _target.ObjectId)) - RefreshAllSlots(); + _equipmentService.TryAssignSlot(index, _target.ObjectCategory, _target.ObjectId); } private void RefreshAllSlots() diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorInventoryDialog.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorInventoryDialog.cs index 27c810b84..f1f04b8bb 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorInventoryDialog.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorInventoryDialog.cs @@ -122,14 +122,12 @@ public override UniTask Startup() if (!result) return; _inventoryService.DiscardSlot(ctx.SlotView.SlotIndex); - SceneComponent.ApplySlots(); break; case ContextActionType.Equip: _result = new HorrorInventoryResult { EquipCategory = info.ObjectCategory, EquipId = info.ObjectId }; break; case ContextActionType.Shortcut: await HorrorEquipmentShortcutDialog.RunAsync(info); - SceneComponent.RefreshSlots(); break; } }) diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorInventoryDialogComponent.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorInventoryDialogComponent.cs index f1f8516fe..a6b737e77 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorInventoryDialogComponent.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Dialogs/HorrorInventoryDialogComponent.cs @@ -31,6 +31,7 @@ public class HorrorInventoryDialogComponent : GameSceneComponent private IInputSystemService _inputService; private IHorrorInventoryService _inventoryService; private IHorrorKeyItemService _keyItemService; + private IHorrorEquipmentService _equipmentService; private HorrorInventorySlotView _selectedSlot; @@ -46,6 +47,7 @@ public void Initialize() _inputService = GameServiceManager.Resolve(); _inventoryService = GameServiceManager.Resolve(); _keyItemService = GameServiceManager.Resolve(); + _equipmentService = GameServiceManager.Resolve(); _tabGroup.Initialize(); BindSlots(); @@ -61,6 +63,14 @@ public void Initialize() .Subscribe(_ => OnSubmenuClosed()) .AddTo(Disposables); } + + _inventoryService.SlotsChanged + .ThrottleLastFrame(1) + .Subscribe(_ => ApplySlots()) + .AddTo(Disposables); + _equipmentService.EquipmentChanged + .Subscribe(_ => ApplySlots()) + .AddTo(Disposables); } public void NextTab() => _tabGroup.NextTab(); @@ -89,7 +99,7 @@ private void BindSlots() /// インベントリデータをスロット表示へ反映する(再入可能)。 /// スロット破棄などでデータが変わった後に呼び、グリッドと詳細ペインを最新化する。 /// - public void ApplySlots() + private void ApplySlots() { // 位置(SlotNo)→行の一時テーブルを構築して View と 1:1 で対応させる。範囲外の行は表示しない(正規化後は発生しない) var rows = new HorrorInventorySlotData[_slots.Length]; @@ -111,16 +121,6 @@ public void ApplySlots() UpdateDetail(_selectedSlot); } - // 入力デバイス変更などによるアイコンの再解決のみ行う(個数テキストは更新しない)。 - // データ変更の反映には ApplySlots を使うこと。 - public void RefreshSlots() - { - for (int i = 0; i < _slots.Length; i++) - { - _slots[i].RefreshSlot(); - } - } - private void UpdateDetail(HorrorInventorySlotView slot) { _selectedSlot = slot; @@ -199,11 +199,6 @@ private void BindCraft() if (_craftView == null) return; _craftView.Initialize(); - - // クラフトはインベントリの中身を変えるため、グリッド表示へ反映する - _craftView.OnCrafted - .Subscribe(_ => ApplySlots()) - .AddTo(Disposables); } #endregion diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Inventory/HorrorCraftView.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Inventory/HorrorCraftView.cs index 0c0e351e5..d6a2d7ab2 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Inventory/HorrorCraftView.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Inventory/HorrorCraftView.cs @@ -7,7 +7,6 @@ using R3; using TMPro; using UnityEngine; -using UnityEngine.InputSystem; using UnityEngine.UI; namespace Game.Horror.Inventory @@ -37,13 +36,7 @@ public class HorrorCraftView : MonoBehaviour #endregion - private readonly Subject _onCrafted = new(); - - /// クラフトが成立したときに通知する(インベントリ表示の再反映用)。 - public Observable OnCrafted => _onCrafted; - private readonly List _recipeViews = new(); - private readonly List _materialViews = new(); private readonly CompositeDisposable _disposables = new(); private IInputSystemService _inputService; @@ -74,6 +67,16 @@ public void Initialize() .Subscribe(_ => UpdateDetail(_selected)) .AddTo(_disposables); + // 在庫の変化(自クラフト・他タブでの使用や破棄)に所持数と素材表示を追従させる + _inventoryService.SlotsChanged + .ThrottleLastFrame(1) // 同一フレームの連続変更(クラフト=素材数+1回発行)を最終状態1回に合流する + .Subscribe(_ => + { + RefreshPossessedCounts(); + UpdateDetail(_selected); + }) + .AddTo(_disposables); + BuildRecipes(); ValidateHoldGauge(); SetHoldProgress(0f); @@ -160,8 +163,6 @@ private void BuildMaterials(int craftId) child.gameObject.SafeDestroy(); } - _materialViews.Clear(); - foreach (var material in _craftService.GetMaterials(craftId)) { var view = Instantiate(_materialPrefab, _materialContentRoot); @@ -171,7 +172,6 @@ private void BuildMaterials(int craftId) material.ObjectId, material.Count, _inventoryService.GetCount(material.ObjectCategory, material.ObjectId)); - _materialViews.Add(view); } } @@ -211,13 +211,7 @@ private void Execute() ResetHold(); _awaitRelease = true; - - if (!_craftService.TryCraft(craftId)) - return; - - RefreshPossessedCounts(); - UpdateDetail(_selected); - _onCrafted.OnNext(Unit.Default); + _craftService.TryCraft(craftId); } private void ResetHold() @@ -264,7 +258,6 @@ private void OnEnable() private void OnDestroy() { _disposables.Dispose(); - _onCrafted.Dispose(); } } } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Inventory/HorrorInventorySlotView.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Inventory/HorrorInventorySlotView.cs index e856b264a..83016aa1a 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Inventory/HorrorInventorySlotView.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Inventory/HorrorInventorySlotView.cs @@ -3,7 +3,6 @@ using Game.Horror.Services.Interfaces; using Game.Shared.Enums; using Game.Shared.Interfaces; -using Game.Shared.Services; using R3; using R3.Triggers; using TMPro; @@ -85,12 +84,6 @@ public void SetEmpty() SetInputActionIcon(SlotInfo); } - public void RefreshSlot() - { - SetIcon(SlotInfo?.IconAssetName); - SetInputActionIcon(SlotInfo); - } - private void SetIcon(string assetName) { Sprite sprite = null; diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/HorrorEquipmentService.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/HorrorEquipmentService.cs index 76893b8a0..c561f8293 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/HorrorEquipmentService.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/HorrorEquipmentService.cs @@ -24,6 +24,9 @@ public class HorrorEquipmentService : IHorrorEquipmentService private HorrorWeaponMaster _equippedWeaponMaster; + private readonly Subject _equipmentChanged = new(); + public Observable EquipmentChanged => _equipmentChanged; + public HorrorEquipmentService(IHorrorSaveRepository repository, IHorrorInventoryService inventoryService, IScriptableDatabaseService databaseService) { _repository = repository; @@ -31,6 +34,15 @@ public HorrorEquipmentService(IHorrorSaveRepository repository, IHorrorInventory _databaseService = databaseService; } + /// ゲームモード終了時に変更通知の Subject を破棄する。 + public void Shutdown() => _equipmentChanged.Dispose(); + + private void MarkDirtyAndNotify() + { + _repository.MarkDirty(); + _equipmentChanged.OnNext(Unit.Default); + } + /// /// 指定 (SlotType, Id) が装備可能か判定する。装備対象は Weapon のみで、かつ所持している必要がある。 /// @@ -56,7 +68,7 @@ public bool TryEquip(ObjectCategory type, int id) data.ObjectCategory = type; data.Id = id; _equippedWeaponMaster = master; - _repository.MarkDirty(); + MarkDirtyAndNotify(); return true; } @@ -74,7 +86,7 @@ public void Unequip() data.ObjectCategory = ObjectCategory.None; data.Id = 0; - _repository.MarkDirty(); + MarkDirtyAndNotify(); } /// 現在装備中の (SlotType, Id) を取得する。未装備または未ロードなら false。 @@ -114,7 +126,7 @@ public void ResolveEquippedWeaponMaster() Debug.LogError($"装備中の武器マスターが見つかりません Id={data.Id}。未装備へ戻します"); data.ObjectCategory = ObjectCategory.None; data.Id = 0; - _repository.MarkDirty(); + MarkDirtyAndNotify(); } /// @@ -155,7 +167,7 @@ public bool TrySetSlot(int index, ObjectCategory slotType, int id) var slot = data.Slots[index]; slot.ObjectCategory = slotType; slot.Id = id; - _repository.MarkDirty(); + MarkDirtyAndNotify(); return true; } @@ -185,7 +197,7 @@ public bool TryAssignSlot(int destIndex, ObjectCategory slotType, int id) // dest に対象を置く(未登録時は上書き) dest.ObjectCategory = slotType; dest.Id = id; - _repository.MarkDirty(); + MarkDirtyAndNotify(); return true; } @@ -236,7 +248,7 @@ public bool ClearSlot(int index) var slot = data.Slots[index]; slot.ObjectCategory = ObjectCategory.None; slot.Id = 0; - _repository.MarkDirty(); + MarkDirtyAndNotify(); return true; } @@ -258,7 +270,7 @@ public bool TryClearSlotOf(ObjectCategory category, int id) var slot = data.Slots[index]; slot.ObjectCategory = ObjectCategory.None; slot.Id = 0; - _repository.MarkDirty(); + MarkDirtyAndNotify(); return true; } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/HorrorInventoryService.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/HorrorInventoryService.cs index 9ce39ece9..c662f9051 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/HorrorInventoryService.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/HorrorInventoryService.cs @@ -6,6 +6,7 @@ using Game.Horror.SaveData; using Game.Horror.Services.Interfaces; using Game.Shared.Enums; +using R3; using UnityEngine; namespace Game.Horror.Services @@ -25,6 +26,9 @@ public class HorrorInventoryService : IHorrorInventoryService private readonly IReadOnlyList _emptySlots = Array.Empty(); + private readonly Subject _slotsChanged = new(); + public Observable SlotsChanged => _slotsChanged; + private readonly IHorrorSaveRepository _repository; public HorrorInventoryService(IHorrorSaveRepository repository) @@ -32,6 +36,14 @@ public HorrorInventoryService(IHorrorSaveRepository repository) _repository = repository; } + public void Shutdown() => _slotsChanged.Dispose(); + + private void MarkDirtyAndNotify() + { + _repository.MarkDirty(); + _slotsChanged.OnNext(Unit.Default); + } + /// /// アイテムをインベントリに追加する。既存スタックを SlotNo 昇順に maxCount まで充填し、 /// 超過分は最小の空き位置から新規スタックとして分割配置する。 @@ -84,7 +96,7 @@ public bool TryAdd(ObjectCategory category, int id, int addCount, int maxCount) remaining -= fill; } - _repository.MarkDirty(); + MarkDirtyAndNotify(); return true; } @@ -142,7 +154,7 @@ public bool TryConsume(ObjectCategory category, int id, int count) data.Slots.RemoveAll(s => s.ObjectCategory == category && s.Id == id && s.Count <= 0); - _repository.MarkDirty(); + MarkDirtyAndNotify(); return true; } @@ -169,7 +181,7 @@ public bool TryConsumeAt(ObjectCategory category, int id, int slotNo, int count) if (slot.Count <= 0) data.Slots.RemoveAt(index); - _repository.MarkDirty(); + MarkDirtyAndNotify(); return true; } @@ -212,7 +224,7 @@ public bool DiscardSlot(int slotIndex) data.Slots.RemoveAt(index); - _repository.MarkDirty(); + MarkDirtyAndNotify(); return true; } diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/Interfaces/IHorrorEquipmentService.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/Interfaces/IHorrorEquipmentService.cs index 5703de302..78cca6d7f 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/Interfaces/IHorrorEquipmentService.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/Interfaces/IHorrorEquipmentService.cs @@ -3,6 +3,7 @@ using Game.Shared.Enums; using Game.Shared.Scriptable.Database.Tables; using Game.Shared.Services.Interfaces; +using R3; namespace Game.Horror.Services.Interfaces { @@ -11,6 +12,9 @@ namespace Game.Horror.Services.Interfaces /// public interface IHorrorEquipmentService : IGameService { + /// 装備・ショートカットスロットが変化したときに通知する + Observable EquipmentChanged { get; } + /// 指定 (SlotType, Id) が装備可能か判定する。装備対象は Weapon のみで、かつ所持している必要がある。 bool CanEquip(ObjectCategory type, int id); diff --git a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/Interfaces/IHorrorInventoryService.cs b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/Interfaces/IHorrorInventoryService.cs index 6dfe6ca9c..694d14cde 100644 --- a/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/Interfaces/IHorrorInventoryService.cs +++ b/src/Game.Client/Assets/Programs/Runtime/MVC/Horror/Services/Interfaces/IHorrorInventoryService.cs @@ -3,6 +3,7 @@ using Game.Horror.SaveData; using Game.Shared.Enums; using Game.Shared.Services.Interfaces; +using R3; namespace Game.Horror.Services.Interfaces { @@ -14,6 +15,9 @@ public interface IHorrorInventoryService : IGameService /// 所持アイテム一覧(読み取り専用、疎。位置は各行の SlotNo が持ち、並び順に意味はない)。未ロード時は空。 IReadOnlyList Slots { get; } + /// スロット内容が変化したときに通知する(追加・消費・破棄の成功時。判定系では発行しない)。 + Observable SlotsChanged { get; } + /// /// アイテムをインベントリに追加する。既存スタックを SlotNo 昇順に maxCount まで充填し、 /// 超過分は最小の空き位置から新規スタックとして分割配置する。