squad-proto/src/ui/menu.h
z.kirill 92a8864726 Крафт стал выбором: закалка, отпечаток, направленные операции (M33)
Прежние пять операций были СЛЕПЫМИ: перековка бросала всё заново,
прививка ставила случайный тег, отсечение снимало последний аффикс,
возвышение поднимало случайный. Игрок не принимал ни одного решения - жал
кнопку и смотрел, что выпало. Это не крафт, это второй бросок предмета за
материалы.

Теперь половина операций направленные: игрок показывает пальцем, какой
аффикс трогать, и платит за это вдвое - вот это уже решение
("перебросить всё дёшево или снять один дорого").

Два новых глагола:
  ЗАКАЛКА двигает рулон выбранного аффикса к максимуму его тира на треть
  остатка. Дёшево, понятно, повторяемо - то самое "доработать почти
  хорошую вещь". Тир не трогает: за ступень платят возвышением.

  ОТПЕЧАТОК переносит лучший аффикс с ДРУГОЙ вещи, сжигая её. Именно он
  превращает хлам из схрона в ресурс, а не в мусор, который лежит.
  Правило "один тег один раз" им не обходится, бюджет проверяется.

В мастерской: клик по вещи кладёт её на верстак, повторный клик по той же
назначает донором (помечается кровью - он сгорит, и видеть это надо ДО
нажатия). Строки аффиксов стали кнопками: по ним выбирается цель.

Проверка меряет НАПРАВЛЕННОСТЬ, а не факт срабатывания:

  направленность: отсечение по цели, возвышение по цели,
                  закалка двигает рулон, отпечаток перенёс и сжёг донора

Отсечение пробуется на СРЕДНЕМ аффиксе из трёх: операция, которая
"работает", снимая последний, прошла бы и слепую версию.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 19:37:07 +03:00

147 lines
9.1 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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, // ячейка схрона в мастерской: что дорабатываем
CRAFT_AFFIX, // строка аффикса на верстаке: цель направленной операции
};
// Прямоугольник в координатах КАДРА (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; // какая вещь схрона лежит на верстаке
// Какой аффикс выбран на верстаке и какая вещь назначена донором.
// Половина операций направленные (sim/craft.h), и без выбора крафт
// возвращается к «жми кнопку и смотри, что выпало».
int craftAffix = 0;
int craftDonor = -1; // -1 — донор не выбран
// Чем кончилась последняя операция. Показывается строкой под верстаком:
// «не хватило материалов» и «рассыпалось» — разные события, и молча
// проглотить их нельзя, особенно второе.
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);