Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using Game.Horror.Constants;
Expand All @@ -6,9 +7,14 @@
using Game.Horror.Services.Interfaces;
using Game.Shared.Enums;
using Game.Shared.SaveData;
using Game.Shared.Scriptable.Database;
using Game.Shared.Scriptable.Database.Tables;
using Game.Shared.Services;
using NSubstitute;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;

namespace Game.Tests.MVC.Horror
{
Expand All @@ -22,6 +28,8 @@ public class HorrorEquipmentServiceTests
private IHorrorInventoryService _mockInventory;
private IHorrorSaveRepository _repository;
private IHorrorEquipmentService _service;
private HorrorWeaponMasterTable _weaponTable;
private ScriptableDatabase _database;

[SetUp]
public void Setup()
Expand All @@ -30,7 +38,14 @@ public void Setup()
_mockDatabase = Substitute.For<IScriptableDatabaseService>();
_mockInventory = Substitute.For<IHorrorInventoryService>();
_repository = new HorrorSaveRepository(_mockStorage, _mockDatabase);
_service = new HorrorEquipmentService(_repository, _mockInventory);
_service = new HorrorEquipmentService(_repository, _mockInventory, _mockDatabase);
}

[TearDown]
public void TearDown()
{
if (_weaponTable != null) Object.DestroyImmediate(_weaponTable);
if (_database != null) Object.DestroyImmediate(_database);
}

