convert game playing..

This commit is contained in:
2026-09-23 12:53:52 +09:00
parent 37c7942b71
commit 5d11288a6f
136 changed files with 15856 additions and 299 deletions
@@ -3,6 +3,7 @@ using BumpCombat.Audio;
using BumpCombat.Combat;
using BumpCombat.Core;
using BumpCombat.Player;
using BumpCombat.Defense;
using BumpCombat.Spawning;
using BumpCombat.UI;
using UnityEngine;
@@ -30,6 +31,7 @@ namespace BumpCombat.Progression
private readonly Queue<RewardSource> pendingRewards = new();
private readonly HashSet<RewardSource> queuedSources = new();
private readonly PassiveRelicId[] shownRelics = new PassiveRelicId[DebugOptionCount];
private readonly ActiveArtifactDefinition[] shownArtifacts =
new ActiveArtifactDefinition[DebugOptionCount];
private ActiveArtifactController artifacts;
@@ -42,6 +44,9 @@ namespace BumpCombat.Progression
private int shownCount;
private int selectedIndex;
private bool debugStartupSelection;
private bool showingPassiveRelicReward;
private bool pendingPassiveRelicReward;
private PassiveRelicController passiveRelics;
private int startupSelectionCount;
private bool debugStartupSelectionPending;
private int debugStartupSelectionEarliestFrame;
@@ -148,7 +153,7 @@ namespace BumpCombat.Progression
RunManager.Instance?.StartupArtifactSelectionCount ?? 3);
}
private void QueueStartupSelection(int selectionCount)
public void QueueStartupSelection(int selectionCount)
{
startupSelectionCount = Mathf.Max(0, selectionCount);
debugStartupSelectionPending = true;
@@ -157,15 +162,25 @@ namespace BumpCombat.Progression
private bool ChooseOption(int optionIndex)
{
if (showingPassiveRelicReward)
{
return ChoosePassiveRelicOption(optionIndex);
}
if (panel == null
|| !panel.activeSelf
|| optionIndex < 0
|| optionIndex >= shownCount
|| !artifacts.TryAddArtifact(shownArtifacts[optionIndex]))
|| optionIndex >= shownCount)
{
return false;
}
if (!artifacts.TryAddArtifact(shownArtifacts[optionIndex]))
{
Debug.LogWarning($"[ArtifactRewardController] Could not add artifact {shownArtifacts[optionIndex]?.name}. Closing selection to prevent lock.");
CloseSelection();
return false;
}
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
if (artifacts.OwnedArtifactCount == 1)
{
@@ -201,14 +216,55 @@ namespace BumpCombat.Progression
return true;
}
public void QueuePassiveRelicReward()
{
pendingPassiveRelicReward = true;
TryOpenPendingReward();
}
private bool ChoosePassiveRelicOption(int optionIndex)
{
if (panel == null
|| !panel.activeSelf
|| optionIndex < 0
|| optionIndex >= shownCount
|| passiveRelics == null
|| !passiveRelics.TryAcquire(shownRelics[optionIndex]))
{
return false;
}
showingPassiveRelicReward = false;
pendingPassiveRelicReward = false;
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
DefensePrototypeController.Instance?.ResolvePassiveRelicChoice();
CloseSelection();
return true;
}
private bool OpenStartupSelection()
{
if (artifacts == null)
{
artifacts = FindAnyObjectByType<ActiveArtifactController>();
}
if (panel == null)
{
CreatePanel();
}
if (RunManager.Instance == null
|| !RunManager.Instance.TryOpenSelection(this))
{
return false;
}
if (artifacts == null)
{
RunManager.Instance.CloseSelection(this);
return false;
}
debugSelectionsRequired = Mathf.Min(
startupSelectionCount,
artifacts.MaxOwnedArtifacts - artifacts.OwnedArtifactCount);
@@ -243,6 +299,26 @@ namespace BumpCombat.Progression
}
ArtifactColor requiredColor = GetStartupColor(selectedCount);
if (artifacts.GetOwnedArtifactColorCount(requiredColor) >= artifacts.MaxArtifactsPerColor)
{
bool foundAvailableColor = false;
for (int c = 0; c < StartupColorCount; c++)
{
ArtifactColor candidateColor = GetStartupColor(c);
if (artifacts.GetOwnedArtifactColorCount(candidateColor) < artifacts.MaxArtifactsPerColor)
{
requiredColor = candidateColor;
foundAvailableColor = true;
break;
}
}
if (!foundAvailableColor)
{
return false;
}
}
for (int i = 0;
i < artifacts.CatalogCount && shownCount < DebugOptionCount;
i++)
@@ -284,6 +360,12 @@ namespace BumpCombat.Progression
private void HandleEventEnemyDefeated(RunTimedEvent timedEvent)
{
if (Stage.StageManager.IsStageModeActive)
{
// In stage mode, StageManager.HandleMajorBossDefeated coordinates rewards
return;
}
if (timedEvent == RunTimedEvent.Elite)
{
if (RunManager.Instance?.IsProductionRun == true)
@@ -311,8 +393,41 @@ namespace BumpCombat.Progression
}
}
public void QueueColorReward(ArtifactColor color)
{
if (artifacts == null)
{
artifacts = FindAnyObjectByType<ActiveArtifactController>();
}
if (artifacts == null
|| artifacts.OwnedArtifactCount >= artifacts.MaxOwnedArtifacts)
{
return;
}
if (artifacts.GetOwnedArtifactColorCount(color) >= artifacts.MaxArtifactsPerColor)
{
return;
}
RewardSource source = color switch
{
ArtifactColor.Red => RewardSource.EliteRed,
ArtifactColor.Blue => RewardSource.EliteBlue,
_ => RewardSource.Elite,
};
QueueReward(source);
}
private void QueueReward(RewardSource source)
{
if (artifacts == null)
{
artifacts = FindAnyObjectByType<ActiveArtifactController>();
}
if (artifacts == null
|| artifacts.OwnedArtifactCount >= artifacts.MaxOwnedArtifacts)
{
@@ -330,8 +445,19 @@ namespace BumpCombat.Progression
private void TryOpenPendingReward()
{
if (pendingRewards.Count == 0 || artifacts == null)
if (artifacts == null)
{
artifacts = FindAnyObjectByType<ActiveArtifactController>();
}
if (artifacts == null || (pendingRewards.Count == 0 && !pendingPassiveRelicReward))
{
return;
}
if (pendingPassiveRelicReward)
{
TryOpenPassiveRelicReward();
return;
}
@@ -341,6 +467,11 @@ namespace BumpCombat.Progression
return;
}
if (panel == null)
{
CreatePanel();
}
if (RunManager.Instance == null
|| !RunManager.Instance.TryOpenSelection(this))
{
@@ -359,7 +490,8 @@ namespace BumpCombat.Progression
if (definition != null
&& !artifacts.OwnsArtifact(definition)
&& (!hasRequiredColor
|| definition.ArtifactColor == requiredColor))
|| definition.ArtifactColor == requiredColor)
&& artifacts.GetOwnedArtifactColorCount(definition.ArtifactColor) < artifacts.MaxArtifactsPerColor)
{
candidates.Add(definition);
}
@@ -384,16 +516,65 @@ namespace BumpCombat.Progression
titleText.text = source switch
{
RewardSource.Elite => "ELITE REWARD - CHOOSE ARTIFACT",
RewardSource.Elite => "ELITE REWARD - CHOOSE RED ARTIFACT",
RewardSource.EliteRed => "ELITE REWARD - CHOOSE RED ARTIFACT",
RewardSource.EliteBlue => "ELITE REWARD - CHOOSE BLUE ARTIFACT",
RewardSource.MidBoss => "MID BOSS REWARD - CHOOSE ARTIFACT",
RewardSource.MidBoss => "MID BOSS REWARD - CHOOSE BLUE ARTIFACT",
_ => "CHOOSE ARTIFACT",
};
selectedIndex = 0;
ShowOptions();
}
private void TryOpenPassiveRelicReward()
{
passiveRelics = FindAnyObjectByType<PassiveRelicController>();
if (passiveRelics == null || RunManager.Instance == null)
{
return;
}
if (panel == null)
{
CreatePanel();
}
if (!RunManager.Instance.TryOpenSelection(this))
{
return;
}
PassiveRelicId[] available = passiveRelics.GetAvailableRelics();
shownCount = Mathf.Min(3, available.Length);
if (shownCount == 0)
{
RunManager.Instance.CloseSelection(this);
pendingPassiveRelicReward = false;
DefensePrototypeController.Instance?.ResolvePassiveRelicChoice();
return;
}
for (int i = 0; i < shownCount; i++)
{
shownRelics[i] = available[i];
}
showingPassiveRelicReward = true;
selectedIndex = 0;
titleText.text = "PASSIVE RELIC - CHOOSE ONE";
ShowOptions();
}
private static string GetPassiveRelicName(PassiveRelicId id)
{
return id switch
{
PassiveRelicId.TwinHornTalisman => "01 TWIN HORN TALISMAN\n15% chance: ordinary bump deals 2x damage",
PassiveRelicId.ResonanceCore => "02 RESONANCE CORE\n8% chance: echo current artifact at 50% damage",
PassiveRelicId.RuptureSeed => "03 RUPTURE SEED\n25% chance: player kill explodes for 10% max HP",
_ => id.ToString(),
};
}
private void HandleSelectionChanged(bool isOpen)
{
if (!isOpen && (panel == null || !panel.activeSelf))
@@ -450,6 +631,13 @@ namespace BumpCombat.Progression
continue;
}
if (showingPassiveRelicReward)
{
optionIconImages[i].gameObject.SetActive(false);
optionTexts[i].text = GetPassiveRelicName(shownRelics[i]);
continue;
}
ActiveArtifactDefinition definition = shownArtifacts[i];
Sprite icon = PresentationUiStyle.GetArtifactIcon(definition);
optionIconImages[i].sprite = icon;
@@ -518,9 +706,11 @@ namespace BumpCombat.Progression
{
switch (source)
{
case RewardSource.Elite:
case RewardSource.EliteRed:
color = ArtifactColor.Red;
return true;
case RewardSource.MidBoss:
case RewardSource.EliteBlue:
color = ArtifactColor.Blue;
return true;
@@ -605,6 +795,7 @@ namespace BumpCombat.Progression
#if UNITY_EDITOR
public int DebugShownCount => shownCount;
public bool DebugIsSelectionVisible => panel != null && panel.activeSelf;
public int DebugShownPassiveRelicCount => showingPassiveRelicReward ? shownCount : 0;
public ActiveArtifactDefinition DebugGetShownArtifact(int index)
{
@@ -0,0 +1,268 @@
using System.Collections.Generic;
using System.Linq;
using BumpCombat.Combat;
using BumpCombat.Constants;
using BumpCombat.Enemies;
using BumpCombat.Player;
using UnityEngine;
namespace BumpCombat.Progression
{
public enum PassiveRelicId
{
TwinHornTalisman,
ResonanceCore,
RuptureSeed,
}
public static class PassiveRelicRules
{
private static readonly PassiveRelicId[] Catalog =
{
PassiveRelicId.TwinHornTalisman,
PassiveRelicId.ResonanceCore,
PassiveRelicId.RuptureSeed,
};
public static bool RollsChance(float chance, float roll) =>
chance > 0f && roll >= 0f && roll < Mathf.Clamp01(chance);
public static PassiveRelicId[] GetAvailableRelics(
IReadOnlyCollection<PassiveRelicId> owned)
{
List<PassiveRelicId> available = new(Catalog.Length);
foreach (PassiveRelicId id in Catalog)
{
if (owned == null || !owned.Contains(id))
{
available.Add(id);
}
}
return available.ToArray();
}
public static float ApplyTwinHornDamage(
float damage,
bool ownsRelic,
bool isOrdinaryBump,
bool hasArtifactColor,
float chance,
float roll)
{
return ownsRelic
&& isOrdinaryBump
&& !hasArtifactColor
&& RollsChance(chance, roll)
? damage * 2f
: damage;
}
public static float GetExplosionDamage(float maximumHealth, float fraction) =>
Mathf.Max(0f, maximumHealth) * Mathf.Clamp01(fraction);
public static float GetExplosionRadius(float bodyDiameter, float diameterMultiplier) =>
Mathf.Max(0f, bodyDiameter) * Mathf.Max(0f, diameterMultiplier);
public static bool IsPlayerCreditedKill(EnemyDamageSource source) =>
source == EnemyDamageSource.Player;
}
public sealed class PassiveRelicController : MonoBehaviour
{
public const string EchoSourceId = "passive_relic_echo";
private readonly HashSet<PassiveRelicId> owned = new();
private readonly HashSet<int> processedDeaths = new();
private ActiveArtifactController artifacts;
private PlayerController player;
private Collider2D playerCollider;
private PlayerHealth playerHealth;
private bool active;
private float nextEchoAllowedAt;
public static PassiveRelicController Instance { get; private set; }
public int OwnedCount => owned.Count;
public bool Owns(PassiveRelicId id) => owned.Contains(id);
public int AvailableCount => GetAvailableRelics().Length;
#if UNITY_EDITOR
public static System.Func<float> RandomRollForTests { get; set; }
#endif
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(this);
return;
}
Instance = this;
}
private void OnEnable()
{
CombatEvents.OnValidHit += HandleValidHit;
CombatEvents.OnEnemyDied += HandleEnemyDied;
}
private void OnDisable()
{
active = false;
CombatEvents.OnValidHit -= HandleValidHit;
CombatEvents.OnEnemyDied -= HandleEnemyDied;
}
private void OnDestroy()
{
if (Instance == this)
{
Instance = null;
}
}
public void BeginDefenseRun()
{
owned.Clear();
processedDeaths.Clear();
nextEchoAllowedAt = 0f;
player = GetComponent<PlayerController>();
playerCollider = GetComponent<Collider2D>();
playerHealth = GetComponent<PlayerHealth>();
artifacts = GetComponent<ActiveArtifactController>();
active = true;
}
public void EndDefenseRun()
{
active = false;
}
public PassiveRelicId[] GetAvailableRelics() =>
PassiveRelicRules.GetAvailableRelics(owned);
public bool TryAcquire(PassiveRelicId id)
{
if (!active || !System.Array.Exists(GetAvailableRelics(), candidate => candidate == id))
{
return false;
}
return owned.Add(id);
}
public float ApplyOrdinaryBumpDamageMultiplier(
float damage,
bool isOrdinaryBump,
bool hasArtifactColor)
{
if (!active || !owned.Contains(PassiveRelicId.TwinHornTalisman))
{
return damage;
}
return PassiveRelicRules.ApplyTwinHornDamage(
damage,
true,
isOrdinaryBump,
hasArtifactColor,
GameplayConstants.Current.PassiveRelics.TwinHornDoubleDamageChance,
GetRandomRoll());
}
private void HandleValidHit(CombatHitResult hit)
{
if (!active
|| !owned.Contains(PassiveRelicId.ResonanceCore)
|| hit.Attacker != gameObject
|| hit.Damage <= 0f
|| playerHealth == null
|| playerHealth.CurrentHealth <= 0f
|| hit.SourceId == EchoSourceId
|| Time.time < nextEchoAllowedAt
|| !RollsChance(GameplayConstants.Current.PassiveRelics.ResonanceEchoChance)
|| artifacts == null)
{
return;
}
if (artifacts.TryEmitPassiveRelicEcho(
hit.HitPosition,
hit.KnockbackDirection,
GameplayConstants.Current.PassiveRelics.ResonanceEchoDamageMultiplier))
{
nextEchoAllowedAt = Time.time
+ GameplayConstants.Current.PassiveRelics.ResonanceEchoCooldownSeconds;
}
}
private void HandleEnemyDied(GameObject target)
{
if (!active || !owned.Contains(PassiveRelicId.RuptureSeed) || target == null)
{
return;
}
EnemyController victim = target.GetComponent<EnemyController>();
if (victim == null
|| !PassiveRelicRules.IsPlayerCreditedKill(victim.LastDamageSource)
|| !processedDeaths.Add(victim.SpawnLifetimeIdentity)
|| !RollsChance(GameplayConstants.Current.PassiveRelics.RuptureSeedChance))
{
return;
}
float damage = PassiveRelicRules.GetExplosionDamage(
victim.MaximumHealthAtDeath,
GameplayConstants.Current.PassiveRelics.RuptureExplosionMaxHealthFraction);
float diameter = playerCollider != null
? Mathf.Max(playerCollider.bounds.size.x, playerCollider.bounds.size.y)
: 0f;
float radius = PassiveRelicRules.GetExplosionRadius(
diameter,
GameplayConstants.Current.PassiveRelics.RuptureExplosionPlayerBodyDiameters);
if (damage <= 0f || radius <= 0f || playerHealth == null || playerHealth.CurrentHealth <= 0f)
{
return;
}
Collider2D[] hits = Physics2D.OverlapCircleAll(victim.transform.position, radius);
HashSet<EnemyController> damagedEnemies = new();
foreach (Collider2D hit in hits)
{
EnemyController enemy = hit != null ? hit.GetComponentInParent<EnemyController>() : null;
if (enemy == null
|| enemy == victim
|| enemy.IsDead
|| !damagedEnemies.Add(enemy))
{
continue;
}
Vector2 direction = ((Vector2)enemy.transform.position - (Vector2)victim.transform.position).normalized;
enemy.TryTakeDamage(
damage,
direction,
0f,
true,
false,
EnemyDamageSource.PassiveRelicExplosion);
}
}
private static float GetRandomRoll()
{
#if UNITY_EDITOR
if (RandomRollForTests != null)
{
return RandomRollForTests();
}
#endif
return Random.value;
}
private static bool RollsChance(float chance)
{
return PassiveRelicRules.RollsChance(chance, GetRandomRoll());
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b531569e5b6419493b2af9d6a0be4ee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: