Добавить управление световыми спецэффектами и скорректировать позиции ф初

This commit is contained in:
Сергей Бондарь 2026-09-16 11:00:48 +03:00
parent 70c4dbd081
commit 8b22a1e69a
12 changed files with 598 additions and 21 deletions

View File

@ -31,7 +31,7 @@ Transform:
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 15.84, y: -5.86, z: 0}
m_LocalScale: {x: 9, y: 9, z: 1}
m_LocalScale: {x: 7, y: 7, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7212224704880402101}

View File

@ -230,6 +230,9 @@ namespace CharacterSystem
Character newCharacter = GetLoadedCharacter(newIndex);
Sequence sequence = DOTween.Sequence();
if (newIndex <= 4)
LightSystem.SpecialEffect(43 + newIndex, 2);
if (_loadedCharacters.TryGetValue(oldIndex, out LoadedCharacter oldCharacter))
{
sequence.Append(oldCharacter.Character.visualModule.GetHideSequence());

View File

@ -71,10 +71,17 @@ public class GameManager : MonoBehaviour
private const int PvpWinningScore = 5;
private const int VictoryNameLength = 10;
private const int FirstDebugSpecialEffect = 40;
private const int LastDebugSpecialEffect = 49;
private const int PveStartSpecialEffect = 48;
private const int PvpStartSpecialEffect = 49;
private const int SpecialEffectDurationSeconds = 2;
private const float SpecialEffectDebugIntervalSeconds = 6f;
private bool _isLeavingGameplay;
private int _paidEntryAmount;
private bool _canRefundEntry;
private bool _returnToMenuAfterPrize;
private Coroutine _specialEffectsDebugCoroutine;
public void Initialize()
{
@ -107,6 +114,10 @@ public class GameManager : MonoBehaviour
_canRefundEntry = true;
SetMode(mode);
_isLeavingGameplay = false;
LightSystem.EnableButtonLighting();
LightSystem.SpecialEffect(
mode == GameMode.pve ? PveStartSpecialEffect : PvpStartSpecialEffect,
SpecialEffectDurationSeconds);
if (mode == GameMode.pvp)
{
@ -153,12 +164,21 @@ public class GameManager : MonoBehaviour
private void Update()
{
if (Input.GetKeyDown(KeyCode.O))
ToggleSpecialEffectsDebug();
if (increaseVsBotTimer)
vsBotThinkTime += Time.deltaTime;
}
private void OnDestroy()
{
if (_specialEffectsDebugCoroutine != null)
{
StopCoroutine(_specialEffectsDebugCoroutine);
_specialEffectsDebugCoroutine = null;
}
if (timerManager != null && turnManager != null)
timerManager.TimerEnded -= turnManager.HandleTimerEnded;
@ -175,6 +195,7 @@ public class GameManager : MonoBehaviour
RefundEntryIfAvailable();
_isLeavingGameplay = true;
LightSystem.DisableButtonLighting();
CancelPrizeFlow();
VitrinaView.Instance?.Deactivate();
ControlsPerformer.Instance.SetControlable(null);
@ -409,11 +430,42 @@ public class GameManager : MonoBehaviour
private void StartRound()
{
LightSystem.SpecialEffect(41, SpecialEffectDurationSeconds);
turnManager.StartGame(gameMode);
PrepareRound(Team.Player1);
OnRoundStarted?.Invoke();
}
private void ToggleSpecialEffectsDebug()
{
if (_specialEffectsDebugCoroutine == null)
{
_specialEffectsDebugCoroutine = StartCoroutine(SpecialEffectsDebugCoroutine());
Debug.Log("Отладочный цикл спецэффектов включён.");
return;
}
StopCoroutine(_specialEffectsDebugCoroutine);
_specialEffectsDebugCoroutine = null;
Debug.Log("Отладочный цикл спецэффектов выключен.");
}
private IEnumerator SpecialEffectsDebugCoroutine()
{
WaitForSecondsRealtime interval = new(SpecialEffectDebugIntervalSeconds);
while (true)
{
for (int effectNumber = FirstDebugSpecialEffect;
effectNumber <= LastDebugSpecialEffect;
effectNumber++)
{
LightSystem.SpecialEffect(effectNumber, SpecialEffectDurationSeconds);
yield return interval;
}
}
}
private static List<Prize> GetAvailablePrizes(IEnumerable<Prize> prizes)
{
List<Prize> result = new();

View File

@ -5,6 +5,39 @@ using UnityEngine;
public static class LightSystem
{
private const string LIGHT_COMMAND_PREFIX = "80";
private const string ButtonLightingOnCommand = "2171";
private const string ButtonLightingOffCommand = "2170";
private const string SpecialEffectCommandPrefix = "8560";
private const int MinimumSpecialEffect = 40;
private const int MaximumSpecialEffect = 49;
public static void EnableButtonLighting()
{
ComWrapper.Send(ButtonLightingOnCommand);
}
public static void DisableButtonLighting()
{
ComWrapper.Send(ButtonLightingOffCommand);
}
public static void SpecialEffect(int effectNumber, int durationSeconds = 2)
{
if (effectNumber < MinimumSpecialEffect || effectNumber > MaximumSpecialEffect)
{
Debug.LogError($"Недопустимый номер спецэффекта: {effectNumber}.");
return;
}
if (durationSeconds < 0 || durationSeconds > 9)
{
Debug.LogError($"Недопустимая длительность спецэффекта: {durationSeconds}.");
return;
}
ComWrapper.Send($"{SpecialEffectCommandPrefix}{effectNumber:D2}{durationSeconds}");
}
public static void Idle()
{
if (Random.Range(0, 2) == 0)
@ -153,4 +186,4 @@ public enum LightMode
Dog,
Sam,
Mistress
}
}

View File

@ -44,8 +44,6 @@ public class Board : MonoBehaviour
public void ApplyPvpColors()
=> SetColorsImmediately(_pvpInnerColor, _pvpOutlineColor, _pvpCellsColor);
public Vector2 GetMiddleStickedPosition() => new Vector2(GetXPosition(Columns/2), config.stickedYOffset);
public Vector2 GetStickedPosition(int column) => new Vector2(GetXPosition(column), config.topLeftCell.position.y + config.stickedYOffset);
public Vector2 GetTargetPosition(int column) => new Vector2(GetXPosition(column), GetYPosition(column));

View File

@ -361,8 +361,7 @@ public class Bot : BasePlayer, IGamePlayer
private void SetInitialChipPosition()
{
// Начальная позиция – сверху с оффсетом
stickedTargetPosition = board.GetMiddleStickedPosition();
stickedTargetPosition.x = board.GetStickedPosition(SelectedColumn).x;
stickedTargetPosition = board.GetStickedPosition(SelectedColumn);
currentChip.transform.position = stickedTargetPosition + Vector2.up * 5f;
}

View File

@ -79,6 +79,7 @@ public class EndGameHandler : MonoBehaviour
{
if (winner == Team.Player1)
{
LightSystem.SpecialEffect(42, 2);
MachineCountersService.RecordBotResult(true);
EventBus.TriggerEvent(GameEvent.PlayerWin);
ShowResultAfterDelay(ShowPveWinResult);

View File

@ -221,8 +221,7 @@ public class Player : BasePlayer, IGamePlayer
private void SetInitialChipPosition()
{
stickedTargetPosition = board.GetMiddleStickedPosition();
stickedTargetPosition.x = board.GetStickedPosition(SelectedColumn).x;
stickedTargetPosition = board.GetStickedPosition(SelectedColumn);
currentChip.transform.position = stickedTargetPosition + Vector2.up * chipYOffset;
}

View File

@ -9,7 +9,7 @@
/// - Игрок 1: WASD + Space
/// - Игрок 2: Стрелки + RightControl
/// - Служебные: 1, 2, 3, F5
/// - Датчики хопера: O, P
/// - Датчики хопера: H, P (O переключает отладочный цикл спецэффектов)
/// </remarks>
public class KeyboardInput : InputSource
{
@ -132,7 +132,7 @@ public class KeyboardInput : InputSource
InSignal.extra => KeyCode.Alpha3,
InSignal.hasPrize => KeyCode.O,
InSignal.hasPrize => KeyCode.H,
InSignal.prizeGiven => KeyCode.P,
_ => KeyCode.None,
@ -166,7 +166,7 @@ public class KeyboardInput : InputSource
13 => KeyCode.Alpha3, // extra
14 => KeyCode.O, // hasPrize
14 => KeyCode.H, // hasPrize
15 => KeyCode.P, // prizeGiven
_ => KeyCode.None, // невалидный индекс

View File

@ -149,6 +149,7 @@ namespace PrizeSystem
if (success)
{
PrizeStockService.RecordSuccessfulCountableIssue();
LightSystem.SpecialEffect(40, 2);
//Счетчики
IncreasePrizeCounters(Type);

File diff suppressed because one or more lines are too long

302
CONTEXT_DUMP_2026-09-16.md Normal file
View File

@ -0,0 +1,302 @@
# Дамп контекста проекта chip-gameV3
Дата формирования: 2026-09-16
Часовой пояс: Europe/Moscow
Назначение: восстановление рабочего контекста в новом чате Codex.
## 1. Быстрый старт для нового чата
Проект находится в:
`C:\Users\bondars\UnityProjects\chip-gameV3 WORK`
Перед любой задачей:
1. Полностью прочитать `AGENTS.md` в корне проекта.
2. Запросить релевантный контекст через MCP-сервер `agentmemory`.
3. Проверить `git status --short --branch`, текущую ветку и чужие незакоммиченные изменения.
4. Если задача касается Unity, обязательно сначала прочитать состояние и релевантные ресурсы через `unityMCP`.
5. После Unity-изменений проверить через `unityMCP` компиляцию, Console, тесты и визуальный результат, когда он применим.
6. Не считать Unity-задачу завершённой без этой проверки. Если `unityMCP` недоступен — прямо сообщить пользователю.
Основной язык общения и сообщений коммитов — русский.
## 2. Состояние Git на момент дампа
- Активная ветка: `master`.
- HEAD: `70c4dbd081fc602a6fa54ca66f92347667d85442`.
- Коммит: `Унифицировать работу с контроллером и улучшить загрузку персонажей`.
- `master`, `chip-gameV3-remote/master`, `feature/redesign-sega` и `chip-gameV3-remote/feature/redesign-sega` указывают на `70c4dbd`.
- Перед созданием этого файла рабочее дерево было чистым.
- Remote: `chip-gameV3-remote` → `http://172.11.0.14:3000/Robotail/chip-gameV3`.
- Старое состояние `master` пользователю не требуется; текущая версия уже опубликована в удалённый `master` fast-forward push.
- Сам этот дамп создан после синхронизации и на момент создания ещё не закоммичен.
Последние значимые коммиты:
```text
70c4dbd Унифицировать работу с контроллером и улучшить загрузку персонажей
ca42cf5 Усильте авторизацию плат и восстановление COM-соединения
ded80f0 Обновить игровые системы и интерфейс
826ff57 Доработать игровые режимы, призы и настройки
16847fe Обновить игровые системы и удалить устаревший код
a656816 Удалить устаревший код и упростить структуру проекта
f86eed2 Улучшить PvP-аватары, цвета поля и настройку таймеров
16ab9e4 Добавить оплату повторов в PvE и PvP
ebc47a8 Доработать PvP и ранний выход из игры
```
## 3. Unity и зависимости
- Unity: `6000.4.8f1` (`f8b72d3d7343`).
- Основная сцена: `Assets/Scenes/GameScene3D.unity`.
- Render Pipeline: URP `17.4.0`.
- Addressables: `2.9.1`.
- uGUI: `2.0.0`.
- В проекте используются DOTween, UniTask, Newtonsoft.Json, SQLite, QRCoder и NuGetForUnity.
- MCP for Unity подключён пакетом `com.coplaydev.unity-mcp`.
Последняя подтверждённая Unity-проверка перед дампом:
- компиляция завершалась без ошибок;
- Unity Console: 0 ошибок;
- короткий Play Mode smoke test: 0 ошибок;
- EditMode: 1/1 существующий тест успешно;
- тест независимого чтения COM-осей: `horizontal=-1`, затем `vertical=1`, `success=True`.
Важно: непосредственно при создании дампа `unityMCP` был недоступен на `http://127.0.0.1:8080/mcp`, поэтому текущее состояние Editor повторно не считано. Это нужно перепроверить в новом чате после запуска/подключения сервера.
## 4. Обязательные соглашения проекта
### DOTween
Новые `Tween` и `Sequence` в этом проекте автоматически не запускаются. После создания обязательно вызывать `PlayForward()` или `Play()`. Если метод только возвращает анимацию, запуск делает вызывающий код.
### Структура C#-скриптов
Строгий порядок:
1. `public event`;
2. `public` поля;
3. `[SerializeField] private` поля;
4. `private` поля;
5. свойства;
6. Unity-методы в стандартном порядке;
7. публичные методы;
8. приватные методы.
Именование:
- публичные поля — `camelCase`;
- публичные события — `PascalCase`;
- сериализованные приватные и обычные приватные поля — `_camelCase`;
- свойства — `PascalCase`;
- параметры — `camelCase`.
Не добавлять `/// <summary>`. Комментарии использовать только для сложной или неочевидной логики.
### Работа с файлами и Git
- Сохранять чужие и несвязанные изменения.
- Для ручных правок файлов использовать `apply_patch`.
- Не применять `git reset --hard` или `git checkout --` без прямого запроса.
- Сообщения коммитов писать только по-русски.
## 5. Основная архитектура
Ключевые каталоги `Assets/Scripts`:
- `Core` — bootstrap и порядок инициализации;
- `Common` — `GameManager` и общие сервисы;
- `Gameplay` — поле, игроки, боты, ходы, таймеры, level bar, результаты;
- `Characters` — PvE-персонажи, определения и Addressables-загрузка;
- `Input` — единый ввод, COM, клавиатура, конфиг пинов;
- `Payment` — баланс, цены входа и реванша;
- `PrizeSystem` и `Vitrina` — призы, TakeOrRisk, QR, витрина, остатки;
- `UI` — меню, переходы, модальные панели;
- `Settings` — настройки, пароль, тесты, счётчики, остатки;
- `Statistics` и `Leaderboard` — статистика и рейтинг;
- `Audio` — музыка и SFX;
- `Media` — видеофоны и связанная логика.
Ключевые точки входа:
- `Assets/Scripts/Core/Bootstrap.cs`
- `Assets/Scripts/Common/GameManager.cs`
- `Assets/Scripts/Gameplay/TurnManager.cs`
- `Assets/Scripts/Gameplay/Player.cs`
- `Assets/Scripts/Characters/CharactersController.cs`
- `Assets/Scripts/Characters/CharacterDefinition.cs`
- `Assets/Scripts/Input/In.cs`
- `Assets/Scripts/Input/COMinput.cs`
- `Assets/Scripts/Input/ComInputConfiguration.cs`
- `Assets/Scripts/Input/ComSystem/ComWrapper.cs`
- `Assets/Scripts/Input/ComSystem/ComPortScanner.cs`
- `Assets/Scripts/Input/ComSystem/BoardAuthentication.cs`
- `Assets/Scripts/Payment/System/PaymentSystem.cs`
## 6. Актуальная система ввода и COM
### Физический ввод
- Контроллер сам присылает строки `INPUT<decimal>` при изменениях входа. Опрос ввода командой не нужен.
- Десятичная маска преобразуется в строго 26-битную строку с ведущими нулями и разворотом порядка битов.
- Некорректные, нечисловые и выходящие за 26 бит пакеты отклоняются.
- `COMinput` дополнительно защищён от пустых и коротких пакетов; прежний `Substring` больше не вызывает `ArgumentOutOfRangeException`.
- Логические входы active-low: символ `0` означает нажатое состояние.
Конфиг управления загружается из:
`Assets/StreamingAssets/ComInputConfig.json`
Актуальная конфигурация в репозитории:
```json
{
"leftJoystick": {
"up": 0,
"right": 2,
"down": 3,
"left": 1,
"action": 8
},
"rightJoystick": {
"up": 4,
"right": 7,
"down": 6,
"left": 5,
"action": 9
},
"serviceButton": 10
}
```
Не заменять эту схему на первоначально предполагавшиеся последовательные индексы без повторной проверки реального контроллера: текущая схема получена после диагностики фактических сообщений платы.
### Независимое чтение осей
`In.GetAxis(..., Discrete)` исторически потреблял события обеих осей, даже если вызывающий код использовал только `.x` или `.y`. Это ломало:
- перемещение первой фишки до первого хода, потому что `BackToMenuController` съедал горизонталь;
- вертикальную прокрутку левой панели меню, потому что `Menu` съедал вертикаль.
Исправление:
- добавлены `In.GetHorizontalAxis(...)` и `In.GetVerticalAxis(...)`;
- `BackToMenuController`, `Player`, `Menu` и `MenuPanelsScroll` читают только нужную ось;
- при `JoystickSide.Both` оба физических события опрашиваются без short-circuit, чтобы второе одновременное нажатие не оставалось в буфере.
### Режимы управления
- PvE: первый игрок управляется обоими джойстиками (`Player.ControlSide = JoystickSide.Both`).
- PvP: игрок 1 — левый джойстик, игрок 2 — правый.
### Инициализация и авторизация платы
- При подключении сначала отправляется команда перезагрузки `00`.
- Нужно дождаться сообщения с префиксом `ver_`; оно означает готовность контроллера после перезагрузки.
- Затем компьютер отправляет `07` + 32 HEX-символа.
- Контроллер отвечает `CHAL` + 32 HEX-символа.
- Компьютер отвечает `08` + AES-256 результат, используя существующий обфусцированный ключ в `BoardAuthentication.cs`.
- `RAES`, `AUTH_PC_OK` и промежуточные статусы не означают завершение авторизации.
- Авторизация завершена только после отдельной точной строки `READY!`.
- При длительном отсутствии сообщений выполняется ping, затем переподключение.
- При запуске приложения отправляется `856001` для включения подсветки.
- При выходе отправляется `856000` для выключения подсветки.
- Используется один тип периферийного контроллера: `DeviceType.Controller`.
- `PrizeShowcase` остаётся отдельным устройством витрины, а не вторым типом контроллера.
## 7. Персонажи и Addressables
- Тяжёлые prefab-персонажи и их видеофоны вынесены в Addressables.
- Лёгкие данные хранятся в `CharacterDefinition`/ScriptableObject и доступны без загрузки prefab.
- В меню предварительно загружается первый PvE-персонаж — Куб — и его видеофон.
- Для текущего PvE-персонажа поддерживается окно соседей, доступных для следующего перехода.
- `CharactersController.ShowCharacter` теперь ожидает только целевого персонажа и его видеофон через `PrepareTargetAsync`.
- Остаток окна подгружается асинхронно через `PreloadWindowRemainderAsync` и не задерживает начало раунда или переход, если нужный персонаж уже готов.
- При смене окна ненужные экземпляры персонажей и video handles освобождаются.
- При переходе в PvP PvE-окно приостанавливается; PvP-персонажи 6 и 7 обслуживаются `DualAvatarController`.
## 8. Игровые режимы и оплата
- Вход в PvE и PvP требует успешного списания через `PaymentSystem`.
- Неуспешная оплата воспроизводит звук и показывает мигающий `NotEnough` в `CoinCounter`.
- Реванши PvE и PvP имеют отдельные цены и также выполняют попытку списания.
- Ранний выход до первого хода доступен только после подготовки первой фишки.
- При раннем выходе первая подготовленная фишка возвращается в пул.
- Стоимость входа возвращается на баланс при раннем выходе из PvE/PvP; возврат не попадает в счётчики выручки.
- Игра считается запущенной для статистики только после первого фактического хода.
PvE:
- учитываются только время размышлений игрока и его ходы;
- паузы и время бота не включаются;
- результат показывается с задержкой 3 секунды;
- `GameplayLevelBar` отражает пройденных/текущего бота, наличие приза и таймер;
- на одного бота выделяется 2 минуты, истечение времени считается поражением;
- после победы над последним ботом показывается `Victory/NameInput` с русскими буквами и цифрами.
PvP:
- раунд показывает `Win PvP`, победителя и кнопки меню/повтора;
- матч идёт до 5 очков;
- после полной победы повтор сбрасывает очки;
- используются персонажи 6 слева и 7 справа;
- при входе: `Sleep` → перемещение в кадр → `Appear`;
- победителю отправляется `Win`, `Lose` отсутствует;
- при выходе персонажи уводятся на скрытые позиции, Animator отключается;
- поле окрашивается в три настраиваемых цвета;
- на ход даётся 30 секунд; по таймауту текущая фишка автоматически сбрасывается;
- подписи таймеров упрощены до `Игрок 1` и `Игрок 2`.
## 9. Призы, настройки и статистика
Реализованный функциональный контекст:
- несколько призов на одного персонажа;
- таблица призов в меню использует `IconSmall` визуального модуля персонажа;
- модальное окно призов открывается действием на левой панели и закрывается повторным действием или через 20 секунд бездействия;
- модальное окно рейтинга показывает последние 100 записей, последнюю запись внизу и ускоренную прокрутку обоими джойстиками;
- в меню показывается топ-3 рейтинга;
- `TakeOrRisk`: взять приз и выйти либо пропустить и продолжить к более ценному призу;
- QR-приз показывает QR-панель с таймаутом 5 минут;
- витринный приз сверяет физически доступные ячейки, предлагает выбор и ждёт подтверждение выдачи;
- Countable-приз уменьшает остаток только после успешной выдачи; QR и Vitrina не уменьшают этот счётчик;
- контроль остатков содержит включатель, добавление, установку количества и ускоренное перелистывание;
- надпись об окончании маленьких призов показывается только при включённом контроле и нулевом остатке;
- реализованы экранный RGB-тест, тест управления, тест звука;
- пароль настроек — комбинация из 6 движений джойстика, хранится отдельным файлом в `persistentDataPath`;
- панель счётчиков содержит сбрасываемые и несбрасываемые показатели игр, выручки, призов, времени и результатов по каждому боту/сложности;
- статистика побед/поражений ведётся по каждому из 5 ботов и каждой сложности.
## 10. Звук и визуальные эффекты
- `SoundManager` использует два `AudioSource`: музыка и SFX.
- SFX проигрываются через `PlayOneShot` с передачей `AudioClip` вызывающей стороной.
- Лишний пул дочерних `AudioSource` удалён.
- Очистка поля проигрывает звук только если поле действительно было непустым.
- `WinDots` на победных фишках использует синхронную покадровую анимацию `blik` со скоростью 40 FPS.
- Анимированный объект `WinDots` не перемещается по диагонали: локальная позиция `(0, 0)`, scale `1`.
- Для Robowolf перед `Appear` должен вызываться триггер `Sleep` как сброс Animator.
## 11. Известные условия и проверки для следующей задачи
- Реальный COM-контроллер периодически подключался к `COM3`, но в последней проверке ввода был отключён. Перед аппаратной задачей сначала проверить доступность порта и не считать программную симуляцию полной аппаратной проверкой.
- При работе с COM не возвращать активный polling ввода: плата сама присылает `INPUT`.
- При работе с Addressables не заставлять старт раунда ждать всё окно соседей.
- При работе с дискретным вводом не использовать чтение полной оси там, где нужна только `.x` или `.y`, если рядом есть второй потребитель ввода.
- Любые новые DOTween-анимации запускать явно.
- Не менять `Assets/StreamingAssets/ComInputConfig.json` без фактической проверки сигналов платы.
- Перед следующим изменением повторно проверить `git status`: сам файл дампа делает рабочее дерево изменённым, пока не будет закоммичен или удалён пользователем.
## 12. Что считать источником истины
При расхождении этого дампа с проектом приоритет такой:
1. актуальные файлы и сериализованные данные текущего `master`;
2. состояние Unity Editor, прочитанное через `unityMCP`;
3. `AGENTS.md`;
4. свежий контекст из `agentmemory`;
5. этот дамп.
Этот файл описывает рабочий контекст, но не заменяет чтение кода и проверку сцены перед изменениями.