fixeo general

This commit is contained in:
2025-10-21 05:20:20 -03:00
parent 6cbd4aaab9
commit c89f14debd
23 changed files with 3225 additions and 596 deletions
+316 -367
View File
@@ -2,466 +2,415 @@ using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Spawner dinámico data-driven con fallback por Inspector.
/// - Lee LevelConfig.dynamic / dynamicPools cuando están disponibles.
/// - Si el SO no está o está sin pools, cae a los prefabs del Inspector y spawnea igual.
/// - Spawnea en bordes de cámara (dentro o fuera), con ligera variación angular.
/// - Primer avión opcional tintado (para tutorial).
/// - Usa reloj en tiempo real (realtime) y respeta flags del GameManager,
/// pero NO se cuelga si Time.timeScale = 0.
/// </summary>
[DisallowMultipleComponent]
[DefaultExecutionOrder(-5)]
public class DynamicSpawnerSystem : MonoBehaviour
{
[Header("Debug/Diagnóstico")]
public bool verbose = true;
public bool heartbeatLogs = true;
[Tooltip("Segundos entre heartbeats (estado, bloqueos, caps, etc.).")]
public float heartbeatEvery = 2f;
[Header("Estado")]
[SerializeField] private bool running = false;
[SerializeField] private bool usingCfg = false;
[Header("Modo / Fallback")]
[Tooltip("Si no se recibe LevelConfig válido, o si no tiene pools, arrancar con los parámetros del Inspector.")]
public bool autoStartIfNoConfig = true;
[Header("Debug")]
[SerializeField] private bool verbose = false;
[SerializeField] private float statusLogEvery = 10f;
[Tooltip("Spawnear fuera de la cámara (true) o pegado al borde interno (false).")]
public bool spawnOutsideCameraEdges = true;
[Header("Spawn fuera de cámara")]
[Tooltip("Distancia en PÍXELES por fuera del borde de la cámara donde aparece el avión.")]
public int spawnMarginPixels = 100;
[Tooltip("Inset en unidades de mundo para el spawn en bordes (positivo = fuera si 'spawnOutsideCameraEdges' = true).")]
public float edgeInsetWorld = 0.8f;
// ----- Config cargada -----
private LevelConfig _cfg;
private LevelConfig.DynamicSettings _dyn;
private List<LevelConfig.DynamicPool> _pools;
[Tooltip("Variación angular aleatoria del rumbo hacia adentro.")]
public float headingJitterDeg = 8f;
// ----- Conteo activo por tipo -----
private int _activePlane = 0;
private int _activeHeli = 0;
[Header("Fallback Prefabs (Inspector)")]
public GameObject[] planePrefabsFallback;
public GameObject[] heliPrefabsFallback;
// tokens vivos
private readonly HashSet<DynamicSpawnToken> _tokens = new HashSet<DynamicSpawnToken>();
[Header("Fallback Ritmo")]
public float baseIntervalFallback = 2.0f;
public Vector2 intervalRangeFallback = new Vector2(1.0f, 3.0f);
public float startDelayFallback = 0.2f;
// control de ritmo
private float _nextSpawnAtRealtime = 0f;
private float _lastStatusLog = -999f;
[Header("Fallback Caps")]
public int maxActivePlaneFallback = 4;
public int maxActiveHelicopterFallback = 1;
// ramp (dificultad, NO velocidad)
private float _rampStartRealtime = 0f;
private int _rampSteps = 0;
[Header("Primer avión (tutorial)")]
public bool forceBlueFirstPlane = false;
public PlaneType firstPlaneType = PlaneType.Plane;
public Color firstPlaneTint = new Color(0.25f, 0.8f, 1f, 1f);
// ===== NUEVO: tiempo para normalizar curvas 0..1 =====
private float _difficultyStartRealtime = 0f;
// ========= Estado general =========
private bool running = false;
private Coroutine co;
// pools cache
private readonly List<GameObject> _planePrefabs = new List<GameObject>();
private readonly List<float> _planeWeights = new List<float>();
private readonly List<GameObject> _heliPrefabs = new List<GameObject>();
private readonly List<float> _heliWeights = new List<float>();
// Config activa (SO o fallback)
private bool usingConfig = false;
// ===== NUEVO: mapear cada prefab a su pool para leer overrides =====
private readonly List<LevelConfig.DynamicPool> _planePoolRef = new List<LevelConfig.DynamicPool>();
private readonly List<LevelConfig.DynamicPool> _heliPoolRef = new List<LevelConfig.DynamicPool>();
private float currentInterval;
private Vector2 currentRange;
private int capPlane;
private int capHeli;
private Coroutine _runner;
private float nextSpawnAtRealtime = 0f;
private float lastSpawnRealtime = -999f;
// =====================================================================
// API
// =====================================================================
private bool firstForcedDone = false;
// Contadores activos
private int activePlane = 0;
private int activeHeli = 0;
private int spawnedSoFar = 0;
// Pools activos (normalizados)
private GameObject[] poolPlane;
private GameObject[] poolHeli;
// Tokens vivos
private readonly HashSet<DynamicSpawnToken> tokens = new HashSet<DynamicSpawnToken>();
// Bloqueos externos
private bool tutorialBlocked = false;
// Diagnóstico
private string lastBlockedReason = "";
private float nextHeartbeatAt = 0f;
// ========= API pública =========
/// <summary>Llamado por GameManager cuando el nivel quiere usar spawner dinámico.</summary>
public void StartWithLevel(LevelConfig cfg)
{
StopRun();
StopAllSpawning();
// Intentar usar SO
bool cfgOk = (cfg != null && cfg.useDynamicSpawning && cfg.dynamic != null && cfg.dynamic.enabled);
_cfg = cfg;
usingCfg = (_cfg != null);
if (!usingCfg) { if (verbose) Debug.LogWarning("[DynamicSpawnerSystem] StartWithLevel sin LevelConfig."); return; }
if (cfgOk)
{
usingConfig = true;
_dyn = _cfg.dynamic;
_pools = _cfg.dynamicPools ?? new List<LevelConfig.DynamicPool>();
// Pools desde SO
poolPlane = FilterPrefabs(ResolvePoolFromSO(cfg, PlaneType.Plane));
poolHeli = FilterPrefabs(ResolvePoolFromSO(cfg, PlaneType.Helicopter));
BuildTypePools();
// Si NO hay pools en el SO → fallback automático a Inspector
if (!HasAnyPool(poolPlane, poolHeli))
{
if (verbose)
Debug.LogWarning("[DynamicSpawnerSystem] LevelConfig.dynamicPools no tiene prefabs válidos. Usando FALLBACK de Inspector.");
_activePlane = 0;
_activeHeli = 0;
_tokens.Clear();
ApplyFallbackConfig(); // carga pools/ritmo/caps del Inspector
usingConfig = false; // dejamos claro que estamos en fallback
StartCommon(startDelayFallback);
return;
}
_rampStartRealtime = Time.realtimeSinceStartup;
_difficultyStartRealtime = _rampStartRealtime; // punto 0 para curvas 0..1
_rampSteps = 0;
// Ritmo + multiplicador global de nivel
float rateMul = Mathf.Max(0.01f, cfg.spawnRateMultiplier <= 0f ? 1f : cfg.spawnRateMultiplier);
currentInterval = Mathf.Max(0.05f, cfg.dynamic.baseInterval / rateMul);
currentRange = new Vector2(
Mathf.Min(cfg.dynamic.intervalRange.x, cfg.dynamic.intervalRange.y),
Mathf.Max(cfg.dynamic.intervalRange.x, cfg.dynamic.intervalRange.y)
);
running = (_cfg.useDynamicSpawning && _dyn != null && _dyn.enabled);
if (!running) { if (verbose) Debug.Log("[DynamicSpawnerSystem] Dynamic desactivado por config."); return; }
// Caps
capPlane = Mathf.Max(0, cfg.dynamic.maxActivePlane);
capHeli = Mathf.Max(0, cfg.dynamic.maxActiveHelicopter);
// Borde
spawnOutsideCameraEdges = cfg.dynamic.spawnOutsideCameraEdges;
edgeInsetWorld = cfg.dynamic.edgeInsetWorld;
// Primer avión
forceBlueFirstPlane = cfg.dynamic.forceBlueFirstPlane;
firstPlaneType = cfg.dynamic.firstPlaneType;
firstPlaneTint = cfg.dynamic.firstPlaneTint;
if (verbose)
{
Debug.Log($"[DynamicSpawnerSystem] Iniciado con SO '{cfg.levelId}'. " +
$"Pools: plane={(poolPlane!=null?poolPlane.Length:0)} heli={(poolHeli!=null?poolHeli.Length:0)} | " +
$"interval≈{currentInterval:0.00}s [{currentRange.x:0.00},{currentRange.y:0.00}] | " +
$"caps P/H={capPlane}/{capHeli} | spawnOutside={spawnOutsideCameraEdges} inset={edgeInsetWorld:0.00}");
}
StartCommon(cfg.dynamic.startDelay);
}
else
{
// Fallback total si no hay SO válido
if (!autoStartIfNoConfig)
{
if (verbose) Debug.Log("[DynamicSpawnerSystem] SO inválido y 'autoStartIfNoConfig' = false → no inicia.");
return;
}
usingConfig = false;
ApplyFallbackConfig();
if (verbose)
{
Debug.Log($"[DynamicSpawnerSystem] Iniciado en FALLBACK (Inspector). " +
$"Pools: plane={(poolPlane!=null?poolPlane.Length:0)} heli={(poolHeli!=null?poolHeli.Length:0)} | " +
$"interval≈{currentInterval:0.00}s [{currentRange.x:0.00},{currentRange.y:0.00}] | " +
$"caps P/H={capPlane}/{capHeli} | spawnOutside={spawnOutsideCameraEdges} inset={edgeInsetWorld:0.00}");
}
StartCommon(startDelayFallback);
}
_nextSpawnAtRealtime = Time.realtimeSinceStartup + Mathf.Max(0f, _dyn.startDelay);
_runner = StartCoroutine(CoRunRealtime());
if (verbose) Debug.Log("[DynamicSpawnerSystem] Iniciado.");
}
/// <summary>Reanuda el loop (mantiene config actual).</summary>
public void StartRun()
{
if (running) return;
if (!HasAnyPool(poolPlane, poolHeli))
{
if (verbose) Debug.LogWarning("[DynamicSpawnerSystem] StartRun: no hay pools cargados.");
return;
}
running = true;
if (nextSpawnAtRealtime <= 0f) nextSpawnAtRealtime = Time.realtimeSinceStartup + 0.1f;
co = StartCoroutine(CoRunRealtime());
if (verbose) Debug.Log("[DynamicSpawnerSystem] Reanudado.");
}
public void StopStreaming() => running = false;
/// <summary>Detiene el loop de spawns (no destruye nada).</summary>
public void StopRun()
public void StopAllSpawning()
{
running = false;
if (co != null) StopCoroutine(co);
co = null;
if (verbose) Debug.Log("[DynamicSpawnerSystem] Detenido.");
if (_runner != null) StopCoroutine(_runner);
_runner = null;
_tokens.Clear();
}
/// <summary>El tutorial abre/cierra: gateo explícito.</summary>
public void SetTutorialBlocked(bool blocked)
// Compat.
private void Stop() => StopAllSpawning();
// Callbacks desde DynamicSpawnToken
public void OnTokenEnteredViewport(DynamicSpawnToken token) { /* opcional */ }
public void OnTokenDestroyed(DynamicSpawnToken token)
{
tutorialBlocked = blocked;
if (verbose) Debug.Log("[DynamicSpawnerSystem] TutorialBlocked=" + blocked);
if (token == null) return;
if (_tokens.Remove(token))
{
if (token.Type == PlaneType.Plane) _activePlane = Mathf.Max(0, _activePlane - 1);
else if (token.Type == PlaneType.Helicopter) _activeHeli = Mathf.Max(0, _activeHeli - 1);
}
}
// ========= Internals =========
private void ApplyFallbackConfig()
{
poolPlane = FilterPrefabs(planePrefabsFallback);
poolHeli = FilterPrefabs(heliPrefabsFallback);
currentInterval = Mathf.Max(0.05f, baseIntervalFallback);
currentRange = new Vector2(
Mathf.Min(intervalRangeFallback.x, intervalRangeFallback.y),
Mathf.Max(intervalRangeFallback.x, intervalRangeFallback.y)
);
capPlane = Mathf.Max(0, maxActivePlaneFallback);
capHeli = Mathf.Max(0, maxActiveHelicopterFallback);
}
private void StartCommon(float startDelay)
{
firstForcedDone = false;
activePlane = activeHeli = spawnedSoFar = 0;
tokens.Clear();
float now = Time.realtimeSinceStartup;
nextSpawnAtRealtime = now + Mathf.Max(0f, startDelay);
running = true;
nextHeartbeatAt = now + Mathf.Max(0.5f, heartbeatEvery);
co = StartCoroutine(CoRunRealtime());
}
// =====================================================================
// LOOP
// =====================================================================
private IEnumerator CoRunRealtime()
{
while (running)
{
// Heartbeat (diagnóstico)
if (heartbeatLogs && Time.realtimeSinceStartup >= nextHeartbeatAt)
{
nextHeartbeatAt += Mathf.Max(0.5f, heartbeatEvery);
Debug.Log("[DynamicSpawnerSystem] " + GetStatusLine());
}
var wait = new WaitForSecondsRealtime(0.05f);
// Bloqueos globales
if (IsGloballyBlocked(out string reason))
while (true)
{
if (!running || !usingCfg || _dyn == null)
{
lastBlockedReason = reason;
yield return null;
yield return wait;
continue;
}
// Caps y pools
if (!HasAnyPool(poolPlane, poolHeli))
TryRampUp(); // solo ritmo/caps
if (verbose && Time.realtimeSinceStartup - _lastStatusLog >= statusLogEvery)
{
lastBlockedReason = "no-pools";
yield return null; continue;
_lastStatusLog = Time.realtimeSinceStartup;
Debug.Log($"[DynamicSpawnerSystem] running={running} usingCfg={usingCfg} activeTot={_activePlane + _activeHeli} plane={_activePlane} heli={_activeHeli} pools P/H={_planePrefabs.Count}/{_heliPrefabs.Count}");
}
bool fullPlane = (activePlane >= Mathf.Max(0, capPlane));
bool fullHeli = (activeHeli >= Mathf.Max(0, capHeli));
if ((poolPlane == null || poolPlane.Length == 0) && fullHeli) { lastBlockedReason = "heli-cap"; yield return null; continue; }
if ((poolHeli == null || poolHeli.Length == 0) && fullPlane){ lastBlockedReason = "plane-cap"; yield return null; continue; }
if (fullPlane && fullHeli) { lastBlockedReason = "caps-full"; yield return null; continue; }
// Espera hasta el próximo spawn
float now = Time.realtimeSinceStartup;
if (now < nextSpawnAtRealtime) { yield return null; continue; }
// Elegir tipo
PlaneType picked = PickTypeRespectingCaps();
if (!firstForcedDone && forceBlueFirstPlane) picked = firstPlaneType;
if (TrySpawnOne(picked, tintBlue: (!firstForcedDone && forceBlueFirstPlane)))
if (Time.realtimeSinceStartup >= _nextSpawnAtRealtime)
{
firstForcedDone = true;
lastBlockedReason = "";
lastSpawnRealtime = now;
TrySpawnTick();
ProgramNextSpawn();
}
// Programar siguiente
float waitRnd = Random.Range(currentRange.x, currentRange.y);
float wait = Mathf.Clamp(waitRnd, 0.05f, Mathf.Max(0.06f, currentInterval * 2f));
nextSpawnAtRealtime = Time.realtimeSinceStartup + wait;
yield return null;
yield return wait;
}
}
private bool IsGloballyBlocked(out string reason)
{
reason = "";
if (tutorialBlocked) { reason = "tutorial"; return true; }
// =====================================================================
// SPAWN
// =====================================================================
var gm = GameManager.Instance;
if (gm != null)
{
if (gm.isPaused) { reason = "paused"; return true; }
if (gm.isPausedByGameOver) { reason = "gameover"; return true; }
if (gm.IsWin()) { reason = "win"; return true; }
}
return false;
private void TrySpawnTick()
{
bool canPlane = _planePrefabs.Count > 0 && _activePlane < _dyn.maxActivePlane;
bool canHeli = _heliPrefabs.Count > 0 && _activeHeli < _dyn.maxActiveHelicopter;
if (!canPlane && !canHeli) return;
int roomPlane = Mathf.Max(0, _dyn.maxActivePlane - _activePlane);
int roomHeli = Mathf.Max(0, _dyn.maxActiveHelicopter - _activeHeli);
if (canPlane && (!canHeli || roomPlane >= roomHeli)) TrySpawnOne(PlaneType.Plane);
else if (canHeli) TrySpawnOne(PlaneType.Helicopter);
}
private bool TrySpawnOne(PlaneType type, bool tintBlue)
private void TrySpawnOne(PlaneType t)
{
GameObject[] list = (type == PlaneType.Helicopter) ? poolHeli : poolPlane;
if (list == null || list.Length == 0)
GameObject prefab = null;
LevelConfig.DynamicPool poolRef = null;
if (t == PlaneType.Plane)
{
if (verbose) Debug.LogWarning("[DynamicSpawnerSystem] No hay prefabs configurados para '" + type + "'.");
return false;
if (_planePrefabs.Count == 0) return;
int idx = WeightedPickIndex(_planeWeights);
prefab = _planePrefabs[idx];
poolRef = _planePoolRef[idx];
}
else
{
if (_heliPrefabs.Count == 0) return;
int idx = WeightedPickIndex(_heliWeights);
prefab = _heliPrefabs[idx];
poolRef = _heliPoolRef[idx];
}
if (type == PlaneType.Plane && activePlane >= Mathf.Max(0, capPlane)) return false;
if (type == PlaneType.Helicopter && activeHeli >= Mathf.Max(0, capHeli)) return false;
if (prefab == null || poolRef == null) return;
GameObject prefab = list[Random.Range(0, list.Length)];
if (!prefab) return false;
// Cap por tipo (opcional)
int aliveByType = CountAlive(t);
if (poolRef.maxAlive > 0 && aliveByType >= poolRef.maxAlive)
return;
// Posición y rumbo
ResolveEdgeSpawn(out var pos, out var dirIn);
float deg = Mathf.Atan2(dirIn.y, dirIn.x) * Mathf.Rad2Deg;
Quaternion rot = Quaternion.Euler(0, 0, deg);
// Instanciar
var go = Instantiate(prefab, pos, rot);
spawnedSoFar++;
// Token
var tok = go.GetComponent<DynamicSpawnToken>();
if (!tok) tok = go.AddComponent<DynamicSpawnToken>();
tok.Initialize(this, type);
tokens.Add(tok);
// Config PlaneController (rumbo y colisión ON)
var pc = go.GetComponent<PlaneController>();
if (pc != null)
if (!TryGetSpawnOutsideViewportPixels(out Vector3 pos, out Vector3 inwardDir))
{
pc.isLanding = false;
pc.noCol = false;
if (pc.boxCollider2D != null) pc.boxCollider2D.enabled = true;
pc.SetInitialDirection(dirIn);
// fallback: si no hay cámara, no spawneamos
return;
}
if (tintBlue)
var go = Instantiate(prefab, pos, Quaternion.identity);
go.name = prefab.name + "_Dyn";
// Token de housekeeping
var token = go.GetComponent<DynamicSpawnToken>();
if (token == null) token = go.AddComponent<DynamicSpawnToken>();
token.Initialize(this, t);
_tokens.Add(token);
// Dirección hacia adentro
var plane = go.GetComponent<PlaneController>();
if (plane) plane.SetInitialDirection(inwardDir);
// ===== NUEVO: velocidad final por tipo con curvas =====
float t01 = EvalDifficultyT01();
float globalSpeedMul = SafeEval(_dyn.globalSpeedMultiplier, t01, 1f);
float perTypeSpeedMul = SafeEval(poolRef.speedMultiplierOverTime, t01, 1f);
float finalSpeed = plane != null ? Mathf.Max(0f, plane.speed) : 0f;
// Si el pool define un rango, lo usamos como base. Si no, respetamos el speed del prefab/controller.
if (poolRef.speedRange != Vector2.zero)
{
var sr = go.GetComponent<SpriteRenderer>();
if (sr) sr.color = firstPlaneTint;
float baseSpeed = Random.Range(
Mathf.Min(poolRef.speedRange.x, poolRef.speedRange.y),
Mathf.Max(poolRef.speedRange.x, poolRef.speedRange.y)
);
finalSpeed = baseSpeed;
}
if (type == PlaneType.Plane) activePlane++;
else activeHeli++;
// Multiplicadores globales y por tipo + tuning del nivel
finalSpeed *= globalSpeedMul;
finalSpeed *= perTypeSpeedMul;
finalSpeed *= Mathf.Max(0.01f, _cfg != null ? _cfg.speedMultiplier : 1f);
if (verbose)
Debug.Log($"[DynamicSpawnerSystem] Spawn {type} | active P/H={activePlane}/{activeHeli} total={spawnedSoFar}");
if (plane) plane.speed = finalSpeed;
if (t == PlaneType.Plane) _activePlane++;
else _activeHeli++;
}
/// <summary>
/// Calcula un punto de spawn exactamente a 'spawnMarginPixels' FUERA del rectángulo visible.
/// No modifica velocidad de los aviones: solo su posición inicial y la dirección hacia adentro.
/// </summary>
private bool TryGetSpawnOutsideViewportPixels(out Vector3 worldPos, out Vector3 inwardDir)
{
worldPos = Vector3.zero;
inwardDir = Vector3.right;
var cam = Camera.main;
if (cam == null) return false;
int W = Mathf.Max(1, Screen.width);
int H = Mathf.Max(1, Screen.height);
float mx = (float)spawnMarginPixels / W;
float my = (float)spawnMarginPixels / H;
int edge = Random.Range(0, 4); // 0=L,1=R,2=B,3=T
Vector3 innerVP = new Vector3(0.5f, 0.5f, 0f); // centro
Vector3 spawnVP;
switch (edge)
{
case 0: spawnVP = new Vector3(-mx, Random.value, 0f); break;
case 1: spawnVP = new Vector3(1f + mx, Random.value, 0f); break;
case 2: spawnVP = new Vector3(Random.value, -my, 0f); break;
default: spawnVP = new Vector3(Random.value, 1f + my, 0f); break;
}
float depthToZ0 = Mathf.Abs(cam.transform.position.z);
Vector3 spawnWorld = cam.ViewportToWorldPoint(new Vector3(spawnVP.x, spawnVP.y, depthToZ0));
Vector3 innerWorld = cam.ViewportToWorldPoint(new Vector3(innerVP.x, innerVP.y, depthToZ0));
spawnWorld.z = 0f;
innerWorld.z = 0f;
Vector3 dir = innerWorld - spawnWorld;
if (dir.sqrMagnitude < 1e-6f)
{
var camCenter = cam.ViewportToWorldPoint(new Vector3(0.5f, 0.5f, depthToZ0));
camCenter.z = 0f;
dir = camCenter - spawnWorld;
}
inwardDir = dir.normalized;
worldPos = spawnWorld;
return true;
}
private void ResolveEdgeSpawn(out Vector3 spawnPos, out Vector3 dirIn)
// =====================================================================
// Ritmo / dificultad (NO velocidad)
// =====================================================================
private void ProgramNextSpawn()
{
Camera cam = GetActiveOrthoCamera();
if (cam == null)
if (_dyn == null) { _nextSpawnAtRealtime = Time.realtimeSinceStartup + 1f; return; }
// base global
float baseIvl = Mathf.Max(0.05f, _dyn.baseInterval);
float lo = Mathf.Min(_dyn.intervalRange.x, _dyn.intervalRange.y);
float hi = Mathf.Max(_dyn.intervalRange.x, _dyn.intervalRange.y);
float randAdd = (hi > lo) ? Random.Range(lo, hi) - baseIvl : 0f;
float ivl = Mathf.Max(0.05f, baseIvl + randAdd);
// ===== NUEVO: curva global de rate + normalización t =====
float t01 = EvalDifficultyT01();
float globalRateMul = SafeEval(_dyn.globalSpawnRate, t01, 1f);
ivl /= Mathf.Max(0.05f, globalRateMul);
// tuning de nivel
if (_cfg != null) ivl /= Mathf.Max(0.01f, _cfg.spawnRateMultiplier);
_nextSpawnAtRealtime = Time.realtimeSinceStartup + ivl;
}
private void TryRampUp()
{
if (_dyn == null) return;
if (_dyn.rampEverySeconds <= 0f) return;
float t = Time.realtimeSinceStartup - _rampStartRealtime;
int steps = Mathf.FloorToInt(t / _dyn.rampEverySeconds);
if (steps <= _rampSteps) return;
int newSteps = steps - _rampSteps;
_rampSteps = steps;
for (int i = 0; i < newSteps; i++)
{
spawnPos = Vector3.zero; dirIn = Vector3.right; return;
_dyn.baseInterval *= Mathf.Clamp(_dyn.intervalMultiplierPerRamp, 0.2f, 1.0f);
_dyn.maxActivePlane = Mathf.Max(0, _dyn.maxActivePlane + _dyn.extraPlanePerRamp);
_dyn.maxActiveHelicopter = Mathf.Max(0, _dyn.maxActiveHelicopter + _dyn.extraHeliPerRamp);
}
float halfH = cam.orthographicSize;
float halfW = halfH * cam.aspect;
Vector3 c = cam.transform.position; c.z = 0f;
float inset = Mathf.Max(0f, edgeInsetWorld);
int side = Random.Range(0, 4);
switch (side)
{
case 0: // izquierda
spawnPos = new Vector3(c.x - halfW + (spawnOutsideCameraEdges ? -inset : inset),
Random.Range(c.y - halfH + inset, c.y + halfH - inset), 0f);
dirIn = Vector3.right; break;
case 1: // derecha
spawnPos = new Vector3(c.x + halfW + (spawnOutsideCameraEdges ? inset : -inset),
Random.Range(c.y - halfH + inset, c.y + halfH - inset), 0f);
dirIn = Vector3.left; break;
case 2: // arriba
spawnPos = new Vector3(Random.Range(c.x - halfW + inset, c.x + halfW - inset),
c.y + halfH + (spawnOutsideCameraEdges ? inset : -inset), 0f);
dirIn = Vector3.down; break;
default: // abajo
spawnPos = new Vector3(Random.Range(c.x - halfW + inset, c.x + halfW - inset),
c.y - halfH + (spawnOutsideCameraEdges ? -inset : inset), 0f);
dirIn = Vector3.up; break;
}
float jitter = Mathf.Clamp(headingJitterDeg, 0f, 45f);
dirIn = (Quaternion.Euler(0, 0, Random.Range(-jitter, jitter)) * dirIn).normalized;
if (verbose)
Debug.Log($"[DynamicSpawnerSystem] Ramp-up x{newSteps}: baseInterval={_dyn.baseInterval:0.00} caps P/H={_dyn.maxActivePlane}/{_dyn.maxActiveHelicopter}");
}
private Camera GetActiveOrthoCamera()
private void BuildTypePools()
{
if (Camera.main != null && Camera.main.orthographic) return Camera.main;
var cams = Camera.allCameras;
for (int i = 0; i < cams.Length; i++)
if (cams[i] && cams[i].orthographic) return cams[i];
return null;
}
_planePrefabs.Clear(); _planeWeights.Clear(); _planePoolRef.Clear();
_heliPrefabs.Clear(); _heliWeights.Clear(); _heliPoolRef.Clear();
private PlaneType PickTypeRespectingCaps()
{
bool planeOk = (poolPlane != null && poolPlane.Length > 0) && activePlane < Mathf.Max(0, capPlane);
bool heliOk = (poolHeli != null && poolHeli.Length > 0) && activeHeli < Mathf.Max(0, capHeli);
if (_pools == null) return;
if (planeOk && !heliOk) return PlaneType.Plane;
if (!planeOk && heliOk) return PlaneType.Helicopter;
if (!planeOk && !heliOk) return PlaneType.Plane;
return (Random.value < 0.7f) ? PlaneType.Plane : PlaneType.Helicopter;
}
private GameObject[] ResolvePoolFromSO(LevelConfig cfg, PlaneType type)
{
if (cfg.dynamicPools == null || cfg.dynamicPools.Count == 0) return null;
foreach (var p in cfg.dynamicPools)
foreach (var p in _pools)
{
if (p == null || p.prefabs == null || p.prefabs.Length == 0) continue;
if (p.type == type) return p.prefabs;
float w = Mathf.Max(0.01f, p.weight);
if (p.type == PlaneType.Helicopter)
{
foreach (var pr in p.prefabs)
{
if (pr)
{
_heliPrefabs.Add(pr);
_heliWeights.Add(w);
_heliPoolRef.Add(p); // guardar referencia al pool
}
}
}
else
{
foreach (var pr in p.prefabs)
{
if (pr)
{
_planePrefabs.Add(pr);
_planeWeights.Add(w);
_planePoolRef.Add(p); // guardar referencia al pool
}
}
}
}
return null;
}
private static GameObject[] FilterPrefabs(GameObject[] src)
private int WeightedPickIndex(List<float> weights)
{
if (src == null || src.Length == 0) return null;
List<GameObject> list = new List<GameObject>(src.Length);
for (int i = 0; i < src.Length; i++)
if (src[i] != null) list.Add(src[i]);
return (list.Count > 0) ? list.ToArray() : null;
if (weights == null || weights.Count == 0) return 0;
float sum = 0f; for (int i = 0; i < weights.Count; i++) sum += Mathf.Max(0.0001f, weights[i]);
float r = Random.value * sum;
float acc = 0f;
for (int i = 0; i < weights.Count; i++)
{
acc += Mathf.Max(0.0001f, weights[i]);
if (r <= acc) return i;
}
return weights.Count - 1;
}
private static bool HasAnyPool(GameObject[] a, GameObject[] b)
private int CountAlive(PlaneType type)
{
return (a != null && a.Length > 0) || (b != null && b.Length > 0);
var all = FindObjectsOfType<PlaneController>();
int count = 0;
for (int i = 0; i < all.Length; i++)
if (all[i].planeType == type && !all[i].isLanding) count++;
return count;
}
// ========= Callbacks desde los tokens =========
public void OnTokenEnteredViewport(DynamicSpawnToken tok) { /* hook opcional */ }
public void OnTokenDestroyed(DynamicSpawnToken tok)
private float SafeEval(AnimationCurve curve, float t, float fallback)
{
if (tok == null) return;
if (tokens.Contains(tok)) tokens.Remove(tok);
if (tok.Type == PlaneType.Plane) activePlane = Mathf.Max(0, activePlane - 1);
if (tok.Type == PlaneType.Helicopter) activeHeli = Mathf.Max(0, activeHeli - 1);
if (curve == null || curve.length == 0) return fallback;
return curve.Evaluate(t);
}
// ========= Diagnóstico =========
public string GetStatusLine()
// t normalizado 0..1 según difficultyHorizonSeconds (o 0 si no aplica)
private float EvalDifficultyT01()
{
float since = (lastSpawnRealtime < -100f) ? 999f : (Time.realtimeSinceStartup - lastSpawnRealtime);
int activeTot = activePlane + activeHeli;
return $"running={running} usingCfg={usingConfig} activeTot={activeTot} plane={activePlane} heli={activeHeli} " +
$"caps P/H={capPlane}/{capHeli} pools P/H={(poolPlane!=null?poolPlane.Length:0)}/{(poolHeli!=null?poolHeli.Length:0)} " +
$"interval≈{currentInterval:0.00}s lastBlocked='{lastBlockedReason}' lastSpawnT={since:0.00}s ago";
float horizon = (_dyn != null && _dyn.difficultyHorizonSeconds > 0f) ? _dyn.difficultyHorizonSeconds : 0f;
if (horizon <= 0f) return 0f;
float elapsed = Time.realtimeSinceStartup - _difficultyStartRealtime;
return Mathf.Clamp01(elapsed / horizon);
}
}
}
@@ -31,7 +31,30 @@ MonoBehaviour:
landingDirectionMode: 0
presetForwardOverride: {x: 1, y: 0}
spawnAnchors: []
spawnerRules: []
spawnerRules:
- spawnerId: 0
enable: 1
overrideType: 0
type: 0
planePrefabsOverride:
- {fileID: 8887022185975264338, guid: 1f94160a177d8411984fe48801bb9541, type: 3}
- {fileID: 2464552394801184783, guid: 760b99edcaf9e44b49903c0806d02239, type: 3}
overrideTiming: 0
randomizeInterval: 0
spawnInterval: 0
intervalRange: {x: 0, y: 0}
maxSpawns: 0
- spawnerId: 1
enable: 1
overrideType: 0
type: 1
planePrefabsOverride:
- {fileID: 8887022185975264338, guid: 1f94160a177d8411984fe48801bb9541, type: 3}
overrideTiming: 0
randomizeInterval: 0
spawnInterval: 0
intervalRange: {x: 0, y: 0}
maxSpawns: 0
typeCaps: []
randomPickWhenExceedCap: 1
waves: []
@@ -48,16 +71,15 @@ MonoBehaviour:
speedMultiplier: 1
spawnRateMultiplier: 1
scoreMultiplier: 1
useSimpleTutorialUI: 0
dialog: {fileID: 0}
isTutorial: 0
useDynamicSpawning: 1
dynamic:
enabled: 1
baseInterval: 3
baseInterval: 2.6999998
intervalRange: {x: 1.5, y: 4}
startDelay: 0.5
maxActivePlane: 6
maxActiveHelicopter: 2
maxActivePlane: 7
maxActiveHelicopter: 3
rampEverySeconds: 35
intervalMultiplierPerRamp: 0.9
extraPlanePerRamp: 1
@@ -67,8 +89,109 @@ MonoBehaviour:
forceBlueFirstPlane: 0
firstPlaneType: 1
firstPlaneTint: {r: 0.2, g: 0.9, b: 1, a: 1}
difficultyHorizonSeconds: 120
globalSpawnRate:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
globalSpeedMultiplier:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 34
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 34
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
dynamicPools:
- type: 0
prefabs:
- {fileID: 8887022185975264338, guid: 1f94160a177d8411984fe48801bb9541, type: 3}
weight: 0
maxAlive: 0
baseSpawnInterval: 0
intervalRange: {x: 0, y: 0}
speedRange: {x: 0, y: 0}
spawnIntervalOverTime:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
speedMultiplierOverTime:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 34
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 34
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 992642e39ca424642a79f24dceb98581
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,148 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d7420bd8a7628a8429e53eb8069649f4, type: 3}
m_Name: LevelConfigSummerIslands
m_EditorClassIdentifier:
levelId: Level_03
displayName: Nivel 2
description: Nivel de puntaje Summer Islands
recommendedDifficulty: 1
tileset: {fileID: 11400000, guid: 0a5243d3b357e714293207eb8064216e, type: 2}
worldOrigin: {x: 0, y: 0}
worldSize: {x: 100, y: 60}
requiredTotalLandings: 1
unlockHintOverride:
useLandingWinCondition: 1
winCount: 50
runways:
- preset: {fileID: 11400000, guid: 01f4af7339a2a13449409af583b33556, type: 2}
position: {x: 9.59, y: -1.45}
rotationZ: 90
runwayId: 1
landingDirectionMode: 0
presetForwardOverride: {x: 1, y: 0}
spawnAnchors: []
- preset: {fileID: 11400000, guid: ccc1dcfd545fbfc4f801e8957e21bbb5, type: 2}
position: {x: 11.82, y: -1.98}
rotationZ: 0
runwayId: 1
landingDirectionMode: 0
presetForwardOverride: {x: 1, y: 0}
spawnAnchors: []
- preset: {fileID: 11400000, guid: fd619444b585428478fde4ac10a9dffb, type: 2}
position: {x: -12.41, y: -1.98}
rotationZ: 0
runwayId: 1
landingDirectionMode: 0
presetForwardOverride: {x: 1, y: 0}
spawnAnchors: []
spawnerRules:
- spawnerId: A
enable: 1
overrideType: 0
type: 0
planePrefabsOverride:
- {fileID: 8887022185975264338, guid: 1f94160a177d8411984fe48801bb9541, type: 3}
overrideTiming: 1
randomizeInterval: 1
spawnInterval: 0
intervalRange: {x: 0, y: 0}
maxSpawns: 28
- spawnerId: B
enable: 1
overrideType: 0
type: 1
planePrefabsOverride:
- {fileID: 8887022185975264338, guid: 1f94160a177d8411984fe48801bb9541, type: 3}
overrideTiming: 1
randomizeInterval: 1
spawnInterval: 2
intervalRange: {x: 0, y: 0}
maxSpawns: 28
- spawnerId: C
enable: 1
overrideType: 0
type: 2
planePrefabsOverride:
- {fileID: 8887022185975264338, guid: 1f94160a177d8411984fe48801bb9541, type: 3}
overrideTiming: 1
randomizeInterval: 1
spawnInterval: 2
intervalRange: {x: 0, y: 0}
maxSpawns: 28
typeCaps: []
randomPickWhenExceedCap: 1
waves:
- waveId: 1
startTime: 3
duration: 500
entries:
- planeType: 0
count: 28
spawnRate: 7
spawnPosition: {x: 0, y: 0}
alignWithRunwayForward: 1
initialDirection: {x: 0, y: 0}
initialSpeed: 0
targetRunwayId: 1
scoreValue: 0
allowCurves: 0
separationNoise: 0
useRunwayAnchor: 0
fromRunwayId: RWY_A
anchorId: A1
angleOffset: 0
- planeType: 1
count: 1000
spawnRate: 7
spawnPosition: {x: 0, y: 0}
alignWithRunwayForward: 1
initialDirection: {x: 0, y: 0}
initialSpeed: 0
targetRunwayId: 1
scoreValue: 0
allowCurves: 0
separationNoise: 0
useRunwayAnchor: 0
fromRunwayId: RWY_A
anchorId: A1
angleOffset: 0
- planeType: 2
count: 1000
spawnRate: 7
spawnPosition: {x: 0, y: 0}
alignWithRunwayForward: 1
initialDirection: {x: 0, y: 0}
initialSpeed: 0
targetRunwayId: 1
scoreValue: 0
allowCurves: 0
separationNoise: 0
useRunwayAnchor: 0
fromRunwayId: RWY_A
anchorId: A1
angleOffset: 0
weather: {fileID: 11400000, guid: 027a9c7e8ea49fe44bed2df133d42d5f, type: 2}
objectives:
- type: 1
targetValue: 1
tolerance: 1
note: 1
timeLimitSeconds: 0
lives: 1
startingScore: 0
scoreTarget: 0
speedMultiplier: 1
spawnRateMultiplier: 1
scoreMultiplier: 1
useSimpleTutorialUI: 0
dialog: {fileID: 0}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5e3f058d9125913449b0a15767a2bf05
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,21 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 957313a006f1cd84a9dae79ecc62f738, type: 3}
m_Name: RunwayPresetDiagonal
m_EditorClassIdentifier:
runwayType: 0
runwayPrefab: {fileID: 4776354397367489766, guid: 2d2a3835ba639e041bd1d4fed715033f, type: 3}
landingDirection: {x: 0, y: 0}
defaultForward: {x: 0, y: 0}
prefabVisualZOffset: 0
visualRootName: Visual
rotateVisualOnly: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ccc1dcfd545fbfc4f801e8957e21bbb5
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,21 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 957313a006f1cd84a9dae79ecc62f738, type: 3}
m_Name: RunwayPresetHelipuerto
m_EditorClassIdentifier:
runwayType: 0
runwayPrefab: {fileID: 6233147436181382244, guid: a648fec6188409c4697f42ecf5f30933, type: 3}
landingDirection: {x: 0, y: 0}
defaultForward: {x: 1, y: 0}
prefabVisualZOffset: 0
visualRootName: Visual
rotateVisualOnly: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fd619444b585428478fde4ac10a9dffb
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,21 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 957313a006f1cd84a9dae79ecc62f738, type: 3}
m_Name: RunwayPresetRecta
m_EditorClassIdentifier:
runwayType: 0
runwayPrefab: {fileID: 451076641906574467, guid: 26042d6d00aba0c49ba58968d1f54176, type: 3}
entryLocal: {x: 1, y: 0}
finishLocal: {x: 0, y: 0}
landingDirection: {x: 0, y: 0}
defaultForward: {x: 1, y: 0}
prefabVisualZOffset: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dfbd1d2b3572c734ebbe3b6e2e94746f
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,17 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d2d4f6b9206e7164fb0041fac5b761ef, type: 3}
m_Name: TilesetPresetSummerIslands
m_EditorClassIdentifier:
mapPrefab: {fileID: 198719004240592418, guid: b90922ad5a8044af7a08e521a532f26e, type: 3}
ambience: {fileID: 8300000, guid: 3e572e74eda88d8479e212343c77f8b2, type: 3}
ambientLight: {r: 1, g: 1, b: 1, a: 1}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0a5243d3b357e714293207eb8064216e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fc18d27254db9c345888a7fc44f3f8bf, type: 3}
m_Name: WeatherProfile
m_EditorClassIdentifier:
weatherType: 0
visibility: 1
windDirection: {x: 1, y: 0}
windSpeed: 0
gustiness: 0
turbulence: 0
lightning: 0
night: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 027a9c7e8ea49fe44bed2df133d42d5f
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -31,12 +31,42 @@ MonoBehaviour:
landingDirectionMode: 0
presetForwardOverride: {x: 1, y: 0}
spawnAnchors: []
spawnerRules: []
spawnerRules:
- spawnerId: 0
enable: 1
overrideType: 0
type: 0
planePrefabsOverride:
- {fileID: 8887022185975264338, guid: 1f94160a177d8411984fe48801bb9541, type: 3}
overrideTiming: 0
randomizeInterval: 0
spawnInterval: 0
intervalRange: {x: 0, y: 0}
maxSpawns: 0
typeCaps:
- type: 0
maxActiveSpawners: 4
randomPickWhenExceedCap: 1
waves: []
waves:
- waveId: 0
startTime: 0
duration: 1
entries:
- planeType: 0
count: 1
spawnRate: 0
spawnPosition: {x: -20.87, y: 0}
alignWithRunwayForward: 0
initialDirection: {x: 0, y: 0}
initialSpeed: 0
targetRunwayId:
scoreValue: 0
allowCurves: 0
separationNoise: 0
useRunwayAnchor: 0
fromRunwayId:
anchorId:
angleOffset: 0
weather: {fileID: 11400000, guid: 8b14bf9148d48f14e96376c9c42ad8e1, type: 2}
objectives:
- type: 1
@@ -54,11 +84,11 @@ MonoBehaviour:
useDynamicSpawning: 1
dynamic:
enabled: 1
baseInterval: 3
baseInterval: 0.68630344
intervalRange: {x: 1.5, y: 4}
startDelay: 0.5
maxActivePlane: 6
maxActiveHelicopter: 2
maxActivePlane: 20
maxActiveHelicopter: 16
rampEverySeconds: 35
intervalMultiplierPerRamp: 0.9
extraPlanePerRamp: 1
@@ -68,8 +98,73 @@ MonoBehaviour:
forceBlueFirstPlane: 1
firstPlaneType: 0
firstPlaneTint: {r: 0.2, g: 0.9, b: 1, a: 1}
difficultyHorizonSeconds: 120
globalSpawnRate:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1.0078583
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
globalSpeedMultiplier:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
dynamicPools:
- type: 0
prefabs:
- {fileID: 8887022185975264338, guid: 1f94160a177d8411984fe48801bb9541, type: 3}
weight: 0.1
maxAlive: 0
baseSpawnInterval: 0
intervalRange: {x: 0, y: 0}
speedRange: {x: 0, y: 0}
spawnIntervalOverTime:
serializedVersion: 2
m_Curve: []
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
speedMultiplierOverTime:
serializedVersion: 2
m_Curve: []
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
+84 -48
View File
@@ -59,9 +59,10 @@ public class LevelConfig : ScriptableObject
public float spawnRateMultiplier = 1f;
public float scoreMultiplier = 1f;
[Header("Tutorial")]
[Tooltip("solo si es necesario para activar tutorial")]
public bool isTutorial;
// ================== TUTORIAL (simple overlay/GO) ==================
[Header("Tutorial")]
[Tooltip("Si está activo, al cargar el nivel se mostrará tu overlay de tutorial (un GameObject de UI) y el juego se pausará.")]
public bool isTutorial = false;
// =====================================================================
// ====================== DYNAMIC SPAWNER (DIFICULTAD) ==================
@@ -77,65 +78,100 @@ public class LevelConfig : ScriptableObject
[Tooltip("Pools de prefabs por tipo (con pesos). Al menos uno por tipo que quieras spawnear.")]
public List<DynamicPool> dynamicPools = new List<DynamicPool>();
[Serializable]
public class DynamicSettings
{
[Header("Estado")]
public bool enabled = true;
[Serializable]
public class DynamicSettings
{
[Header("Estado")]
public bool enabled = true;
[Header("Ritmo base")]
[Tooltip("Intervalo medio de spawn (seg).")]
public float baseInterval = 3.0f;
[Header("Ritmo base")]
[Tooltip("Intervalo medio de spawn (seg).")]
public float baseInterval = 3.0f;
[Tooltip("Rango aleatorio del intervalo (seg).")]
public Vector2 intervalRange = new Vector2(1.5f, 4.0f);
[Tooltip("Rango aleatorio del intervalo (seg).")]
public Vector2 intervalRange = new Vector2(1.5f, 4.0f);
[Tooltip("Delay inicial antes del primer spawn (seg).")]
public float startDelay = 0.5f;
[Tooltip("Delay inicial antes del primer spawn (seg).")]
public float startDelay = 0.5f;
[Header("Capacidad")]
[Tooltip("Máximo de aviones (Plane) activos simultáneamente.")]
public int maxActivePlane = 6;
[Header("Capacidad")]
[Tooltip("Máximo de aviones (Plane) activos simultáneamente.")]
public int maxActivePlane = 6;
[Tooltip("Máximo de helicópteros (Helicopter) activos simultáneamente.")]
public int maxActiveHelicopter = 2;
[Tooltip("Máximo de helicópteros (Helicopter) activos simultáneamente.")]
public int maxActiveHelicopter = 2;
[Header("Ramp-up (dificultad)")]
[Tooltip("Cada cuántos segundos se acelera y aumenta la capacidad.")]
public float rampEverySeconds = 35f;
[Header("Ramp-up (dificultad)")]
[Tooltip("Cada cuántos segundos se acelera y aumenta la capacidad.")]
public float rampEverySeconds = 35f;
[Tooltip("Multiplicador por ramp para reducir el intervalo (0.9 = 10% más rápido).")]
public float intervalMultiplierPerRamp = 0.9f;
[Tooltip("Multiplicador por ramp para reducir el intervalo (0.9 = 10% más rápido).")]
public float intervalMultiplierPerRamp = 0.9f;
[Tooltip("Aumento de capacidad por ramp para Plane.")]
public int extraPlanePerRamp = 1;
[Tooltip("Aumento de capacidad por ramp para Plane.")]
public int extraPlanePerRamp = 1;
[Tooltip("Aumento de capacidad por ramp para Helicopter.")]
public int extraHeliPerRamp = 1;
[Tooltip("Aumento de capacidad por ramp para Helicopter.")]
public int extraHeliPerRamp = 1;
[Header("Borde / Cámara")]
[Tooltip("Spawn fuera de cámara (true) o pegado al borde interno (false).")]
public bool spawnOutsideCameraEdges = true;
[Header("Borde / Cámara")]
[Tooltip("Spawn fuera de cámara (true) o pegado al borde interno (false).")]
public bool spawnOutsideCameraEdges = true;
[Tooltip("Inset en mundo desde el borde de cámara.")]
public float edgeInsetWorld = 0.35f;
[Tooltip("Inset en mundo desde el borde de cámara.")]
public float edgeInsetWorld = 0.35f;
[Header("Primer avión (opcional)")]
[Tooltip("Forzar primer spawn como 'azul' (para onboarding).")]
public bool forceBlueFirstPlane = false;
[Header("Primer avión (opcional)")]
[Tooltip("Forzar primer spawn como 'azul' (para onboarding).")]
public bool forceBlueFirstPlane = false;
public PlaneType firstPlaneType = PlaneType.Plane;
public Color firstPlaneTint = new Color(0.2f, 0.9f, 1f, 1f);
}
public PlaneType firstPlaneType = PlaneType.Plane;
public Color firstPlaneTint = new Color(0.2f, 0.9f, 1f, 1f);
[Serializable]
public class DynamicPool
{
public PlaneType type = PlaneType.Plane;
public GameObject[] prefabs;
[Range(0.01f, 10f)] public float weight = 1f;
}
// ====== NUEVO: horizonte y curvas globales ======
[Header("Curvas Globales de Dificultad")]
[Tooltip("Segundos para normalizar t en curvas 0..1 (si 0, se usa ramp).")]
public float difficultyHorizonSeconds = 120f;
[Tooltip("Multiplicador global de spawn rate (1 = sin cambio).")]
public AnimationCurve globalSpawnRate = AnimationCurve.Linear(0, 1, 1, 1);
[Tooltip("Multiplicador global de velocidad (1 = sin cambio).")]
public AnimationCurve globalSpeedMultiplier = AnimationCurve.Linear(0, 1, 1, 1);
}
[Serializable]
public class DynamicPool
{
public PlaneType type = PlaneType.Plane;
public GameObject[] prefabs;
[Range(0.01f, 10f)] public float weight = 1f;
// ====== NUEVO: límites y tiempos por tipo (opcionales) ======
[Header("Cap por tipo (opcional)")]
[Tooltip("Máximo simultáneo de este tipo. Si <=0, usa el cap global del tipo.")]
public int maxAlive = 0;
[Header("Tiempos por tipo (opcional)")]
[Tooltip("Intervalo medio propio (si <=0, usa baseInterval global).")]
public float baseSpawnInterval = 0f;
[Tooltip("Rango aleatorio propio (si x==y==0, usa intervalRange global).")]
public Vector2 intervalRange = Vector2.zero;
[Header("Velocidad por tipo (opcional)")]
[Tooltip("Rango de velocidad en unidades/seg para este tipo (si x==y==0, no modifica).")]
public Vector2 speedRange = Vector2.zero;
[Header("Curvas por tipo (0..1 / opcional)")]
[Tooltip("Multiplicador sobre el intervalo (1 = sin cambio).")]
public AnimationCurve spawnIntervalOverTime = new AnimationCurve(); // vacío = sin efecto
[Tooltip("Multiplicador de velocidad (1 = sin cambio).")]
public AnimationCurve speedMultiplierOverTime = new AnimationCurve(); // vacío = sin efecto
}
// =====================================================================
// ============================ TIPOS ==================================
// =====================================================================
@@ -243,4 +279,4 @@ public class LevelConfig : ScriptableObject
public int tolerance = 0;
public string note;
}
}
}
+7 -22
View File
@@ -21,7 +21,7 @@ public class LevelLoader : MonoBehaviour
public System.Action<LevelConfig> OnLevelLoaded;
// <- Este flag es el que usan desde GameManager para encender/apagar el overlay simple
[Tooltip("Flag de conveniencia para saber si el nivel cargado es de tutorial.")]
public bool tutorialActivated;
struct AnchorInfo
@@ -46,9 +46,9 @@ public class LevelLoader : MonoBehaviour
{
CleanupScene();
// 🔧 FIX: primero asignamos el level actual y recién después leemos sus flags
// ⚠️ ANTES: se leía currentLevel.isTutorial ANTES de asignar currentLevel → siempre mal
currentLevel = cfg;
tutorialActivated = (currentLevel != null) && currentLevel.isTutorial;
tutorialActivated = (currentLevel != null && currentLevel.isTutorial);
// Tileset
if (cfg.tileset && cfg.tileset.mapPrefab)
@@ -74,21 +74,20 @@ public class LevelLoader : MonoBehaviour
float logicZ = r.rotationZ;
go.transform.rotation = Quaternion.Euler(0, 0, logicZ);
// 2) Offset VISUAL (NO debe afectar Entry/Finish)
// 2) Offset visual (si el prefab lo requiere)
ApplyVisualOffset(go.transform, r.preset);
// Leer Entry/Finish del PREFAB (sólo lectura)
// Leer Entry/Finish del PREFAB
var entry = FindChildDeep(go.transform, "Entry");
var finish = FindChildDeep(go.transform, "Finish");
if (finish != null) runwayFinish[r.runwayId] = finish;
// FORWARD (LandingDirection) — por posición Entry→Finish
// FORWARD (por marcadores o por rotación)
Vector2 forward;
if (r.landingDirectionMode == LandingDirectionMode.FromPrefabMarkers && entry != null && finish != null)
{
forward = (finish.position - entry.position).normalized;
if (forward.sqrMagnitude < 0.0001f)
{
Debug.LogWarning($"[{r.runwayId}] Entry y Finish superpuestos. Usando forward por rotación del root.");
@@ -109,7 +108,7 @@ public class LevelLoader : MonoBehaviour
}
runwayForward[r.runwayId] = forward.sqrMagnitude < 0.001f ? Vector2.right : forward.normalized;
// ANCLAS (spawners)
// ANCLAS (spawners) → locales a la pista → convertir a mundo según root (NO offset visual)
foreach (var a in r.spawnAnchors)
{
var worldPos = go.transform.TransformPoint(a.localPosition);
@@ -122,18 +121,6 @@ public class LevelLoader : MonoBehaviour
runwayForward = runwayForward[r.runwayId]
};
}
#if UNITY_EDITOR
if (entry && finish)
{
Debug.DrawLine(entry.position, finish.position, Color.green, 3f);
Debug.Log($"[{r.runwayId}] Entry:{entry.position} Finish:{finish.position} Forward:{runwayForward[r.runwayId]}");
}
else
{
Debug.LogWarning($"[{r.runwayId}] NO se encontraron Entry/Finish en el prefab.");
}
#endif
}
// Ambient
@@ -188,13 +175,11 @@ public class LevelLoader : MonoBehaviour
if (visual != null)
{
// Rotación local del hijo visual: NO afecta Entry/Finish
visual.localRotation = Quaternion.Euler(0, 0, preset.prefabVisualZOffset) * visual.localRotation;
return;
}
}
// Fallback: si no hay hijo visual, rotamos el root (esto sí afectaría Entry/Finish)
runwayRoot.rotation = Quaternion.Euler(0, 0, runwayRoot.eulerAngles.z + preset.prefabVisualZOffset);
}
+3
View File
@@ -341,6 +341,9 @@ MonoBehaviour:
isLanding: 0
noCol: 0
scoreValue: 1
edgePaddingWorld: 0.5
bounceStrength: 1.5
viewportMargin: 0.02
oscillationAmplitude: 0.5
oscillationFrequency: 1
autoResolveCameraBounds: 1
+54 -48
View File
@@ -1036,7 +1036,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 1}
m_AnchorMax: {x: 0.5, y: 1}
m_AnchoredPosition: {x: -162, y: -60}
m_AnchoredPosition: {x: -198, y: -50}
m_SizeDelta: {x: 200, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &280612377
@@ -3353,6 +3353,55 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 822435449}
m_CullTransparentMesh: 1
--- !u!1 &862270259
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 862270261}
- component: {fileID: 862270260}
m_Layer: 0
m_Name: spawn
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &862270260
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 862270259}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 62cc583ebfacb4e68bd5c8012df8f638, type: 3}
m_Name:
m_EditorClassIdentifier:
running: 0
usingCfg: 0
verbose: 1
statusLogEvery: 10
spawnMarginPixels: 500
--- !u!4 &862270261
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 862270259}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 726.364, y: 403.8052, z: -10.0989}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &931356569
GameObject:
m_ObjectHideFlags: 0
@@ -4637,7 +4686,6 @@ GameObject:
- component: {fileID: 1399668022}
- component: {fileID: 1399668021}
- component: {fileID: 1399668020}
- component: {fileID: 1399668027}
m_Layer: 0
m_Name: MainManager
m_TagString: Untagged
@@ -4756,36 +4804,6 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1399668027
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1399668019}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 62cc583ebfacb4e68bd5c8012df8f638, type: 3}
m_Name:
m_EditorClassIdentifier:
verbose: 1
heartbeatLogs: 1
heartbeatEvery: 2
autoStartIfNoConfig: 1
spawnOutsideCameraEdges: 1
edgeInsetWorld: 0.8
headingJitterDeg: 8
planePrefabsFallback:
- {fileID: 8887022185975264338, guid: 1f94160a177d8411984fe48801bb9541, type: 3}
heliPrefabsFallback: []
baseIntervalFallback: 2
intervalRangeFallback: {x: 1, y: 3}
startDelayFallback: 0.2
maxActivePlaneFallback: 4
maxActiveHelicopterFallback: 1
forceBlueFirstPlane: 0
firstPlaneType: 1
firstPlaneTint: {r: 0.25, g: 0.8, b: 1, a: 1}
--- !u!1 &1520870961
GameObject:
m_ObjectHideFlags: 0
@@ -4795,7 +4813,6 @@ GameObject:
serializedVersion: 6
m_Component:
- component: {fileID: 1520870963}
- component: {fileID: 1520870968}
- component: {fileID: 1520870971}
- component: {fileID: 1520870970}
- component: {fileID: 1520870969}
@@ -4821,18 +4838,6 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1520870968
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1520870961}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 133f96389d108b64bb7bc8fcd871a5aa, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1520870969
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -5369,7 +5374,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 0
m_UiScaleMode: 1
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 800, y: 600}
@@ -6064,7 +6069,7 @@ MonoBehaviour:
repeatLog: 1
repeatEverySeconds: 5
createDynamicIfNoneFound: 1
dynamicSpawnerPrefab: {fileID: 1399668027}
dynamicSpawnerPrefab: {fileID: 0}
--- !u!1 &2102060756
GameObject:
m_ObjectHideFlags: 0
@@ -6237,7 +6242,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &2109539263
RectTransform:
m_ObjectHideFlags: 0
@@ -6522,3 +6527,4 @@ SceneRoots:
- {fileID: 401981673}
- {fileID: 1334721174}
- {fileID: 2058553546}
- {fileID: 862270261}
File diff suppressed because it is too large Load Diff
+10 -13
View File
@@ -63,6 +63,7 @@ public class GameManager : MonoBehaviour
private DynamicSpawnerSystem dynSpawner;
[Header("Tutorial GO")]
[Tooltip("Asigna aquí tu overlay/canvas de tutorial. Se activará si LevelConfig.isTutorial = true.")]
public GameObject tutorialUI;
// ---------------------------------------------------------------------
@@ -298,12 +299,7 @@ public class GameManager : MonoBehaviour
{
var c = t.GetComponent<Canvas>();
if (c != null) { t.gameObject.SetActive(false); return; }
// Case-insensitive sin usar .ToLower() ni .contains
if (t.name.IndexOf("gameover", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
t.gameObject.SetActive(false);
return;
}
if (t.name.ToLower().Contains("gameover")) { t.gameObject.SetActive(false); return; }
t = t.parent;
}
}
@@ -425,11 +421,12 @@ public class GameManager : MonoBehaviour
var loader = FindObjectOfType<LevelLoader>(true);
if (loader) loader.Load(cfg);
// 🔧 Aquí se decide si mostrar el overlay/tutorial simple
bool tutorialFlag = (loader != null) ? loader.tutorialActivated : cfg.isTutorial;
if (tutorialUI != null) tutorialUI.SetActive(tutorialFlag);
// ---------- TUTORIAL ----------
// Mostrar/ocultar tu overlay según LO QUE DICE EL SO (cfg.isTutorial)
if (tutorialUI != null)
tutorialUI.SetActive(cfg.isTutorial);
// Dynamic Spawner o spawners clásicos
// ---------- Dynamic Spawner o spawners clásicos ----------
StartCoroutine(CoChooseSpawnerNextFrame(cfg));
}
@@ -443,7 +440,7 @@ public class GameManager : MonoBehaviour
{
dynSpawner.gameObject.SetActive(true);
// SAFE STOP: evita dependencia de nombres de método
// SAFE STOP por si venís de otra escena/estado
SafeStopDynamicSpawner(dynSpawner);
// Iniciar con el LevelConfig actual
@@ -452,13 +449,13 @@ public class GameManager : MonoBehaviour
}
else
{
// Fallback: aplica reglas a PlaneSpawners en escena
// Fallback: aplica reglas a spawners clásicos
ApplySpawnerRules(cfg);
Debug.Log("[GameManager] PlaneSpawners clásicos activos (Dynamic desactivado o no presente).");
}
}
// Evita errores si tu spawner no tiene el mismo método
// Evita errores si el spawner no define los mismos métodos entre ramas
private void SafeStopDynamicSpawner(DynamicSpawnerSystem sp)
{
if (!sp) return;
+83 -23
View File
@@ -21,9 +21,18 @@ public class PlaneController : MonoBehaviour
[Header("Contención en cámara")]
public float edgePaddingWorld = 0.5f;
public float bounceStrength = 1.5f;
public float bounceStrength = 1.5f; // (se conserva para compatibilidad, ya no se usa en el reflect)
[Range(0f, 0.2f)] public float viewportMargin = 0.02f;
[Header("Bounces")]
[Tooltip("Segundos sin oscilación después de rebotar.")]
public float bounceCooldown = 0.2f;
[Tooltip("Empujón mínimo hacia adentro tras el rebote (unidades mundo).")]
public float edgeNudge = 0.02f;
[Tooltip("Apagar oscilación mientras dure el bounceCooldown.")]
public bool disableOscillationDuringBounce = true;
private float bounceTimer = 0f;
[Header("Oscilación de crucero")]
public float oscillationAmplitude = 0.5f;
public float oscillationFrequency = 1f;
@@ -39,6 +48,10 @@ public class PlaneController : MonoBehaviour
public Vector2 fallbackBoundsMax = new Vector2( 50f, 30f);
public float minBoundsSize = 0.1f;
// Activa los límites sólo cuando el avión está completamente en pantalla
private bool hasEnteredCamera = false;
public bool enableBoundsAfterEntry = true;
public GameObject proximityCircle;
public float proximityRadius = 2f;
@@ -120,6 +133,9 @@ public class PlaneController : MonoBehaviour
ResolveCameraBounds(force:false);
}
// Cooldown de rebote (para desactivar oscilación momentáneamente)
if (bounceTimer > 0f) bounceTimer -= Time.deltaTime;
tiempo += Time.deltaTime;
if (allowEmergency && !inEmergency && !isLanding && tiempo >= autoEmergencyAfter)
@@ -148,10 +164,38 @@ public class PlaneController : MonoBehaviour
if (path != null && pathIndex < path.Length) FollowPath();
else ContinueMovement();
KeepWithinBounds();
// Aplicar límites sólo cuando esté completamente dentro del visor
if (enableBoundsAfterEntry)
{
if (!hasEnteredCamera)
{
if (IsFullyInsideCamera())
hasEnteredCamera = true;
}
if (hasEnteredCamera)
KeepWithinBounds();
}
else
{
KeepWithinBounds();
}
CheckProximity();
}
private bool IsFullyInsideCamera()
{
var cam = Camera.main;
if (cam == null) return true;
var vp = cam.WorldToViewportPoint(transform.position);
if (vp.z < 0) return false;
float m = viewportMargin;
return (vp.x >= 0f + m && vp.x <= 1f - m && vp.y >= 0f + m && vp.y <= 1f - m);
}
void KeepWithinBounds()
{
Camera cam = Camera.main;
@@ -170,40 +214,46 @@ public class PlaneController : MonoBehaviour
if (minY > maxY) { float m = (minY + maxY) * 0.5f; minY = maxY = m; }
Vector3 pos = transform.position;
bool bouncedX = false;
bool bouncedY = false;
bool bounced = false;
// Rebotar en X
if (pos.x < minX)
{
pos.x = minX;
currentDirection.x = Mathf.Abs(currentDirection.x) * bounceStrength;
bouncedX = true;
pos.x = minX + edgeNudge; // nudge hacia adentro
currentDirection = Vector2.Reflect(currentDirection, Vector2.right).normalized; // normal +X
bounced = true;
}
else if (pos.x > maxX)
{
pos.x = maxX;
currentDirection.x = -Mathf.Abs(currentDirection.x) * bounceStrength;
bouncedX = true;
pos.x = maxX - edgeNudge;
currentDirection = Vector2.Reflect(currentDirection, Vector2.left).normalized; // normal -X
bounced = true;
}
// Rebotar en Y
if (pos.y < minY)
{
pos.y = minY;
currentDirection.y = Mathf.Abs(currentDirection.y) * bounceStrength;
bouncedY = true;
pos.y = minY + edgeNudge;
currentDirection = Vector2.Reflect(currentDirection, Vector2.up).normalized; // normal +Y
bounced = true;
}
else if (pos.y > maxY)
{
pos.y = maxY;
currentDirection.y = -Mathf.Abs(currentDirection.y) * bounceStrength;
bouncedY = true;
pos.y = maxY - edgeNudge;
currentDirection = Vector2.Reflect(currentDirection, Vector2.down).normalized; // normal -Y
bounced = true;
}
if (bouncedX || bouncedY)
if (bounced)
{
if (currentDirection.sqrMagnitude < 1e-4f) currentDirection = Vector3.right;
currentDirection = currentDirection.normalized;
// Reset de oscilación y cooldown para despegarse del borde
oscillationTime = 0f;
bounceTimer = bounceCooldown;
// Alinear rotación inmediata con la nueva dirección
float angle = Mathf.Atan2(currentDirection.y, currentDirection.x) * Mathf.Rad2Deg - 90f;
transform.rotation = Quaternion.Euler(0, 0, angle);
}
@@ -215,10 +265,20 @@ public class PlaneController : MonoBehaviour
{
Vector3 direction = currentDirection.sqrMagnitude < 1e-6f ? Vector3.right : currentDirection.normalized;
Vector3 perpendicular = Vector3.Cross(direction, Vector3.forward).normalized;
oscillationTime += Time.deltaTime * oscillationFrequency;
float oscillationOffset = Mathf.Sin(oscillationTime) * oscillationAmplitude;
Vector3 finalDirection = (direction + perpendicular * oscillationOffset).normalized;
Vector3 finalDirection;
// Durante el cooldown de rebote, evitamos oscilación para que se despegue del borde
if (disableOscillationDuringBounce && bounceTimer > 0f)
{
finalDirection = direction; // sin componente perpendicular
}
else
{
Vector3 perpendicular = Vector3.Cross(direction, Vector3.forward).normalized;
oscillationTime += Time.deltaTime * oscillationFrequency;
float oscillationOffset = Mathf.Sin(oscillationTime) * oscillationAmplitude;
finalDirection = (direction + perpendicular * oscillationOffset).normalized;
}
float angle = Mathf.Atan2(finalDirection.y, finalDirection.x) * Mathf.Rad2Deg - 90f;
Quaternion targetRotation = Quaternion.Euler(0, 0, angle);
@@ -326,7 +386,7 @@ public class PlaneController : MonoBehaviour
if (GameManager.Instance != null) GameManager.Instance.scoreGral += scoreValue;
GameManager.Instance?.NotifyPlaneLanded(planeType, targetRunway != null ? targetRunway.name : "");
// (tutorial eliminado) — antes: TutorialEvents.RaiseLanding(...)
// (tutorial eliminado)
Destroy(gameObject);
}
@@ -462,4 +522,4 @@ public class PlaneController : MonoBehaviour
{
return (maxBounds.x - minBounds.x) > minBoundsSize && (maxBounds.y - minBounds.y) > minBoundsSize;
}
}
}