Десять вех одним состоянием: разделять их пофайлово нечем, потому что
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>
161 lines
9.1 KiB
C++
161 lines
9.1 KiB
C++
#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();
|