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
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// Предметы и снаряжение: ЧТО боец несёт на себе.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Разделение то же, что у оружия (sim/weapon_defs.h):
|
|
|
|
|
|
// * ItemDef — неизменяемое описание предмета, одна запись каталога;
|
|
|
|
|
|
// * Inventory — компонент бойца: что в руках, что надето, что в сумке.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Снаряжение НЕ трогает правила боя. Оно даёт бойцу четыре числа —
|
|
|
|
|
|
// броня, скорость, запас здоровья, дальность обзора, — и всё. Никакой предмет
|
|
|
|
|
|
// не может ослабить правило «не бить своих»: у него просто нет такой ручки.
|
|
|
|
|
|
// Это не самоограничение, а следствие устройства: FireWeapons и UpdateMelee
|
|
|
|
|
|
// про Inventory не знают вообще, они читают уже готовые статы агента.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Оружие живёт двумя сущностями: ItemId в инвентаре — это ЧТО взято в руки,
|
|
|
|
|
|
// а сущность-оружие ECS хранит рабочие статы. SyncLoadout переносит одно
|
|
|
|
|
|
// в другое каждый шаг, поэтому «поменять ствол» — это одна запись ItemId.
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
|
|
|
|
|
|
|
#include "sim/weapon_defs.h"
|
|
|
|
|
|
|
|
|
|
|
|
// Сколько патронов в найденной пачке. Не «магазин»: магазины у стволов разные,
|
|
|
|
|
|
// а пачка — это просто запас, из которого магазин потом набивается.
|
|
|
|
|
|
constexpr int AMMO_BOX_ROUNDS = 40;
|
|
|
|
|
|
|
|
|
|
|
|
enum class ItemKind : uint8_t
|
|
|
|
|
|
{
|
|
|
|
|
|
NONE = 0,
|
|
|
|
|
|
FIREARM, // в руки, класс RANGED
|
|
|
|
|
|
BLADE, // в руки, класс MELEE
|
|
|
|
|
|
ARMOR, // на корпус
|
|
|
|
|
|
TRINKET, // в карман: свет, чутьё, живучесть
|
|
|
|
|
|
CONSUMABLE, // расходник
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
enum class ItemId : uint8_t
|
|
|
|
|
|
{
|
|
|
|
|
|
NONE = 0,
|
|
|
|
|
|
|
|
|
|
|
|
// --- стволы ---
|
|
|
|
|
|
RIFLE, PISTOL, SHOTGUN, SMG, MAGNUM, BOW,
|
|
|
|
|
|
// --- клинки ---
|
|
|
|
|
|
SABER, KNIFE, CLEAVER,
|
|
|
|
|
|
// --- броня ---
|
|
|
|
|
|
JACKET, // куртка: почти ничего, зато не мешает
|
|
|
|
|
|
VEST, // жилет: ровный размен брони на скорость
|
|
|
|
|
|
PLATE, // плита: держит удар, но боец становится обозом
|
|
|
|
|
|
SHROUD, // саван: брони нет, зато тише и быстрее
|
|
|
|
|
|
// --- карман ---
|
|
|
|
|
|
LANTERN, // фонарь: видно дальше, и тебя тоже
|
|
|
|
|
|
CHARM, // оберег: запас здоровья
|
|
|
|
|
|
SPURS, // шпоры: скорость
|
|
|
|
|
|
// --- расходники ---
|
|
|
|
|
|
BANDAGE,
|
|
|
|
|
|
STIMPACK,
|
|
|
|
|
|
AMMO, // пачка патронов: уходит в подсумок, когда стрелять нечем
|
Свет как расходник: заряд тратится за путь, а не за время (M23)
Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает
до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он
меняется с тварями местами. Батарея встаёт сама, как бинт.
Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия
считала по секундам, то есть была таймером, которого в игре нет по прямому
требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как
секундомер, только без циферблата, и думать над этажом стало бы дорого.
За расстояние получается тот же размен, что и с весом, но с другой стороны:
вес делает дорогим «унести всё», свет — «обшарить всё».
Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это
был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем
дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды.
Проверка в --descend меряет правило целиком, и первая её строка не про
баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже
был неправ — делил потраченное на смещение отряда, а не на пройденный
путь, и показывал 1.55 за тайл при 1.10 в каталоге.
свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге
глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска
обзор 1.00 -> 0.35 в темноте, батарея встала сама
Хватает ли заряда на этаж считается в этажах и по средней длине забега:
путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы
удачу, а не баланс.
--accept PASS без изменений: в песочнице света бесконечно, как и патронов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
|
|
|
|
BATTERY, // батарея: уходит в фонарь, когда тот сел
|
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
|
|
|
|
|
|
|
|
|
|
COUNT,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct ItemDef
|
|
|
|
|
|
{
|
|
|
|
|
|
const char* name = "-";
|
|
|
|
|
|
ItemKind kind = ItemKind::NONE;
|
|
|
|
|
|
|
|
|
|
|
|
// Что взять в руки, если kind — оружие.
|
|
|
|
|
|
FirearmId gun = FirearmId::RIFLE;
|
|
|
|
|
|
BladeId blade = BladeId::SABER;
|
|
|
|
|
|
|
|
|
|
|
|
// Модификаторы бойца. Складываются по всем надетым предметам.
|
|
|
|
|
|
float armor = 0.0f; // доля поглощённого урона, суммарно жёстко режется
|
|
|
|
|
|
float speedMul = 1.0f; // множитель скорости хода
|
|
|
|
|
|
float hpBonus = 0.0f;
|
|
|
|
|
|
float sightMul = 1.0f; // множитель радиуса обзора (render-only)
|
|
|
|
|
|
|
|
|
|
|
|
float heal = 0.0f; // расходник: сколько здоровья возвращает
|
|
|
|
|
|
int rounds = 0; // расходник: сколько патронов добавляет в запас
|
Свет как расходник: заряд тратится за путь, а не за время (M23)
Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает
до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он
меняется с тварями местами. Батарея встаёт сама, как бинт.
Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия
считала по секундам, то есть была таймером, которого в игре нет по прямому
требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как
секундомер, только без циферблата, и думать над этажом стало бы дорого.
За расстояние получается тот же размен, что и с весом, но с другой стороны:
вес делает дорогим «унести всё», свет — «обшарить всё».
Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это
был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем
дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды.
Проверка в --descend меряет правило целиком, и первая её строка не про
баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже
был неправ — делил потраченное на смещение отряда, а не на пройденный
путь, и показывал 1.55 за тайл при 1.10 в каталоге.
свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге
глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска
обзор 1.00 -> 0.35 в темноте, батарея встала сама
Хватает ли заряда на этаж считается в этажах и по средней длине забега:
путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы
удачу, а не баланс.
--accept PASS без изменений: в песочнице света бесконечно, как и патронов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
|
|
|
|
float charge = 0.0f; // расходник: сколько секунд света возвращает
|
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
|
|
|
|
|
|
|
|
|
|
// Сколько предмет весит. Единица условная, но соотношения — нет: плита
|
|
|
|
|
|
// тяжелее всего, что можно взять в руки, а карманное почти ничего не весит.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Вес — это то, чем «унести всё» перестало быть бесплатным. До него решение
|
|
|
|
|
|
// на выходе сводилось к счёту свободных ячеек, а шесть бинтов и шесть плит
|
|
|
|
|
|
// стоили одинаково (docs/17-extraction.md).
|
|
|
|
|
|
//
|
|
|
|
|
|
// По умолчанию НОЛЬ, а не единица: ItemId::NONE — это пустая ячейка и
|
|
|
|
|
|
// «брони нет», и весить пустота не должна. С единицей боец без снаряжения
|
|
|
|
|
|
// таскал бы три килограмма ничего.
|
|
|
|
|
|
float weight = 0.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Цвет иконки в инвентаре. Нейтральные байты RGB — в sim/ типов рендера нет.
|
|
|
|
|
|
uint8_t r = 200, g = 200, b = 200;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Броня складывается, но потолок жёсткий: полностью неуязвимых в отряде нет.
|
|
|
|
|
|
constexpr float ARMOR_CAP = 0.72f;
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// Переноска. Порогов ДВА, и это главное решение всей вехи.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Один жёсткий предел означал бы «сумка кончилась» — то же самое, что счёт
|
|
|
|
|
|
// ячеек, только считать труднее. Настоящий выбор возникает там, где лишнее
|
|
|
|
|
|
// взять МОЖНО, но за это платят: до нормы отряд идёт как шёл, дальше — тем
|
|
|
|
|
|
// медленнее, чем больше нагрёб. А наверх идти через весь этаж, который уже
|
|
|
|
|
|
// растревожен.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Верхний предел всё-таки есть: без него отряд уносил бы этаж целиком со
|
|
|
|
|
|
// скоростью ноль, и решение снова исчезло бы — только с другой стороны.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Числа выставлены по каталогу лута, а не на глаз. Средняя находка первых
|
|
|
|
|
|
// тиров весит около 1.1, поздних — около 1.7, значит полная сумка из шести
|
|
|
|
|
|
// вещей тянет на 7–10, плюс ствол в руках.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Отсюда пороги. Норма 7.5 — это ствол и три-четыре находки: отряд, который
|
|
|
|
|
|
// взял нужное и ушёл, идёт как шёл. Полная сумка (6.6–10 плюс ствол) неизбежно
|
|
|
|
|
|
// оказывается за нормой — так и задумано: «унести всё» обязано стоить скорости.
|
|
|
|
|
|
// Предел 14 оставляет место найденной плите (6), но с ней в сумке больше почти
|
|
|
|
|
|
// ничего не влезет — и это ровно тот выбор, ради которого вес заводился.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Норму 6 пришлось поднять: прогон показал, что «карабин, сабля, фонарь и
|
|
|
|
|
|
// жилет» весят 6.9, то есть под неё не проходил даже обычный набор. Число
|
|
|
|
|
|
// противоречило собственному замыслу, и заметила это проверка, а не глаз.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Сверяться с прогоном тут ОБЯЗАТЕЛЬНО, но не по столбцу «груз»: `--descend`
|
|
|
|
|
|
// кладёт в сумки бинты, чтобы проверить перенос между этажами, и по ним вес
|
|
|
|
|
|
// выглядит втрое меньше настоящего. Правило проверяется отдельно — там же,
|
|
|
|
|
|
// в прогоне, куском на выдуманном инвентаре.
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
constexpr float CARRY_LIMIT = 7.5f; // до этого веса скорость не страдает
|
|
|
|
|
|
constexpr float CARRY_MAX = 14.0f; // выше не поднимет: руки заняты
|
|
|
|
|
|
constexpr float CARRY_SLOW = 0.55f; // во сколько раз медленнее на пределе
|
|
|
|
|
|
|
|
|
|
|
|
// Множитель скорости от нагрузки. Между нормой и пределом падает линейно:
|
|
|
|
|
|
// нелинейная кривая тут была бы честнее физически и хуже играбельно — игрок
|
|
|
|
|
|
// должен чувствовать «каждая лишняя вещь стоит столько-то».
|
|
|
|
|
|
// bonus — прибавка к норме от улучшений профиля (sim/profile.h). Предел
|
|
|
|
|
|
// двигается вместе с нормой: улучшение обязано увеличивать И то, сколько можно
|
|
|
|
|
|
// нести свободно, И то, сколько вообще можно поднять, — иначе прокачка
|
|
|
|
|
|
// переноски упиралась бы в невидимую стену.
|
|
|
|
|
|
inline float CarryMul(float load, float bonus = 0.0f)
|
|
|
|
|
|
{
|
|
|
|
|
|
const float limit = CARRY_LIMIT + bonus;
|
|
|
|
|
|
const float top = CARRY_MAX + bonus;
|
|
|
|
|
|
if (load <= limit) return 1.0f;
|
|
|
|
|
|
if (load >= top) return CARRY_SLOW;
|
|
|
|
|
|
return 1.0f + (CARRY_SLOW - 1.0f) * (load - limit) / (top - limit);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Свет как расходник: заряд тратится за путь, а не за время (M23)
Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает
до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он
меняется с тварями местами. Батарея встаёт сама, как бинт.
Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия
считала по секундам, то есть была таймером, которого в игре нет по прямому
требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как
секундомер, только без циферблата, и думать над этажом стало бы дорого.
За расстояние получается тот же размен, что и с весом, но с другой стороны:
вес делает дорогим «унести всё», свет — «обшарить всё».
Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это
был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем
дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды.
Проверка в --descend меряет правило целиком, и первая её строка не про
баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже
был неправ — делил потраченное на смещение отряда, а не на пройденный
путь, и показывал 1.55 за тайл при 1.10 в каталоге.
свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге
глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска
обзор 1.00 -> 0.35 в темноте, батарея встала сама
Хватает ли заряда на этаж считается в этажах и по средней длине забега:
путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы
удачу, а не баланс.
--accept PASS без изменений: в песочнице света бесконечно, как и патронов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// Свет. ВТОРОЙ ресурс вылазки после патронов.
|
|
|
|
|
|
//
|
|
|
|
|
|
// ГЛАВНОЕ РЕШЕНИЕ: заряд тратится за ПРОЙДЕННОЕ РАССТОЯНИЕ, а не за время.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Расход по секундам был бы таймером — тем самым, которого в этой игре нет по
|
|
|
|
|
|
// прямому требованию к жанру (docs/17-extraction.md): давит исчерпание, а не
|
|
|
|
|
|
// часы. Фонарь, садящийся сам по себе, торопил бы игрока ровно как таймер,
|
|
|
|
|
|
// только без циферблата: думать стало бы дорого, а думать здесь и есть игра.
|
|
|
|
|
|
//
|
|
|
|
|
|
// За расстояние — совсем другое решение и ровно то, которое нужно: стоять и
|
|
|
|
|
|
// разглядывать этаж бесплатно, а вот ОБОЙТИ его весь ради лута стоит света.
|
|
|
|
|
|
// Тот же выбор, что и вес, только с другой стороны: «унести всё» стоит
|
|
|
|
|
|
// скорости, «обшарить всё» стоит темноты. Прямой путь к спуску дёшев, полная
|
|
|
|
|
|
// зачистка — дорога.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Сел фонарь — обзор падает до DARK_SIGHT_MUL, а твари начинают замечать отряд
|
|
|
|
|
|
// дальше. Отряд не слепнет насовсем (иначе игра просто кончалась бы) — он
|
|
|
|
|
|
// меняется с тварями местами: теперь первым видит не он.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Заряд считается ПО ОТРЯДУ, а не по фонарю в кармане каждого: отряд ходит
|
|
|
|
|
|
// одной кучей, и пять личных фонарей были бы пятью копиями одного числа.
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// Единица заряда — тайл пути на первом этаже. Число выставлено по РАЗМЕРУ
|
|
|
|
|
|
// ЭТАЖА, а не на глаз: путь до спуска на первых этажах — шесть-восемь десятков
|
|
|
|
|
|
// тайлов, к восьмому — полторы сотни. Полного заряда должно хватать на этаж с
|
|
|
|
|
|
// осмотром и НЕ хватать на несколько этажей подряд, иначе свет ничего не
|
|
|
|
|
|
// связывает. Проверяется в `--descend`: там он сравнивается с длиной пути до
|
|
|
|
|
|
// спуска на текущей глубине.
|
|
|
|
|
|
constexpr float LIGHT_FULL = 420.0f;
|
|
|
|
|
|
constexpr float DARK_SIGHT_MUL = 0.35f; // во что превращается обзор без света
|
|
|
|
|
|
constexpr float DARK_AGGRO_MUL = 1.35f; // насколько дальше твари замечают в темноте
|
|
|
|
|
|
|
|
|
|
|
|
// Сколько заряда съедает один тайл пути. Чем глубже, тем дороже шаг: на сотом
|
|
|
|
|
|
// этаже полного заряда хватает вдвое меньше, чем на первом, — а сами этажи там
|
|
|
|
|
|
// больше. Расход растёт с глубиной по той же причине, что и всё остальное:
|
|
|
|
|
|
// иначе к лейтгейму свет перестал бы быть ресурсом.
|
|
|
|
|
|
inline float LightDrain(int depth)
|
|
|
|
|
|
{
|
|
|
|
|
|
return 1.0f + 0.012f * float(depth > 1 ? depth : 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
inline ItemDef ItemDefOf(ItemId id)
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemDef d;
|
|
|
|
|
|
switch (id)
|
|
|
|
|
|
{
|
|
|
|
|
|
// --- стволы ---
|
|
|
|
|
|
case ItemId::RIFLE:
|
|
|
|
|
|
d.name = "CARBINE"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::RIFLE;
|
|
|
|
|
|
d.weight = 2.2f;
|
|
|
|
|
|
d.r = 208; d.g = 196; d.b = 150; break;
|
|
|
|
|
|
case ItemId::PISTOL:
|
|
|
|
|
|
d.name = "PISTOL"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::PISTOL;
|
|
|
|
|
|
d.weight = 0.7f;
|
|
|
|
|
|
d.r = 190; d.g = 180; d.b = 168; break;
|
|
|
|
|
|
case ItemId::SHOTGUN:
|
|
|
|
|
|
d.name = "SHOTGUN"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::SHOTGUN;
|
|
|
|
|
|
d.weight = 2.8f;
|
|
|
|
|
|
d.r = 214; d.g = 158; d.b = 104; break;
|
|
|
|
|
|
case ItemId::SMG:
|
|
|
|
|
|
d.name = "SMG"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::SMG;
|
|
|
|
|
|
d.weight = 1.6f;
|
|
|
|
|
|
d.r = 176; d.g = 200; d.b = 220; break;
|
|
|
|
|
|
case ItemId::MAGNUM:
|
|
|
|
|
|
d.name = "MAGNUM"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::MAGNUM;
|
|
|
|
|
|
d.weight = 1.5f;
|
|
|
|
|
|
d.r = 226; d.g = 210; d.b = 176; break;
|
|
|
|
|
|
case ItemId::BOW:
|
|
|
|
|
|
d.name = "BOW"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::BOW;
|
|
|
|
|
|
d.weight = 1.4f;
|
|
|
|
|
|
d.r = 154; d.g = 190; d.b = 164; break;
|
|
|
|
|
|
|
|
|
|
|
|
// --- клинки ---
|
|
|
|
|
|
case ItemId::SABER:
|
|
|
|
|
|
d.name = "SABER"; d.kind = ItemKind::BLADE; d.blade = BladeId::SABER;
|
|
|
|
|
|
d.weight = 1.2f;
|
|
|
|
|
|
d.r = 200; d.g = 212; d.b = 228; break;
|
|
|
|
|
|
case ItemId::KNIFE:
|
|
|
|
|
|
d.name = "KNIFE"; d.kind = ItemKind::BLADE; d.blade = BladeId::KNIFE;
|
|
|
|
|
|
d.speedMul = 1.06f;
|
|
|
|
|
|
d.weight = 0.4f;
|
|
|
|
|
|
d.r = 214; d.g = 222; d.b = 232; break;
|
|
|
|
|
|
case ItemId::CLEAVER:
|
|
|
|
|
|
d.name = "CLEAVER"; d.kind = ItemKind::BLADE; d.blade = BladeId::CLEAVER;
|
|
|
|
|
|
d.speedMul = 0.94f;
|
|
|
|
|
|
d.weight = 2.2f;
|
|
|
|
|
|
d.r = 196; d.g = 174; d.b = 160; break;
|
|
|
|
|
|
|
|
|
|
|
|
// --- броня ---
|
|
|
|
|
|
case ItemId::JACKET:
|
|
|
|
|
|
d.name = "JACKET"; d.kind = ItemKind::ARMOR;
|
|
|
|
|
|
d.armor = 0.10f; d.speedMul = 1.00f;
|
|
|
|
|
|
d.weight = 1.2f;
|
|
|
|
|
|
d.r = 122; d.g = 112; d.b = 96; break;
|
|
|
|
|
|
case ItemId::VEST:
|
|
|
|
|
|
d.name = "VEST"; d.kind = ItemKind::ARMOR;
|
|
|
|
|
|
d.armor = 0.28f; d.speedMul = 0.95f; d.hpBonus = 10.0f;
|
|
|
|
|
|
d.weight = 3.0f;
|
|
|
|
|
|
d.r = 108; d.g = 118; d.b = 108; break;
|
|
|
|
|
|
case ItemId::PLATE:
|
|
|
|
|
|
d.name = "PLATE"; d.kind = ItemKind::ARMOR;
|
|
|
|
|
|
d.armor = 0.48f; d.speedMul = 0.82f; d.hpBonus = 25.0f;
|
|
|
|
|
|
d.weight = 6.0f;
|
|
|
|
|
|
d.r = 140; d.g = 146; d.b = 156; break;
|
|
|
|
|
|
case ItemId::SHROUD:
|
|
|
|
|
|
d.name = "SHROUD"; d.kind = ItemKind::ARMOR;
|
|
|
|
|
|
d.armor = 0.04f; d.speedMul = 1.12f; d.sightMul = 0.92f;
|
|
|
|
|
|
d.weight = 0.8f;
|
|
|
|
|
|
d.r = 88; d.g = 84; d.b = 100; break;
|
|
|
|
|
|
|
|
|
|
|
|
// --- карман ---
|
|
|
|
|
|
case ItemId::LANTERN:
|
|
|
|
|
|
d.name = "LANTERN"; d.kind = ItemKind::TRINKET;
|
|
|
|
|
|
d.sightMul = 1.35f;
|
|
|
|
|
|
d.weight = 0.5f;
|
|
|
|
|
|
d.r = 240; d.g = 206; d.b = 130; break;
|
|
|
|
|
|
case ItemId::CHARM:
|
|
|
|
|
|
d.name = "CHARM"; d.kind = ItemKind::TRINKET;
|
|
|
|
|
|
d.hpBonus = 30.0f;
|
|
|
|
|
|
d.weight = 0.3f;
|
|
|
|
|
|
d.r = 196; d.g = 130; d.b = 190; break;
|
|
|
|
|
|
case ItemId::SPURS:
|
|
|
|
|
|
d.name = "SPURS"; d.kind = ItemKind::TRINKET;
|
|
|
|
|
|
d.speedMul = 1.14f;
|
|
|
|
|
|
d.weight = 0.3f;
|
|
|
|
|
|
d.r = 198; d.g = 184; d.b = 120; break;
|
|
|
|
|
|
|
|
|
|
|
|
// --- расходники ---
|
|
|
|
|
|
case ItemId::BANDAGE:
|
|
|
|
|
|
d.name = "BANDAGE"; d.kind = ItemKind::CONSUMABLE; d.heal = 25.0f;
|
|
|
|
|
|
d.weight = 0.2f;
|
|
|
|
|
|
d.r = 216; d.g = 208; d.b = 196; break;
|
|
|
|
|
|
case ItemId::STIMPACK:
|
|
|
|
|
|
d.name = "STIM"; d.kind = ItemKind::CONSUMABLE; d.heal = 60.0f;
|
|
|
|
|
|
d.weight = 0.25f;
|
|
|
|
|
|
d.r = 130; d.g = 220; d.b = 170; break;
|
|
|
|
|
|
case ItemId::AMMO:
|
|
|
|
|
|
d.name = "AMMO"; d.kind = ItemKind::CONSUMABLE; d.rounds = AMMO_BOX_ROUNDS;
|
|
|
|
|
|
d.weight = 1.0f;
|
|
|
|
|
|
d.r = 200; d.g = 176; d.b = 96; break;
|
Свет как расходник: заряд тратится за путь, а не за время (M23)
Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает
до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он
меняется с тварями местами. Батарея встаёт сама, как бинт.
Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия
считала по секундам, то есть была таймером, которого в игре нет по прямому
требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как
секундомер, только без циферблата, и думать над этажом стало бы дорого.
За расстояние получается тот же размен, что и с весом, но с другой стороны:
вес делает дорогим «унести всё», свет — «обшарить всё».
Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это
был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем
дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды.
Проверка в --descend меряет правило целиком, и первая её строка не про
баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже
был неправ — делил потраченное на смещение отряда, а не на пройденный
путь, и показывал 1.55 за тайл при 1.10 в каталоге.
свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге
глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска
обзор 1.00 -> 0.35 в темноте, батарея встала сама
Хватает ли заряда на этаж считается в этажах и по средней длине забега:
путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы
удачу, а не баланс.
--accept PASS без изменений: в песочнице света бесконечно, как и патронов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
|
|
|
|
case ItemId::BATTERY:
|
|
|
|
|
|
d.name = "BATTERY"; d.kind = ItemKind::CONSUMABLE; d.charge = LIGHT_FULL;
|
|
|
|
|
|
d.weight = 0.3f;
|
|
|
|
|
|
d.r = 150; d.g = 210; d.b = 200; break;
|
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
|
|
|
|
|
|
|
|
|
|
default: break;
|
|
|
|
|
|
}
|
|
|
|
|
|
return d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
inline const char* ItemName(ItemId id) { return ItemDefOf(id).name; }
|
|
|
|
|
|
|
|
|
|
|
|
inline bool IsWeaponItem(ItemId id)
|
|
|
|
|
|
{
|
|
|
|
|
|
const ItemKind k = ItemDefOf(id).kind;
|
|
|
|
|
|
return k == ItemKind::FIREARM || k == ItemKind::BLADE;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- инвентарь ----------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// Сумка — ОДНА НА ОТРЯД, а не шесть ячеек на каждого.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Раньше каждый боец таскал своё, и это порождало ровно одну проблему, которую
|
|
|
|
|
|
// нечем было решить: пачка патронов, поднятая бойцом с клинком, доставалась
|
|
|
|
|
|
// только ему и лежала мёртвым грузом до конца забега. Перекладывать между
|
|
|
|
|
|
// бойцами игра не умела, и научить её значило бы завести целый обмен.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Отряд ходит одной кучей и дерётся как одно целое — пусть и несёт как одно
|
|
|
|
|
|
// целое. Личным остаётся НАДЕТОЕ: руки, броня, карман. Их не разделить, они и
|
|
|
|
|
|
// есть «кто этот боец».
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
constexpr int SQUAD_BAG_SLOTS = 16;
|
|
|
|
|
|
|
|
|
|
|
|
struct SquadBag
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemId slots[SQUAD_BAG_SLOTS] = {};
|
|
|
|
|
|
|
|
|
|
|
|
float Load() const
|
|
|
|
|
|
{
|
|
|
|
|
|
float w = 0.0f;
|
|
|
|
|
|
for (int i = 0; i < SQUAD_BAG_SLOTS; ++i) w += ItemDefOf(slots[i]).weight;
|
|
|
|
|
|
return w;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int Count() const
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = 0;
|
|
|
|
|
|
for (int i = 0; i < SQUAD_BAG_SLOTS; ++i)
|
|
|
|
|
|
if (slots[i] != ItemId::NONE) ++n;
|
|
|
|
|
|
return n;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int FreeSlot() const
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < SQUAD_BAG_SLOTS; ++i)
|
|
|
|
|
|
if (slots[i] == ItemId::NONE) return i;
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int Find(ItemKind kind) const
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < SQUAD_BAG_SLOTS; ++i)
|
|
|
|
|
|
if (slots[i] != ItemId::NONE && ItemDefOf(slots[i]).kind == kind) return i;
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Влезет ли ещё одна вещь. Ячейки — это «сколько разных вещей», вес —
|
|
|
|
|
|
// «сколько они тянут» на КАЖДОГО, и одно другое не заменяет.
|
|
|
|
|
|
//
|
|
|
|
|
|
// carriers — сколько живых бойцов делят ношу. Меньше живых — тяжелее всем,
|
|
|
|
|
|
// и это не побочный эффект: потерять бойца значит взвалить его долю на
|
|
|
|
|
|
// остальных, и обратно наверх они пойдут медленнее.
|
|
|
|
|
|
bool CanTake(ItemId id, int carriers, float weaponLoad, float bonus = 0.0f) const
|
|
|
|
|
|
{
|
|
|
|
|
|
if (FreeSlot() < 0) return false;
|
|
|
|
|
|
const int n = (carriers > 0) ? carriers : 1;
|
|
|
|
|
|
return weaponLoad + (Load() + ItemDefOf(id).weight) / float(n) <= CARRY_MAX + bonus;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool Take(ItemId id, int carriers, float weaponLoad, float bonus = 0.0f)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!CanTake(id, carriers, weaponLoad, bonus)) return false;
|
|
|
|
|
|
const int slot = FreeSlot();
|
|
|
|
|
|
if (slot < 0) return false;
|
|
|
|
|
|
slots[slot] = id;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Clear()
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < SQUAD_BAG_SLOTS; ++i) slots[i] = ItemId::NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Компонент ECS на сущности бойца: только НАДЕТОЕ.
|
|
|
|
|
|
struct Inventory
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemId weapon = ItemId::RIFLE; // в руках
|
|
|
|
|
|
ItemId armor = ItemId::NONE;
|
|
|
|
|
|
ItemId trinket = ItemId::NONE;
|
|
|
|
|
|
|
|
|
|
|
|
// Суммарные модификаторы надетого. Оружие в руках тоже считается: тесак
|
|
|
|
|
|
// весит, нож — нет.
|
|
|
|
|
|
float Armor() const
|
|
|
|
|
|
{
|
|
|
|
|
|
const float a = ItemDefOf(armor).armor + ItemDefOf(trinket).armor;
|
|
|
|
|
|
return (a < ARMOR_CAP) ? a : ARMOR_CAP;
|
|
|
|
|
|
}
|
|
|
|
|
|
float SpeedMul() const
|
|
|
|
|
|
{
|
|
|
|
|
|
return ItemDefOf(armor).speedMul * ItemDefOf(trinket).speedMul *
|
|
|
|
|
|
ItemDefOf(weapon).speedMul;
|
|
|
|
|
|
}
|
|
|
|
|
|
float HpBonus() const { return ItemDefOf(armor).hpBonus + ItemDefOf(trinket).hpBonus; }
|
Свет как расходник: заряд тратится за путь, а не за время (M23)
Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает
до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он
меняется с тварями местами. Батарея встаёт сама, как бинт.
Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия
считала по секундам, то есть была таймером, которого в игре нет по прямому
требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как
секундомер, только без циферблата, и думать над этажом стало бы дорого.
За расстояние получается тот же размен, что и с весом, но с другой стороны:
вес делает дорогим «унести всё», свет — «обшарить всё».
Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это
был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем
дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды.
Проверка в --descend меряет правило целиком, и первая её строка не про
баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже
был неправ — делил потраченное на смещение отряда, а не на пройденный
путь, и показывал 1.55 за тайл при 1.10 в каталоге.
свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге
глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска
обзор 1.00 -> 0.35 в темноте, батарея встала сама
Хватает ли заряда на этаж считается в этажах и по средней длине забега:
путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы
удачу, а не баланс.
--accept PASS без изменений: в песочнице света бесконечно, как и патронов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
|
|
|
|
|
|
|
|
|
|
// lanternLit = у отряда ещё есть заряд. Погасший фонарь не даёт НИЧЕГО:
|
|
|
|
|
|
// без этого он продолжал бы светить на треть от своих 35% и «сел» значило
|
|
|
|
|
|
// бы «стал слабее», а не «кончился».
|
|
|
|
|
|
float SightMul(bool lanternLit = true) const
|
|
|
|
|
|
{
|
|
|
|
|
|
const float tr = (!lanternLit && trinket == ItemId::LANTERN)
|
|
|
|
|
|
? 1.0f
|
|
|
|
|
|
: ItemDefOf(trinket).sightMul;
|
|
|
|
|
|
return ItemDefOf(armor).sightMul * tr;
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
// Что боец несёт САМ — ствол в руках. Доля общей сумки прибавляется снаружи
|
|
|
|
|
|
// (SyncLoadout): инвентарь про отряд не знает.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Надетое сюда НЕ входит, и это не поблажка. За броню уже заплачено
|
|
|
|
|
|
// множителем скорости (плита — 0.82), и считать её вес вторично значило бы
|
|
|
|
|
|
// наказывать за один выбор дважды. Зато плита, НАЙДЕННАЯ и лежащая в
|
|
|
|
|
|
// сумке, весит полной мерой: тащить её наверх — это и есть выбор.
|
|
|
|
|
|
float Load() const { return ItemDefOf(weapon).weight; }
|
|
|
|
|
|
};
|