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
@@ -44,6 +44,7 @@ namespace BumpCombat.Spawning
// current elite is still alive and the event must wait in the queue.
private int nextEliteScheduleOrdinal;
private int nextEliteSpawnOrdinal;
private static int nextEncounterGroupId;
private const int SummonSpawnAngleSteps = 32;
private static RunConstants Tuning => GameplayConstants.Current.Run;
@@ -148,6 +149,477 @@ namespace BumpCombat.Spawning
return HasTimedEventEnemy(timedEvent);
}
public void SpawnZoneEncounter(BumpCombat.Stage.ZoneInstance zone)
{
if (zone == null) return;
EnsureTuningLoaded();
Vector2 center = zone.ZoneCenter;
Vector2 extents = zone.PlayableHalfExtents;
BumpCombat.Stage.RoomPreset preset = zone.Preset;
if (zone.NodeType == BumpCombat.Stage.StageNodeType.Elite)
{
Vector2 spawnPos = center;
if (preset != null && preset.SpawnPoints.Count > 0)
{
var pt = preset.SpawnPoints.Find(p => p.Role == BumpCombat.Stage.SpawnRole.Elite);
if (pt.Role == BumpCombat.Stage.SpawnRole.Elite)
{
spawnPos = preset.CellToWorldPosition(center, pt.CellPosition);
}
}
RunEventEnemyTuning tuning = GetEliteTuning(0);
EnemyController prefab = GetEventPrefab(RunTimedEvent.Elite, tuning.PrefabKind);
if (prefab != null)
{
EnemyController enemy = Instantiate(prefab, spawnPos, Quaternion.identity);
enemy.name = "Stage 1 Elite";
enemy.ConfigureRunEventEnemy(RunTimedEvent.Elite, tuning);
enemy.OnDied += HandleEventEnemyDied;
GetEventList(RunTimedEvent.Elite).Add(enemy);
zone.RegisterSpawnedEnemy(enemy);
OnEventEnemySpawned?.Invoke(RunTimedEvent.Elite, enemy);
}
SpawnPresetSupportMinions(zone, center, preset);
return;
}
if (zone.NodeType == BumpCombat.Stage.StageNodeType.MidBoss)
{
Vector2 spawnPos = center;
if (preset != null && preset.SpawnPoints.Count > 0)
{
var pt = preset.SpawnPoints.Find(p => p.Role == BumpCombat.Stage.SpawnRole.MidBoss);
if (pt.Role == BumpCombat.Stage.SpawnRole.MidBoss)
{
spawnPos = preset.CellToWorldPosition(center, pt.CellPosition);
}
}
RunEventEnemyTuning tuning = GetEventEnemyTuning(RunTimedEvent.MidBoss);
EnemyController prefab = GetEventPrefab(RunTimedEvent.MidBoss, tuning.PrefabKind);
if (prefab != null)
{
EnemyController enemy = Instantiate(prefab, spawnPos, Quaternion.identity);
enemy.name = "Stage 2 MidBoss";
enemy.ConfigureRunEventEnemy(RunTimedEvent.MidBoss, tuning);
enemy.OnDied += HandleEventEnemyDied;
GetEventList(RunTimedEvent.MidBoss).Add(enemy);
zone.RegisterSpawnedEnemy(enemy);
OnEventEnemySpawned?.Invoke(RunTimedEvent.MidBoss, enemy);
}
SpawnPresetSupportMinions(zone, center, preset);
return;
}
if (zone.NodeType == BumpCombat.Stage.StageNodeType.FinalBoss)
{
Vector2 spawnPos = center;
if (preset != null && preset.SpawnPoints.Count > 0)
{
var pt = preset.SpawnPoints.Find(p => p.Role == BumpCombat.Stage.SpawnRole.FinalBoss);
if (pt.Role == BumpCombat.Stage.SpawnRole.FinalBoss)
{
spawnPos = preset.CellToWorldPosition(center, pt.CellPosition);
}
}
RunEventEnemyTuning tuning = GetEventEnemyTuning(RunTimedEvent.FinalBoss);
EnemyController prefab = GetEventPrefab(RunTimedEvent.FinalBoss, tuning.PrefabKind);
if (prefab != null)
{
EnemyController enemy = Instantiate(prefab, spawnPos, Quaternion.identity);
enemy.name = "Stage 3 FinalBoss";
enemy.ConfigureRunEventEnemy(RunTimedEvent.FinalBoss, tuning);
enemy.OnDied += HandleEventEnemyDied;
GetEventList(RunTimedEvent.FinalBoss).Add(enemy);
zone.RegisterSpawnedEnemy(enemy);
OnEventEnemySpawned?.Invoke(RunTimedEvent.FinalBoss, enemy);
}
SpawnPresetSupportMinions(zone, center, preset);
return;
}
if (zone.NodeType == BumpCombat.Stage.StageNodeType.Combat || zone.NodeType == BumpCombat.Stage.StageNodeType.Fork)
{
if (preset != null && preset.EncounterPool != null && preset.EncounterPool.Count > 0)
{
SpawnPresetEncounterPool(zone, center, preset);
return;
}
if (preset != null && preset.SpawnPoints.Count > 0)
{
SpawnPresetAnchorsFallback(zone, center, preset);
return;
}
// Fallback: Random area spawn if no preset anchors
if (enemyPrefabs == null || enemyPrefabs.Length == 0) return;
int count = UnityEngine.Random.Range(3, 6);
for (int i = 0; i < count; i++)
{
EnemyRole role = (i % 2 == 0) ? EnemyRole.Normal : EnemyRole.Crowd;
EnemyController prefab = GetRandomPrefab(role) ?? enemyPrefabs[0];
Vector2 offset = new Vector2(
UnityEngine.Random.Range(-extents.x * 0.6f, extents.x * 0.6f),
UnityEngine.Random.Range(-extents.y * 0.6f, extents.y * 0.6f));
EnemyController enemy = Instantiate(prefab, center + offset, Quaternion.identity);
enemy.transform.SetParent(zone.transform, true);
zone.RegisterSpawnedEnemy(enemy);
}
}
}
private void SpawnPresetEncounterPool(BumpCombat.Stage.ZoneInstance zone, Vector2 center, BumpCombat.Stage.RoomPreset preset)
{
if (preset.EncounterPool == null || preset.EncounterPool.Count == 0)
{
SpawnPresetAnchorsFallback(zone, center, preset);
return;
}
// Select encounter (default Encounter A for S1-A first verification)
BumpCombat.Stage.RoomEncounter encounter = preset.EncounterPool[0];
if (encounter.Items == null || encounter.Items.Count == 0)
{
SpawnPresetAnchorsFallback(zone, center, preset);
return;
}
PlayerController player = FindAnyObjectByType<PlayerController>();
Vector2 playerPos = (player != null)
? (Vector2)player.transform.position
: zone.GetEntryPosition(BumpCombat.Stage.StageDirection.West);
List<Vector2> occupiedPositions = new();
// Sort items so NormalMelee / Ranged claim authored anchors first before Crowd
List<BumpCombat.Stage.RoomEncounterItem> sortedItems = new(encounter.Items);
sortedItems.Sort((a, b) =>
{
bool aCrowd = a.EnemyKind.Equals("Bat", StringComparison.OrdinalIgnoreCase) || a.EnemyKind.Equals("Slime", StringComparison.OrdinalIgnoreCase);
bool bCrowd = b.EnemyKind.Equals("Bat", StringComparison.OrdinalIgnoreCase) || b.EnemyKind.Equals("Slime", StringComparison.OrdinalIgnoreCase);
return aCrowd.CompareTo(bCrowd);
});
foreach (var item in sortedItems)
{
EnemyController prefab = ResolvePrefabForKind(item.EnemyKind);
if (prefab == null) continue;
bool isFast = item.EnemyKind.Equals("Bat", StringComparison.OrdinalIgnoreCase);
bool isRanged = item.EnemyKind.Contains("Archer", StringComparison.OrdinalIgnoreCase);
float requiredSafeDist = isRanged ? 7.0f : (isFast ? 5.0f : 4.0f);
int remaining = item.Count;
while (remaining > 0)
{
int groupSize = Mathf.Min(
SelectEncounterGroupSize(preset.StageIndex, UnityEngine.Random.value),
remaining);
nextEncounterGroupId = nextEncounterGroupId == int.MaxValue
? 1
: nextEncounterGroupId + 1;
int groupId = nextEncounterGroupId;
Vector2 groupAnchor = Vector2.zero;
for (int memberIndex = 0; memberIndex < groupSize; memberIndex++)
{
Vector2 spawnWorldPos = memberIndex == 0
? FindSafeSpawnPosition(
preset,
center,
playerPos,
item.EnemyKind,
requiredSafeDist,
occupiedPositions)
: FindSafeGroupSpawnPosition(
preset,
center,
playerPos,
requiredSafeDist,
groupAnchor,
occupiedPositions);
if (memberIndex == 0)
{
groupAnchor = spawnWorldPos;
}
EnemyController enemy = Instantiate(prefab, spawnWorldPos, Quaternion.identity);
enemy.name = $"{item.EnemyKind}_{occupiedPositions.Count + 1}";
enemy.transform.SetParent(zone.transform, true);
enemy.ConfigureSightRangeMultiplier(
GameplayConstants.Current.Run.StageEncounterSightRangeMultiplier);
enemy.ConfigureEncounterGroup(groupId, groupAnchor);
zone.RegisterSpawnedEnemy(enemy);
occupiedPositions.Add(spawnWorldPos);
}
remaining -= groupSize;
}
}
}
public static int SelectEncounterGroupSize(int stageIndex, float roll)
{
roll = Mathf.Clamp01(roll);
if (stageIndex <= 1)
{
if (roll < 0.55f) return 1;
return roll < 0.90f ? 2 : 3;
}
if (stageIndex == 2)
{
if (roll < 0.35f) return 1;
return roll < 0.80f ? 2 : 3;
}
if (roll < 0.20f) return 1;
return roll < 0.70f ? 2 : 3;
}
private Vector2 FindSafeGroupSpawnPosition(
BumpCombat.Stage.RoomPreset preset,
Vector2 zoneCenter,
Vector2 playerPos,
float safeDist,
Vector2 groupAnchor,
List<Vector2> occupied)
{
EnemyConstants tuning = GameplayConstants.Current.Enemies;
Vector2 best = groupAnchor;
float bestDistance = float.MaxValue;
for (int cy = 0; cy < preset.GridHeight; cy++)
{
for (int cx = 0; cx < preset.GridWidth; cx++)
{
if (preset.GridMap[cx, cy] != BumpCombat.Stage.PresetCellType.Walkable)
{
continue;
}
Vector2 candidate = preset.CellToWorldPosition(
zoneCenter,
new Vector2Int(cx, cy));
float groupDistance = Vector2.Distance(groupAnchor, candidate);
if (groupDistance < tuning.EncounterGroupMinSpacing
|| groupDistance > tuning.EncounterGroupMaxSpacing
|| Vector2.Distance(playerPos, candidate) < safeDist
|| !IsSeparatedFromOccupied(candidate, occupied, 0.8f))
{
continue;
}
float targetDistance = Mathf.Abs(
groupDistance
- (tuning.EncounterGroupMinSpacing + tuning.EncounterGroupMaxSpacing) * 0.5f);
if (targetDistance < bestDistance)
{
bestDistance = targetDistance;
best = candidate;
}
}
}
return bestDistance < float.MaxValue
? best
: FindSafeSpawnPosition(
preset,
zoneCenter,
playerPos,
string.Empty,
safeDist,
occupied);
}
private Vector2 FindSafeSpawnPosition(BumpCombat.Stage.RoomPreset preset, Vector2 zoneCenter, Vector2 playerPos,
string enemyKind, float safeDist, List<Vector2> occupied)
{
BumpCombat.Stage.SpawnRole targetRole = BumpCombat.Stage.SpawnRole.NormalMelee;
if (enemyKind.Equals("Bat", StringComparison.OrdinalIgnoreCase) || enemyKind.Equals("Slime", StringComparison.OrdinalIgnoreCase))
{
targetRole = BumpCombat.Stage.SpawnRole.Crowd;
}
else if (enemyKind.Contains("Archer", StringComparison.OrdinalIgnoreCase))
{
targetRole = BumpCombat.Stage.SpawnRole.Ranged;
}
// 1. Check matching preset anchors first
for (int i = 0; i < preset.SpawnPoints.Count; i++)
{
var pt = preset.SpawnPoints[i];
if (pt.Role != targetRole) continue;
Vector2 wPos = preset.CellToWorldPosition(zoneCenter, pt.CellPosition);
if (Vector2.Distance(playerPos, wPos) >= safeDist && IsSeparatedFromOccupied(wPos, occupied, 0.8f))
{
return wPos;
}
}
// 2. Search spiral from anchor or center in Walkable cells
Vector2Int originCell = (targetRole == BumpCombat.Stage.SpawnRole.Crowd && preset.SpawnPoints.Count > 0)
? preset.SpawnPoints.Find(p => p.Role == BumpCombat.Stage.SpawnRole.Crowd).CellPosition
: new Vector2Int(preset.GridWidth / 2, preset.GridHeight / 2);
if (originCell.x < 0 || originCell.x >= preset.GridWidth) originCell = new Vector2Int(preset.GridWidth / 2, preset.GridHeight / 2);
int maxRadius = Mathf.Max(preset.GridWidth, preset.GridHeight);
for (int r = 0; r <= maxRadius; r++)
{
for (int dy = -r; dy <= r; dy++)
{
for (int dx = -r; dx <= r; dx++)
{
if (Mathf.Abs(dx) != r && Mathf.Abs(dy) != r) continue;
int cx = originCell.x + dx;
int cy = originCell.y + dy;
if (cx < 0 || cx >= preset.GridWidth || cy < 0 || cy >= preset.GridHeight) continue;
if (preset.GridMap[cx, cy] != BumpCombat.Stage.PresetCellType.Walkable) continue;
Vector2 candidatePos = preset.CellToWorldPosition(zoneCenter, new Vector2Int(cx, cy));
if (Vector2.Distance(playerPos, candidatePos) >= safeDist && IsSeparatedFromOccupied(candidatePos, occupied, 0.8f))
{
return candidatePos;
}
}
}
}
// Fallback: farthest walkable cell from player
Vector2 farthestPos = zoneCenter;
float maxDist = -1f;
for (int cy = 0; cy < preset.GridHeight; cy++)
{
for (int cx = 0; cx < preset.GridWidth; cx++)
{
if (preset.GridMap[cx, cy] != BumpCombat.Stage.PresetCellType.Walkable) continue;
Vector2 p = preset.CellToWorldPosition(zoneCenter, new Vector2Int(cx, cy));
float d = Vector2.Distance(playerPos, p);
if (d > maxDist && IsSeparatedFromOccupied(p, occupied, 0.6f))
{
maxDist = d;
farthestPos = p;
}
}
}
return farthestPos;
}
private static bool IsSeparatedFromOccupied(Vector2 pos, List<Vector2> occupied, float minSeparation)
{
for (int i = 0; i < occupied.Count; i++)
{
if (Vector2.Distance(pos, occupied[i]) < minSeparation)
{
return false;
}
}
return true;
}
private void SpawnPresetAnchorsFallback(BumpCombat.Stage.ZoneInstance zone, Vector2 center, BumpCombat.Stage.RoomPreset preset)
{
foreach (var spawnPoint in preset.SpawnPoints)
{
if (spawnPoint.Role == BumpCombat.Stage.SpawnRole.Elite ||
spawnPoint.Role == BumpCombat.Stage.SpawnRole.MidBoss ||
spawnPoint.Role == BumpCombat.Stage.SpawnRole.FinalBoss)
{
continue;
}
EnemyController prefab = ResolvePrefabForRole(spawnPoint.Role);
if (prefab != null)
{
Vector2 worldPos = preset.CellToWorldPosition(center, spawnPoint.CellPosition);
EnemyController enemy = Instantiate(prefab, worldPos, Quaternion.identity);
enemy.transform.SetParent(zone.transform, true);
zone.RegisterSpawnedEnemy(enemy);
}
}
}
private EnemyController ResolvePrefabForKind(string kind)
{
if (string.IsNullOrEmpty(kind)) return null;
if (Enum.TryParse<EnemyKind>(kind, true, out EnemyKind parsedKind))
{
EnemyController prefab = GetPrefab(parsedKind);
if (prefab != null) return prefab;
}
string lower = kind.ToLowerInvariant();
if (lower.Contains("bat")) return GetPrefab(EnemyKind.Bat) ?? GetRandomPrefab(EnemyRole.Crowd);
if (lower.Contains("archer")) return GetPrefab(EnemyKind.SkeletonArcher) ?? GetRandomPrefab(EnemyRole.Normal);
if (lower.Contains("skeleton")) return GetPrefab(EnemyKind.Skeleton) ?? GetRandomPrefab(EnemyRole.Normal);
if (lower.Contains("slime")) return GetPrefab(EnemyKind.Slime) ?? GetRandomPrefab(EnemyRole.Crowd);
if (lower.Contains("golem")) return GetPrefab(EnemyKind.NecroGolem) ?? GetRandomPrefab(EnemyRole.Normal);
return null;
}
private void SpawnPresetSupportMinions(BumpCombat.Stage.ZoneInstance zone, Vector2 center, BumpCombat.Stage.RoomPreset preset)
{
if (preset == null || preset.SpawnPoints.Count == 0) return;
foreach (var pt in preset.SpawnPoints)
{
if (pt.Role == BumpCombat.Stage.SpawnRole.Elite ||
pt.Role == BumpCombat.Stage.SpawnRole.MidBoss ||
pt.Role == BumpCombat.Stage.SpawnRole.FinalBoss)
{
continue;
}
EnemyController prefab = ResolvePrefabForRole(pt.Role);
if (prefab != null)
{
Vector2 worldPos = preset.CellToWorldPosition(center, pt.CellPosition);
EnemyController minion = Instantiate(prefab, worldPos, Quaternion.identity);
zone.RegisterSpawnedEnemy(minion);
}
}
}
private EnemyController ResolvePrefabForRole(BumpCombat.Stage.SpawnRole role)
{
switch (role)
{
case BumpCombat.Stage.SpawnRole.Ranged:
return GetPrefab(EnemyKind.SkeletonArcher)
?? GetPrefab(EnemyKind.Warlock)
?? GetPrefab(EnemyKind.Necrofire)
?? GetRandomPrefab(EnemyRole.Normal)
?? (enemyPrefabs != null && enemyPrefabs.Length > 0 ? enemyPrefabs[0] : null);
case BumpCombat.Stage.SpawnRole.Crowd:
return GetPrefab(EnemyKind.Bat)
?? GetPrefab(EnemyKind.Slime)
?? GetRandomPrefab(EnemyRole.Crowd)
?? (enemyPrefabs != null && enemyPrefabs.Length > 0 ? enemyPrefabs[0] : null);
case BumpCombat.Stage.SpawnRole.NormalMelee:
default:
return GetPrefab(EnemyKind.Skeleton)
?? GetPrefab(EnemyKind.Lancer)
?? GetPrefab(EnemyKind.GreatswordSkeleton)
?? GetRandomPrefab(EnemyRole.Normal)
?? (enemyPrefabs != null && enemyPrefabs.Length > 0 ? enemyPrefabs[0] : null);
}
}
private void Start()
{
if (RunManager.Instance != null)
@@ -175,6 +647,11 @@ namespace BumpCombat.Spawning
{
return;
}
if (BumpCombat.Stage.StageManager.IsStageModeActive
|| RunManager.Instance?.IsDefensePrototypeRun == true)
{
return;
}
TrySpawnPendingEvents();
float elapsedTime = RunManager.Instance?.ProgressionTime ?? 0f;
bool productionRun = RunManager.Instance?.IsProductionRun == true;
@@ -732,11 +1209,21 @@ namespace BumpCombat.Spawning
if (role == RunTimedEvent.MidBoss)
{
midBossDefeated = true;
RunManager.Instance?.UnlockArtifactCharge();
BumpCombat.Stage.StageManager.Instance?.HandleMajorBossDefeated(
BumpCombat.Stage.StageNodeType.MidBoss,
enemy.GetComponentInParent<BumpCombat.Stage.ZoneInstance>());
}
else if (role == RunTimedEvent.Elite)
{
RunManager.Instance?.UnlockGuard();
BumpCombat.Stage.StageManager.Instance?.HandleMajorBossDefeated(
BumpCombat.Stage.StageNodeType.Elite,
enemy.GetComponentInParent<BumpCombat.Stage.ZoneInstance>());
}
else if (role == RunTimedEvent.FinalBoss)
{
BumpCombat.Stage.StageManager.Instance?.HandleMajorBossDefeated(
BumpCombat.Stage.StageNodeType.FinalBoss,
enemy.GetComponentInParent<BumpCombat.Stage.ZoneInstance>());
}
OnEventEnemyDefeated?.Invoke(role);
@@ -837,6 +1324,18 @@ namespace BumpCombat.Spawning
return null;
}
public EnemyController SpawnDefenseSkeleton(Vector2 position)
{
EnsureTuningLoaded();
EnemyController prefab = GetPrefab(EnemyKind.Skeleton);
if (prefab == null)
{
return null;
}
return Instantiate(prefab, position, Quaternion.identity);
}
private EnemyController GetPrefab(EnemyKind kind)
{
if (enemyPrefabs == null)
@@ -1254,11 +1753,20 @@ namespace BumpCombat.Spawning
return false;
}
EnemyRole role = crowd ? EnemyRole.Crowd : EnemyRole.Normal;
EnemyController[] candidates = new EnemyController[count];
for (int i = 0; i < count; i++)
{
candidates[i] = GetRandomPrefab(role);
if (crowd)
{
EnemyKind preferredKind = i % 2 == 0 ? EnemyKind.Bat : EnemyKind.Slime;
EnemyKind fallbackKind = preferredKind == EnemyKind.Bat ? EnemyKind.Slime : EnemyKind.Bat;
candidates[i] = GetPrefab(preferredKind) ?? GetPrefab(fallbackKind);
}
else
{
candidates[i] = GetRandomPrefab(EnemyRole.Normal);
}
if (candidates[i] == null)
{
return false;
@@ -1405,6 +1913,14 @@ namespace BumpCombat.Spawning
{
summonedEnemies.Add(enemy);
enemy.OnDied += HandleSummonedEnemyDied;
EnemyController owner = enemy.SummonOwner;
BumpCombat.Stage.ZoneInstance ownerZone =
owner != null ? owner.GetComponentInParent<BumpCombat.Stage.ZoneInstance>() : null;
if (ownerZone != null)
{
ownerZone.RegisterSpawnedEnemy(enemy);
}
}
private void HandleSummonedEnemyDied(EnemyController enemy)