private async Task LoadDefaultData()
Expand All @@ -54,6 +69,7 @@ private int CountOf(ObjectCategory type, int id)
public async Task TryEquip_WeaponPossessed_SucceedsAndMarksDirty()
{
await LoadDefaultData();
SetupRealDatabase(5);
_mockInventory.HasObject(ObjectCategory.Weapon, 5).Returns(true);

var ok = _service.TryEquip(ObjectCategory.Weapon, 5);
Expand Down Expand Up @@ -95,6 +111,7 @@ public async Task TryEquip_NotPossessed_ReturnsFalse()
public async Task TryEquip_SameWeaponAgain_ReturnsTrueIdempotently()
{
await LoadDefaultData();
SetupRealDatabase(5);
_mockInventory.HasObject(ObjectCategory.Weapon, 5).Returns(true);
_service.TryEquip(ObjectCategory.Weapon, 5);

Expand Down Expand Up @@ -254,6 +271,7 @@ public void SlotMutators_WhenDataNull_DoNotThrowAndReturnFalse()
public async Task TryEquip_DoesNotAffectSlots()
{
await LoadDefaultData();
SetupRealDatabase(7);
_service.TrySetSlot(0, ObjectCategory.Weapon, 5);
_mockInventory.HasObject(ObjectCategory.Weapon, 7).Returns(true);

Expand All @@ -268,6 +286,7 @@ public async Task TryEquip_DoesNotAffectSlots()
public async Task Assign_DoesNotAffectEquipped()
{
await LoadDefaultData();
SetupRealDatabase(5);
_mockInventory.HasObject(ObjectCategory.Weapon, 5).Returns(true);
_service.TryEquip(ObjectCategory.Weapon, 5);

Expand All @@ -282,6 +301,7 @@ public async Task Assign_DoesNotAffectEquipped()
public async Task Clear_DoesNotAffectEquipped()
{
await LoadDefaultData();
SetupRealDatabase(5);
_mockInventory.HasObject(ObjectCategory.Weapon, 5).Returns(true);
_service.TrySetSlot(0, ObjectCategory.Weapon, 5);
_service.TryEquip(ObjectCategory.Weapon, 5);
Expand Down Expand Up @@ -355,5 +375,177 @@ public void MagazineMutators_WhenDataNull_DoNotThrow()
Assert.That(_service.GetMagazineCount(5, 30), Is.EqualTo(30));
});
}

// 装備中武器のマスター解決:解決前・未ロード・未装備は null。プレイ開始時の解決
// (ResolveEquippedWeaponMaster)と装備切替(TryEquip)で確定し、解決できない記録は未装備へ戻す。

/// <summary>
/// 指定 Id の武器レコードを持つ実テーブル+DB を組み立てて mock に接続する。
/// EditorImportRows は列名をメンバ名と完全一致でマッピングし、無い列は既定値のままとなるため Id 列のみ投入する。
/// </summary>
private void SetupRealDatabase(params int[] weaponIds)
{
_weaponTable = ScriptableObject.CreateInstance<HorrorWeaponMasterTable>();
var rows = new List<IReadOnlyList<string>>();
foreach (var id in weaponIds)
rows.Add(new[] { id.ToString() });
_weaponTable.EditorImportRows(new[] { "Id" }, rows, mergeByPrimaryKey: false);

_database = ScriptableObject.CreateInstance<ScriptableDatabase>();
var so = new SerializedObject(_database);
so.FindProperty("horrorWeaponMasterTable").objectReferenceValue = _weaponTable;
so.ApplyModifiedPropertiesWithoutUndo();
_mockDatabase.Database.Returns(_database);
}

[Test]
public void EquippedWeaponMaster_BeforeResolve_IsNull()
{
Assert.That(_service.EquippedWeaponMaster, Is.Null);
}

[Test]
public void ResolveEquippedWeaponMaster_WhenDataNull_ResolvesNull()
{
// LoadAsync 未実行 = Data は null。DB へも触れない
Assert.DoesNotThrow(() => _service.ResolveEquippedWeaponMaster());
Assert.That(_service.EquippedWeaponMaster, Is.Null);
}

[Test]
public async Task ResolveEquippedWeaponMaster_Unequipped_ResolvesNull()
{
await LoadDefaultData();

_service.ResolveEquippedWeaponMaster();

Assert.That(_service.EquippedWeaponMaster, Is.Null);
Assert.That(_repository.IsDirty, Is.False);
}

[Test]
public async Task ResolveEquippedWeaponMaster_RestoresFromSaveData()
{
await LoadDefaultData();
SetupRealDatabase(5);
_mockInventory.HasObject(ObjectCategory.Weapon, 5).Returns(true);
_service.TryEquip(ObjectCategory.Weapon, 5);

// 装備記録の残るセーブデータに対し、未解決のサービス(=セーブのロード直後)を作り直す
var service = new HorrorEquipmentService(_repository, _mockInventory, _mockDatabase);
Assert.That(service.EquippedWeaponMaster, Is.Null);

service.ResolveEquippedWeaponMaster();

Assert.That(service.EquippedWeaponMaster, Is.Not.Null);
Assert.That(service.EquippedWeaponMaster.Id, Is.EqualTo(5));
}

[Test]
public async Task ResolveEquippedWeaponMaster_MasterNotFound_LogsErrorAndClearsEquipment()
{
await LoadDefaultData();
SetupRealDatabase(7); // 記録された Id=5 はテーブル未登録 → 解決失敗(不変条件違反)経路
_repository.Data.Equipment.ObjectCategory = ObjectCategory.Weapon;
_repository.Data.Equipment.Id = 5;

LogAssert.Expect(LogType.Error, "装備中の武器マスターが見つかりません Id=5。未装備へ戻します");
_service.ResolveEquippedWeaponMaster();

Assert.That(_service.EquippedWeaponMaster, Is.Null);
Assert.That(_service.TryGetEquipped(out _, out _), Is.False); // 記録も実体(未装備)へ合わせる
Assert.That(_repository.IsDirty, Is.True);
}

[Test]
public async Task TryEquip_MasterNotFound_LogsErrorAndDoesNotEquip()
{
await LoadDefaultData();
SetupRealDatabase(7); // 装備する Id=5 はテーブル未登録 → 装備を成立させない
_mockInventory.HasObject(ObjectCategory.Weapon, 5).Returns(true);

LogAssert.Expect(LogType.Error, "装備中の武器マスターが見つかりません Id=5");

Assert.That(_service.TryEquip(ObjectCategory.Weapon, 5), Is.False);
Assert.That(_service.EquippedWeaponMaster, Is.Null);
Assert.That(_service.TryGetEquipped(out _, out _), Is.False);
Assert.That(_repository.IsDirty, Is.False);
}

[Test]
public async Task EquippedWeaponMaster_Resolved_FollowsEquipChange()
{
await LoadDefaultData();
SetupRealDatabase(5, 7);
_mockInventory.HasObject(ObjectCategory.Weapon, 5).Returns(true);
_mockInventory.HasObject(ObjectCategory.Weapon, 7).Returns(true);

_service.TryEquip(ObjectCategory.Weapon, 5);
Assert.That(_service.EquippedWeaponMaster, Is.Not.Null);
Assert.That(_service.EquippedWeaponMaster.Id, Is.EqualTo(5));

_service.TryEquip(ObjectCategory.Weapon, 7);
Assert.That(_service.EquippedWeaponMaster, Is.Not.Null);
Assert.That(_service.EquippedWeaponMaster.Id, Is.EqualTo(7));
}

// 装備候補一覧(GetEquippableWeaponMasters):スロット0→3→装備中の順で武器のみを解決し、
// 同一 Id は重複排除、マスター未解決のスロット登録は無音でスキップする。

[Test]
public async Task GetEquippableWeaponMasters_NoSlotsNoEquip_ReturnsEmpty()
{
await LoadDefaultData();

Assert.That(_service.GetEquippableWeaponMasters(), Is.Empty);
}

[Test]
public async Task GetEquippableWeaponMasters_CollectsSlotWeapons_SkipsNonWeaponAndUnresolvable()
{
await LoadDefaultData();
SetupRealDatabase(5, 7);
_service.TrySetSlot(0, ObjectCategory.Weapon, 5);
_service.TrySetSlot(1, ObjectCategory.Weapon, 7);
_service.TrySetSlot(2, ObjectCategory.Item, 3);
_service.TrySetSlot(3, ObjectCategory.Weapon, 9); // テーブル未登録 → 無音スキップ

var masters = _service.GetEquippableWeaponMasters();

Assert.That(masters, Has.Count.EqualTo(2));
Assert.That(masters[0].Id, Is.EqualTo(5));
Assert.That(masters[1].Id, Is.EqualTo(7));
}

[Test]
public async Task GetEquippableWeaponMasters_DeduplicatesEquippedAlreadyInSlots()
{
await LoadDefaultData();
SetupRealDatabase(5);
_service.TrySetSlot(0, ObjectCategory.Weapon, 5);
_mockInventory.HasObject(ObjectCategory.Weapon, 5).Returns(true);
_service.TryEquip(ObjectCategory.Weapon, 5);

var masters = _service.GetEquippableWeaponMasters();

Assert.That(masters, Has.Count.EqualTo(1));
Assert.That(masters[0].Id, Is.EqualTo(5));
}

[Test]
public async Task GetEquippableWeaponMasters_AppendsEquippedNotInSlots()
{
await LoadDefaultData();
SetupRealDatabase(5, 7);
_service.TrySetSlot(0, ObjectCategory.Weapon, 5);
_mockInventory.HasObject(ObjectCategory.Weapon, 7).Returns(true);
_service.TryEquip(ObjectCategory.Weapon, 7);

var masters = _service.GetEquippableWeaponMasters();

Assert.That(masters, Has.Count.EqualTo(2));
Assert.That(masters[0].Id, Is.EqualTo(5));
Assert.That(masters[1].Id, Is.EqualTo(7), "装備中は末尾に合流する");
}
}
}
Loading
Loading