Initial commit: Tiny Tackle Heroes Unity project

This commit is contained in:
2026-09-15 17:37:42 +09:00
commit 591fa9a826
1623 changed files with 182420 additions and 0 deletions
@@ -0,0 +1,627 @@
using System.Collections.Generic;
using BumpCombat.Audio;
using BumpCombat.Combat;
using BumpCombat.Core;
using BumpCombat.Player;
using BumpCombat.Spawning;
using BumpCombat.UI;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace BumpCombat.Progression
{
public sealed class ArtifactRewardController : MonoBehaviour
{
private enum RewardSource
{
Elite,
EliteRed,
EliteBlue,
MidBoss,
}
private const int RewardOptionCount = 2;
private const int DebugOptionCount = 6;
private const int StartupColorCount = 3;
private const float OptionCardWidth = 380f;
private const float OptionCardHeight = 128f;
private const float OptionLabelHeight = 64f;
private readonly Queue<RewardSource> pendingRewards = new();
private readonly HashSet<RewardSource> queuedSources = new();
private readonly ActiveArtifactDefinition[] shownArtifacts =
new ActiveArtifactDefinition[DebugOptionCount];
private ActiveArtifactController artifacts;
private SpawnDirector spawnDirector;
private GameObject panel;
private Text titleText;
private Text[] optionTexts;
private Image[] optionCardImages = System.Array.Empty<Image>();
private Image[] optionIconImages = System.Array.Empty<Image>();
private int shownCount;
private int selectedIndex;
private bool debugStartupSelection;
private int startupSelectionCount;
private bool debugStartupSelectionPending;
private int debugStartupSelectionEarliestFrame;
private int debugSelectionsRequired;
private int debugSelectionsRemaining;
private int ignoreInputThroughFrame;
#if UNITY_EDITOR
public static bool GrantCatalogForTests { get; set; }
#endif
private void Start()
{
artifacts = FindAnyObjectByType<ActiveArtifactController>();
spawnDirector = FindAnyObjectByType<SpawnDirector>();
if (artifacts == null || spawnDirector == null)
{
enabled = false;
return;
}
CreatePanel();
spawnDirector.OnEventEnemyDefeated += HandleEventEnemyDefeated;
if (RunManager.Instance != null)
{
RunManager.Instance.OnSelectionChanged += HandleSelectionChanged;
RunManager.Instance.OnRunStarted += HandleRunStarted;
}
#if UNITY_EDITOR
if (GrantCatalogForTests)
{
artifacts.DebugGrantCatalogArtifacts();
return;
}
#endif
QueueStartupSelection(
RunManager.Instance?.StartupArtifactSelectionCount ?? 3);
}
private void OnDestroy()
{
if (spawnDirector != null)
{
spawnDirector.OnEventEnemyDefeated -= HandleEventEnemyDefeated;
}
if (RunManager.Instance != null)
{
RunManager.Instance.OnSelectionChanged -= HandleSelectionChanged;
RunManager.Instance.OnRunStarted -= HandleRunStarted;
}
}
private void Update()
{
if (debugStartupSelectionPending
&& Time.frameCount >= debugStartupSelectionEarliestFrame
&& RunManager.Instance != null
&& !RunManager.Instance.IsTitleScreen
&& !RunManager.Instance.IsSelectionOpen
&& !RunManager.Instance.IsPaused
&& !RunManager.Instance.IsGameOver)
{
if (OpenStartupSelection())
{
debugStartupSelectionPending = false;
return;
}
}
if (panel == null
|| !panel.activeSelf
|| shownCount == 0
|| Time.frameCount <= ignoreInputThroughFrame
|| Keyboard.current == null)
{
return;
}
if (Keyboard.current.leftArrowKey.wasPressedThisFrame
|| Keyboard.current.upArrowKey.wasPressedThisFrame)
{
selectedIndex = (selectedIndex + shownCount - 1) % shownCount;
BumpCombatAudioService.Instance?.PlayUi("ui_move");
UpdateSelectionVisual();
}
else if (Keyboard.current.rightArrowKey.wasPressedThisFrame
|| Keyboard.current.downArrowKey.wasPressedThisFrame)
{
selectedIndex = (selectedIndex + 1) % shownCount;
BumpCombatAudioService.Instance?.PlayUi("ui_move");
UpdateSelectionVisual();
}
else if (Keyboard.current.spaceKey.wasPressedThisFrame)
{
ChooseOption(selectedIndex);
}
}
private void HandleRunStarted()
{
QueueStartupSelection(
RunManager.Instance?.StartupArtifactSelectionCount ?? 3);
}
private void QueueStartupSelection(int selectionCount)
{
startupSelectionCount = Mathf.Max(0, selectionCount);
debugStartupSelectionPending = true;
debugStartupSelectionEarliestFrame = Time.frameCount + 1;
}
private bool ChooseOption(int optionIndex)
{
if (panel == null
|| !panel.activeSelf
|| optionIndex < 0
|| optionIndex >= shownCount
|| !artifacts.TryAddArtifact(shownArtifacts[optionIndex]))
{
return false;
}
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
if (artifacts.OwnedArtifactCount == 1)
{
GetComponent<RunTutorialController>()?.NotifyArtifactAcquired();
}
if (debugStartupSelection)
{
debugSelectionsRemaining--;
if (debugSelectionsRemaining > 0)
{
// Startup selection is deliberately color ordered. Each
// step exposes only the two definitions in the next color
// group, so a failed duplicate-color click cannot consume
// a slot or leave an invalid loadout.
if (!PopulateStartupOptions())
{
debugStartupSelection = false;
CloseSelection();
return true;
}
selectedIndex = 0;
UpdateDebugTitle();
ShowOptions();
return true;
}
debugStartupSelection = false;
}
CloseSelection();
return true;
}
private bool OpenStartupSelection()
{
if (RunManager.Instance == null
|| !RunManager.Instance.TryOpenSelection(this))
{
return false;
}
debugSelectionsRequired = Mathf.Min(
startupSelectionCount,
artifacts.MaxOwnedArtifacts - artifacts.OwnedArtifactCount);
if (debugSelectionsRequired <= 0)
{
RunManager.Instance.CloseSelection(this);
return true;
}
debugStartupSelection = true;
debugSelectionsRemaining = debugSelectionsRequired;
if (!PopulateStartupOptions())
{
debugStartupSelection = false;
RunManager.Instance.CloseSelection(this);
return true;
}
selectedIndex = 0;
ShowOptions();
UpdateDebugTitle();
return true;
}
private bool PopulateStartupOptions()
{
shownCount = 0;
int selectedCount = debugSelectionsRequired - debugSelectionsRemaining;
if (selectedCount < 0 || selectedCount >= StartupColorCount)
{
return false;
}
ArtifactColor requiredColor = GetStartupColor(selectedCount);
for (int i = 0;
i < artifacts.CatalogCount && shownCount < DebugOptionCount;
i++)
{
ActiveArtifactDefinition definition =
artifacts.GetCatalogArtifactAt(i);
if (definition == null
|| definition.ArtifactColor != requiredColor
|| artifacts.OwnsArtifact(definition))
{
continue;
}
shownArtifacts[shownCount++] = definition;
}
return shownCount > 0;
}
private static ArtifactColor GetStartupColor(int selectionIndex)
{
return selectionIndex switch
{
0 => ArtifactColor.Green,
1 => ArtifactColor.Red,
_ => ArtifactColor.Blue,
};
}
private static string GetArtifactColorName(ArtifactColor color)
{
return color switch
{
ArtifactColor.Green => "GREEN",
ArtifactColor.Red => "RED",
_ => "BLUE",
};
}
private void HandleEventEnemyDefeated(RunTimedEvent timedEvent)
{
if (timedEvent == RunTimedEvent.Elite)
{
if (RunManager.Instance?.IsProductionRun == true)
{
int spawnedEliteCount =
spawnDirector?.TimedEliteSpawnCount ?? 0;
if (spawnedEliteCount == 2)
{
QueueReward(RewardSource.EliteRed);
}
else if (spawnedEliteCount == 3)
{
QueueReward(RewardSource.EliteBlue);
}
}
else
{
QueueReward(RewardSource.Elite);
}
}
else if (timedEvent == RunTimedEvent.MidBoss
&& RunManager.Instance?.IsProductionRun != true)
{
QueueReward(RewardSource.MidBoss);
}
}
private void QueueReward(RewardSource source)
{
if (artifacts == null
|| artifacts.OwnedArtifactCount >= artifacts.MaxOwnedArtifacts)
{
return;
}
if (!queuedSources.Add(source))
{
return;
}
pendingRewards.Enqueue(source);
TryOpenPendingReward();
}
private void TryOpenPendingReward()
{
if (pendingRewards.Count == 0 || artifacts == null)
{
return;
}
if (artifacts.OwnedArtifactCount >= artifacts.MaxOwnedArtifacts)
{
pendingRewards.Clear();
return;
}
if (RunManager.Instance == null
|| !RunManager.Instance.TryOpenSelection(this))
{
return;
}
RewardSource source = pendingRewards.Peek();
bool hasRequiredColor = TryGetRewardColor(
source,
out ArtifactColor requiredColor);
List<ActiveArtifactDefinition> candidates = new();
for (int i = 0; i < artifacts.CatalogCount; i++)
{
ActiveArtifactDefinition definition =
artifacts.GetCatalogArtifactAt(i);
if (definition != null
&& !artifacts.OwnsArtifact(definition)
&& (!hasRequiredColor
|| definition.ArtifactColor == requiredColor))
{
candidates.Add(definition);
}
}
shownCount = Mathf.Min(RewardOptionCount, candidates.Count);
if (shownCount == 0)
{
pendingRewards.Clear();
RunManager.Instance.CloseSelection(this);
return;
}
pendingRewards.Dequeue();
debugStartupSelection = false;
for (int i = 0; i < shownCount; i++)
{
int choiceIndex = Random.Range(0, candidates.Count);
shownArtifacts[i] = candidates[choiceIndex];
candidates.RemoveAt(choiceIndex);
}
titleText.text = source switch
{
RewardSource.Elite => "ELITE REWARD - CHOOSE ARTIFACT",
RewardSource.EliteRed => "ELITE REWARD - CHOOSE RED ARTIFACT",
RewardSource.EliteBlue => "ELITE REWARD - CHOOSE BLUE ARTIFACT",
RewardSource.MidBoss => "MID BOSS REWARD - CHOOSE ARTIFACT",
_ => "CHOOSE ARTIFACT",
};
selectedIndex = 0;
ShowOptions();
}
private void HandleSelectionChanged(bool isOpen)
{
if (!isOpen && (panel == null || !panel.activeSelf))
{
TryOpenPendingReward();
}
}
private void UpdateSelectionVisual()
{
for (int i = 0; i < optionTexts.Length; i++)
{
bool ownedDebugOption = debugStartupSelection
&& i < shownCount
&& artifacts.OwnsArtifact(shownArtifacts[i]);
bool selected = i == selectedIndex && i < shownCount;
optionTexts[i].color = ownedDebugOption
? new Color(0.35f, 1f, 0.55f)
: selected
? new Color(1f, 0.85f, 0.2f)
: Color.white;
if (i < optionCardImages.Length)
{
PresentationUiStyle.ApplyCard(
optionCardImages[i],
selected || ownedDebugOption);
optionCardImages[i].gameObject.SetActive(
optionTexts[i].gameObject.activeSelf);
}
}
}
private void ShowOptions()
{
RefreshOptionTexts();
LayoutOptions();
panel.SetActive(true);
ignoreInputThroughFrame = Time.frameCount;
UpdateSelectionVisual();
}
private void RefreshOptionTexts()
{
for (int i = 0; i < optionTexts.Length; i++)
{
bool visible = i < shownCount;
optionTexts[i].gameObject.SetActive(visible);
if (!visible)
{
if (i < optionIconImages.Length)
{
optionIconImages[i].gameObject.SetActive(false);
}
continue;
}
ActiveArtifactDefinition definition = shownArtifacts[i];
Sprite icon = PresentationUiStyle.GetArtifactIcon(definition);
optionIconImages[i].sprite = icon;
optionIconImages[i].enabled = icon != null;
optionIconImages[i].gameObject.SetActive(icon != null);
optionTexts[i].text = icon != null
? definition.DisplayName
: $"{definition.PlaceholderSymbol}\n{definition.DisplayName}";
if (debugStartupSelection && artifacts.OwnsArtifact(definition))
{
optionTexts[i].text += "\nSELECTED";
}
}
}
private void LayoutOptions()
{
bool twoRows = shownCount > RewardOptionCount;
for (int i = 0; i < shownCount; i++)
{
int row = twoRows ? i / RewardOptionCount : 0;
int column = twoRows ? i % RewardOptionCount : i;
int rowStart = row * RewardOptionCount;
int rowCount = Mathf.Min(
RewardOptionCount,
shownCount - rowStart);
Vector2 cardPosition = new Vector2(
(column - (rowCount - 1) * 0.5f) * 430f,
twoRows ? 90f - row * 190f : 0f);
if (i < optionCardImages.Length)
{
optionCardImages[i].rectTransform.anchoredPosition =
cardPosition;
}
optionTexts[i].rectTransform.anchoredPosition =
cardPosition + Vector2.down * 25f;
if (i < optionIconImages.Length)
{
optionIconImages[i].rectTransform.anchoredPosition =
cardPosition + Vector2.up * 20f;
}
}
}
private void UpdateDebugTitle()
{
int selectedCount =
debugSelectionsRequired - debugSelectionsRemaining;
if (startupSelectionCount <= 1)
{
titleText.text = "CHOOSE GREEN ARTIFACT "
+ $"({selectedCount}/{debugSelectionsRequired})";
return;
}
ArtifactColor requiredColor = GetStartupColor(
Mathf.Clamp(selectedCount, 0, StartupColorCount - 1));
titleText.text =
$"CHOOSE {GetArtifactColorName(requiredColor)} ARTIFACT "
+ $"({selectedCount}/{debugSelectionsRequired})";
}
private static bool TryGetRewardColor(
RewardSource source,
out ArtifactColor color)
{
switch (source)
{
case RewardSource.EliteRed:
color = ArtifactColor.Red;
return true;
case RewardSource.EliteBlue:
color = ArtifactColor.Blue;
return true;
default:
color = ArtifactColor.Green;
return false;
}
}
private void CloseSelection()
{
panel.SetActive(false);
RunManager.Instance?.CloseSelection(this);
}
private void CreatePanel()
{
Font font = PresentationUiStyle.GetGalmuriFont();
panel = PresentationUiStyle.CreatePanel(
transform,
"Artifact Reward Selection",
new Vector2(0.5f, 0.5f),
Vector2.zero,
new Vector2(1500f, 650f),
new Color(0.03f, 0.04f, 0.08f, 0.96f));
PresentationUiStyle.ApplyPanel(panel.GetComponent<Image>());
titleText = PresentationUiStyle.CreateText(
panel.transform,
"Title",
font,
38,
TextAnchor.MiddleCenter,
new Vector2(0f, 255f),
new Vector2(1200f, 60f));
optionTexts = new Text[DebugOptionCount];
optionCardImages = new Image[DebugOptionCount];
optionIconImages = new Image[DebugOptionCount];
for (int i = 0; i < optionTexts.Length; i++)
{
optionTexts[i] = PresentationUiStyle.CreateText(
panel.transform,
$"Option {i + 1}",
font,
26,
TextAnchor.MiddleCenter,
Vector2.zero,
new Vector2(OptionCardWidth, OptionLabelHeight));
optionCardImages[i] = PresentationUiStyle.CreateCardBackdrop(
optionTexts[i],
$"{optionTexts[i].name} Card",
new Vector2(OptionCardWidth, OptionCardHeight));
RectTransform optionRect = optionTexts[i].rectTransform;
optionIconImages[i] = PresentationUiStyle.CreateImage(
optionTexts[i].transform.parent,
$"{optionTexts[i].name} Icon",
Vector2.zero,
new Vector2(48f, 48f));
optionIconImages[i].rectTransform.anchorMin = optionRect.anchorMin;
optionIconImages[i].rectTransform.anchorMax = optionRect.anchorMax;
optionIconImages[i].rectTransform.pivot = optionRect.pivot;
optionIconImages[i].rectTransform.anchoredPosition =
optionRect.anchoredPosition + Vector2.up * 20f;
optionIconImages[i].transform.SetSiblingIndex(
optionTexts[i].transform.GetSiblingIndex());
optionIconImages[i].enabled = false;
}
Text help = PresentationUiStyle.CreateText(
panel.transform,
"Help",
font,
22,
TextAnchor.MiddleCenter,
new Vector2(0f, -275f),
new Vector2(900f, 40f));
help.text = "Arrow keys: select Space: confirm";
PresentationUiStyle.ApplyText(help, 22);
panel.SetActive(false);
}
#if UNITY_EDITOR
public int DebugShownCount => shownCount;
public bool DebugIsSelectionVisible => panel != null && panel.activeSelf;
public ActiveArtifactDefinition DebugGetShownArtifact(int index)
{
return index >= 0 && index < shownCount
? shownArtifacts[index]
: null;
}
public bool DebugChooseOption(int optionIndex)
{
return ChooseOption(optionIndex);
}
public void DebugQueueRedArtifactReward()
{
QueueReward(RewardSource.EliteRed);
}
#endif
}
}