squad-proto/src/render/sprites.h
z.kirill 7d6266aaf3 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-13 21:07:19 +03:00

118 lines
6.0 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
// -----------------------------------------------------------------------------
// Процедурная генерация всех спрайтов при старте: базовый набор не зависит ни
// от одного файла на диске.
//
// Тайлы можно подменить на собранные SpriteForge (LoadTiles, dev-режим
// --sf-tiles). Бойцы, твари и иконки остаются процедурными сознательно: на
// 480x270 боец занимает 14 пикселей, и цвет там занят НОМЕРОМ слота, а силуэт —
// КЛАССОМ оружия. И то, и другое живёт в коде, а не в манифесте
// (см. docs/12-assets.md).
//
// Правило чтения картинки, из которого всё вытекает:
// * номер бойца -> цвет тела;
// * класс оружия -> силуэт (ствол/клинок, длинный/короткий/тяжёлый);
// * вид твари -> и силуэт, и цвет; их всего пять, и путать их нельзя.
// -----------------------------------------------------------------------------
#include "engine/hash.h"
#include "engine/pixel.h"
#include "engine/tileset.h"
#include "engine/framebuffer.h"
#include "sim/items.h"
#include "sim/target.h"
#include "tuning.h"
class SpriteLibrary;
// Силуэт бойца. Не «какое именно оружие», а «как оно читается»: карабин и
// автомат на 14 пикселях — один и тот же длинный ствол, и притворяться, что
// игрок их различит, бессмысленно.
enum class BodyLook : uint8_t
{
GUN_LONG = 0, // карабин, автомат
GUN_SHORT, // пистолет
GUN_HEAVY, // дробовик, магнум: толстый ствол, широкие плечи
BLADE_LONG, // сабля, тесак
BLADE_SHORT, // нож
COUNT,
};
BodyLook LookOf(ItemId weapon);
struct SpriteSet
{
// Пол и стены живут в движке: ими рисует и игра, и редактор комнат
// (engine/tileset.h). Здесь остаётся только то, что знает про правила, —
// бойцы, твари и иконки предметов.
TileSet tiles;
// Тело бойца: силуэт по оружию x цвет по слоту.
Sprite body[int(BodyLook::COUNT)][NUM_AGENTS];
// Павший: тот же цвет, но лежит. Отряд обязан видеть, кого потерял.
Sprite corpse[NUM_AGENTS];
// Твари. DUMMY — прежняя мишень приёмки, её вид не менялся.
Sprite enemy[int(EnemyKind::COUNT)];
Sprite target; // синоним enemy[DUMMY], чтобы не ломать вызовы
// Анимированный вариант твари: ID ассета SpriteForge, 0 — «нет, рисуй
// процедурный». Кадры здесь НЕ хранятся: они лежат в SpriteLibrary в одном
// экземпляре на ассет, и рендер достаёт нужный по времени клипа.
//
// Пока клип один — цикл шага. Появятся idle/attack — это будут соседние
// массивы и соседние ассеты <kind>_idle, а не вторая анимация внутри
// этого: бэкенд import имён клипов не читает (assets/manifests/enemies.yaml).
uint32_t enemyWalk[int(EnemyKind::COUNT)] = {};
// Иконки предметов для инвентаря и HUD.
Sprite icon[int(ItemId::COUNT)];
// Спрайта пули нет: снаряд рисуется светящимся трассером (render/fx.cpp).
void Generate();
// Заменить пол и стену на тайлы из библиотеки SpriteForge. Всё или ничего:
// при любой осечке набор остаётся тем, что дал Generate(), и возвращается
// false. Вызывать только после Generate().
bool LoadTiles(const SpriteLibrary& lib);
// Включить анимированных тварей из библиотеки. Тоже всё или ничего: одна
// анимированная тварь среди пяти статичных выглядит хуже пяти статичных.
// Мишень приёмки (DUMMY) и обстановка не анимируются никогда.
bool LoadEnemies(const SpriteLibrary& lib);
const Sprite& AgentBody(int slot, ItemId weapon) const
{
const int s = (slot >= 0 && slot < NUM_AGENTS) ? slot : 0;
return body[int(LookOf(weapon))][s];
}
const Sprite& EnemyBody(EnemyKind kind) const
{
const int k = (int(kind) >= 0 && int(kind) < int(EnemyKind::COUNT)) ? int(kind) : 0;
return enemy[k];
}
const Sprite& Icon(ItemId id) const
{
const int i = (int(id) >= 0 && int(id) < int(ItemId::COUNT)) ? int(id) : 0;
return icon[i];
}
// Какой ромб пола и какой блок стены лежат на тайле. Одно место на весь
// рендер: запечённая кровь копирует ИМЕННО этот ромб, иначе шахматка
// поедет (render/blood.h). Тему тайл несёт сам (Tilemap::ThemeAt).
const Sprite& FloorTile(int tx, int ty, uint8_t theme) const
{
return tiles.Floor(tx, ty, theme);
}
const Sprite& WallTile(int tx, int ty, uint8_t theme) const
{
return tiles.Wall(tx, ty, theme);
}
};
// Цвет слота отряда: номер бойца читается по цвету и больше ничем.
uint32_t SlotColor(int slot);