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/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
{
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 まで充填し、
/// 超過分は最小の空き位置から新規スタックとして分割配置する。