#pragma once // ----------------------------------------------------------------------------- // Экраны вне боя: главное меню, пауза, отряд с инвентарём, экран поражения. // // Это ВТОРОЕ и последнее место в игре, где разрешены примитивы raylib // (первое — debug/overlay). Причина та же: здесь нужны слова, а на 480x270 // собственный шрифт стоил бы дороже, чем весь остальной UI вместе взятый. // Мир по-прежнему рисуется только в софтверный фреймбуфер — меню ложится // поверх уже готового кадра и никак на него не влияет. // // Ui не владеет игрой: он читает её и просит Game сделать конкретные вещи // (Reset, смена оружия). Никакой логики боя здесь нет. // // Управление мышиное: навёл — подсветилось, кликнул — выбралось, потащил // предмет — переложился. Клавиатура осталась целиком, это второй путь к тем же // действиям, а не запасной. // ----------------------------------------------------------------------------- #include "engine/framebuffer.h" #include "sim/items.h" struct Game; enum class Screen : uint8_t { MENU = 0, // главное меню GAME, // бой PAUSE, SQUAD, // отряд: снаряжение, сумки и схрон DEPTH, // с какого чекпоинта начать вылазку UPGRADE, // во что вложить вынесенный опыт WORKSHOP, // мастерская: материалы и операции над вещью SUMMARY, // итог вылазки: что вынесли и что потеряли DEFEAT, // отряд полёг (устарел, ведёт на SUMMARY) }; // Роль интерактивного элемента. Она же — адрес ячейки инвентаря: слот руки, // брони и кармана существует по одному, у сумки роль общая, а номер в index. enum class UiPart : uint8_t { NONE = 0, MENU_ITEM, // пункт главного меню PAUSE_ITEM, DEFEAT_ITEM, AGENT, // карточка бойца HANDS, ARMOR, POCKET, BAG, STASH, // ячейка схрона: то, что лежит на базе между вылазками STAT, // строка характеристик: наводишь — читаешь, что это значит UPGRADE_ITEM, // строка улучшения: клик покупает следующий уровень DEPTH_ITEM, // кнопка выбора чекпоинта SUMMARY_ITEM, // кнопка на экране итога CRAFT_ITEM, // строка операции мастерской CRAFT_SLOT, // ячейка схрона в мастерской: что дорабатываем }; // Прямоугольник в координатах КАДРА (480x270), а не окна: кадр лежит в окне // со сдвигом и целым масштабом, и всё, что считает координаты в окне, съезжает. struct UiRect { int x = 0, y = 0, w = 0, h = 0; bool Has(float fx, float fy) const { return fx >= float(x) && fy >= float(y) && fx < float(x + w) && fy < float(y + h); } }; struct Ui { Screen screen = Screen::MENU; bool quit = false; bool started = false; // была ли начата вылазка (появляется «продолжить») int menuIndex = 0; int agentIndex = 0; // выбранный боец на экране отряда int slotIndex = 0; // выбранная ячейка сумки int depthIndex = 0; // выбранный чекпоинт на экране глубины int stashScroll = 0; // прокрутка схрона: он длиннее видимой сетки // --- мастерская ---------------------------------------------------------- int craftIndex = 0; // какая вещь схрона лежит на верстаке // Чем кончилась последняя операция. Показывается строкой под верстаком: // «не хватило материалов» и «рассыпалось» — разные события, и молча // проглотить их нельзя, особенно второе. int craftMessage = 0; // 0 — нет, иначе CraftResult + 1 int craftOpDone = 0; // какая операция это была float craftFlash = 0.0f; // сколько ещё держать сообщение float blink = 0.0f; // общий таймер для мигания подсказок bool cursorHidden = false; // системный курсор скрыт (рисуем свой) // Меню перезапустило мир. Забирает main и сбрасывает View: кровь и туман // войны прошлой вылазки не должны переезжать в новую. bool worldReset = false; // --- мышь ---------------------------------------------------------------- // Курсор в координатах КАДРА и то, над чем он висит. Всё это считается // в Update и только читается в Draw: рисование не имеет права трогать // состояние, иначе подсветка начнёт зависеть от порядка вызовов. float mx = 0.0f, my = 0.0f; bool mouseIn = false; // курсор внутри кадра (не на чёрных полях) UiPart hoverPart = UiPart::NONE; int hoverIndex = -1; // Перетаскивание предмета: что тащим и откуда взяли. bool dragging = false; UiPart dragFrom = UiPart::NONE; int dragIndex = -1; ItemId dragItem = ItemId::NONE; bool dropOk = false; // слот под курсором примет предмет? // Ввод меню: мышь и клавиатура в одном месте. fit — куда лёг фреймбуфер // в окне, по нему считается позиция курсора в координатах кадра. // Возвращает true, если симуляцию на этом кадре шагать НЕ надо. bool Update(Game& game, const ScreenRect& fit, float dt); // Поверх готового кадра. fit — куда лёг фреймбуфер в окне. void Draw(const Game& game, const ScreenRect& fit) const; }; // ----------------------------------------------------------------------------- // Нажать пункт так, как будто по нему кликнули. Наружу вынесено ради ОДНОЙ // проверки — `--ui-check`, — и вынесено не для полноты API. // // Причина конкретная: пункт меню UPGRADES четыре вехи выходил из игры. В // обработчике стоял `default: quit`, экран улучшений появился четвёртым // пунктом, и обработчик о нём просто не узнал. Снаружи это выглядело как // падение игры при открытии улучшений. // // Ни один прогон этого не ловил, потому что все проверки в проекте меряют // СИМУЛЯЦИЮ, а меню не проверял никто. Теперь его щупают так же, как всё // остальное: код возврата, а не глаз. void UiActivate(Ui& ui, Game& game, UiPart part, int index); // Сколько пунктов в главном меню и как называется i-й. Нужны той же проверке: // она обязана пройти по ВСЕМ пунктам, а не по тем, о которых знает. int UiMenuItemCount(); const char* UiMenuItemName(int index);