Danil Iarosh 78d7679e2b
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(prizes, ui): complete Mar 24-Apr 2 sprint ahead of schedule
- завершен спринт 24 марта - 2 апреля с опережением плана и закрытием задач раньше срока
- расширена и стабилизирована система призов:
  добавлены и доработаны QR-tier конфиги, новые типы и модели данных,
  сохранение `qrCodes` в `prizes.json`, fallback-логика и синхронизация
  через `PrizeSystemManager`, `PrizeDatabase` и `LevelPrizeConfig`
- переработан flow настройки QR-призов:
  добавлены `QRCodeSettingsModal` и связанный UI-код,
  разнесены короткий текст приза и полный текст сообщения,
  добавлены шаблоны форматирования, корректная подстановка текста
  в QR и в UI-модалках, а также сохранение конфигурации между запусками
- доработан flow суперприза и текстовых настроек:
  добавлены fallback-правила показа, корректная обработка пустых строк
  и строк из одних пробелов, обновлены binders и показ текста в меню
- реализована и обновлена отдельная настройка витрины:
  добавлена `PrizeStandSettingsModal`, обновлена навигация строк,
  логика выбора ячейки и порогов очков, циклический выбор ячеек,
  ускорение изменения значений и исправления для keyboard/gamepad UX
- доработана игровая модалка витрины `PrizeStandModal`:
  добавлен вывод очков игрока, форматируемый текст счета,
  улучшено отображение ячеек и связанная логика выбора приза
- исправлен подсчет очков и интеграция с prize flow:
  `ScoreSystem` переведен на событийное обновление текущего счета,
  добавлены `CurrentScore`, `CurrentScoreChanged` и корректный display score,
  витрина и связанные UI-элементы теперь получают актуальные очки,
  а не нулевые или несвоевременные preview-значения
- переработаны модальные окна и навигация UI:
  улучшены `Modal`, `ModalManager`, `UIRowPointer`, `RulesModal`,
  `QRCodeModal`, `PrizeStandModal`, `LevelPrizeSettingsModal` и др.,
  чтобы фокус, указатель и submit работали консистентно без мыши
- обновлен win/prize UI:
  доработаны `WinBannerController`, `GameEndUIController`,
  `MainMenu`, `QRCodeUIController`, `TopPanelTextController` и настройки UI,
  улучшены тексты баннеров, отображение призов и итогового счета
- обновлены сцены и ассеты под новый flow:
  `MainMenu`, `Level UI`, prefab'ы строк настроек и кнопок,
  глобальные префабы, шрифты TMP, иконки, recovery-данные и ресурсы UI
- добавлены вспомогательные UI-компоненты и служебные скрипты:
  `ButtonTextFocusColor`, `SelectableDescriptionTarget`, `QRTier`
  и другие изменения, поддерживающие новый конфигурируемый prize UI
2026-04-01 12:47:17 +03:00

185 lines
6.1 KiB
C#

