Files

887 lines
41 KiB
C#

using System.Collections;
using System.Reflection;
using BumpCombat.Audio;
using BumpCombat.Combat;
using BumpCombat.Core;
using BumpCombat.Enemies;
using BumpCombat.Player;
using BumpCombat.Progression;
using BumpCombat.Spawning;
using BumpCombat.UI;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.UI;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace BumpCombat.PlayModeTests
{
public sealed class PresentationAudioMenuTests
{
private const string SceneName = "CombatPrototype";
private float originalMaster;
private float originalMusic;
private float originalSfx;
private bool capturedVolumes;
private InputSettings.BackgroundBehavior previousBackgroundBehavior;
private InputSettings.EditorInputBehaviorInPlayMode previousEditorInputBehavior;
private bool previousRunInBackground;
private Keyboard virtualKeyboard;
private bool virtualKeyboardInputActive;
[SetUp]
public void SetUp()
{
ArtifactRewardController.GrantCatalogForTests = false;
RunManager.ForceProductionModeForTests = true;
Time.timeScale = 1f;
capturedVolumes = false;
}
[TearDown]
public void TearDown()
{
EndVirtualKeyboardInput();
BumpCombatAudioService service =
Object.FindAnyObjectByType<BumpCombatAudioService>();
if (service != null && capturedVolumes)
{
service.SetMasterVolume(originalMaster);
service.SetMusicVolume(originalMusic);
service.SetSfxVolume(originalSfx);
}
ArtifactRewardController.GrantCatalogForTests = false;
RunManager.ForceProductionModeForTests = false;
Time.timeScale = 1f;
}
[Test]
public void DevelopmentStartVisibility_RequiresEditorOrDevelopmentBuild()
{
MethodInfo predicate = typeof(RunMenuController).GetMethod(
"IsDevelopmentStartVisible",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.That(predicate, Is.Not.Null);
Assert.That(predicate.Invoke(null, new object[] { false, false }), Is.False);
Assert.That(predicate.Invoke(null, new object[] { true, false }), Is.True);
Assert.That(predicate.Invoke(null, new object[] { false, true }), Is.True);
}
[UnityTest]
public IEnumerator TitleStart_UnlocksRunAfterExplicitStart()
{
yield return LoadScene();
RunManager manager = Object.FindAnyObjectByType<RunManager>();
Assert.That(manager, Is.Not.Null);
Assert.That(manager.IsTitleScreen, Is.True);
Assert.That(RunManager.GameplayInputEnabled, Is.False);
Assert.That(Time.timeScale, Is.Zero);
Keyboard keyboard = BeginVirtualKeyboardInput();
try
{
yield return PressAndRelease(keyboard, Key.Space);
Assert.That(manager.IsTitleScreen, Is.True,
"Space opens the mode submenu without starting a run.");
yield return PressAndRelease(keyboard, Key.Enter);
}
finally
{
EndVirtualKeyboardInput();
}
Assert.That(manager.IsTitleScreen, Is.False);
Assert.That(manager.IsProductionRun, Is.False);
Assert.That(manager.UseDebugEventTimes, Is.True);
Assert.That(manager.IsArtifactChargeUnlocked, Is.True);
ArtifactRewardController rewards =
Object.FindAnyObjectByType<ArtifactRewardController>();
Assert.That(rewards, Is.Not.Null);
yield return null;
Assert.That(rewards.DebugIsSelectionVisible, Is.True);
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
RunTutorialController tutorials =
Object.FindAnyObjectByType<RunTutorialController>();
Assert.That(tutorials.DebugCurrentTutorial,
Is.EqualTo(RunTutorialController.TutorialKind.Artifact));
Assert.That(tutorials.DebugContinue(), Is.True);
Assert.That(tutorials.DebugCurrentTutorial,
Is.EqualTo(RunTutorialController.TutorialKind.Guard));
Assert.That(tutorials.DebugContinue(), Is.True);
Assert.That(tutorials.DebugCurrentTutorial,
Is.EqualTo(RunTutorialController.TutorialKind.ArtifactEnhancement));
Assert.That(tutorials.DebugContinue(), Is.True);
Assert.That(RunManager.GameplayInputEnabled, Is.True);
Assert.That(Time.timeScale, Is.EqualTo(1f));
}
[UnityTest]
public IEnumerator TitleProductionStart_IsSecondKeyboardItemAndLabelsFit()
{
yield return LoadScene();
RunManager manager = Object.FindAnyObjectByType<RunManager>();
Transform menu = Object.FindAnyObjectByType<RunMenuController>()
.transform.Find("Run Menu");
Text title = menu.Find("Title").GetComponent<Text>();
Assert.That(title.text, Is.EqualTo("Tiny Tackle Heroes"));
float titlePreferredWidth = title.cachedTextGeneratorForLayout
.GetPreferredWidth(
title.text,
title.GetGenerationSettings(Vector2.zero))
/ title.pixelsPerUnit;
Assert.That(title.cachedTextGeneratorForLayout.lineCount, Is.EqualTo(1));
Assert.That(titlePreferredWidth,
Is.LessThanOrEqualTo(title.rectTransform.rect.width),
"The game title must fit in its single-line title label.");
string[] expectedLabels = { "게임시작", "설정", "업적", "나가기" };
for (int i = 0; i < expectedLabels.Length; i++)
{
Text label = menu.Find($"Button {i + 1}/Label").GetComponent<Text>();
Assert.That(label.text, Is.EqualTo(expectedLabels[i]));
float preferredWidth = label.cachedTextGeneratorForLayout
.GetPreferredWidth(
label.text,
label.GetGenerationSettings(Vector2.zero))
/ label.pixelsPerUnit;
Assert.That(label.cachedTextGeneratorForLayout.lineCount, Is.EqualTo(1));
float labelWidth = label.rectTransform.rect.width;
Assert.That(
preferredWidth,
Is.LessThanOrEqualTo(labelWidth),
$"{expectedLabels[i]} must fit in its single-line menu label.");
}
Keyboard keyboard = BeginVirtualKeyboardInput();
try
{
yield return PressAndRelease(keyboard, Key.Enter);
Assert.That(manager.IsTitleScreen, Is.True,
"Opening the mode submenu must not begin a run.");
Transform submenu = menu;
bool showDevelopment = Application.isEditor || Debug.isDebugBuild;
var submenuLabels = new System.Collections.Generic.List<string>();
if (showDevelopment)
{
submenuLabels.Add("개발용 시작");
}
submenuLabels.Add("기본 모드");
submenuLabels.Add("챌린지 모드(준비 중)");
submenuLabels.Add("하드코어 모드(준비 중)");
submenuLabels.Add("뒤로");
for (int i = 0; i < submenuLabels.Count; i++)
{
Button button = submenu.Find($"Button {i + 1}").GetComponent<Button>();
Text label = button.GetComponentInChildren<Text>();
Assert.That(button.gameObject.activeSelf, Is.True);
Assert.That(label.text, Is.EqualTo(submenuLabels[i]));
float preferredWidth = label.cachedTextGeneratorForLayout
.GetPreferredWidth(
label.text,
label.GetGenerationSettings(Vector2.zero))
/ label.pixelsPerUnit;
Assert.That(label.cachedTextGeneratorForLayout.lineCount, Is.EqualTo(1));
Assert.That(preferredWidth, Is.LessThanOrEqualTo(label.rectTransform.rect.width),
$"{label.text} must fit in its single-line menu label.");
Assert.That(button.interactable,
Is.EqualTo(i < submenuLabels.Count - 3 || i == submenuLabels.Count - 1),
label.text);
}
int placeholderIndex = showDevelopment ? 2 : 1;
for (int i = placeholderIndex; i < placeholderIndex + 2; i++)
{
submenu.Find($"Button {i + 1}").GetComponent<Button>().onClick.Invoke();
Assert.That(manager.IsTitleScreen, Is.True,
"A preparation placeholder cannot launch a run.");
}
if (showDevelopment)
{
yield return PressAndRelease(keyboard, Key.DownArrow);
}
Assert.That(
EventSystem.current.currentSelectedGameObject
.GetComponentInChildren<Text>().text,
Is.EqualTo("기본 모드"));
yield return PressAndRelease(keyboard, Key.DownArrow);
Assert.That(
EventSystem.current.currentSelectedGameObject
.GetComponentInChildren<Text>().text,
Is.EqualTo("뒤로"),
"Keyboard navigation skips disabled mode placeholders.");
yield return PressAndRelease(keyboard, Key.UpArrow);
Assert.That(
EventSystem.current.currentSelectedGameObject
.GetComponentInChildren<Text>().text,
Is.EqualTo("기본 모드"));
yield return PressAndRelease(keyboard, Key.Enter);
}
finally
{
EndVirtualKeyboardInput();
}
Assert.That(manager.IsTitleScreen, Is.False);
Assert.That(manager.IsProductionRun, Is.True);
Assert.That(manager.UseDebugEventTimes, Is.False);
Assert.That(manager.IsGuardUnlocked, Is.False);
}
[UnityTest]
public IEnumerator UiInitialization_ReusesEventSystemAndPreservesPointerModule()
{
yield return LoadScene();
EventSystem original = EventSystem.current;
Assert.That(original, Is.Not.Null);
Object.Destroy(original.gameObject);
yield return null;
EnsurePresentationEventSystem(false);
EventSystem eventSystem = EventSystem.current;
Assert.That(eventSystem, Is.Not.Null);
Assert.That(CountActiveSceneEventSystems(), Is.EqualTo(1));
InputSystemUIInputModule module =
eventSystem.GetComponent<InputSystemUIInputModule>();
Assert.That(module, Is.Not.Null);
Assert.That(module.point, Is.Not.Null);
Assert.That(module.leftClick, Is.Not.Null);
Assert.That(module.scrollWheel, Is.Not.Null);
Assert.That(module.move, Is.Not.Null);
Assert.That(module.submit, Is.Not.Null);
Assert.That(module.cancel, Is.Not.Null);
var pointAction = module.point;
var clickAction = module.leftClick;
var scrollAction = module.scrollWheel;
var moveAction = module.move;
var submitAction = module.submit;
var cancelAction = module.cancel;
// Exercise HUD -> menu -> HUD initialization, including repeats.
EnsurePresentationEventSystem(false);
Assert.That(module.move, Is.SameAs(moveAction));
Assert.That(module.submit, Is.SameAs(submitAction));
Assert.That(module.cancel, Is.SameAs(cancelAction));
EnsurePresentationEventSystem(true);
EnsurePresentationEventSystem(false);
Assert.That(EventSystem.current, Is.SameAs(eventSystem));
Assert.That(CountActiveSceneEventSystems(), Is.EqualTo(1));
Assert.That(eventSystem.GetComponents<BaseInputModule>(),
Has.Length.EqualTo(1));
Assert.That(eventSystem.GetComponent<InputSystemUIInputModule>(),
Is.SameAs(module));
Assert.That(module.move, Is.Null);
Assert.That(module.submit, Is.Null);
Assert.That(module.cancel, Is.Null);
Assert.That(module.point, Is.SameAs(pointAction));
Assert.That(module.leftClick, Is.SameAs(clickAction));
Assert.That(module.scrollWheel, Is.SameAs(scrollAction));
Object.Destroy(module);
yield return null;
StandaloneInputModule existingModule =
eventSystem.gameObject.AddComponent<StandaloneInputModule>();
existingModule.enabled = false;
EnsurePresentationEventSystem(false);
EnsurePresentationEventSystem(true);
Assert.That(eventSystem.GetComponent<BaseInputModule>(),
Is.SameAs(existingModule));
Assert.That(eventSystem.GetComponent<InputSystemUIInputModule>(),
Is.Null);
Assert.That(CountActiveSceneEventSystems(), Is.EqualTo(1));
InputSystemUIInputModule secondaryModule =
eventSystem.gameObject.AddComponent<InputSystemUIInputModule>();
var secondaryPoint = secondaryModule.point;
var secondaryClick = secondaryModule.leftClick;
var secondaryScroll = secondaryModule.scrollWheel;
EnsurePresentationEventSystem(false);
EnsurePresentationEventSystem(true);
EnsurePresentationEventSystem(false);
Assert.That(eventSystem.GetComponent<BaseInputModule>(),
Is.SameAs(existingModule));
Assert.That(eventSystem.GetComponent<InputSystemUIInputModule>(),
Is.SameAs(secondaryModule));
Assert.That(eventSystem.GetComponents<BaseInputModule>(),
Has.Length.EqualTo(2));
Assert.That(secondaryModule.move, Is.Null);
Assert.That(secondaryModule.submit, Is.Null);
Assert.That(secondaryModule.cancel, Is.Null);
Assert.That(secondaryModule.point, Is.SameAs(secondaryPoint));
Assert.That(secondaryModule.leftClick, Is.SameAs(secondaryClick));
Assert.That(secondaryModule.scrollWheel, Is.SameAs(secondaryScroll));
Object.Destroy(secondaryModule);
Object.Destroy(existingModule);
yield return null;
EnsurePresentationEventSystem(false);
InputSystemUIInputModule restoredModule =
eventSystem.GetComponent<InputSystemUIInputModule>();
Assert.That(restoredModule, Is.Not.Null);
Assert.That(eventSystem.GetComponents<BaseInputModule>(),
Has.Length.EqualTo(1));
Assert.That(CountActiveSceneEventSystems(), Is.EqualTo(1));
EnsurePresentationEventSystem(true);
Assert.That(restoredModule.move, Is.Null);
Assert.That(restoredModule.submit, Is.Null);
Assert.That(restoredModule.cancel, Is.Null);
Assert.That(restoredModule.point, Is.Not.Null);
Assert.That(restoredModule.leftClick, Is.Not.Null);
}
[UnityTest]
public IEnumerator ArtifactSelection_SharedCardsKeepIconAndTextLayout()
{
ArtifactRewardController.GrantCatalogForTests = false;
RunManager.ForceProductionModeForTests = true;
yield return LoadScene();
RunManager manager = Object.FindAnyObjectByType<RunManager>();
ArtifactRewardController rewards =
Object.FindAnyObjectByType<ArtifactRewardController>();
Assert.That(manager.BeginRun(), Is.True);
yield return null;
Assert.That(rewards.DebugIsSelectionVisible, Is.True);
GameObject panel = GameObject.Find("Artifact Reward Selection");
Text optionText = panel.transform.Find("Option 1").GetComponent<Text>();
Image card = panel.transform.Find("Option 1 Card").GetComponent<Image>();
Image icon = panel.transform.Find("Option 1 Icon").GetComponent<Image>();
RectTransform textRect = optionText.rectTransform;
RectTransform cardRect = card.rectTransform;
RectTransform iconRect = icon.rectTransform;
Assert.That(panel.GetComponent<Image>().type, Is.EqualTo(Image.Type.Sliced));
Assert.That(optionText.alignment, Is.EqualTo(TextAnchor.MiddleCenter));
Assert.That(optionText.font, Is.SameAs(
Resources.Load<Font>("Presentation/Fonts/Galmuri9")));
Assert.That(optionText.raycastTarget, Is.False);
Assert.That(textRect.sizeDelta, Is.EqualTo(new Vector2(380f, 64f)));
Assert.That(cardRect.anchorMin, Is.EqualTo(textRect.anchorMin));
Assert.That(cardRect.anchorMax, Is.EqualTo(textRect.anchorMax));
Assert.That(cardRect.pivot, Is.EqualTo(textRect.pivot));
Assert.That(cardRect.anchoredPosition,
Is.EqualTo(textRect.anchoredPosition + Vector2.up * 25f));
Assert.That(cardRect.sizeDelta, Is.EqualTo(new Vector2(380f, 128f)));
Assert.That(card.transform.GetSiblingIndex(),
Is.EqualTo(optionText.transform.GetSiblingIndex() - 2));
Assert.That(icon.transform.GetSiblingIndex(),
Is.EqualTo(optionText.transform.GetSiblingIndex() - 1));
Assert.That(card.raycastTarget, Is.False);
Assert.That(icon.preserveAspect, Is.True);
Assert.That(icon.raycastTarget, Is.False);
Assert.That(iconRect.anchorMin, Is.EqualTo(textRect.anchorMin));
Assert.That(iconRect.anchorMax, Is.EqualTo(textRect.anchorMax));
Assert.That(iconRect.pivot, Is.EqualTo(textRect.pivot));
Assert.That(iconRect.anchoredPosition,
Is.EqualTo(textRect.anchoredPosition + Vector2.up * 45f));
Assert.That(iconRect.sizeDelta, Is.EqualTo(new Vector2(48f, 48f)));
}
[UnityTest]
public IEnumerator MenuKeyboard_SettingsSliderAndBack_AreSingleStep()
{
yield return LoadScene();
RunManager manager = Object.FindAnyObjectByType<RunManager>();
BumpCombatAudioService service =
Object.FindAnyObjectByType<BumpCombatAudioService>();
Keyboard keyboard = BeginVirtualKeyboardInput();
try
{
yield return PressAndRelease(keyboard, Key.DownArrow);
GameObject selected = EventSystem.current.currentSelectedGameObject;
Assert.That(selected, Is.Not.Null);
Assert.That(selected.GetComponentInChildren<Text>().text, Is.EqualTo("설정"));
yield return PressAndRelease(keyboard, Key.Enter);
Slider slider = GameObject.Find("Master Slider").GetComponent<Slider>();
Assert.That(slider.gameObject.activeSelf, Is.True);
slider.value = 0.5f;
float before = slider.value;
yield return PressAndRelease(keyboard, Key.RightArrow);
Assert.That(slider.value, Is.EqualTo(before + 0.05f).Within(0.001f));
yield return PressAndRelease(keyboard, Key.Escape);
Assert.That(slider.gameObject.activeSelf, Is.False);
Assert.That(manager.IsTitleScreen, Is.True);
Assert.That(service.MasterVolume, Is.EqualTo(slider.value).Within(0.001f));
}
finally
{
EndVirtualKeyboardInput();
}
}
[UnityTest]
public IEnumerator SettingsFocus_IsVisibleAndPointerBackSyncsKeyboardSelection()
{
yield return LoadScene();
RunManager manager = Object.FindAnyObjectByType<RunManager>();
RunMenuController menuController = Object.FindAnyObjectByType<RunMenuController>();
Assert.That(menuController, Is.Not.Null);
Transform menu = menuController.transform.Find("Run Menu");
Text inputHelp = menu.Find("Menu Input Help").GetComponent<Text>();
Assert.That(inputHelp.text, Is.EqualTo("항목 이동: 위/아래 선택: Enter/Space"));
Assert.That(inputHelp.rectTransform.anchorMin, Is.EqualTo(Vector2.zero));
Assert.That(inputHelp.alignment, Is.EqualTo(TextAnchor.MiddleLeft));
Assert.That(menu.Find("Button 1/Focus Marker").gameObject.activeSelf, Is.True);
Keyboard keyboard = BeginVirtualKeyboardInput();
try
{
menu.Find("Button 2").GetComponent<Button>().onClick.Invoke();
yield return null;
Assert.That(inputHelp.text, Is.EqualTo(
"항목 이동: 위/아래 음량 조절: 왼쪽/오른쪽\n뒤로 선택: Enter/Space 즉시 뒤로: Esc"));
GameObject masterFocus = menu.Find("Settings Focus 0").gameObject;
GameObject musicFocus = menu.Find("Settings Focus 1").gameObject;
GameObject backFocus = menu.Find("Settings Focus 3").gameObject;
Slider masterSlider = menu.Find("Master Slider").GetComponent<Slider>();
Button backButton = menu.Find("Button 4").GetComponent<Button>();
Assert.That(masterFocus.activeSelf, Is.True);
Assert.That(EventSystem.current.currentSelectedGameObject, Is.EqualTo(masterSlider.gameObject));
yield return PressAndRelease(keyboard, Key.DownArrow);
Assert.That(masterFocus.activeSelf, Is.False);
Assert.That(musicFocus.activeSelf, Is.True);
ExecuteEvents.Execute(
backButton.gameObject,
new PointerEventData(EventSystem.current),
ExecuteEvents.pointerEnterHandler);
Assert.That(EventSystem.current.currentSelectedGameObject, Is.EqualTo(backButton.gameObject));
Assert.That(backFocus.activeSelf, Is.True);
yield return PressAndRelease(keyboard, Key.DownArrow);
Assert.That(masterFocus.activeSelf, Is.True);
Assert.That(backFocus.activeSelf, Is.False);
Assert.That(EventSystem.current.currentSelectedGameObject, Is.EqualTo(masterSlider.gameObject));
yield return PressAndRelease(keyboard, Key.UpArrow);
yield return PressAndRelease(keyboard, Key.Enter);
}
finally
{
EndVirtualKeyboardInput();
}
Assert.That(manager.IsTitleScreen, Is.True);
Assert.That(inputHelp.text, Is.EqualTo("항목 이동: 위/아래 선택: Enter/Space"));
}
[UnityTest]
public IEnumerator PauseAndSelection_KeepEachOtherInterlocked()
{
yield return LoadScene();
RunManager manager = Object.FindAnyObjectByType<RunManager>();
Assert.That(manager.BeginRun(), Is.True);
yield return null;
ArtifactRewardController rewards =
Object.FindAnyObjectByType<ArtifactRewardController>();
Assert.That(rewards.DebugIsSelectionVisible, Is.True);
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
CloseArtifactTutorialModal(manager);
GameObject ownerObject = new("Selection Owner Test");
SelectionOwner owner = ownerObject.AddComponent<SelectionOwner>();
try
{
Assert.That(manager.TryOpenSelection(owner), Is.True);
Assert.That(manager.TryOpenPause(), Is.False);
Assert.That(RunManager.GameplayInputEnabled, Is.False);
Assert.That(manager.CloseSelection(owner), Is.True);
Assert.That(manager.TryOpenPause(), Is.True);
Text inputHelp = GameObject.Find("Menu Input Help").GetComponent<Text>();
Assert.That(inputHelp.text, Is.EqualTo(
"항목 이동: 위/아래 선택: Enter/Space 계속: Esc"));
Transform menu = Object.FindAnyObjectByType<RunMenuController>().transform.Find("Run Menu");
Assert.That(menu.Find("Button 1/Focus Marker").gameObject.activeSelf, Is.True);
Assert.That(manager.TryOpenSelection(owner), Is.False);
Assert.That(manager.ClosePause(), Is.True);
}
finally
{
Object.Destroy(ownerObject);
}
}
[UnityTest]
public IEnumerator StartupSelection_RecoversWhenPausedBeforeItsFirstFrame()
{
yield return LoadScene();
RunManager manager = Object.FindAnyObjectByType<RunManager>();
ArtifactRewardController rewards =
Object.FindAnyObjectByType<ArtifactRewardController>();
Assert.That(manager, Is.Not.Null);
Assert.That(rewards, Is.Not.Null);
Assert.That(manager.BeginRun(), Is.True);
Assert.That(manager.TryOpenPause(), Is.True);
Assert.That(manager.IsPaused, Is.True);
// The startup request is queued by BeginRun. A paused frame must
// leave it pending so resuming can open the first color step.
yield return null;
Assert.That(rewards.DebugIsSelectionVisible, Is.False);
Assert.That(manager.IsSelectionOpen, Is.False);
Assert.That(RunManager.GameplayInputEnabled, Is.False);
Assert.That(manager.ClosePause(), Is.True);
yield return null;
Assert.That(rewards.DebugIsSelectionVisible, Is.True);
Assert.That(rewards.DebugShownCount, Is.EqualTo(2));
Assert.That(
rewards.DebugGetShownArtifact(0).ArtifactColor,
Is.EqualTo(ArtifactColor.Green));
Assert.That(
rewards.DebugGetShownArtifact(1).ArtifactColor,
Is.EqualTo(ArtifactColor.Green));
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
Assert.That(rewards.DebugIsSelectionVisible, Is.True);
Assert.That(rewards.DebugShownCount, Is.EqualTo(2));
Assert.That(
rewards.DebugGetShownArtifact(0).ArtifactColor,
Is.EqualTo(ArtifactColor.Red));
Assert.That(
rewards.DebugGetShownArtifact(1).ArtifactColor,
Is.EqualTo(ArtifactColor.Red));
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
Assert.That(rewards.DebugIsSelectionVisible, Is.True);
Assert.That(rewards.DebugShownCount, Is.EqualTo(2));
Assert.That(
rewards.DebugGetShownArtifact(0).ArtifactColor,
Is.EqualTo(ArtifactColor.Blue));
Assert.That(
rewards.DebugGetShownArtifact(1).ArtifactColor,
Is.EqualTo(ArtifactColor.Blue));
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
CloseArtifactTutorialModal(manager);
Assert.That(rewards.DebugIsSelectionVisible, Is.False);
Assert.That(manager.IsSelectionOpen, Is.False);
Assert.That(RunManager.GameplayInputEnabled, Is.True);
Assert.That(
Object.FindAnyObjectByType<ActiveArtifactController>().OwnedArtifactCount,
Is.EqualTo(3));
}
[UnityTest]
public IEnumerator AudioService_LoadsContractClipsAndCapsVoices()
{
yield return LoadScene();
BumpCombatAudioService service =
Object.FindAnyObjectByType<BumpCombatAudioService>();
Assert.That(service, Is.Not.Null);
Assert.That(service.VoiceCount, Is.EqualTo(8));
string[] ids =
{
"ui_move", "ui_confirm", "hit_light", "hit_heavy",
"player_hurt", "enemy_defeat", "xp_pickup", "level_up",
"gauge_ready", "run_end", "charge_start", "artifact_dash",
"artifact_pulse", "artifact_ray", "artifact_cyclone",
"artifact_thunder", "artifact_arc", "bgm_courtyard", "bgm_boss",
};
for (int i = 0; i < ids.Length; i++)
{
Assert.That(service.DebugHasClip(ids[i]), Is.True, ids[i]);
}
}
[UnityTest]
public IEnumerator VolumeSettings_PersistNormalizedValues()
{
yield return LoadScene();
BumpCombatAudioService service =
Object.FindAnyObjectByType<BumpCombatAudioService>();
service.SetMasterVolume(0.31f);
service.SetMusicVolume(0.62f);
service.SetSfxVolume(0.47f);
Assert.That(service.MasterVolume, Is.EqualTo(0.31f).Within(0.0001f));
Assert.That(service.MusicVolume, Is.EqualTo(0.62f).Within(0.0001f));
Assert.That(service.SfxVolume, Is.EqualTo(0.47f).Within(0.0001f));
Assert.That(PlayerPrefs.GetFloat("BumpCombat.MasterVolume"), Is.EqualTo(0.31f).Within(0.0001f));
Assert.That(PlayerPrefs.GetFloat("BumpCombat.MusicVolume"), Is.EqualTo(0.62f).Within(0.0001f));
Assert.That(PlayerPrefs.GetFloat("BumpCombat.SfxVolume"), Is.EqualTo(0.47f).Within(0.0001f));
}
[UnityTest]
public IEnumerator SuccessfulArtifactCast_UsesArtifactClipOnce()
{
ArtifactRewardController.GrantCatalogForTests = true;
yield return LoadScene();
BumpCombatAudioService service =
Object.FindAnyObjectByType<BumpCombatAudioService>();
ActiveArtifactController artifacts =
Object.FindAnyObjectByType<ActiveArtifactController>();
artifacts.AddMovementCharge(20f);
Assert.That(artifacts.TryUseCurrent(false), Is.True);
Assert.That(service.LastPlayedClipId, Is.EqualTo("artifact_dash"));
Assert.That(artifacts.TryUseCurrent(false), Is.False);
Assert.That(service.LastPlayedClipId, Is.EqualTo("artifact_dash"));
}
[UnityTest]
public IEnumerator BossSpawn_SwitchesToBossMusic()
{
ArtifactRewardController.GrantCatalogForTests = true;
RunManager.ForceProductionModeForTests = false;
yield return LoadScene();
BumpCombatAudioService service =
Object.FindAnyObjectByType<BumpCombatAudioService>();
GameObject midBossButtonObject = GameObject.Find("Debug Spawn MidBoss");
GameObject finalBossButtonObject = GameObject.Find("Debug Spawn FinalBoss");
Assert.That(midBossButtonObject, Is.Not.Null);
Assert.That(finalBossButtonObject, Is.Not.Null);
Button midBossButton = midBossButtonObject.GetComponent<Button>();
Button finalBossButton = finalBossButtonObject.GetComponent<Button>();
Assert.That(CountPlayingMusicSources(), Is.EqualTo(1));
Assert.That(midBossButton, Is.Not.Null);
Assert.That(finalBossButton, Is.Not.Null);
midBossButton.onClick.Invoke();
Assert.That(service.ActiveMusicClipId, Is.EqualTo("bgm_boss"));
Assert.That(
CountPlayingMusicSources(),
Is.EqualTo(1),
"Starting boss music must stop ambient music before the fade-in.");
yield return null;
Assert.That(CountPlayingMusicSources(), Is.EqualTo(1));
finalBossButton.onClick.Invoke();
Assert.That(service.ActiveMusicClipId, Is.EqualTo("bgm_boss"));
Assert.That(CountPlayingMusicSources(), Is.EqualTo(1));
yield return null;
Assert.That(CountPlayingMusicSources(), Is.EqualTo(1));
}
[UnityTest]
public IEnumerator AudioService_DestroyAndReadd_ClearsLegacyMusicChildren()
{
ArtifactRewardController.GrantCatalogForTests = true;
RunManager.ForceProductionModeForTests = false;
yield return LoadScene();
BumpCombatAudioService service =
Object.FindAnyObjectByType<BumpCombatAudioService>();
Canvas canvas = service.GetComponentInParent<Canvas>();
GameObject legacyObject = new("Music A");
legacyObject.transform.SetParent(service.transform, false);
AudioSource legacySource = legacyObject.AddComponent<AudioSource>();
AudioClip legacyClip = AudioClip.Create("Legacy Music", 4410, 1, 44100, false);
legacySource.clip = legacyClip;
legacySource.loop = true;
legacySource.Play();
Assert.That(CountPlayingMusicSources(), Is.EqualTo(2));
Object.Destroy(service);
yield return null;
Assert.That(CountPlayingMusicSources(), Is.Zero);
canvas.gameObject.AddComponent<BumpCombatAudioService>();
yield return null;
Assert.That(CountPlayingMusicSources(), Is.EqualTo(1));
Object.Destroy(legacyClip);
}
[UnityTest]
public IEnumerator MusicDuckAndGameOverFade_StopsMusicSource()
{
ArtifactRewardController.GrantCatalogForTests = false;
RunManager.ForceProductionModeForTests = true;
yield return LoadScene();
RunManager manager = Object.FindAnyObjectByType<RunManager>();
BumpCombatAudioService service =
Object.FindAnyObjectByType<BumpCombatAudioService>();
float titleGain = service.CurrentMusicOutputGain;
Assert.That(titleGain, Is.GreaterThan(0f));
Assert.That(manager.BeginRun(), Is.True);
yield return null;
ArtifactRewardController rewards =
Object.FindAnyObjectByType<ArtifactRewardController>();
Assert.That(rewards.DebugIsSelectionVisible, Is.True);
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
Assert.That(rewards.DebugChooseOption(0), Is.True);
yield return null;
CloseArtifactTutorialModal(manager);
Assert.That(service.CurrentMusicOutputGain, Is.GreaterThan(titleGain));
service.SetMasterVolume(0f);
RunManager.ForceProductionModeForTests = false;
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
Assert.That(director.TryDebugSpawnEventEnemy(RunTimedEvent.MidBoss), Is.True);
yield return null;
Assert.That(service.CurrentMusicOutputGain, Is.Zero);
manager.EndRun();
yield return new WaitForSecondsRealtime(0.6f);
Assert.That(service.AreMusicSourcesStopped, Is.True);
}
[UnityTest]
public IEnumerator SpawnDirector_DoesNotWaveSpawnWhileTitleIsBlocked()
{
ArtifactRewardController.GrantCatalogForTests = false;
RunManager.ForceProductionModeForTests = true;
yield return LoadScene();
RunManager manager = Object.FindAnyObjectByType<RunManager>();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
Assert.That(manager.IsTitleScreen, Is.True);
typeof(SpawnDirector)
.GetField("nextWaveTime", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(director, -1f);
yield return null;
Assert.That(EnemyController.AliveCount, Is.Zero);
Assert.That(RunManager.GameplayInputEnabled, Is.False);
}
private IEnumerator LoadScene()
{
AsyncOperation load = SceneManager.LoadSceneAsync(SceneName);
while (!load.isDone)
{
yield return null;
}
yield return null;
BumpCombatAudioService service =
Object.FindAnyObjectByType<BumpCombatAudioService>();
if (service != null && !capturedVolumes)
{
originalMaster = service.MasterVolume;
originalMusic = service.MusicVolume;
originalSfx = service.SfxVolume;
capturedVolumes = true;
}
}
private static IEnumerator PressAndRelease(Keyboard keyboard, Key key)
{
InputSystem.QueueStateEvent(keyboard, new KeyboardState(key));
yield return null;
Assert.That(keyboard[key].isPressed, Is.True);
InputSystem.QueueStateEvent(keyboard, new KeyboardState());
yield return null;
Assert.That(keyboard[key].isPressed, Is.False);
}
private static void EnsurePresentationEventSystem(
bool clearMenuNavigationActions)
{
System.Type styleType = typeof(RunHUD).Assembly.GetType(
"BumpCombat.UI.PresentationUiStyle",
true);
MethodInfo ensureMethod = styleType.GetMethod(
"EnsureEventSystem",
BindingFlags.Public | BindingFlags.Static);
Assert.That(ensureMethod, Is.Not.Null);
ensureMethod.Invoke(null, new object[] { clearMenuNavigationActions });
}
private static void CloseArtifactTutorialModal(RunManager manager)
{
RunTutorialController tutorials =
Object.FindAnyObjectByType<RunTutorialController>();
Assert.That(tutorials, Is.Not.Null);
Assert.That(tutorials.DebugIsVisible, Is.True,
"The first acquired artifact opens its tutorial modal before the next interaction.");
Assert.That(tutorials.DebugContinue(), Is.True);
Assert.That(tutorials.DebugIsVisible, Is.False);
Assert.That(manager.IsSelectionOpen, Is.False);
}
private static int CountActiveSceneEventSystems()
{
int count = 0;
GameObject[] roots = SceneManager.GetActiveScene().GetRootGameObjects();
foreach (GameObject root in roots)
{
count += root.GetComponentsInChildren<EventSystem>(true).Length;
}
return count;
}
private static int CountPlayingMusicSources()
{
AudioSource[] sources = Object.FindObjectsByType<AudioSource>(
FindObjectsInactive.Include,
FindObjectsSortMode.None);
int playingCount = 0;
for (int i = 0; i < sources.Length; i++)
{
if (sources[i].name.StartsWith("Music", System.StringComparison.Ordinal)
&& sources[i].isPlaying)
{
playingCount++;
}
}
return playingCount;
}
private Keyboard BeginVirtualKeyboardInput()
{
Assert.That(virtualKeyboardInputActive, Is.False);
previousBackgroundBehavior = InputSystem.settings.backgroundBehavior;
previousEditorInputBehavior = InputSystem.settings.editorInputBehaviorInPlayMode;
previousRunInBackground = Application.runInBackground;
InputSystem.settings.backgroundBehavior =
InputSettings.BackgroundBehavior.IgnoreFocus;
InputSystem.settings.editorInputBehaviorInPlayMode =
InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView;
Application.runInBackground = true;
virtualKeyboard = InputSystem.AddDevice<Keyboard>();
virtualKeyboard.MakeCurrent();
Assert.That(Keyboard.current, Is.SameAs(virtualKeyboard));
virtualKeyboardInputActive = true;
return virtualKeyboard;
}
private void EndVirtualKeyboardInput()
{
if (!virtualKeyboardInputActive)
{
return;
}
if (virtualKeyboard != null && virtualKeyboard.added)
{
InputSystem.RemoveDevice(virtualKeyboard);
}
InputSystem.settings.backgroundBehavior = previousBackgroundBehavior;
InputSystem.settings.editorInputBehaviorInPlayMode = previousEditorInputBehavior;
Application.runInBackground = previousRunInBackground;
virtualKeyboard = null;
virtualKeyboardInputActive = false;
}
private sealed class SelectionOwner : MonoBehaviour
{
}
}
}