squad-proto/samples/walk/main.cpp
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

158 lines
6.5 KiB
C++
Raw Permalink 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.

// -----------------------------------------------------------------------------
// 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;
}