squad-proto/samples/walk/main.cpp

158 lines
6.5 KiB
C++
Raw 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
// -----------------------------------------------------------------------------
// Tile2D walk — самое маленькое приложение, которое имеет смысл.
//
// Ни игры, ни редактора: открыть проект, собрать этаж, походить по нему
// камерой. Существует ради одного вопроса — сколько кода нужно, чтобы что-то
// показать на этом движке. Ответ виден целиком на одном экране, и если он
// однажды перестанет туда влезать, это будет значить, что движок оброс тем,
// что должно было остаться в игре.
//
// Проверяет заодно то, что нельзя проверить ни игрой, ни редактором: движок
// собирается и работает БЕЗ них. Игра могла бы незаметно унести к себе что-то
// общее — тайлсет, проекцию, цикл, — и никто бы не заметил, пока не появился
// второй потребитель. Вот он.
//
// tile2d_walk [--project <имя>] [--depth N] [--seed N] [--windowed] [--shot N]
// WASD / стрелки — идти, R — пересобрать этаж, Esc — выход
// -----------------------------------------------------------------------------
#include <raylib.h>
#include <cstdio>
#include <cstring>
#include "engine/app.h"
#include "engine/catalog.h"
#include "engine/level_gen.h"
#include "engine/project.h"
#include "engine/room.h"
#include "engine/tilemap.h"
#include "engine/tilemap_draw.h"
#include "engine/tileset.h"
#include "engine/view2d.h"
int main(int argc, char** argv)
{
const char* projectName = nullptr;
int depth = 1;
uint32_t seed = 1;
bool windowed = false;
int shotFrame = -1; // --shot N: снять кадр и выйти (картинки в документацию)
for (int i = 1; i < argc; ++i)
{
if (std::strcmp(argv[i], "--project") == 0 && i + 1 < argc) projectName = argv[++i];
else if (std::strcmp(argv[i], "--depth") == 0 && i + 1 < argc) depth = std::atoi(argv[++i]);
else if (std::strcmp(argv[i], "--seed") == 0 && i + 1 < argc)
seed = uint32_t(std::atoi(argv[++i]));
else if (std::strcmp(argv[i], "--windowed") == 0) windowed = true;
else if (std::strcmp(argv[i], "--shot") == 0 && i + 1 < argc)
shotFrame = std::atoi(argv[++i]);
}
// --- проект: пути, вид отображения, кривая глубины -------------------------
Project project;
std::string why;
const std::string dir = FindProjectDir(projectName ? projectName : "");
if (dir.empty() || !project.Load(dir, &why))
{
std::printf("проект не открыт: %s\n", dir.empty() ? "не выбран" : why.c_str());
for (const std::string& n : AvailableProjects()) std::printf(" доступен: %s\n", n.c_str());
return 1;
}
project.Apply(); // ставит проекцию и кривую на движок
EntityCatalog catalog;
RoomLibrary rooms;
if (!catalog.Load(project.catalogPath, &why) || !rooms.Load(project.roomsDir, &why))
{
std::printf("содержимое не читается: %s\n", why.c_str());
return 1;
}
// --- этаж -----------------------------------------------------------------
static Tilemap map;
static GeneratedLevel level;
auto build = [&]() {
if (GenerateLevel(rooms, catalog, DescribeFloor(depth), seed, map, level, &why)) return true;
std::printf("этаж не собрался: %s\n", why.c_str());
return false;
};
if (!build()) return 1;
std::printf("%s: этаж %d (%s), сид %u, комнат %d, тварей %d\n", project.name.c_str(),
level.floor.depth, ThemeName(level.floor.theme), unsigned(seed), level.roomsPlaced,
level.monstersPlaced);
// --- окно и цикл ----------------------------------------------------------
App app;
AppConfig cfg;
cfg.title = "Tile2D walk";
cfg.borderless = !windowed;
if (!app.Init(cfg)) return 1;
TileSet tiles;
tiles.Generate();
v2d::Camera cam;
cam.SnapTo(level.squadStart);
Vec2 pos = level.squadStart;
while (app.NextFrame())
{
// Ввод в ЭКРАННЫХ осях переводится в мировые проекцией — тем же
// способом, что и в игре. Без этого WASD в изометрии ведёт вбок.
Vec2 move{};
if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) move.x -= 1.0f;
if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) move.x += 1.0f;
if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) move.y -= 1.0f;
if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) move.y += 1.0f;
const int steps = app.Advance(false);
for (int i = 0; i < steps; ++i)
{
if (LengthSq(move) > 0.0f)
{
const Vec2 step = v2d::ScreenDirToWorld(move);
pos = pos + step * (6.0f * FIXED_DT);
map.ResolveCircle(pos, 0.3f); // сквозь стены не ходим
}
}
if (IsKeyPressed(KEY_R))
{
++seed;
if (build()) { pos = level.squadStart; cam.SnapTo(pos); }
}
if (IsKeyPressed(KEY_ESCAPE)) app.quit = true;
cam.Follow(pos, app.frameDt, 9.0f);
int ox = 0, oy = 0;
cam.ScreenOffset(ox, oy);
app.fb.Clear(RGBA(8, 8, 10));
DrawTilemap(app.fb, map, tiles, ox, oy);
// Где стоим, где спуск. Кружком, а не спрайтом: спрайты — дело игры,
// движок про то, как выглядит отряд, не знает ничего.
auto dot = [&](Vec2 w, uint32_t c) {
const Vec2 s = v2d::WorldToScreen(w);
app.fb.CircleFilled(int(s.x) + ox, int(s.y) + oy, 3, c);
};
dot(level.exitPos, RGBA(60, 200, 90));
dot(pos, RGBA(240, 230, 200));
app.Present();
app.Finish();
if (shotFrame >= 0 && --shotFrame <= 0)
{
app.Screenshot("walk.png");
break;
}
}
app.Shutdown();
return 0;
}