squad-proto/engine/app.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

93 lines
4.7 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.

#pragma once
// -----------------------------------------------------------------------------
// Приложение Tile2D: окно, кадр и ход времени.
//
// До этого окно, безрамочный режим, фреймбуфер, блиттер и fixed timestep жили
// двумя почти одинаковыми копиями — в игре и в редакторе. Третьему приложению
// пришлось бы написать третью, и она бы уже отличалась: где-то забытый
// `SetExitKey`, где-то другой потолок шагов, где-то апскейл не целым числом.
//
// Здесь НЕ инверсия управления: цикл остаётся у приложения, а App отвечает на
// вопросы «пора ли закрываться», «сколько шагов симуляции прошло» и «куда лёг
// кадр в окне». Колбэки сделали бы порядок «шаг — рендер — интерфейс»
// невидимым, а он у каждого приложения свой и в нём вся суть.
//
// Типичный цикл целиком:
//
// App app;
// app.Init({"Моя игра"});
// while (app.NextFrame())
// {
// for (int i = 0, n = app.Advance(false); i < n; ++i) game.Step(FIXED_DT);
// Render(game, app.fb, app.Alpha());
// app.Present(); // кадр на экран
// DrawUi(app.fit); // поверх кадра, примитивами raylib
// app.Finish();
// }
// app.Shutdown();
// -----------------------------------------------------------------------------
#include "engine/config.h"
#include "engine/framebuffer.h"
struct AppConfig
{
const char* title = "Tile2D";
// Безрамочное на весь монитор — обычный режим. Окно нужно для отладки и
// для съёмки картинок: на полном экране их не сравнить между собой.
bool borderless = true;
bool vsync = true;
// Разрешение кадра. По умолчанию — размер из engine/config.h.
int frameW = INTERNAL_W;
int frameH = INTERNAL_H;
};
struct App
{
Framebuffer fb;
ScreenBlitter screen;
// --- состояние текущего кадра ---------------------------------------------
int winW = 0;
int winH = 0;
// Куда лёг кадр в окне: на безрамочном экране он центрирован и вокруг него
// чёрные поля. Интерфейс и оверлей обязаны считать координаты ЧЕРЕЗ это.
ScreenRect fit;
float frameDt = 0.0f;
// Приложение может попроситься наружу само (пункт меню «выход», Esc в
// редакторе). Отдельный флаг, а не возврат из NextFrame: решение о выходе
// принимается в середине кадра, а закрыться надо в его конце.
bool quit = false;
bool Init(const AppConfig& cfg);
void Shutdown();
// Начало кадра: закрытие окна, F11, размеры, ScreenFitRect, dt.
// false — пора выходить.
bool NextFrame();
// Сколько раз выполнить шаг симуляции в этом кадре. Шаг постоянный
// (FIXED_DT), лишнее время копится до следующего кадра, потолок
// MAX_STEPS_PER_FRAME спасает от спирали смерти.
//
// halted — пауза: время НЕ копится. Иначе после меню симуляция догоняла бы
// пропущенные секунды одним рывком.
int Advance(bool halted);
// Доля незакрытого шага, [0..1). По ней интерполируется картинка: рендер
// идёт чаще симуляции, и без неё движение дёргалось бы на 60+ Гц.
float Alpha() const;
void Present(); // BeginDrawing + кадр на экран; дальше можно рисовать UI
void Finish(); // EndDrawing
// Снять кадр в файл. Здесь, а не в приложении, потому что снимать надо
// ПОСЛЕ Finish, и об этом легко забыть.
void Screenshot(const char* file) const;
private:
double accumulator = 0.0;
};