squad-proto/engine/sfa_library.h

161 lines
9.1 KiB
C
Raw Permalink Normal View History

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
#pragma once
// -----------------------------------------------------------------------------
// Чтение библиотеки спрайтов, собранной SpriteForge (`sf build`).
//
// Игра не знает про YAML, генераторы и PNG. Она видит только каталог с файлами
// `<asset_id>.sfa` и общей палитрой `palettes.sfp`, а обращается к ассетам по
// числовым идентификаторам из generated/spriteforge_assets.h. Строковых ID
// ассетов в коде игры быть не должно.
//
// Слой ровно один: .sfa (индексы палитры + RLE по строкам) -> игровой Sprite
// (RGBA8 с альфа-тестом). Дальше по коду разницы между процедурным спрайтом
// и собранным нет.
//
// Распаковка идёт целиком на Open() и только там. Ленивое декодирование стоило
// бы RLE-прохода и аллокации холста на КАЖДУЮ анимированную сущность в КАЖДОМ
// кадре игры; готовые Sprite лежат в памяти, наружу отдаются ссылки на них.
// -----------------------------------------------------------------------------
#include <array>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
#include "engine/framebuffer.h"
#include "spriteforge_assets.h" // sf_asset_id, SF_ASSET_*, SF_ANIM_*
// Куда положить кадр .sfa на холст игрового спрайта.
//
// SpriteForge обрезает кадр по силуэту, поэтому размер кадра плывёт вместе с
// содержимым. Тайлам это не подходит: ShadeQuad считается в координатах
// спрайта и предполагает канонический холст с ромбом в известном месте
// (Lighting::QuadFor). Placement кладёт кадр на такой холст: pivot из .sfa
// совмещается с (pivotX, pivotY).
struct FramePlacement
{
int canvasW = 0; // 0 — холст по размеру кадра, без дополнения
int canvasH = 0;
int pivotX = 0; // куда на холсте попадает pivot кадра
int pivotY = 0;
int anchorX = -1; // Sprite::ax; -1 — привязка в точке pivot
int anchorY = -1; // Sprite::ay
};
// Один клип внутри ассета. Имя приходит только хешем (SF_ANIM_*): строковых
// имён анимаций в игре, как и строковых ID ассетов, быть не должно.
struct AnimClip
{
uint32_t nameHash = 0;
int firstFrame = 0; // индекс первого кадра клипа в плоском списке кадров
int framesPerDir = 0; // кадров на одно направление
};
class SpriteLibrary
{
public:
// Открыть каталог сборки (обычно build/sprites). Файлы читаются целиком
// и распаковываются сразу: битый ассет просто не попадает в набор.
bool Open(const std::string& dir);
void Close();
bool IsOpen() const { return open_; }
const std::string& Dir() const { return dir_; }
int AssetCount() const { return int(assets_.size()); }
// Все кадры ассета скопом, по всем анимациям и направлениям.
int FrameCount(uint32_t assetId) const;
// Кадр как игровой Sprite. false — нет такого ассета или кадра; out в этом
// случае не трогается. Копия: вызывающий волен её пережить и подшаманить
// (так грузятся тайлы, им нужен канонический холст).
bool Read(uint32_t assetId, int frame, const FramePlacement& place, Sprite& out) const;
bool Read(uint32_t assetId, int frame, Sprite& out) const
{
return Read(assetId, frame, FramePlacement{}, out);
}
// --- анимации ---
//
// Раскладка кадров в .sfa НАПРАВЛЕНЧЕСКИ-МАЖОРНАЯ, клипы идут подряд:
// index = clip.firstFrame + dirIndex * clip.framesPerDir + frameInClip
// Поэтому firstFrame анимации — это не ноль, а конец предыдущей.
int AnimationCount(uint32_t assetId) const;
// Клип по хешу имени (SF_ANIM_* из generated/spriteforge_assets.h).
bool FindAnimation(uint32_t assetId, uint32_t animNameHash, AnimClip& out) const;
// Сколько направлений отрисовано у ассета; 1 — вид один на все стороны.
int DirectionCount(uint32_t assetId) const;
// Индекс направления, ближайшего к мировому углу (радианы, как Agent::facing).
// Нет ассета — 0: рисовать всё равно нечего, а ветвиться у вызывающего лишнее.
int DirectionOf(uint32_t assetId, float angleRad) const;
// Кадр анимации: timeSec — время от её начала, loop — зациклить, иначе клип
// замирает на последнем кадре (смерть так и остаётся лежать).
// nullptr — нет такого ассета или анимации.
//
// ВНИМАНИЕ: указатель живёт до ближайшего PollReload(), вернувшего true.
// После пересборки библиотеки все выданные раньше кадры недействительны,
// держать их между кадрами игры нельзя.
const Sprite* Frame(uint32_t assetId, uint32_t animNameHash, int dirIndex,
float timeSec, bool loop) const;
// Длительность клипа целиком, сек. 0 — нет такой анимации либо все кадры
// нулевой длительности (статичная поза).
float AnimationLength(uint32_t assetId, uint32_t animNameHash) const;
// Watch-режим `sf watch` атомарно переписывает reload.json после успешной
// пересборки. Вызов раз в кадр; true — библиотека перечитана и спрайты,
// взятые из неё раньше, устарели. Смена набора анимаций переваривается
// сама собой: пересобирается вся библиотека целиком, а не отдельные кадры.
bool PollReload();
// Имя ассета (оно же имя файла) — только для отладочного отчёта.
const char* NameOf(uint32_t assetId) const;
// Все идентификаторы, отсортированные по имени. Тоже только для отчёта.
std::vector<uint32_t> Ids() const;
private:
struct Asset
{
std::string name;
std::vector<Sprite> frames; // уже RGBA8; индекс — как в .sfa
std::vector<uint16_t> durationsMs; // длительность кадра, параллельно frames
std::vector<AnimClip> clips;
std::vector<int> dirs; // сотые доли градуса, приведённые к [0, 36000)
};
// Отпечаток reload.json: время правки и размер. 0 — файла нет.
struct Stamp
{
int64_t time = 0;
uint64_t size = 0;
bool operator!=(const Stamp& o) const { return time != o.time || size != o.size; }
};
// Разобрать .sfa целиком: все кадры в Sprite, клипы, направления.
// false — файл битый или его раскладка не сходится сама с собой.
bool DecodeAsset(const uint8_t* bytes, size_t size, Asset& out) const;
const Asset* FindAsset(uint32_t assetId) const;
static const AnimClip* FindClip(const Asset& asset, uint32_t animNameHash);
static float ClipLength(const Asset& asset, const AnimClip& clip);
static int FrameInClip(const Asset& asset, const AnimClip& clip, float timeSec, bool loop);
Stamp ReloadStamp() const;
std::unordered_map<uint32_t, Asset> assets_;
std::array<uint8_t, 768> palette_{}; // 256 RGB-троек, индекс 0 прозрачен
std::string dir_;
Stamp stamp_;
bool open_ = false;
};
// Первый каталог сборки, в котором лежит palettes.sfp: рядом с бинарником
// (build/bin -> ../sprites) или относительно корня репозитория. Пустая строка —
// библиотека не собрана.
std::string FindSpriteLibraryDir();