using UnityEngine;
using RR.Common.Interfaces;
using RR.Common.Utility;
using RR.Game2D.Core.Managers;
using RR.Game2D.Behaviour.ScriptableObjects;
namespace RR.Game2D.Behaviour
{
[RequireComponent(typeof(Rigidbody2D),
typeof(Collider2D),
typeof(Animator))]
public class EnemyController : MonoBehaviour, IDamageable
{
[SerializeField] public EnemySFXAsset sfxAsset;
[SerializeField] private EnemyControllerConfiguration config;
private Health health;
[ReadOnlyInInspector] public float CurrentHealth => health?.CurrentHealth ?? 0;
[HideInInspector] public float MaxHealth => health?.MaxHealth ?? 0;
private Animator _animator;
private Rigidbody2D _rb;
private Collider2D _col;
private bool _isDead;
private GameObject _playerObj;
// Аниматор
private static readonly int Speed = Animator.StringToHash("Speed");
private void Start()
{
_rb = GetComponent<Rigidbody2D>();
_col = GetComponent<Collider2D>();
_animator = GetComponent<Animator>();
_playerObj = GameManager.Instance.PlayerObj;
if (_playerObj == null)
{
_playerObj = GameObject.FindGameObjectWithTag("Player");
}
health = GetComponent<Health>();
if (health == null)
{
health = gameObject.AddComponent<Health>();
}
health.ResetHealth();
health.SetOwner(this);
health.SetMaxHealth(config.maxHealth);
}
private void FixedUpdate()
{
if (!_playerObj) return;
_playerObj.TryGetComponent(out PlayerController playerController);
Vector3 playerCenter = _playerObj.transform.position;
if (PlayerInSight() && !playerController.isDeadAtMoment)
{
Vector3 toPlayer = playerCenter - transform.position;
float dist = toPlayer.magnitude + 0.1f;
if (dist > 0)
{
_rb.linearVelocity = new Vector2(config.moveSpeed * toPlayer.normalized.x, 0);
}
// if (dist > 0)
// rb.linearVelocity = toPlayer / dist * moveSpeed;
// else
// rb.linearVelocity = Vector2.zero;
}
else
{
_rb.linearVelocity = Vector2.MoveTowards(
_rb.linearVelocity, Vector2.zero, config.acceleration * Time.fixedDeltaTime);
}
_animator.SetFloat(Speed, Mathf.Abs(_rb.linearVelocity.magnitude));
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (!collision.collider.CompareTag("Player")) return;
Rigidbody2D playerRb = collision.collider.GetComponent<Rigidbody2D>();
_playerObj.TryGetComponent(out PlayerController playerController);
if (playerRb == null) return;
Vector2 contactNormal = collision.GetContact(0).normal;
if (playerController.isDeadAtMoment) return;
if (contactNormal.y < -0.5f)
{
playerRb.linearVelocity = new Vector2(playerRb.linearVelocity.x, 35f);
TakeDamage(MaxHealth);
// TakeDamage(1);
}
else
{
playerController.TakeDamage(1f);
}
}
private bool PlayerInSight()
{
if (!_playerObj) return false;
Vector3 playerPos = _playerObj.transform.position;
Collider2D playerCol = _playerObj.GetComponent<Collider2D>();
playerPos.y = playerCol.bounds.center.y;
Vector2 eyePos = new(_rb.position.x, _col.bounds.center.y);
Vector2 toPlayer = (Vector2)playerPos - eyePos;
float dist = toPlayer.magnitude;
if (dist > config.viewDistance) return false;
Vector2 forward = transform.localEulerAngles.y >= 180f ? Vector2.right : Vector2.left;
float cosHalf = Mathf.Cos(0.5f * config.viewAngle * Mathf.Deg2Rad);
float dot = Vector2.Dot(forward.normalized, toPlayer / Mathf.Max(dist, 1e-6f));
if (dot < cosHalf) return false;
int mask = config.obstacleMask;
mask &= ~(1 << gameObject.layer);
mask &= ~(1 << _playerObj.layer);
Vector2 origin = eyePos + forward.normalized * 0.05f;
RaycastHit2D hit = Physics2D.Raycast(origin, toPlayer.normalized, dist, mask);
return !hit.collider;
}
public void TakeDamage(float amount)
{
health.TakeDamage(amount);
}
public void Heal(float amount)
{
health.Heal(amount);
}
public void Die()
{
if (_isDead) return;
_isDead = true;
_animator.Play("Death");
// Destroy(gameObject);
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
if (!_playerObj) return;
Vector2 origin = new(_rb.position.x, _col.bounds.center.y);
Vector2 forward = transform.localEulerAngles.y >= 180f ? Vector2.right : Vector2.left;
float half = config.viewAngle * 0.5f;
Vector2 edgeA = (Quaternion.Euler(0, 0, +half) * forward);
Vector2 edgeB = (Quaternion.Euler(0, 0, -half) * forward);
Gizmos.color = Color.yellow;
Gizmos.DrawLine(origin, origin + edgeA * config.viewDistance);
Gizmos.DrawLine(origin, origin + edgeB * config.viewDistance);
Gizmos.DrawWireSphere(origin, config.viewDistance);
Vector2 toPlayer = (Vector2)_playerObj.transform.position - origin;
if (!(toPlayer.magnitude <= config.viewDistance)) return;
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position, _playerObj.transform.position);
}
#endif
}
}