Danro/Assets/Scripts/Entities/Props/EnvironmentalCollectibleBase.cs
Danil Iarosh cb2e83a596
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(ui, prizes, spawners, editor): expand prize flow and add spawning/editor tooling
- доработан flow призовой витрины и связанного UI:
  добавлены `PrizeStandButtonView` и `RulesPrizeListPage`, обновлены
  `PrizeStandController`, `PrizeStandPrize`, `PrizeStandModal`,
  `PrizeStandSettingsModal`, `LevelPrizeSettingsModal`, `QRCodeSettingsModal`,
  `BalanceDisplay`, `ButtonTextFocusColor` и связанные prefab'ы,
  чтобы улучшить настройку, навигацию, отображение кнопок и prize flow
- расширена система настроек и сохранения значений:
  `OptionPicker` получил отдельный формат `ValueSelector`,
  обновлены `SettingsDatabase`, `SettingEntryDrawer`, `SettingUIItem`,
  `SettingsUIGenerator`, `ValueSelectorRow` и `MainSettingsDatabase.asset`,
  а сохранение option picker-значений, включая `gameplay.gameDifficulty`,
  исправлено и синхронизировано с остальными настройками
- добавлены универсальные спавнеры сущностей:
  реализованы `EntitySpawner` и `EntitySpawnerManager` с поддержкой
  весов префабов, точек спавна, заполнения по проценту, ограничений
  на количество и автоспавна при инициализации
- доработана логика игрового спавна и сложности:
  процент инициализационного спавна может зависеть от настройки
  `gameplay.gameDifficulty` с градациями 25/50/75/100,
  в одном цикле массового спавна повторно не используется та же точка,
  а gizmo спавнера теперь показывает только сами точки без линий
- упрощена работа в редакторе Unity и добавлены editor-инструменты:
  в `GameManager` лицензия пропускается при запуске в `UNITY_EDITOR`,
  а `HierarchySelectionCounter`, `HierarchySelectionCountOverlay`
  и `HierarchyBulkRenamerWindow` добавляют подсчет выделения,
  realtime overlay в `SceneView` и массовое переименование объектов
  по правилам нумерации из Unity Project Settings
- синхронизированы сцены, ресурсы и UI-ассеты под новый flow:
  обновлены `MainMenu`, `Level UI`, уровни `1-1` и `1-2`,
  строки настроек, кнопки, TMP/font assets, изображения UI,
  `prizes.json` и recovery-данные после правок интерфейса и логики
- очищены и переразложены часть внешних и служебных ресурсов:
  удалён пакет `Lava_Flowing_Shader`, переименованы/заменены
  отдельные UI-prefab'ы и добавлены сопутствующие проектные артефакты,
  которые сейчас используются обновлённой конфигурацией проекта
2026-04-28 14:05:05 +03:00

157 lines
4.5 KiB
C#

using System;
using RR.Game2D.Behaviour;
using UnityEngine;
namespace RR.Common.Entities.Props
{
[RequireComponent(typeof(Collider2D))]
public abstract class EnvironmentalCollectibleBase : MonoBehaviour
{
[SerializeField] private int points = 1;
[SerializeField] private string playerTag = "Player";
[Tooltip("Время жизни в секундах. 0 или меньше — бесконечно.")]
[SerializeField] private float defaultLifetime;
private bool _collected;
private int _defaultPoints;
private float _defaultLifetimeBackup;
private float _lifetime;
private float _age;
private Collider2D _trigger;
private Vector3 _spawnPosition;
public event Action<EnvironmentalCollectibleBase> Collected;
public int Points => points;
public Vector3 SpawnPosition => _spawnPosition;
public virtual bool CountsTowardsSharedCollectionLimit => false;
protected virtual void Awake()
{
_defaultPoints = Mathf.Max(0, points);
_defaultLifetimeBackup = defaultLifetime;
_trigger = GetComponent<Collider2D>();
ValidateTrigger();
}
protected virtual void Update()
{
float dt = Time.deltaTime;
if (_lifetime > 0f && !_collected)
{
_age += dt;
if (_age >= _lifetime)
{
Despawn();
return;
}
}
TickBehaviour(dt);
}
protected virtual void OnValidate()
{
if (points < 0) points = 0;
_defaultPoints = points;
_defaultLifetimeBackup = defaultLifetime;
ValidateTrigger();
}
private void OnTriggerEnter2D(Collider2D other)
{
// Unity 2D trigger callbacks usually require at least one of the colliding objects to have a Rigidbody2D.
if (_collected || other == null) return;
if (!other.CompareTag(playerTag)) return;
Collect(other.transform);
}
public void InitializeForSpawn(
Vector3 spawnPosition,
int? pointsOverride,
float? lifetimeOverride = null)
{
_spawnPosition = spawnPosition;
points = pointsOverride.HasValue ? Mathf.Max(0, pointsOverride.Value) : _defaultPoints;
_lifetime = lifetimeOverride.HasValue ? lifetimeOverride.Value : _defaultLifetimeBackup;
_age = 0f;
ResetStateForReuse();
}
public void ResetStateForReuse()
{
_collected = false;
_age = 0f;
_spawnPosition = transform.position;
OnSpawned();
}
protected void Collect(Transform collector)
{
if (_collected) return;
_collected = true;
OnCollected();
if (CountsTowardsSharedCollectionLimit)
{
ScoreSystem.Instance?.RegisterSharedEnvironmentalCollectibleCollected(points);
}
else if (points > 0)
{
ScoreSystem.Instance?.RegisterEnvironmentalPoints(points);
}
Collected?.Invoke(this);
Destroy(gameObject);
}
/// <summary>
/// Деспаун по истечении времени жизни — без начисления очков.
/// </summary>
private void Despawn()
{
if (_collected) return;
_collected = true;
OnDespawned();
Collected?.Invoke(this);
Destroy(gameObject);
}
protected virtual void OnSpawned()
{
}
protected virtual void OnCollected()
{
}
protected virtual void OnDespawned()
{
}
protected abstract void TickBehaviour(float dt);
private void ValidateTrigger()
{
if (_trigger == null)
_trigger = GetComponent<Collider2D>();
if (_trigger == null)
{
Debug.LogError($"[{nameof(EnvironmentalCollectibleBase)}] Missing Collider2D on {name}.", this);
return;
}
if (!_trigger.isTrigger)
Debug.LogError($"[{nameof(EnvironmentalCollectibleBase)}] Collider2D on {name} must have isTrigger enabled.", this);
}
}
}