Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20)
Десять вех одним состоянием: разделять их пофайлово нечем, потому что
src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений,
все проверки проходят.
ДВИЖОК (engine/, цель tile2d)
Каталог свой, и решение «это движок» принимается тем, куда положен файл.
Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра
и карты уехали в engine/config.h, потолок населения этажа стал своим
(MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала
центром карты вместо позиции отряда.
engine/projection.* единственное место, где движок знает про изометрию;
видов сразу два — абстракция с одной реализацией ничем
не проверена и обычно оказывается неправильной
engine/project.* манифест проекта: пути, вид, кривая глубины
engine/app.* окно, кадр, fixed timestep — вместо двух копий цикла
engine/tilemap_draw движок научился показывать свою карту
РЕДАКТОР (editor/, цель tile2d_editor)
Открывает любой проект и ни одной игры по имени не знает.
--tool одиннадцать безоконных команд: комнаты, каталог, кривая, сборка
этажа, ASCII-карта, проверка набора. Правку делают файлами,
последствия читают командой (docs/19-agent.md)
терминал снизу: те же инструменты, git, сборка и запросы к claude/codex,
открытый проект уходит потомкам через TILE2D_PROJECT
ПРИМЕР (samples/walk, цель tile2d_walk)
Второй потребитель движка. Игра и редактор самостоятельность движка
проверить не могут: редактор мира не рисует, а игра — то место, куда общий
код и уезжает незаметно.
ИГРА (src/)
Экстракшен: вес и два порога переноски, конечный боезапас, расходники
применяются сами, общая сумка отряда, трата опыта на улучшения.
Каждое из правил проверяется прогоном, а не глазами, и проверки поймали:
вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче);
боец с клинком не тратил найденные патроны при запасном стволе в сумке;
массив виджетов экрана переполнился и молча съел хвост схрона; проверка
улучшений сама ломала забег, вызывая NewRun посреди спуска.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:19 +07:00
|
|
|
|
#include "render/hud.h"
|
|
|
|
|
|
|
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
|
|
|
|
|
|
|
#include "engine/microfont.h"
|
|
|
|
|
|
#include "game.h"
|
|
|
|
|
|
#include "render/sprites.h"
|
|
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
constexpr int CARD_W = 92;
|
|
|
|
|
|
constexpr int CARD_H = 22;
|
|
|
|
|
|
constexpr int CARD_GAP = 2;
|
|
|
|
|
|
constexpr int BAR_W = 52;
|
|
|
|
|
|
|
|
|
|
|
|
const uint32_t COL_FRAME = RGBA(24, 26, 32);
|
|
|
|
|
|
const uint32_t COL_EMPTY = RGBA(44, 48, 56);
|
|
|
|
|
|
const uint32_t COL_DEAD = RGBA(70, 26, 26);
|
|
|
|
|
|
|
|
|
|
|
|
// Рамка карточки: тёмная подложка, чтобы HUD читался поверх любого пола.
|
|
|
|
|
|
void Panel(Framebuffer& fb, int x, int y, int w, int h, uint32_t fill)
|
|
|
|
|
|
{
|
|
|
|
|
|
fb.FillRect(x, y, w, h, fill);
|
|
|
|
|
|
fb.Line(x, y, x + w - 1, y, COL_FRAME);
|
|
|
|
|
|
fb.Line(x, y + h - 1, x + w - 1, y + h - 1, COL_FRAME);
|
|
|
|
|
|
fb.Line(x, y, x, y + h - 1, COL_FRAME);
|
|
|
|
|
|
fb.Line(x + w - 1, y, x + w - 1, y + h - 1, COL_FRAME);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Патроны: по квадратику на выстрел, пока их не больше BAR_W/2. Дальше —
|
|
|
|
|
|
// обычная полоса: сорок квадратиков автомата разглядывать всё равно никто
|
|
|
|
|
|
// не будет, а «сколько осталось» видно и так.
|
|
|
|
|
|
// Запас патронов СВЕРХ магазина — тонкая полоса под патронами.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Это главный ресурс вылазки, и он обязан таять на глазах: игрок решает «ещё
|
|
|
|
|
|
// этаж или наверх» именно по нему. Пустой запас — красная риска на месте
|
|
|
|
|
|
// полосы: не «полоса нулевой длины», которую не отличить от её отсутствия.
|
|
|
|
|
|
void DrawReserve(Framebuffer& fb, int x, int y, const Agent& a)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (a.ammoReserve < 0) return; // песочница: патроны бесконечны
|
|
|
|
|
|
|
|
|
|
|
|
const float k = Clampf(float(a.ammoReserve) / std::max(1.0f, g_tune.startAmmo), 0.0f, 1.0f);
|
|
|
|
|
|
fb.FillRect(x, y, BAR_W, 1, COL_EMPTY);
|
|
|
|
|
|
if (a.ammoReserve > 0)
|
|
|
|
|
|
fb.FillRect(x, y, std::max(1, int(float(BAR_W) * k)), 1,
|
|
|
|
|
|
(k < 0.2f) ? RGBA(220, 120, 60) : RGBA(150, 140, 110));
|
|
|
|
|
|
else
|
|
|
|
|
|
fb.FillRect(x + BAR_W / 2 - 2, y, 4, 1, RGBA(220, 70, 60));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DrawAmmo(Framebuffer& fb, int x, int y, const Agent& a, uint32_t col)
|
|
|
|
|
|
{
|
|
|
|
|
|
const int mag = a.gun.def.magazine;
|
|
|
|
|
|
|
|
|
|
|
|
if (a.gun.Reloading())
|
|
|
|
|
|
{
|
|
|
|
|
|
// Перезарядка: полоса набирается слева направо. Это ЕДИНСТВЕННЫЙ момент,
|
|
|
|
|
|
// когда стрелок беспомощен, и его надо видеть без чтения цифр.
|
|
|
|
|
|
const float k = 1.0f - Clampf(a.gun.reloadTimer / std::max(0.01f, a.gun.def.reloadTime),
|
|
|
|
|
|
0.0f, 1.0f);
|
|
|
|
|
|
fb.FillRect(x, y, BAR_W, 3, COL_EMPTY);
|
|
|
|
|
|
fb.FillRect(x, y, int(float(BAR_W) * k), 3, RGBA(210, 150, 60));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (mag > 0 && mag <= BAR_W / 2)
|
|
|
|
|
|
{
|
|
|
|
|
|
const int step = BAR_W / mag;
|
|
|
|
|
|
for (int i = 0; i < mag; ++i)
|
|
|
|
|
|
fb.FillRect(x + i * step, y, std::max(1, step - 1), 3,
|
|
|
|
|
|
(i < a.gun.ammo) ? col : COL_EMPTY);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fb.FillRect(x, y, BAR_W, 3, COL_EMPTY);
|
|
|
|
|
|
fb.FillRect(x, y, int(float(BAR_W) * a.gun.AmmoFrac()), 3, col);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Клинок: фаза удара. Замах, удар и восстановление — три разных цвета, потому
|
|
|
|
|
|
// что это три разных состояния бойца, а не одна «атака».
|
|
|
|
|
|
void DrawMeleeState(Framebuffer& fb, int x, int y, const Agent& a, uint32_t col)
|
|
|
|
|
|
{
|
|
|
|
|
|
uint32_t c = COL_EMPTY;
|
|
|
|
|
|
float k = 1.0f;
|
|
|
|
|
|
|
|
|
|
|
|
switch (a.melee.state)
|
|
|
|
|
|
{
|
|
|
|
|
|
case MeleeState::WINDUP: c = RGBA(220, 180, 70); k = a.melee.Progress(); break;
|
|
|
|
|
|
case MeleeState::STRIKE: c = RGBA(250, 240, 220); k = 1.0f; break;
|
|
|
|
|
|
case MeleeState::RECOVER: c = RGBA(150, 70, 70); k = 1.0f - a.melee.Progress(); break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
// Готов бить — полоса цвета бойца; иначе видно, сколько ждать.
|
|
|
|
|
|
c = (a.melee.cooldown <= 0.0f) ? col : COL_EMPTY;
|
|
|
|
|
|
k = (a.melee.cooldown <= 0.0f)
|
|
|
|
|
|
? 1.0f
|
|
|
|
|
|
: 1.0f - Clampf(a.melee.cooldown / std::max(0.01f, a.melee.interval),
|
|
|
|
|
|
0.0f, 1.0f);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fb.FillRect(x, y, BAR_W, 3, COL_EMPTY);
|
|
|
|
|
|
fb.FillRect(x, y, int(float(BAR_W) * Clampf(k, 0.0f, 1.0f)), 3, c);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// Строка забега: этаж, отделка, опыт. Единственные БУКВЫ в HUD, и появились
|
|
|
|
|
|
// они по одной причине: номер этажа — это число, и показать его формой нельзя
|
|
|
|
|
|
// (engine/microfont.h). Всё остальное в HUD по-прежнему без единой буквы.
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
void DrawRunBar(Framebuffer& fb, const Game& game)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!game.levelGenerated) return; // на захардкоженной карте этажа нет
|
|
|
|
|
|
|
|
|
|
|
|
const uint32_t COL_TEXT = RGBA(206, 200, 190);
|
|
|
|
|
|
const uint32_t COL_SHADOW = RGBA(6, 6, 8);
|
|
|
|
|
|
const uint32_t COL_READY = RGBA(120, 226, 150); // вниз, глубже
|
|
|
|
|
|
const uint32_t COL_OUT = RGBA(130, 200, 240); // наверх, с добычей
|
|
|
|
|
|
|
|
|
|
|
|
char buf[48];
|
|
|
|
|
|
FloorLabel(game.level.floor, buf, sizeof(buf));
|
|
|
|
|
|
MicroTextShadow(fb, 5, 5, buf, COL_TEXT, COL_SHADOW);
|
|
|
|
|
|
|
|
|
|
|
|
// Добыча — вторая по важности цифра забега после этажа: именно она стоит
|
|
|
|
|
|
// на кону, и знать её нужно ДО того, как решаешь идти глубже.
|
|
|
|
|
|
// Сумка одна на отряд, поэтому и цифра одна: пересчитывать по бойцам
|
|
|
|
|
|
// больше нечего.
|
|
|
|
|
|
const int carried = game.squad.bag.Count();
|
|
|
|
|
|
std::snprintf(buf, sizeof(buf), "XP %d LOOT %d", int(game.xp), carried);
|
|
|
|
|
|
MicroTextShadow(fb, 5, 5 + MICRO_H + 3, buf, COL_TEXT, COL_SHADOW);
|
|
|
|
|
|
|
Свет как расходник: заряд тратится за путь, а не за время (M23)
Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает
до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он
меняется с тварями местами. Батарея встаёт сама, как бинт.
Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия
считала по секундам, то есть была таймером, которого в игре нет по прямому
требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как
секундомер, только без циферблата, и думать над этажом стало бы дорого.
За расстояние получается тот же размен, что и с весом, но с другой стороны:
вес делает дорогим «унести всё», свет — «обшарить всё».
Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это
был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем
дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды.
Проверка в --descend меряет правило целиком, и первая её строка не про
баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже
был неправ — делил потраченное на смещение отряда, а не на пройденный
путь, и показывал 1.55 за тайл при 1.10 в каталоге.
свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге
глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска
обзор 1.00 -> 0.35 в темноте, батарея встала сама
Хватает ли заряда на этаж считается в этажах и по средней длине забега:
путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы
удачу, а не баланс.
--accept PASS без изменений: в песочнице света бесконечно, как и патронов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
|
|
|
|
// Свет — второй ресурс вылазки, и таять он обязан на глазах так же, как
|
|
|
|
|
|
// патроны в карточке бойца. Разница одна: кончившийся заряд показывается
|
|
|
|
|
|
// СЛОВОМ, а не полосой нулевой длины. Темнота меняет правила — и обзор, и
|
|
|
|
|
|
// то, кто кого замечает первым, — а «полоски не видно» читалось бы как
|
|
|
|
|
|
// «полоски тут и не было».
|
|
|
|
|
|
if (game.squad.lightCharge >= 0.0f)
|
|
|
|
|
|
{
|
|
|
|
|
|
const int ly = 5 + (MICRO_H + 3) * 2;
|
|
|
|
|
|
if (game.squad.LightOut())
|
|
|
|
|
|
MicroTextShadow(fb, 5, ly, "DARK", RGBA(225, 95, 70), COL_SHADOW);
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
const float k = Clampf(game.squad.lightCharge / LIGHT_FULL, 0.0f, 1.0f);
|
|
|
|
|
|
fb.FillRect(5, ly + 1, 46, 3, RGBA(44, 48, 56));
|
|
|
|
|
|
fb.FillRect(5, ly + 1, std::max(1, int(46.0f * k)), 3,
|
|
|
|
|
|
(k < 0.25f) ? RGBA(220, 150, 60) : RGBA(232, 214, 150));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20)
Десять вех одним состоянием: разделять их пофайлово нечем, потому что
src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений,
все проверки проходят.
ДВИЖОК (engine/, цель tile2d)
Каталог свой, и решение «это движок» принимается тем, куда положен файл.
Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра
и карты уехали в engine/config.h, потолок населения этажа стал своим
(MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала
центром карты вместо позиции отряда.
engine/projection.* единственное место, где движок знает про изометрию;
видов сразу два — абстракция с одной реализацией ничем
не проверена и обычно оказывается неправильной
engine/project.* манифест проекта: пути, вид, кривая глубины
engine/app.* окно, кадр, fixed timestep — вместо двух копий цикла
engine/tilemap_draw движок научился показывать свою карту
РЕДАКТОР (editor/, цель tile2d_editor)
Открывает любой проект и ни одной игры по имени не знает.
--tool одиннадцать безоконных команд: комнаты, каталог, кривая, сборка
этажа, ASCII-карта, проверка набора. Правку делают файлами,
последствия читают командой (docs/19-agent.md)
терминал снизу: те же инструменты, git, сборка и запросы к claude/codex,
открытый проект уходит потомкам через TILE2D_PROJECT
ПРИМЕР (samples/walk, цель tile2d_walk)
Второй потребитель движка. Игра и редактор самостоятельность движка
проверить не могут: редактор мира не рисует, а игра — то место, куда общий
код и уезжает незаметно.
ИГРА (src/)
Экстракшен: вес и два порога переноски, конечный боезапас, расходники
применяются сами, общая сумка отряда, трата опыта на улучшения.
Каждое из правил проверяется прогоном, а не глазами, и проверки поймали:
вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче);
боец с клинком не тратил найденные патроны при запасном стволе в сумке;
массив виджетов экрана переполнился и молча съел хвост схрона; проверка
улучшений сама ломала забег, вызывая NewRun посреди спуска.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:19 +07:00
|
|
|
|
// Подсказки выходов. Только когда отряд УЖЕ на них: постоянная строка
|
|
|
|
|
|
// «нажми F» висела бы на экране весь забег и перестала бы читаться.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Разные цвета — те же, что у колец на полу: подсказка обязана читаться
|
|
|
|
|
|
// как подпись к тому, на чём стоишь, а не как отдельное сообщение.
|
|
|
|
|
|
const char* hint = nullptr;
|
|
|
|
|
|
uint32_t hintCol = COL_READY;
|
|
|
|
|
|
if (game.atExtract) { hint = "EXTRACT: E"; hintCol = COL_OUT; }
|
|
|
|
|
|
else if (game.atExit) { hint = "DESCEND: F"; hintCol = COL_READY; }
|
|
|
|
|
|
if (!hint) return;
|
|
|
|
|
|
|
|
|
|
|
|
MicroTextShadow(fb, (fb.w - MicroTextWidth(hint)) / 2, 14, hint, hintCol, COL_SHADOW);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
void DrawHud(Framebuffer& fb, const Game& game)
|
|
|
|
|
|
{
|
|
|
|
|
|
DrawRunBar(fb, game);
|
|
|
|
|
|
|
|
|
|
|
|
const int total = NUM_AGENTS * CARD_W + (NUM_AGENTS - 1) * CARD_GAP;
|
|
|
|
|
|
const int x0 = (fb.w - total) / 2;
|
|
|
|
|
|
const int y0 = fb.h - CARD_H - 4;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
const Agent& a = game.squad.agents[i];
|
|
|
|
|
|
const int x = x0 + i * (CARD_W + CARD_GAP);
|
|
|
|
|
|
const uint32_t col = SlotColor(i);
|
|
|
|
|
|
|
|
|
|
|
|
// Подложка вспыхивает, когда бойца ударили: цифры смотреть некогда,
|
|
|
|
|
|
// а красная карточка в углу глаза видна.
|
|
|
|
|
|
uint32_t back = RGBA(12, 13, 16);
|
|
|
|
|
|
if (!a.alive) back = COL_DEAD;
|
|
|
|
|
|
else if (a.hurtTimer > 0.0f)
|
|
|
|
|
|
back = ShadeColor(RGBA(120, 30, 30), 0.4f + a.hurtTimer);
|
|
|
|
|
|
Panel(fb, x, y0, CARD_W, CARD_H, back);
|
|
|
|
|
|
|
|
|
|
|
|
// Метка слота: тот же цвет, что и у тела на карте.
|
|
|
|
|
|
fb.FillRect(x + 3, y0 + 3, 4, CARD_H - 6, a.alive ? col : ShadeColor(col, 0.35f));
|
|
|
|
|
|
|
|
|
|
|
|
if (!a.alive)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Перечёркнутая карточка: потерю отряда нельзя пропустить.
|
|
|
|
|
|
fb.Line(x + 10, y0 + 4, x + CARD_W - 6, y0 + CARD_H - 5, RGBA(180, 60, 60));
|
|
|
|
|
|
fb.Line(x + 10, y0 + CARD_H - 5, x + CARD_W - 6, y0 + 4, RGBA(180, 60, 60));
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Здоровье.
|
|
|
|
|
|
const float hpK = Clampf(a.hp / std::max(1.0f, a.maxHp), 0.0f, 1.0f);
|
|
|
|
|
|
const uint32_t hpCol = (hpK > 0.5f) ? RGBA(150, 200, 130)
|
|
|
|
|
|
: (hpK > 0.25f) ? RGBA(220, 190, 90)
|
|
|
|
|
|
: RGBA(220, 80, 70);
|
|
|
|
|
|
fb.FillRect(x + 10, y0 + 4, BAR_W, 5, COL_EMPTY);
|
|
|
|
|
|
fb.FillRect(x + 10, y0 + 4, int(float(BAR_W) * hpK), 5, hpCol);
|
|
|
|
|
|
|
|
|
|
|
|
// Броня: тонкая полоска под здоровьем. Её нет — нет и полоски.
|
|
|
|
|
|
if (a.armor > 0.0f)
|
|
|
|
|
|
fb.FillRect(x + 10, y0 + 10, int(float(BAR_W) * Clampf(a.armor / ARMOR_CAP, 0.0f, 1.0f)),
|
|
|
|
|
|
2, RGBA(120, 150, 190));
|
|
|
|
|
|
|
|
|
|
|
|
// Нагрузка. Полоска появляется ТОЛЬКО за нормой переноски: пока боец
|
|
|
|
|
|
// идёт как шёл, показывать нечего, а лишняя линия в каждой карточке
|
|
|
|
|
|
// отняла бы внимание у здоровья и патронов.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Она растёт от нормы к пределу и там же кончается — это и есть шкала
|
|
|
|
|
|
// «насколько ты уже обоз».
|
|
|
|
|
|
if (a.load > CARRY_LIMIT)
|
|
|
|
|
|
{
|
|
|
|
|
|
const float over = Clampf((a.load - CARRY_LIMIT) / (CARRY_MAX - CARRY_LIMIT),
|
|
|
|
|
|
0.0f, 1.0f);
|
|
|
|
|
|
fb.FillRect(x + 10, y0 + 12, BAR_W, 2, COL_EMPTY);
|
|
|
|
|
|
fb.FillRect(x + 10, y0 + 12, int(float(BAR_W) * over), 2,
|
|
|
|
|
|
(over > 0.85f) ? RGBA(220, 90, 60) : RGBA(200, 160, 70));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Оружие: патроны или фаза клинка.
|
|
|
|
|
|
if (a.IsMelee()) DrawMeleeState(fb, x + 10, y0 + 14, a, col);
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
DrawAmmo(fb, x + 10, y0 + 14, a, col);
|
|
|
|
|
|
DrawReserve(fb, x + 10, y0 + 19, a);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Иконка того, что в руках.
|
|
|
|
|
|
fb.Blit(game.sprites.Icon(a.held), x + CARD_W - 12, y0 + CARD_H / 2);
|
|
|
|
|
|
|
|
|
|
|
|
// Есть цель — уголок над карточкой. Отряд, который никого не видит,
|
|
|
|
|
|
// должен отличаться от отряда, который работает.
|
|
|
|
|
|
const bool engaged = a.IsMelee() ? a.melee.inReach : a.hasLane;
|
|
|
|
|
|
if (engaged) fb.FillRect(x + CARD_W / 2 - 3, y0 - 3, 6, 2, RGBA(230, 220, 200));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|