Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aea34c11b1b446d3a9c2f4eafbbd3de9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 461719d1ac662d841923bc00a88249e8
|
||||
@@ -0,0 +1,883 @@
|
||||
using System.Collections.Generic;
|
||||
using BumpCombat.Audio;
|
||||
using BumpCombat.Core;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BumpCombat.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Runtime title, pause, and settings panels. The RunManager owns state;
|
||||
/// this component only presents and requests those transitions.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class RunMenuController : MonoBehaviour
|
||||
{
|
||||
private enum Screen
|
||||
{
|
||||
None,
|
||||
Title,
|
||||
TitleModes,
|
||||
Pause,
|
||||
Settings,
|
||||
}
|
||||
|
||||
private Screen currentScreen;
|
||||
private Screen settingsCaller;
|
||||
private RunManager runManager;
|
||||
private BumpCombatAudioService audioService;
|
||||
private GameObject panel;
|
||||
private GameObject modalDim;
|
||||
private Text titleText;
|
||||
private Text subtitleText;
|
||||
private Text inputHelpText;
|
||||
private readonly List<Button> buttons = new();
|
||||
private readonly List<GameObject> buttonFocusMarkers = new();
|
||||
private readonly GameObject[] settingsFocusRows = new GameObject[4];
|
||||
private Slider masterSlider;
|
||||
private Slider musicSlider;
|
||||
private Slider sfxSlider;
|
||||
private Text masterValueText;
|
||||
private Text musicValueText;
|
||||
private Text sfxValueText;
|
||||
private int selectedButton;
|
||||
private int selectedSettingsControl;
|
||||
|
||||
public bool IsVisible => panel != null && panel.activeSelf;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
runManager = RunManager.Instance;
|
||||
audioService = BumpCombatAudioService.Instance;
|
||||
PresentationUiStyle.EnsureEventSystem(
|
||||
clearMenuNavigationActions: true);
|
||||
CreatePanel();
|
||||
if (runManager != null)
|
||||
{
|
||||
runManager.OnTitleChanged += HandleTitleChanged;
|
||||
runManager.OnPauseChanged += HandlePauseChanged;
|
||||
runManager.OnGameOver += HandleGameOver;
|
||||
}
|
||||
|
||||
if (runManager != null && runManager.IsTitleScreen)
|
||||
{
|
||||
SetScreen(Screen.Title);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetScreen(Screen.None);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (runManager != null)
|
||||
{
|
||||
runManager.OnTitleChanged -= HandleTitleChanged;
|
||||
runManager.OnPauseChanged -= HandlePauseChanged;
|
||||
runManager.OnGameOver -= HandleGameOver;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (runManager == null || Keyboard.current == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Keyboard keyboard = Keyboard.current;
|
||||
if (keyboard.escapeKey.wasPressedThisFrame)
|
||||
{
|
||||
if (currentScreen == Screen.Settings)
|
||||
{
|
||||
SetScreen(settingsCaller);
|
||||
}
|
||||
else if (currentScreen == Screen.TitleModes)
|
||||
{
|
||||
SetScreen(Screen.Title);
|
||||
}
|
||||
else if (currentScreen == Screen.Pause)
|
||||
{
|
||||
runManager.ClosePause();
|
||||
}
|
||||
else if (currentScreen == Screen.None
|
||||
&& !runManager.IsSelectionOpen
|
||||
&& !runManager.IsGameOver
|
||||
&& !runManager.IsTitleScreen)
|
||||
{
|
||||
if (runManager.TryOpenPause())
|
||||
{
|
||||
SetScreen(Screen.Pause);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentScreen == Screen.Settings)
|
||||
{
|
||||
HandleSettingsKeyboard(keyboard);
|
||||
return;
|
||||
}
|
||||
|
||||
bool moved = false;
|
||||
if (keyboard.upArrowKey.wasPressedThisFrame)
|
||||
{
|
||||
selectedButton = FindNextActiveButton(-1);
|
||||
moved = true;
|
||||
}
|
||||
else if (keyboard.downArrowKey.wasPressedThisFrame)
|
||||
{
|
||||
selectedButton = FindNextActiveButton(1);
|
||||
moved = true;
|
||||
}
|
||||
if (moved)
|
||||
{
|
||||
SelectButton(selectedButton, true);
|
||||
}
|
||||
if (keyboard.enterKey.wasPressedThisFrame
|
||||
|| keyboard.numpadEnterKey.wasPressedThisFrame
|
||||
|| keyboard.spaceKey.wasPressedThisFrame)
|
||||
{
|
||||
if (selectedButton >= 0 && selectedButton < buttons.Count)
|
||||
{
|
||||
Button selected = buttons[selectedButton];
|
||||
if (selected != null && selected.interactable)
|
||||
{
|
||||
audioService?.PlayUi("ui_confirm");
|
||||
selected.onClick.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!IsVisible || modalDim == null || panel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Transform panelParent = panel.transform.parent;
|
||||
if (panelParent == null || modalDim.transform.parent != panelParent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int lastIndex = panelParent.childCount - 1;
|
||||
int modalIndex = modalDim.transform.GetSiblingIndex();
|
||||
int panelIndex = panel.transform.GetSiblingIndex();
|
||||
if (panelIndex == lastIndex && modalIndex == lastIndex - 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
modalDim.transform.SetAsLastSibling();
|
||||
panel.transform.SetAsLastSibling();
|
||||
}
|
||||
|
||||
private void HandleTitleChanged(bool isOpen)
|
||||
{
|
||||
if (isOpen)
|
||||
{
|
||||
SetScreen(Screen.Title);
|
||||
}
|
||||
else if (currentScreen == Screen.Title)
|
||||
{
|
||||
SetScreen(Screen.None);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePauseChanged(bool isOpen)
|
||||
{
|
||||
if (isOpen)
|
||||
{
|
||||
SetScreen(Screen.Pause);
|
||||
}
|
||||
else if (currentScreen == Screen.Pause)
|
||||
{
|
||||
SetScreen(Screen.None);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGameOver()
|
||||
{
|
||||
SetScreen(Screen.None);
|
||||
}
|
||||
|
||||
private void SetScreen(Screen screen)
|
||||
{
|
||||
currentScreen = screen;
|
||||
if (panel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
panel.SetActive(screen != Screen.None);
|
||||
if (modalDim != null)
|
||||
{
|
||||
modalDim.SetActive(screen != Screen.None);
|
||||
}
|
||||
if (screen == Screen.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
panel.transform.SetAsLastSibling();
|
||||
HideAllControls();
|
||||
switch (screen)
|
||||
{
|
||||
case Screen.Title:
|
||||
titleText.text = "Tiny Tackle Heroes";
|
||||
subtitleText.text = "DIRECTIONAL ACTION SURVIVAL";
|
||||
inputHelpText.text = "항목 이동: 위/아래 선택: Enter/Space";
|
||||
ShowButton(0, "게임시작", OpenTitleModes);
|
||||
ShowButton(1, "설정", OpenTitleSettings);
|
||||
ShowButton(2, "업적", OpenTitleAchievements);
|
||||
ShowButton(3, "나가기", QuitGame);
|
||||
break;
|
||||
|
||||
case Screen.TitleModes:
|
||||
titleText.text = "게임 시작";
|
||||
subtitleText.text = "모드를 선택하세요";
|
||||
inputHelpText.text = "항목 이동: 위/아래 선택: Enter/Space 뒤로: Esc";
|
||||
ShowTitleModeButtons();
|
||||
break;
|
||||
|
||||
case Screen.Pause:
|
||||
titleText.text = "PAUSED";
|
||||
subtitleText.text = "THE RUN IS WAITING";
|
||||
inputHelpText.text = "항목 이동: 위/아래 선택: Enter/Space 계속: Esc";
|
||||
ShowButton(0, "계속", ContinueRun);
|
||||
ShowButton(1, "설정", OpenPauseSettings);
|
||||
ShowButton(2, "다시 시작", RestartRun);
|
||||
ShowButton(3, "타이틀", ReturnToTitle);
|
||||
break;
|
||||
|
||||
case Screen.Settings:
|
||||
titleText.text = "설정";
|
||||
subtitleText.text = "음량";
|
||||
inputHelpText.text = "항목 이동: 위/아래 음량 조절: 왼쪽/오른쪽\n뒤로 선택: Enter/Space 즉시 뒤로: Esc";
|
||||
ShowSlider(masterSlider, masterValueText, "전체", audioService?.MasterVolume ?? 0.8f);
|
||||
ShowSlider(musicSlider, musicValueText, "음악", audioService?.MusicVolume ?? 0.55f);
|
||||
ShowSlider(sfxSlider, sfxValueText, "효과음", audioService?.SfxVolume ?? 0.8f);
|
||||
ShowButton(3, "뒤로", BackFromSettings);
|
||||
break;
|
||||
}
|
||||
|
||||
if (screen == Screen.Settings)
|
||||
{
|
||||
selectedSettingsControl = 0;
|
||||
SelectSettingsControl(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedButton = FindFirstActiveButton();
|
||||
SelectButton(selectedButton, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void HideAllControls()
|
||||
{
|
||||
for (int i = 0; i < buttons.Count; i++)
|
||||
{
|
||||
buttons[i].gameObject.SetActive(false);
|
||||
}
|
||||
SetControlVisible(masterSlider, false);
|
||||
SetControlVisible(musicSlider, false);
|
||||
SetControlVisible(sfxSlider, false);
|
||||
masterValueText.gameObject.SetActive(false);
|
||||
musicValueText.gameObject.SetActive(false);
|
||||
sfxValueText.gameObject.SetActive(false);
|
||||
for (int i = 0; i < settingsFocusRows.Length; i++)
|
||||
{
|
||||
settingsFocusRows[i]?.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowButton(int index, string label, UnityEngine.Events.UnityAction action)
|
||||
{
|
||||
Button button = GetButton(index);
|
||||
button.gameObject.SetActive(true);
|
||||
button.interactable = true;
|
||||
PositionButton(button, 95f - index * 88f);
|
||||
Text labelText = button.GetComponentInChildren<Text>();
|
||||
labelText.text = label;
|
||||
labelText.color = Color.white;
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(action);
|
||||
}
|
||||
|
||||
private Button GetButton(int index)
|
||||
{
|
||||
Transform child = panel.transform.Find($"Button {index + 1}");
|
||||
Button button = child.GetComponent<Button>();
|
||||
buttons[index] = button;
|
||||
return button;
|
||||
}
|
||||
|
||||
private void SelectButton(int index, bool playSound)
|
||||
{
|
||||
if (buttons.Count == 0 || index < 0 || index >= buttons.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Button button = buttons[index];
|
||||
if (!IsButtonSelectable(button))
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < buttonFocusMarkers.Count; i++)
|
||||
{
|
||||
buttonFocusMarkers[i].SetActive(currentScreen != Screen.Settings && i == index);
|
||||
}
|
||||
EventSystem.current?.SetSelectedGameObject(button.gameObject);
|
||||
if (playSound)
|
||||
{
|
||||
audioService?.PlayUi("ui_move");
|
||||
}
|
||||
}
|
||||
|
||||
private int FindFirstActiveButton()
|
||||
{
|
||||
for (int i = 0; i < buttons.Count; i++)
|
||||
{
|
||||
if (IsButtonSelectable(buttons[i]))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int FindNextActiveButton(int direction)
|
||||
{
|
||||
if (buttons.Count == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
int candidate = selectedButton;
|
||||
for (int i = 0; i < buttons.Count; i++)
|
||||
{
|
||||
candidate = (candidate + direction + buttons.Count) % buttons.Count;
|
||||
if (IsButtonSelectable(buttons[candidate]))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return selectedButton;
|
||||
}
|
||||
|
||||
private static bool IsButtonSelectable(Button button)
|
||||
{
|
||||
return button != null
|
||||
&& button.gameObject.activeSelf
|
||||
&& button.interactable;
|
||||
}
|
||||
|
||||
private void HandleSettingsKeyboard(Keyboard keyboard)
|
||||
{
|
||||
if (keyboard.upArrowKey.wasPressedThisFrame)
|
||||
{
|
||||
selectedSettingsControl = (selectedSettingsControl + 3) % 4;
|
||||
SelectSettingsControl(true);
|
||||
}
|
||||
else if (keyboard.downArrowKey.wasPressedThisFrame)
|
||||
{
|
||||
selectedSettingsControl = (selectedSettingsControl + 1) % 4;
|
||||
SelectSettingsControl(true);
|
||||
}
|
||||
|
||||
if (selectedSettingsControl < 3
|
||||
&& (keyboard.leftArrowKey.wasPressedThisFrame
|
||||
|| keyboard.rightArrowKey.wasPressedThisFrame))
|
||||
{
|
||||
Slider slider = GetSettingsSlider(selectedSettingsControl);
|
||||
float delta = keyboard.rightArrowKey.wasPressedThisFrame ? 0.05f : -0.05f;
|
||||
slider.value = Mathf.Clamp01(slider.value + delta);
|
||||
}
|
||||
else if (selectedSettingsControl == 3
|
||||
&& (keyboard.enterKey.wasPressedThisFrame
|
||||
|| keyboard.numpadEnterKey.wasPressedThisFrame
|
||||
|| keyboard.spaceKey.wasPressedThisFrame))
|
||||
{
|
||||
audioService?.PlayUi("ui_confirm");
|
||||
buttons[3].onClick.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private Slider GetSettingsSlider(int index)
|
||||
{
|
||||
return index switch
|
||||
{
|
||||
0 => masterSlider,
|
||||
1 => musicSlider,
|
||||
_ => sfxSlider,
|
||||
};
|
||||
}
|
||||
|
||||
private void SelectSettingsControl(bool playSound)
|
||||
{
|
||||
for (int i = 0; i < settingsFocusRows.Length; i++)
|
||||
{
|
||||
if (settingsFocusRows[i] != null)
|
||||
{
|
||||
settingsFocusRows[i].SetActive(i == selectedSettingsControl);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedSettingsControl == 3)
|
||||
{
|
||||
SelectButton(3, playSound);
|
||||
return;
|
||||
}
|
||||
Slider slider = GetSettingsSlider(selectedSettingsControl);
|
||||
EventSystem.current?.SetSelectedGameObject(slider.gameObject);
|
||||
if (playSound)
|
||||
{
|
||||
audioService?.PlayUi("ui_move");
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginDevelopmentRun()
|
||||
{
|
||||
BeginRun(RunMode.Development);
|
||||
}
|
||||
|
||||
private void BeginProductionRun()
|
||||
{
|
||||
BeginRun(RunMode.Production);
|
||||
}
|
||||
|
||||
private void OpenTitleModes()
|
||||
{
|
||||
SetScreen(Screen.TitleModes);
|
||||
}
|
||||
|
||||
private void ShowTitleModeButtons()
|
||||
{
|
||||
int index = 0;
|
||||
if (IsDevelopmentStartVisible(Application.isEditor, Debug.isDebugBuild))
|
||||
{
|
||||
ShowButton(index++, "개발용 시작", BeginDevelopmentRun);
|
||||
}
|
||||
ShowButton(index++, "기본 모드", BeginProductionRun);
|
||||
ShowDisabledButton(index++, "챌린지 모드(준비 중)");
|
||||
ShowDisabledButton(index++, "하드코어 모드(준비 중)");
|
||||
ShowButton(index, "뒤로", ReturnToTitleMenu);
|
||||
|
||||
int visibleButtonCount = index + 1;
|
||||
float top = (visibleButtonCount - 1) * 36f;
|
||||
for (int buttonIndex = 0; buttonIndex < visibleButtonCount; buttonIndex++)
|
||||
{
|
||||
RectTransform rect = buttons[buttonIndex].GetComponent<RectTransform>();
|
||||
rect.anchoredPosition = new Vector2(
|
||||
rect.anchoredPosition.x,
|
||||
top - buttonIndex * 72f);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowDisabledButton(int index, string label)
|
||||
{
|
||||
Button button = GetButton(index);
|
||||
button.gameObject.SetActive(true);
|
||||
button.interactable = false;
|
||||
PositionButton(button, 95f - index * 88f);
|
||||
Text labelText = button.GetComponentInChildren<Text>();
|
||||
labelText.text = label;
|
||||
labelText.color = new Color32(166, 171, 181, 255);
|
||||
button.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
private static bool IsDevelopmentStartVisible(
|
||||
bool isEditor,
|
||||
bool isDevelopmentBuild)
|
||||
{
|
||||
return isEditor || isDevelopmentBuild;
|
||||
}
|
||||
|
||||
private static void PositionButton(Button button, float y)
|
||||
{
|
||||
RectTransform rect = button.GetComponent<RectTransform>();
|
||||
rect.anchoredPosition = new Vector2(rect.anchoredPosition.x, y);
|
||||
}
|
||||
|
||||
private void OpenTitleAchievements()
|
||||
{
|
||||
MissionAchievementPanel achievements = GetComponent<MissionAchievementPanel>();
|
||||
SetScreen(Screen.None);
|
||||
if (achievements == null
|
||||
|| !achievements.OpenTitleAchievements(ReturnToTitleMenu))
|
||||
{
|
||||
SetScreen(Screen.Title);
|
||||
return;
|
||||
}
|
||||
audioService?.PlayUi("ui_confirm");
|
||||
}
|
||||
|
||||
private void ReturnToTitleMenu()
|
||||
{
|
||||
if (runManager != null && runManager.IsTitleScreen)
|
||||
{
|
||||
SetScreen(Screen.Title);
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginRun(RunMode mode)
|
||||
{
|
||||
if (runManager == null || !runManager.BeginRun(mode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
audioService?.PlayUi("ui_confirm");
|
||||
SetScreen(Screen.None);
|
||||
}
|
||||
|
||||
private void ContinueRun()
|
||||
{
|
||||
audioService?.PlayUi("ui_confirm");
|
||||
runManager?.ClosePause();
|
||||
}
|
||||
|
||||
private void OpenTitleSettings()
|
||||
{
|
||||
settingsCaller = Screen.Title;
|
||||
SetScreen(Screen.Settings);
|
||||
}
|
||||
|
||||
private void OpenPauseSettings()
|
||||
{
|
||||
settingsCaller = Screen.Pause;
|
||||
SetScreen(Screen.Settings);
|
||||
}
|
||||
|
||||
private void BackFromSettings()
|
||||
{
|
||||
SetScreen(settingsCaller);
|
||||
}
|
||||
|
||||
private void RestartRun()
|
||||
{
|
||||
audioService?.PlayUi("ui_confirm");
|
||||
runManager?.RestartRun();
|
||||
}
|
||||
|
||||
private void ReturnToTitle()
|
||||
{
|
||||
audioService?.PlayUi("ui_confirm");
|
||||
runManager?.RestartToTitle();
|
||||
}
|
||||
|
||||
private static void QuitGame()
|
||||
{
|
||||
Application.Quit();
|
||||
}
|
||||
|
||||
private void ShowSlider(
|
||||
Slider slider,
|
||||
Text valueText,
|
||||
string label,
|
||||
float value)
|
||||
{
|
||||
slider.gameObject.SetActive(true);
|
||||
valueText.gameObject.SetActive(true);
|
||||
slider.value = Mathf.Clamp01(value);
|
||||
valueText.text = $"{label} {Mathf.RoundToInt(slider.value * 100f)}%";
|
||||
}
|
||||
|
||||
private void HandleMasterSlider(float value)
|
||||
{
|
||||
audioService?.SetMasterVolume(value);
|
||||
masterValueText.text = $"MASTER {Mathf.RoundToInt(value * 100f)}%";
|
||||
}
|
||||
|
||||
private void HandleMusicSlider(float value)
|
||||
{
|
||||
audioService?.SetMusicVolume(value);
|
||||
musicValueText.text = $"MUSIC {Mathf.RoundToInt(value * 100f)}%";
|
||||
}
|
||||
|
||||
private void HandleSfxSlider(float value)
|
||||
{
|
||||
audioService?.SetSfxVolume(value);
|
||||
sfxValueText.text = $"SFX {Mathf.RoundToInt(value * 100f)}%";
|
||||
}
|
||||
|
||||
private void CreatePanel()
|
||||
{
|
||||
Font font = PresentationUiStyle.GetGalmuriFont();
|
||||
panel = PresentationUiStyle.CreatePanel(
|
||||
transform,
|
||||
"Run Menu",
|
||||
new Vector2(0.5f, 0.5f),
|
||||
Vector2.zero,
|
||||
new Vector2(1180f, 720f),
|
||||
Color.white);
|
||||
PresentationUiStyle.ApplyPanel(panel.GetComponent<Image>());
|
||||
|
||||
modalDim = new GameObject(
|
||||
"Run Menu Modal Dim",
|
||||
typeof(RectTransform),
|
||||
typeof(CanvasRenderer),
|
||||
typeof(Image));
|
||||
modalDim.transform.SetParent(transform, false);
|
||||
RectTransform modalRect = modalDim.GetComponent<RectTransform>();
|
||||
modalRect.anchorMin = Vector2.zero;
|
||||
modalRect.anchorMax = Vector2.one;
|
||||
modalRect.offsetMin = Vector2.zero;
|
||||
modalRect.offsetMax = Vector2.zero;
|
||||
Image modalImage = modalDim.GetComponent<Image>();
|
||||
modalImage.color = new Color(0f, 0f, 0f, 0.72f);
|
||||
modalImage.raycastTarget = true;
|
||||
modalDim.transform.SetSiblingIndex(panel.transform.GetSiblingIndex());
|
||||
|
||||
titleText = CreateText(panel.transform, "Title", font, 46, new Vector2(0f, 260f), new Vector2(1000f, 70f));
|
||||
subtitleText = CreateText(panel.transform, "Subtitle", font, 18, new Vector2(0f, 205f), new Vector2(1000f, 40f));
|
||||
inputHelpText = CreateText(panel.transform, "Menu Input Help", font, 18, Vector2.zero, new Vector2(1100f, 40f));
|
||||
RectTransform inputHelpRect = inputHelpText.rectTransform;
|
||||
inputHelpRect.anchorMin = Vector2.zero;
|
||||
inputHelpRect.anchorMax = Vector2.zero;
|
||||
inputHelpRect.pivot = Vector2.zero;
|
||||
inputHelpRect.anchoredPosition = new Vector2(32f, 24f);
|
||||
inputHelpRect.sizeDelta = new Vector2(1100f, 60f);
|
||||
inputHelpText.alignment = TextAnchor.MiddleLeft;
|
||||
inputHelpText.color = new Color32(200, 208, 218, 255);
|
||||
CreateSettingsFocusRows(panel.transform);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Button button = CreateButton(panel.transform, $"Button {i + 1}", font, new Vector2(0f, 95f - i * 88f));
|
||||
button.gameObject.SetActive(false);
|
||||
buttons.Add(button);
|
||||
buttonFocusMarkers.Add(CreateButtonFocusMarker(button.transform));
|
||||
AddButtonPointerSelection(button, i);
|
||||
}
|
||||
|
||||
masterSlider = CreateSlider(panel.transform, "Master Slider", new Vector2(0f, 90f));
|
||||
musicSlider = CreateSlider(panel.transform, "Music Slider", new Vector2(0f, 0f));
|
||||
sfxSlider = CreateSlider(panel.transform, "Sfx Slider", new Vector2(0f, -90f));
|
||||
masterValueText = CreateText(panel.transform, "Master Value", font, 22, new Vector2(-230f, 90f), new Vector2(280f, 50f));
|
||||
musicValueText = CreateText(panel.transform, "Music Value", font, 22, new Vector2(-230f, 0f), new Vector2(280f, 50f));
|
||||
sfxValueText = CreateText(panel.transform, "Sfx Value", font, 22, new Vector2(-230f, -90f), new Vector2(280f, 50f));
|
||||
masterSlider.onValueChanged.AddListener(HandleMasterSlider);
|
||||
musicSlider.onValueChanged.AddListener(HandleMusicSlider);
|
||||
sfxSlider.onValueChanged.AddListener(HandleSfxSlider);
|
||||
AddSliderPointerSelection(masterSlider, 0);
|
||||
AddSliderPointerSelection(musicSlider, 1);
|
||||
AddSliderPointerSelection(sfxSlider, 2);
|
||||
panel.SetActive(false);
|
||||
}
|
||||
|
||||
private void CreateSettingsFocusRows(Transform parent)
|
||||
{
|
||||
for (int i = 0; i < settingsFocusRows.Length; i++)
|
||||
{
|
||||
bool isBackButton = i == 3;
|
||||
float y = isBackButton ? -169f : 90f - i * 90f;
|
||||
Vector2 size = isBackButton
|
||||
? new Vector2(520f, 76f)
|
||||
: new Vector2(820f, 52f);
|
||||
GameObject row = new($"Settings Focus {i}", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
row.transform.SetParent(parent, false);
|
||||
RectTransform rowRect = row.GetComponent<RectTransform>();
|
||||
rowRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
rowRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
rowRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
rowRect.anchoredPosition = new Vector2(isBackButton ? 0f : 5f, y);
|
||||
rowRect.sizeDelta = size;
|
||||
Image rowImage = row.GetComponent<Image>();
|
||||
rowImage.color = new Color32(75, 128, 148, 72);
|
||||
rowImage.raycastTarget = false;
|
||||
|
||||
GameObject marker = new($"Marker {i}", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
marker.transform.SetParent(row.transform, false);
|
||||
RectTransform markerRect = marker.GetComponent<RectTransform>();
|
||||
markerRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
markerRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
markerRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
markerRect.anchoredPosition = new Vector2(-size.x * 0.5f + 12f, 0f);
|
||||
markerRect.sizeDelta = new Vector2(6f, 34f);
|
||||
Image markerImage = marker.GetComponent<Image>();
|
||||
markerImage.color = new Color32(240, 196, 92, 255);
|
||||
markerImage.raycastTarget = false;
|
||||
row.SetActive(false);
|
||||
settingsFocusRows[i] = row;
|
||||
}
|
||||
}
|
||||
|
||||
private static GameObject CreateButtonFocusMarker(Transform parent)
|
||||
{
|
||||
GameObject marker = new("Focus Marker", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
marker.transform.SetParent(parent, false);
|
||||
RectTransform rect = marker.GetComponent<RectTransform>();
|
||||
rect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
rect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
rect.pivot = new Vector2(0.5f, 0.5f);
|
||||
rect.anchoredPosition = new Vector2(-210f, 0f);
|
||||
rect.sizeDelta = new Vector2(6f, 38f);
|
||||
Image image = marker.GetComponent<Image>();
|
||||
image.color = new Color32(240, 196, 92, 255);
|
||||
image.raycastTarget = false;
|
||||
marker.SetActive(false);
|
||||
return marker;
|
||||
}
|
||||
|
||||
private static Text CreateText(
|
||||
Transform parent,
|
||||
string name,
|
||||
Font font,
|
||||
int fontSize,
|
||||
Vector2 position,
|
||||
Vector2 size)
|
||||
{
|
||||
return PresentationUiStyle.CreateText(
|
||||
parent,
|
||||
name,
|
||||
font,
|
||||
fontSize,
|
||||
TextAnchor.MiddleCenter,
|
||||
position,
|
||||
size);
|
||||
}
|
||||
|
||||
private static Button CreateButton(
|
||||
Transform parent,
|
||||
string name,
|
||||
Font font,
|
||||
Vector2 position)
|
||||
{
|
||||
GameObject buttonObject = new(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button));
|
||||
buttonObject.transform.SetParent(parent, false);
|
||||
RectTransform rect = buttonObject.GetComponent<RectTransform>();
|
||||
rect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
rect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
rect.pivot = new Vector2(0.5f, 0.5f);
|
||||
rect.anchoredPosition = position;
|
||||
rect.sizeDelta = new Vector2(460f, 68f);
|
||||
Button button = buttonObject.GetComponent<Button>();
|
||||
Image image = buttonObject.GetComponent<Image>();
|
||||
button.targetGraphic = image;
|
||||
Navigation navigation = button.navigation;
|
||||
navigation.mode = Navigation.Mode.None;
|
||||
button.navigation = navigation;
|
||||
PresentationUiStyle.ApplyButton(button);
|
||||
Text text = CreateText(buttonObject.transform, "Label", font, 24, Vector2.zero, new Vector2(430f, 58f));
|
||||
text.raycastTarget = false;
|
||||
return button;
|
||||
}
|
||||
|
||||
private static Slider CreateSlider(Transform parent, string name, Vector2 position)
|
||||
{
|
||||
GameObject sliderObject = new(name, typeof(RectTransform), typeof(Slider));
|
||||
sliderObject.transform.SetParent(parent, false);
|
||||
RectTransform rect = sliderObject.GetComponent<RectTransform>();
|
||||
rect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
rect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
rect.pivot = new Vector2(0.5f, 0.5f);
|
||||
rect.anchoredPosition = position + Vector2.right * 130f;
|
||||
rect.sizeDelta = new Vector2(500f, 42f);
|
||||
Slider slider = sliderObject.GetComponent<Slider>();
|
||||
slider.minValue = 0f;
|
||||
slider.maxValue = 1f;
|
||||
slider.wholeNumbers = false;
|
||||
GameObject backgroundObject = new("Background", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
backgroundObject.transform.SetParent(sliderObject.transform, false);
|
||||
RectTransform backgroundRect = backgroundObject.GetComponent<RectTransform>();
|
||||
backgroundRect.anchorMin = new Vector2(0f, 0.5f);
|
||||
backgroundRect.anchorMax = new Vector2(1f, 0.5f);
|
||||
backgroundRect.sizeDelta = new Vector2(0f, 18f);
|
||||
Image background = backgroundObject.GetComponent<Image>();
|
||||
background.color = new Color(0.16f, 0.19f, 0.24f, 1f);
|
||||
|
||||
GameObject fillAreaObject = new("Fill Area", typeof(RectTransform));
|
||||
fillAreaObject.transform.SetParent(sliderObject.transform, false);
|
||||
RectTransform fillAreaRect = fillAreaObject.GetComponent<RectTransform>();
|
||||
fillAreaRect.anchorMin = new Vector2(0f, 0.5f);
|
||||
fillAreaRect.anchorMax = new Vector2(1f, 0.5f);
|
||||
fillAreaRect.sizeDelta = new Vector2(0f, 18f);
|
||||
GameObject fillObject = new("Fill", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
fillObject.transform.SetParent(fillAreaObject.transform, false);
|
||||
RectTransform fillRect = fillObject.GetComponent<RectTransform>();
|
||||
fillRect.anchorMin = Vector2.zero;
|
||||
fillRect.anchorMax = Vector2.one;
|
||||
fillRect.offsetMin = Vector2.zero;
|
||||
fillRect.offsetMax = Vector2.zero;
|
||||
Image fillImage = fillObject.GetComponent<Image>();
|
||||
fillImage.color = new Color32(222, 183, 112, 255);
|
||||
|
||||
GameObject handleAreaObject = new("Handle Area", typeof(RectTransform));
|
||||
handleAreaObject.transform.SetParent(sliderObject.transform, false);
|
||||
RectTransform handleAreaRect = handleAreaObject.GetComponent<RectTransform>();
|
||||
handleAreaRect.anchorMin = new Vector2(0f, 0.5f);
|
||||
handleAreaRect.anchorMax = new Vector2(1f, 0.5f);
|
||||
handleAreaRect.sizeDelta = Vector2.zero;
|
||||
GameObject handleObject = new("Handle", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
handleObject.transform.SetParent(handleAreaObject.transform, false);
|
||||
RectTransform handleRect = handleObject.GetComponent<RectTransform>();
|
||||
handleRect.anchorMin = new Vector2(0f, 0.5f);
|
||||
handleRect.anchorMax = new Vector2(0f, 0.5f);
|
||||
handleRect.sizeDelta = new Vector2(24f, 36f);
|
||||
Image handleImage = handleObject.GetComponent<Image>();
|
||||
handleImage.color = new Color32(240, 216, 153, 255);
|
||||
slider.fillRect = fillRect;
|
||||
slider.handleRect = handleRect;
|
||||
slider.targetGraphic = handleImage;
|
||||
Navigation navigation = slider.navigation;
|
||||
navigation.mode = Navigation.Mode.None;
|
||||
slider.navigation = navigation;
|
||||
slider.direction = Slider.Direction.LeftToRight;
|
||||
return slider;
|
||||
}
|
||||
|
||||
private void AddButtonPointerSelection(Button button, int index)
|
||||
{
|
||||
EventTrigger trigger = button.gameObject.AddComponent<EventTrigger>();
|
||||
EventTrigger.Entry entry = new() { eventID = EventTriggerType.PointerEnter };
|
||||
entry.callback.AddListener(_ =>
|
||||
{
|
||||
if (!button.interactable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
selectedButton = index;
|
||||
if (currentScreen == Screen.Settings && index == 3)
|
||||
{
|
||||
selectedSettingsControl = 3;
|
||||
SelectSettingsControl(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectButton(index, false);
|
||||
}
|
||||
});
|
||||
trigger.triggers.Add(entry);
|
||||
}
|
||||
|
||||
private void AddSliderPointerSelection(Slider slider, int index)
|
||||
{
|
||||
EventTrigger trigger = slider.gameObject.AddComponent<EventTrigger>();
|
||||
EventTrigger.Entry entry = new() { eventID = EventTriggerType.PointerEnter };
|
||||
entry.callback.AddListener(_ =>
|
||||
{
|
||||
selectedSettingsControl = index;
|
||||
SelectSettingsControl(false);
|
||||
});
|
||||
trigger.triggers.Add(entry);
|
||||
}
|
||||
|
||||
private static void SetControlVisible(Component control, bool visible)
|
||||
{
|
||||
if (control != null)
|
||||
{
|
||||
control.gameObject.SetActive(visible);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3be9c01d27a64d09a68f4e2b9c5d7a13
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,447 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using BumpCombat.Audio;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Player;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BumpCombat.UI
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class RunTutorialController : MonoBehaviour
|
||||
{
|
||||
public enum TutorialKind
|
||||
{
|
||||
Artifact,
|
||||
Guard,
|
||||
ArtifactEnhancement,
|
||||
}
|
||||
|
||||
private readonly List<TutorialKind> pendingTutorials = new();
|
||||
private readonly HashSet<TutorialKind> queuedOrShown = new();
|
||||
private RunManager runManager;
|
||||
private GameObject panel;
|
||||
private GameObject modalDim;
|
||||
private TutorialMotionPreview motionPreview;
|
||||
private Text titleText;
|
||||
private Text bodyText;
|
||||
private Text helpText;
|
||||
private Button continueButton;
|
||||
private TutorialKind? currentTutorial;
|
||||
private bool artifactAcquired;
|
||||
private int inputReadyFrame;
|
||||
private bool started;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
runManager = RunManager.Instance;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (runManager == null)
|
||||
{
|
||||
runManager = RunManager.Instance;
|
||||
}
|
||||
if (runManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
runManager.OnGuardAvailabilityChanged += HandleGuardAvailabilityChanged;
|
||||
runManager.OnArtifactChargeAvailabilityChanged +=
|
||||
HandleArtifactChargeAvailabilityChanged;
|
||||
runManager.OnSelectionChanged += HandleSelectionChanged;
|
||||
runManager.OnPauseChanged += HandlePauseChanged;
|
||||
runManager.OnGameOver += HandleGameOver;
|
||||
if (started)
|
||||
{
|
||||
TryShowNextTutorial();
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
PresentationUiStyle.EnsureEventSystem(
|
||||
clearMenuNavigationActions: false);
|
||||
CreatePanel();
|
||||
|
||||
if (runManager != null && runManager.IsDevelopmentRun)
|
||||
{
|
||||
if (runManager.IsGuardUnlocked)
|
||||
{
|
||||
QueueTutorial(TutorialKind.Guard);
|
||||
}
|
||||
if (runManager.IsArtifactChargeUnlocked)
|
||||
{
|
||||
QueueTutorial(TutorialKind.ArtifactEnhancement);
|
||||
}
|
||||
}
|
||||
|
||||
started = true;
|
||||
TryShowNextTutorial();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
motionPreview?.Hide();
|
||||
if (runManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
runManager.OnGuardAvailabilityChanged -= HandleGuardAvailabilityChanged;
|
||||
runManager.OnArtifactChargeAvailabilityChanged -=
|
||||
HandleArtifactChargeAvailabilityChanged;
|
||||
runManager.OnSelectionChanged -= HandleSelectionChanged;
|
||||
runManager.OnPauseChanged -= HandlePauseChanged;
|
||||
runManager.OnGameOver -= HandleGameOver;
|
||||
|
||||
if (currentTutorial.HasValue)
|
||||
{
|
||||
pendingTutorials.Insert(0, currentTutorial.Value);
|
||||
currentTutorial = null;
|
||||
panel?.SetActive(false);
|
||||
modalDim?.SetActive(false);
|
||||
if (runManager.IsSelectionOpen)
|
||||
{
|
||||
runManager.CloseSelection(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!currentTutorial.HasValue
|
||||
|| Time.frameCount <= inputReadyFrame
|
||||
|| Keyboard.current == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Keyboard keyboard = Keyboard.current;
|
||||
if (keyboard.spaceKey.wasPressedThisFrame
|
||||
|| keyboard.enterKey.wasPressedThisFrame
|
||||
|| keyboard.numpadEnterKey.wasPressedThisFrame)
|
||||
{
|
||||
ContinueCurrentTutorial();
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!currentTutorial.HasValue || panel == null || modalDim == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int lastIndex = transform.childCount - 1;
|
||||
int dimIndex = modalDim.transform.GetSiblingIndex();
|
||||
int panelIndex = panel.transform.GetSiblingIndex();
|
||||
if (panelIndex == lastIndex && dimIndex == lastIndex - 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
modalDim.transform.SetAsLastSibling();
|
||||
panel.transform.SetAsLastSibling();
|
||||
}
|
||||
|
||||
internal void NotifyArtifactAcquired()
|
||||
{
|
||||
artifactAcquired = true;
|
||||
QueueTutorial(TutorialKind.Artifact);
|
||||
}
|
||||
|
||||
private void HandleGuardAvailabilityChanged(bool available)
|
||||
{
|
||||
if (available
|
||||
&& (runManager.IsProductionRun || runManager.IsDevelopmentRun))
|
||||
{
|
||||
QueueTutorial(TutorialKind.Guard);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleArtifactChargeAvailabilityChanged(bool available)
|
||||
{
|
||||
if (available
|
||||
&& (runManager.IsProductionRun || runManager.IsDevelopmentRun))
|
||||
{
|
||||
QueueTutorial(TutorialKind.ArtifactEnhancement);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSelectionChanged(bool isOpen)
|
||||
{
|
||||
if (!isOpen)
|
||||
{
|
||||
TryShowNextTutorial();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePauseChanged(bool isOpen)
|
||||
{
|
||||
if (!isOpen)
|
||||
{
|
||||
TryShowNextTutorial();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGameOver()
|
||||
{
|
||||
pendingTutorials.Clear();
|
||||
queuedOrShown.Clear();
|
||||
motionPreview?.Hide();
|
||||
if (currentTutorial.HasValue)
|
||||
{
|
||||
currentTutorial = null;
|
||||
panel?.SetActive(false);
|
||||
modalDim?.SetActive(false);
|
||||
runManager.CloseSelection(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueTutorial(TutorialKind kind)
|
||||
{
|
||||
if (queuedOrShown.Contains(kind))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
queuedOrShown.Add(kind);
|
||||
pendingTutorials.Add(kind);
|
||||
pendingTutorials.Sort();
|
||||
TryShowNextTutorial();
|
||||
}
|
||||
|
||||
private void TryShowNextTutorial()
|
||||
{
|
||||
if (panel == null
|
||||
|| !isActiveAndEnabled
|
||||
|| currentTutorial.HasValue
|
||||
|| pendingTutorials.Count == 0
|
||||
|| (runManager != null
|
||||
&& runManager.IsDevelopmentRun
|
||||
&& !artifactAcquired))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (runManager == null || !runManager.TryOpenSelection(this))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
currentTutorial = pendingTutorials[0];
|
||||
pendingTutorials.RemoveAt(0);
|
||||
RenderCurrentTutorial();
|
||||
modalDim.SetActive(true);
|
||||
panel.SetActive(true);
|
||||
motionPreview?.Show(currentTutorial.Value);
|
||||
inputReadyFrame = Time.frameCount;
|
||||
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
|
||||
}
|
||||
|
||||
private void ContinueCurrentTutorial()
|
||||
{
|
||||
if (!currentTutorial.HasValue || Time.frameCount <= inputReadyFrame)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FinishCurrentTutorial();
|
||||
}
|
||||
|
||||
private void FinishCurrentTutorial()
|
||||
{
|
||||
if (!currentTutorial.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
currentTutorial = null;
|
||||
motionPreview?.Hide();
|
||||
panel.SetActive(false);
|
||||
modalDim.SetActive(false);
|
||||
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
|
||||
runManager.CloseSelection(this);
|
||||
TryShowNextTutorial();
|
||||
}
|
||||
|
||||
private void RenderCurrentTutorial()
|
||||
{
|
||||
switch (currentTutorial)
|
||||
{
|
||||
case TutorialKind.Artifact:
|
||||
titleText.text = "아티팩트 획득";
|
||||
bodyText.text =
|
||||
"1. 아티팩트를 더 얻은 뒤 S로 교체합니다.\n"
|
||||
+ "2. A를 눌렀다 놓으면 일반기를 사용합니다.\n"
|
||||
+ "3. 이동하거나 몸통박치기에 성공하면 공용 게이지가 찹니다.";
|
||||
break;
|
||||
|
||||
case TutorialKind.Guard:
|
||||
PlayerHealth playerHealth =
|
||||
Object.FindAnyObjectByType<PlayerHealth>();
|
||||
float guardDuration = playerHealth != null
|
||||
? playerHealth.GuardDuration
|
||||
: PlayerHealth.DefaultGuardDuration;
|
||||
float guardCooldown = playerHealth != null
|
||||
? playerHealth.GuardCooldownDuration
|
||||
: PlayerHealth.DefaultGuardCooldown;
|
||||
float guardSuccessRechargePercent = playerHealth != null
|
||||
? playerHealth.GuardSuccessCooldownRechargePercent
|
||||
: PlayerHealth.DefaultGuardSuccessCooldownRechargePercent;
|
||||
titleText.text = "가드 해금";
|
||||
bodyText.text =
|
||||
$"D를 누르면 {guardDuration.ToString("0.##", CultureInfo.InvariantCulture)}초 동안 피해를 막습니다.\n"
|
||||
+ $"기본 재사용 대기시간은 {guardCooldown.ToString("0.##", CultureInfo.InvariantCulture)}초입니다.\n"
|
||||
+ $"가드 성공 시 가드 게이지를 {guardSuccessRechargePercent.ToString("0.##", CultureInfo.InvariantCulture)}% 충전합니다.\n"
|
||||
+ "발동당 1회 적용되며, 남은 대기시간이 줄어듭니다.";
|
||||
break;
|
||||
|
||||
case TutorialKind.ArtifactEnhancement:
|
||||
titleText.text = "아티팩트 강화 해금";
|
||||
bodyText.text =
|
||||
"A를 길게 눌러 완충한 뒤 놓으면 강화기를 발동합니다.\n"
|
||||
+ "강화기는 일반기와 같은 양의 게이지를 소모합니다.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void CreatePanel()
|
||||
{
|
||||
Font font = PresentationUiStyle.GetGalmuriFont();
|
||||
modalDim = new GameObject(
|
||||
"Run Tutorial Modal Dim",
|
||||
typeof(RectTransform),
|
||||
typeof(CanvasRenderer),
|
||||
typeof(Image));
|
||||
modalDim.transform.SetParent(transform, false);
|
||||
RectTransform dimRect = modalDim.GetComponent<RectTransform>();
|
||||
dimRect.anchorMin = Vector2.zero;
|
||||
dimRect.anchorMax = Vector2.one;
|
||||
dimRect.offsetMin = Vector2.zero;
|
||||
dimRect.offsetMax = Vector2.zero;
|
||||
Image dimImage = modalDim.GetComponent<Image>();
|
||||
dimImage.color = new Color(0f, 0f, 0f, 0.78f);
|
||||
dimImage.raycastTarget = true;
|
||||
|
||||
panel = PresentationUiStyle.CreatePanel(
|
||||
transform,
|
||||
"Run Tutorial",
|
||||
new Vector2(0.5f, 0.5f),
|
||||
Vector2.zero,
|
||||
new Vector2(1500f, 760f),
|
||||
new Color(0.03f, 0.04f, 0.08f, 0.98f));
|
||||
PresentationUiStyle.ApplyPanel(panel.GetComponent<Image>());
|
||||
|
||||
GameObject previewObject = new(
|
||||
"Tutorial Motion Preview",
|
||||
typeof(RectTransform),
|
||||
typeof(CanvasRenderer),
|
||||
typeof(Image));
|
||||
previewObject.transform.SetParent(panel.transform, false);
|
||||
RectTransform previewRect = previewObject.GetComponent<RectTransform>();
|
||||
previewRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
previewRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
previewRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
previewRect.anchoredPosition = new Vector2(-360f, 20f);
|
||||
previewRect.sizeDelta = new Vector2(560f, 350f);
|
||||
Image previewFrame = previewObject.GetComponent<Image>();
|
||||
previewFrame.color = new Color(0.02f, 0.03f, 0.05f, 0.98f);
|
||||
previewFrame.raycastTarget = false;
|
||||
motionPreview = previewObject.AddComponent<TutorialMotionPreview>();
|
||||
|
||||
titleText = PresentationUiStyle.CreateText(
|
||||
panel.transform,
|
||||
"Title",
|
||||
font,
|
||||
42,
|
||||
TextAnchor.MiddleCenter,
|
||||
new Vector2(0f, 310f),
|
||||
new Vector2(1380f, 76f));
|
||||
bodyText = PresentationUiStyle.CreateText(
|
||||
panel.transform,
|
||||
"Instructions",
|
||||
font,
|
||||
30,
|
||||
TextAnchor.MiddleLeft,
|
||||
new Vector2(350f, 20f),
|
||||
new Vector2(620f, 340f));
|
||||
bodyText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
bodyText.verticalOverflow = VerticalWrapMode.Truncate;
|
||||
bodyText.resizeTextForBestFit = true;
|
||||
bodyText.resizeTextMinSize = 24;
|
||||
bodyText.resizeTextMaxSize = 30;
|
||||
helpText = PresentationUiStyle.CreateText(
|
||||
panel.transform,
|
||||
"Tutorial Help",
|
||||
font,
|
||||
18,
|
||||
TextAnchor.MiddleCenter,
|
||||
new Vector2(0f, -250f),
|
||||
new Vector2(1320f, 40f));
|
||||
helpText.text = "왼쪽은 자동 시연입니다 · Enter / Space 계속";
|
||||
helpText.color = new Color32(200, 208, 218, 255);
|
||||
|
||||
GameObject buttonObject = new(
|
||||
"Continue Button",
|
||||
typeof(RectTransform),
|
||||
typeof(CanvasRenderer),
|
||||
typeof(Image),
|
||||
typeof(Button));
|
||||
buttonObject.transform.SetParent(panel.transform, false);
|
||||
RectTransform buttonRect = buttonObject.GetComponent<RectTransform>();
|
||||
buttonRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
buttonRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
buttonRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
buttonRect.anchoredPosition = new Vector2(0f, -320f);
|
||||
buttonRect.sizeDelta = new Vector2(380f, 72f);
|
||||
continueButton = buttonObject.GetComponent<Button>();
|
||||
continueButton.targetGraphic = buttonObject.GetComponent<Image>();
|
||||
Navigation navigation = continueButton.navigation;
|
||||
navigation.mode = Navigation.Mode.None;
|
||||
continueButton.navigation = navigation;
|
||||
PresentationUiStyle.ApplyButton(continueButton);
|
||||
Text buttonLabel = PresentationUiStyle.CreateText(
|
||||
buttonObject.transform,
|
||||
"Label",
|
||||
font,
|
||||
26,
|
||||
TextAnchor.MiddleCenter,
|
||||
Vector2.zero,
|
||||
new Vector2(350f, 60f));
|
||||
buttonLabel.text = "계속";
|
||||
buttonLabel.raycastTarget = false;
|
||||
continueButton.onClick.AddListener(ContinueCurrentTutorial);
|
||||
|
||||
panel.SetActive(false);
|
||||
modalDim.SetActive(false);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public bool DebugIsVisible => panel != null && panel.activeSelf;
|
||||
public TutorialKind? DebugCurrentTutorial => currentTutorial;
|
||||
public int DebugPendingCount => pendingTutorials.Count;
|
||||
public bool DebugHasRaycastBlocker =>
|
||||
modalDim != null && modalDim.GetComponent<Image>().raycastTarget;
|
||||
public TutorialMotionPreview DebugMotionPreview => motionPreview;
|
||||
public string DebugBodyText => bodyText != null ? bodyText.text : null;
|
||||
public string DebugHelpText => helpText != null ? helpText.text : null;
|
||||
|
||||
public bool DebugContinue()
|
||||
{
|
||||
if (!currentTutorial.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FinishCurrentTutorial();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 885dbb7ec45f4c4fae597bd6b30bfe12
|
||||
@@ -0,0 +1,369 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BumpCombat.Combat;
|
||||
using BumpCombat.Player;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BumpCombat.UI
|
||||
{
|
||||
// Presentation-only replay: uses the authored frames and an unscaled clock.
|
||||
// It never sends input or grants resources to the live player.
|
||||
public sealed class TutorialMotionPreview : MonoBehaviour
|
||||
{
|
||||
private readonly Dictionary<string, Sprite[]> frames = new();
|
||||
private readonly List<Sprite> generatedFrames = new();
|
||||
private readonly Image[] slots = new Image[3];
|
||||
private readonly Color[] colors = { new Color32(100, 229, 145, 255),
|
||||
new Color32(255, 114, 108, 255), new Color32(105, 177, 255, 255) };
|
||||
private static readonly float[] RayFrameDurations = { 0.02f, 0.03f, 0.03f, 0.03f, 0.02f, 0.02f };
|
||||
private static readonly float[] BumpFrameDurations = CreateBumpFrameDurations();
|
||||
private Image actor, target, effect, guard, gaugeFill, skillBack, skillFront;
|
||||
private Text actionLabel, keyLabel, gaugeLabel, pageLabel;
|
||||
private PlayerHealth playerHealth;
|
||||
private ActiveArtifactDefinition cycloneDefinition, rayDefinition;
|
||||
private RunTutorialController.TutorialKind kind;
|
||||
private float clock;
|
||||
private bool playing;
|
||||
|
||||
public void Show(RunTutorialController.TutorialKind tutorial)
|
||||
{
|
||||
playerHealth = FindAnyObjectByType<PlayerHealth>();
|
||||
ActiveArtifactController artifacts = FindAnyObjectByType<ActiveArtifactController>();
|
||||
if (artifacts != null)
|
||||
for (int i = 0; i < artifacts.CatalogCount; i++)
|
||||
{
|
||||
ActiveArtifactDefinition definition = artifacts.GetCatalogArtifactAt(i);
|
||||
if (definition == null) continue;
|
||||
if (definition.Effect == ActiveArtifactEffect.Cyclone) cycloneDefinition = definition;
|
||||
if (definition.Effect == ActiveArtifactEffect.Phoenix) rayDefinition = definition;
|
||||
}
|
||||
if (actor == null) Build();
|
||||
kind = tutorial;
|
||||
clock = 0f;
|
||||
playing = true;
|
||||
gameObject.SetActive(true);
|
||||
Render();
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
playing = false;
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!playing) return;
|
||||
clock += Time.unscaledDeltaTime;
|
||||
Render();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (Sprite sprite in generatedFrames) Destroy(sprite);
|
||||
}
|
||||
|
||||
private void Build()
|
||||
{
|
||||
Image background = gameObject.GetComponent<Image>();
|
||||
if (background == null) background = gameObject.AddComponent<Image>();
|
||||
background.color = new Color32(18, 24, 32, 255);
|
||||
background.raycastTarget = false;
|
||||
gameObject.AddComponent<RectMask2D>();
|
||||
pageLabel = Label("Demo Page", "", 20, new Vector2(235, 150), new Vector2(70, 28));
|
||||
actionLabel = Label("Demo Action", "", 24, new Vector2(0, 113), new Vector2(530, 40));
|
||||
keyLabel = Label("Demo Key", "", 22, new Vector2(0, 73), new Vector2(510, 32));
|
||||
keyLabel.color = new Color32(255, 220, 133, 255);
|
||||
// Keep each complete authored cell; transparent pixels preserve registration.
|
||||
skillBack = Picture("Demo Skill Back", Vector2.zero, new Vector2(300, 300));
|
||||
actor = Picture("Demo Character", new Vector2(-60, -15), new Vector2(400, 400));
|
||||
target = Picture("Demo Enemy", new Vector2(155, -15), new Vector2(300, 300));
|
||||
skillFront = Picture("Demo Skill Front", Vector2.zero, new Vector2(300, 300));
|
||||
effect = Picture("Demo Effect", new Vector2(60, -15), new Vector2(180, 180));
|
||||
guard = Picture("Demo Guard", new Vector2(-60, -15), new Vector2(400, 400));
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
slots[i] = Picture("Demo Slot " + i, new Vector2(-48 + i * 48, -93), new Vector2(38, 12));
|
||||
slots[i].sprite = null;
|
||||
}
|
||||
Image track = Picture("Demo Gauge Track", new Vector2(0, -124), new Vector2(400, 14));
|
||||
track.color = new Color32(45, 53, 65, 255);
|
||||
gaugeFill = Picture("Demo Gauge Fill", new Vector2(-200, -124), new Vector2(400, 14));
|
||||
gaugeFill.rectTransform.pivot = new Vector2(0, 0.5f);
|
||||
gaugeLabel = Label("Demo Gauge Value", "", 18, new Vector2(0, -150), new Vector2(520, 30));
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
actor.rectTransform.anchoredPosition = new Vector2(-60, -15);
|
||||
actor.rectTransform.localScale = Vector3.one;
|
||||
target.rectTransform.anchoredPosition = new Vector2(155, -15);
|
||||
target.color = Color.white;
|
||||
target.enabled = false;
|
||||
effect.enabled = false;
|
||||
effect.rectTransform.sizeDelta = new Vector2(180, 180);
|
||||
skillBack.enabled = false;
|
||||
skillFront.enabled = false;
|
||||
guard.enabled = false;
|
||||
SetFrame(actor, "Tutorial/Swordsman_Idle", clock, 10f);
|
||||
for (int i = 0; i < slots.Length; i++) slots[i].enabled = kind != RunTutorialController.TutorialKind.Guard;
|
||||
switch (kind)
|
||||
{
|
||||
case RunTutorialController.TutorialKind.Artifact: RenderArtifact(); break;
|
||||
case RunTutorialController.TutorialKind.Guard: RenderGuard(); break;
|
||||
default: RenderEnhancement(); break;
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderArtifact()
|
||||
{
|
||||
float t = clock % 12f;
|
||||
pageLabel.text = $"{(t < 2f ? 1 : t < 4f ? 2 : t < 6f ? 3 : t < 9f ? 4 : 5)}/5";
|
||||
int selected = t < 2f ? 2 : 0;
|
||||
SelectSlot(selected);
|
||||
float gauge = 60f;
|
||||
float afterUse = 60f - (cycloneDefinition != null ? cycloneDefinition.NormalGaugeCost : 30f);
|
||||
if (t < 2f)
|
||||
{
|
||||
Step("장비를 확인하세요", "대기");
|
||||
}
|
||||
else if (t < 4f)
|
||||
{
|
||||
Step("바람깃 브로치로 교체합니다", "[ S ] 교체");
|
||||
if (t < 2.6f) ShowEffect("Artifacts/ThreeColor-v1/Charge/" + EffectName(selected) + "-Release-v1", t - 2f, 10f, new Vector2(-60, -15));
|
||||
}
|
||||
else if (t < 6f)
|
||||
{
|
||||
Step("사용하면 게이지가 줄어듭니다", "[ A ] 눌렀다 놓기");
|
||||
gauge = afterUse;
|
||||
UseCyclone(t - 4f);
|
||||
}
|
||||
else if (t < 9f)
|
||||
{
|
||||
Step("이동하면 게이지가 찹니다", "[ ← → ] 이동");
|
||||
float move = (t - 6f) / 3f;
|
||||
actor.rectTransform.anchoredPosition = new Vector2(-110 + Mathf.PingPong(move * 240f, 120f), -15);
|
||||
actor.rectTransform.localScale = new Vector3(move > 0.5f ? -1 : 1, 1, 1);
|
||||
SetFrame(actor, "Tutorial/Swordsman_Walk", t, 10f);
|
||||
gauge = afterUse + (t - 6f) * 2f;
|
||||
}
|
||||
else
|
||||
{
|
||||
Step("몸통박치기 적중으로도 충전됩니다", "[ → ] 적에게 이동");
|
||||
float bump = t - 9f;
|
||||
float travel = Mathf.Clamp01(bump / 0.65f);
|
||||
actor.rectTransform.anchoredPosition = new Vector2(-60 + travel * 143f, -15);
|
||||
target.enabled = true;
|
||||
SetFrame(target, "Tutorial/Slime_Idle", clock, 10f);
|
||||
if (bump < 0.65f) SetFrame(actor, "Tutorial/Swordsman_Walk", bump, 10f);
|
||||
else if (bump < 1.2f) SetFrame(actor, "Tutorial/Swordsman_BumpAttack-v1", bump - 0.65f, 10f, false);
|
||||
if (bump >= 0.65f)
|
||||
{
|
||||
target.rectTransform.anchoredPosition += Vector2.right * Mathf.Min(24f, (bump - 0.65f) * 90f);
|
||||
target.color = bump < 0.85f ? colors[selected] : Color.white;
|
||||
ShowBumpFlash(bump - 0.65f, new Vector2(135f, -22f));
|
||||
}
|
||||
gauge = afterUse + (bump < 0.65f ? 6f : 10f);
|
||||
}
|
||||
Gauge(gauge, colors[selected], "아티팩트 게이지 " + Mathf.FloorToInt(gauge) + " / 100");
|
||||
}
|
||||
|
||||
private void RenderGuard()
|
||||
{
|
||||
float duration = playerHealth != null ? playerHealth.GuardDuration : PlayerHealth.DefaultGuardDuration;
|
||||
float cooldown = playerHealth != null ? playerHealth.GuardCooldownDuration : PlayerHealth.DefaultGuardCooldown;
|
||||
float waitDuration = Mathf.Min(2f, Mathf.Max(0f, cooldown - duration));
|
||||
float t = clock % (duration + waitDuration + 3f);
|
||||
float sinceUse = t - 2f;
|
||||
// Preserve the active guard duration; compress only the remaining wait.
|
||||
float cooldownElapsed = sinceUse <= duration ? sinceUse
|
||||
: waitDuration > 0f ? Mathf.Lerp(duration, cooldown, Mathf.Clamp01((sinceUse - duration) / waitDuration))
|
||||
: cooldown;
|
||||
pageLabel.text = $"{(sinceUse < 0f ? 1 : sinceUse < duration ? 2 : sinceUse < duration + waitDuration ? 3 : 4)}/4";
|
||||
if (sinceUse < 0f)
|
||||
Step("가드를 사용할 준비가 됐습니다", "[ D ] 가드");
|
||||
else if (sinceUse < duration)
|
||||
{
|
||||
Step($"{duration:0.##}초 동안 피해를 막습니다", "[ D ] 무적");
|
||||
guard.enabled = true;
|
||||
SetFrame(guard, "Combat/Guard-v1/Swordsman_Idle-outline", clock, 10f);
|
||||
}
|
||||
else if (sinceUse < duration + waitDuration)
|
||||
Step($"발동부터 {cooldown:0.##}초 뒤 다시 사용합니다", "재사용 대기 · 빠르게 시연");
|
||||
else Step("가드가 다시 준비됐습니다", "[ D ] 사용 가능");
|
||||
float ready = sinceUse < 0f || cooldown <= 0f ? 1f : Mathf.Clamp01(cooldownElapsed / cooldown);
|
||||
Gauge(ready * 100f, new Color32(255, 222, 110, 255), ready >= 1f ? "가드 준비 완료" : "가드 재사용 대기");
|
||||
}
|
||||
|
||||
private void RenderEnhancement()
|
||||
{
|
||||
float chargeDuration = rayDefinition != null ? rayDefinition.ChargeDuration : 0.8f;
|
||||
float releaseAt = 2f + chargeDuration + 0.2f;
|
||||
float t = clock % (releaseAt + 4f);
|
||||
pageLabel.text = $"{(t < 2f ? 1 : t < 2f + chargeDuration ? 2 : t < releaseAt ? 3 : 4)}/4";
|
||||
SelectSlot(1);
|
||||
float gauge = t < releaseAt ? 60f : 60f - (rayDefinition != null ? rayDefinition.ChargedGaugeCost : 25f);
|
||||
if (t < 2f) Step("아티팩트 강화가 해금됐습니다", "대기");
|
||||
else if (t < releaseAt)
|
||||
{
|
||||
float progress = chargeDuration <= 0f ? 1f : Mathf.Clamp01((t - 2f) / chargeDuration);
|
||||
Step(progress < 1f ? "키를 누른 채 충전합니다" : "완충! 이제 키를 놓으세요", "[ A ] 누르기 유지 · " + Mathf.RoundToInt(progress * 100f) + "%");
|
||||
ShowEffect("Artifacts/ThreeColor-v1/Charge/Phoenix-" + (progress < 1f ? "Aura" : "ReadyAura") + "-v1", t - 2f, 12f, new Vector2(-60, -15), true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Step("강화기 발동 · 같은 게이지 비용", "[ A ] 놓기");
|
||||
UseScorchingRay(t - releaseAt);
|
||||
}
|
||||
Gauge(gauge, colors[1], "아티팩트 게이지 " + gauge.ToString("0") + " / 100");
|
||||
}
|
||||
|
||||
private void UseCyclone(float elapsed)
|
||||
{
|
||||
float duration = cycloneDefinition != null ? cycloneDefinition.NormalDuration : 0.36f;
|
||||
float distance = cycloneDefinition != null ? cycloneDefinition.NormalMoveDistance : 0.65f;
|
||||
float progress = Mathf.Clamp01(elapsed / Mathf.Max(0.01f, duration));
|
||||
Vector2 position = new Vector2(-60f + distance * 64f * progress, -15f);
|
||||
actor.rectTransform.anchoredPosition = position;
|
||||
target.enabled = true;
|
||||
SetFrame(target, "Tutorial/Slime_Idle", clock, 10f);
|
||||
target.rectTransform.anchoredPosition = new Vector2(65f + progress * 20f, -15f);
|
||||
if (elapsed >= duration) return;
|
||||
SetFrame(actor, "Artifacts/ThreeColor-v1/Player/Swordsman_ArtifactUse-v1-Green-v1", elapsed, 10f, false);
|
||||
skillBack.rectTransform.anchoredPosition = position + Vector2.up * 32f;
|
||||
skillFront.rectTransform.anchoredPosition = skillBack.rectTransform.anchoredPosition;
|
||||
skillBack.rectTransform.sizeDelta = skillFront.rectTransform.sizeDelta = new Vector2(330, 330);
|
||||
SetFrame(skillBack, "Artifacts/ThreeColor-v1/CycloneOrbit-v3/Cyclone-Normal-Back-v3", elapsed, 20f);
|
||||
SetFrame(skillFront, "Artifacts/ThreeColor-v1/CycloneOrbit-v3/Cyclone-Normal-Front-v3", elapsed, 20f);
|
||||
float hitInterval = cycloneDefinition != null ? cycloneDefinition.HitInterval : 0.14f;
|
||||
float hitAge = elapsed % Mathf.Max(0.01f, hitInterval);
|
||||
ShowEffect("Artifacts/ThreeColor-v1/Combat/ArtifactImpact-Cyclone-Normal-v1", hitAge, 28f, new Vector2(65f, -20f));
|
||||
target.color = hitAge < 0.07f ? colors[0] : Color.white;
|
||||
}
|
||||
|
||||
private void UseScorchingRay(float elapsed)
|
||||
{
|
||||
target.enabled = true;
|
||||
SetFrame(target, "Tutorial/Slime_Idle", clock, 10f);
|
||||
target.rectTransform.anchoredPosition = new Vector2(155f + Mathf.Min(20f, elapsed * 100f), -15f);
|
||||
if (elapsed < 0.5f)
|
||||
SetFrame(actor, "Artifacts/ThreeColor-v1/Player/Swordsman_ArtifactUse-v1-Red-v1", elapsed, 10f, false);
|
||||
// Same six authored frames and 0.15s burst as ScorchingRayRoutine.
|
||||
if (elapsed >= 0.15f) return;
|
||||
Sprite[] sequence = LoadFrames("Artifacts/ThreeColor-v1/ScorchingRay/ScorchingRay-Ghostfire-Charged-v1");
|
||||
skillFront.rectTransform.sizeDelta = new Vector2(360, 180);
|
||||
skillFront.rectTransform.anchoredPosition = new Vector2(95f, -16f);
|
||||
SetTimedFrame(skillFront, sequence, RayFrameDurations, elapsed);
|
||||
target.color = colors[1];
|
||||
ShowEffect("Artifacts/ThreeColor-v1/Combat/ArtifactImpact-SearingRay-Charged-v1", elapsed, 28f, new Vector2(155f, -20f));
|
||||
}
|
||||
|
||||
private void ShowBumpFlash(float age, Vector2 position)
|
||||
{
|
||||
float total = 0f;
|
||||
foreach (float duration in BumpFrameDurations) total += duration;
|
||||
if (age < 0f || age >= total) return;
|
||||
effect.rectTransform.anchoredPosition = position;
|
||||
effect.rectTransform.sizeDelta = new Vector2(260, 260);
|
||||
SetTimedFrame(effect, LoadFrames("Artifacts/ThreeColor-v1/Combat/BumpImpact-Weak-Green-v1"), BumpFrameDurations, age);
|
||||
}
|
||||
|
||||
private static float[] CreateBumpFrameDurations()
|
||||
{
|
||||
float[] result = new float[CombatFeedback.BumpImpactFrameCount];
|
||||
for (int i = 0; i < result.Length; i++) result[i] = CombatFeedback.GetBumpImpactFrameDuration(i);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void SetTimedFrame(Image image, Sprite[] sequence, float[] durations, float age)
|
||||
{
|
||||
if (sequence.Length == 0) { image.enabled = false; return; }
|
||||
int frame = 0;
|
||||
while (frame < durations.Length - 1 && age >= durations[frame]) age -= durations[frame++];
|
||||
image.sprite = sequence[Mathf.Min(frame, sequence.Length - 1)];
|
||||
image.enabled = true;
|
||||
}
|
||||
|
||||
private static string EffectName(int selected) => selected == 0 ? "Cyclone" : selected == 1 ? "Phoenix" : "ThunderCrash";
|
||||
private void Step(string description, string key) { actionLabel.text = description; keyLabel.text = key; }
|
||||
private void SelectSlot(int selected)
|
||||
{
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
slots[i].color = i == selected ? colors[i] : colors[i] * new Color(0.35f, 0.35f, 0.35f, 1f);
|
||||
slots[i].rectTransform.sizeDelta = new Vector2(38, i == selected ? 16 : 8);
|
||||
}
|
||||
}
|
||||
private void Gauge(float value, Color color, string caption)
|
||||
{
|
||||
gaugeFill.color = color;
|
||||
gaugeFill.rectTransform.sizeDelta = new Vector2(400f * Mathf.Clamp01(value / 100f), 14f);
|
||||
gaugeLabel.text = caption;
|
||||
}
|
||||
private void ShowEffect(string path, float time, float fps, Vector2 position, bool loop = false)
|
||||
{
|
||||
effect.rectTransform.anchoredPosition = position;
|
||||
effect.enabled = true;
|
||||
SetFrame(effect, path, time, fps, loop);
|
||||
}
|
||||
private void SetFrame(Image image, string path, float time, float fps, bool loop = true)
|
||||
{
|
||||
Sprite[] sequence = LoadFrames(path);
|
||||
if (sequence.Length == 0) { image.enabled = false; return; }
|
||||
int index = Mathf.FloorToInt(time * fps);
|
||||
image.sprite = sequence[loop ? index % sequence.Length : Mathf.Min(index, sequence.Length - 1)];
|
||||
image.enabled = true;
|
||||
}
|
||||
|
||||
private Sprite[] LoadFrames(string path)
|
||||
{
|
||||
if (!frames.TryGetValue(path, out Sprite[] sequence))
|
||||
{
|
||||
sequence = Resources.LoadAll<Sprite>(path);
|
||||
if (sequence.Length == 0)
|
||||
{
|
||||
Texture2D texture = Resources.Load<Texture2D>(path);
|
||||
int width = path.Contains("CycloneOrbit-v3") ? 100 : path.Contains("ScorchingRay-Ghostfire-Charged") ? 256 : 64;
|
||||
int height = path.Contains("CycloneOrbit-v3") ? 100 : path.Contains("ScorchingRay-Ghostfire-Charged") ? 128 : 64;
|
||||
if (texture != null && texture.height == height && texture.width % width == 0)
|
||||
{
|
||||
sequence = new Sprite[texture.width / width];
|
||||
for (int i = 0; i < sequence.Length; i++)
|
||||
{
|
||||
sequence[i] = Sprite.Create(texture, new Rect(i * width, 0, width, height), new Vector2(0.5f, 0.5f), 32, 0, SpriteMeshType.FullRect);
|
||||
sequence[i].name = path + " Frame " + i;
|
||||
generatedFrames.Add(sequence[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Array.Sort(sequence, (a, b) => a.rect.x.CompareTo(b.rect.x));
|
||||
frames.Add(path, sequence);
|
||||
}
|
||||
return sequence;
|
||||
}
|
||||
private Image Picture(string name, Vector2 position, Vector2 size)
|
||||
{
|
||||
Image image = PresentationUiStyle.CreateImage(transform, name, position, size);
|
||||
image.raycastTarget = false;
|
||||
image.preserveAspect = true;
|
||||
return image;
|
||||
}
|
||||
private Text Label(string name, string content, int size, Vector2 position, Vector2 bounds)
|
||||
{
|
||||
Text text = PresentationUiStyle.CreateText(transform, name, PresentationUiStyle.GetGalmuriFont(), size, TextAnchor.MiddleCenter, position, bounds);
|
||||
text.text = content;
|
||||
text.raycastTarget = false;
|
||||
return text;
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
public void DebugSample(float seconds) { clock = seconds; Render(); }
|
||||
public Sprite DebugActorSprite => actor.sprite;
|
||||
public string DebugAction => actionLabel.text;
|
||||
public float DebugGaugeWidth => gaugeFill.rectTransform.sizeDelta.x;
|
||||
public bool DebugGuardVisible => guard.enabled;
|
||||
public Sprite DebugSkillSprite => skillFront.enabled ? skillFront.sprite : null;
|
||||
public Sprite DebugImpactSprite => effect.enabled ? effect.sprite : null;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8abd446f65a34cebb3d86938862e471b
|
||||
Reference in New Issue
Block a user