1. 08.10.2026 1 коммит
  2. 07.10.2026 25 коммитов
    • Nikitos's avatar
      Фазы 6-8: утверждения $.expect, реактивные запросы $.watch, DevTools...
      · e8ae8bcb
      Nikitos создал
      Фазы 6-8: утверждения $.expect, реактивные запросы $.watch, DevTools $.devtools на RmlUi, engine.ui.loadMarkup v0.1.16
      e8ae8bcb
    • Nikitos's avatar
      Запросы в радиусе и within(), команды query/inspect/profile, запись и...
      · d56510dd
      Nikitos создал
      Запросы в радиусе и within(), команды query/inspect/profile, запись и воспроизведение ввода (--record/--replay), законы движка и правила агентов, сверка доков с кодом v0.1.15
      d56510dd
    • Nikitos's avatar
      Наследование от родителя: visible, alpha и depthRelative
      · c5b87e1a
      Nikitos создал
      Закрывает пункт §11 «z_as_relative и наследование visible/modulate родителем».
      
      ЧТО БЫЛО НЕ ТАК: дети в `$` — отдельные узлы ПЛОСКОГО реестра, поэтому скрытый
      контейнер НЕ скрывал содержимое, а прозрачность родителя на детей не влияла:
      гасишь панель — надписи остаются. Это самая частая причина «почему надпись не
      исчезла».
      
      ЧТО СДЕЛАНО (чистые функции в render.js, экспортированы для тестов):
      * inheritedVisible(node) — скрыт ЛЮБОЙ предок → не виден никто из его детей;
      * inheritedAlpha(node) — произведение alpha по цепочке (modulate): 0.5 × 0.5
        = 0.25, ноль обнуляет всё;
      * effectiveDepth(node) — складывается с родителем, если узел помечен
        depth_relative; по умолчанию глубина АБСОЛЮТНАЯ, как была.
      
      Применение — в ОДНОЙ точке: drawWorldNode считает эффективные видимость и
      прозрачность, а на время отрисовки узла подменяет node.alpha эффективным
      значением. Так все внутренние ветки (свет, частицы, текст, контуры) видят
      правильное значение, и не пришлось править каждую из них.
      
      API: `.depthRelative(true)`, `.isDepthRelative()`, `.effectiveAlpha()`,
      `.effectivelyVisible()`, `.effectiveDepth()`, `$.gfx.effectiveAlpha/
      effectiveVisible/effectiveDepth`.
      
      ПРОВЕРКА: tests/js/light_test.mjs — 6 новых чистых тестов (50 всего): скрытый
      родитель, скрытый дед, произведение прозрачностей, цепочка относительных
      глубин, абсолютный родитель как база.
      tests/agent/highlevel_inherit_test.py — 15 проверок ПО ПИКСЕЛЯМ: ребёнок виден
      сам; hide(родителя) убирает его полностью (было: не убирал); alpha 0.5 делает
      ТУСКЛЕЕ; alpha 0 — убирает; 0.5 × 0.5 = 0.25; depthRelative даёт 15 при
      родительских 10 и своих 5, а без флага — 5.
      
      МОЯ ЖЕ ОШИБКА В ТЕСТЕ: сначала мерил «тусклее» количеством зелёных точек — их
      столько же (порог по цвету проходят и тусклые пиксели). Теперь мерю ЯРКОСТЬ:
      2668800 против 5316750. Отмечено комментарием в тесте.
      
      ДОКИ: render.md §5.2 (таблица, примеры, отладка, почему без кэша),
      HIGH_LEVEL_API.md (строки в списках), TASKS §11.
      
      Полный qjs, агентские тесты, doc_coverage, doc_claims, duplicate_keys, быстрый
      набор — зелёные.
      c5b87e1a
    • Nikitos's avatar
      Реплеи: запись ввода и воспроизведение ($.replay), тесты молчат
      · 8ad81d61
      Nikitos создал
      P2-хвост §12. Детерминизм в движке был (--seed, --fixed-dt, свои генераторы в
      $.random), но записать и проиграть сессию было нечем: к баг-репорту нельзя было
      приложить реплей, а регресс-тест «мир пришёл туда же» — поставить.
      
      * src/highlevel/replay.js: createReplay (чистое ядро) + installReplay ($.replay):
        start/record/stop для записи, play/tick/skip для воспроизведения, load/toText
        для файла, mode/length/position/dropped/header/frames/at для контроля, onEnd
        для конца; compareReplays находит первое расхождение двух записей;
      * заголовок хранит ЗЕРНО ЗАПУСКА и ШАГ ВРЕМЕНИ (engine.seed/engine.fixedDt) —
        без них воспроизведение не совпадёт; игра добавляет свои поля через start();
      * пустой кадр СОХРАНЯЕТСЯ (пропуск сломал бы соответствие кадр записи ↔ кадр
        проигрывания), несериализуемый объект становится пустым кадром, кадр больше
        4096 байт не пишется, предел 36000 кадров с честным счётчиком dropped;
      * тесты: tests/js/replay_test.mjs (11) и tests/agent/highlevel_replay_test.py
        (в движке — номер кадра, зерно запуска, шаг времени, текст записи);
      * docs/highlevel/replay.md: методы, заголовок, «что записывать» и честные
        ограничения (реплей — ввод, а не снимок мира; нет сжатия; нумерация кадров не
        сверяется при проигрывании).
      
      ПРИМЕР В ДОКЕ ИСПРАВЛЕН ПО КОДУ: сначала я написал `$.input.state()` и
      `$.input.apply()`, которых в API НЕТ. Проверил grep'ом и переписал пример на
      честный (игра собирает снимок ввода сама) — иначе это было бы очередное врание
      в документации, ровно то, что вычищал в §11.
      
      ЗАОДНО ПО ПРОСЬБЕ: тесты больше не шумят. tools/agent_client.py подставляет
      SDL_AUDIODRIVER=dummy, если драйвер не задан явно; микшер при этом работает
      по-настоящему (44.1 кГц вместо 48 кГц — выбор dummy-драйвера), проверки звука не
      ломаются. Прогнать со звуком: R2D_TEST_AUDIO=real python3 tests/agent/<тест>.py.
      
      Проверка: полный набор qjs, агентские тесты (включая новый replay), модули,
      doc_coverage, doc_claims — зелёные.
      8ad81d61
    • Nikitos's avatar
      Документация §11.4: страницы и проверки для всех модулей, страж покрытия
      · acdb3b59
      Nikitos создал
      Закрыта последняя дыра §11: у 17 модулей не было страницы, у десяти — теста.
      
      * написаны 21 страница docs/highlevel/<имя>.md: acoustics, agent, api, bootstrap,
        bsp, camera, core, debug, index, input, scene, sound, soundbank, steps, store,
        story_script, time, ui, viewport, window, world — у каждой методы, пример,
        раздел ограничений и ссылка на проверку;
      * добавлены проверки: bsp_test.mjs (8 — перевод данных: плоский массив ↔
        объекты, все принимаемые формы отрезков, метки), loading_test.mjs (11 — экран
        загрузки на макете $: узлы, полоса, прогресс 0..1, шаги по одному за кадр) и
        small_modules_test.mjs (4 — bootstrap кладёт $ в globalThis и ставит метку,
        index отдаёт ТОТ ЖЕ объект, script пробрасывает запрос перезагрузки);
      * tests/doc_coverage_test.py — страж: модуль без страницы, без проверки или со
        страницей-пустышкой валит тест. Раньше об этом узнавали только аудитом;
      * HIGH_LEVEL_API.md и §11.4 обновлены.
      
      НАЙДЕН И ИСПРАВЛЕН БАГ: $.loading.run() обращался к голому `$` внутри модуля
      (там его нет) и падал с «$ is not defined», а его хук $.update невозможно было
      снять через возвращаемую функцию — список шагов крутился каждый кадр и done
      вызывался многократно. Теперь хук берётся из ctx.$ и выключается флагом.
      
      Проверка: полный набор qjs (26 файлов), doc_coverage_test, doc_claims_test,
      агентские тесты и bsp зелёные.
      acdb3b59
    • Nikitos's avatar
      Документация §11: шесть системных враний закрыты и поставлен страж
      · 3151aa4f
      Nikitos создал
      Аудит §11 показал, что доки описывают закрытые пункты как заглушки. Сверил
      каждое утверждение с КОДОМ (скриптом и чтением мест), поправил подтверждённое и
      добавил tests/doc_claims_test.py — он читает код и падает, если в доке снова
      написано «этого нет» про реализованное.
      
      Исправлено:
      * пользовательские шейдеры: «движок не поддерживает» → поддержаны
        ($.gfx.defineShader + .shader/.shaderParam), HIGH_LEVEL_API.md, PERF, GAP,
        layers.md;
      * render target: «не поддержан осознанно» → реализован (привязка текстуры на
        кадр, спрайт прошлого кадра, блит); render.md (§2, §3, §4, ограничения),
        PERF, GAP, VFX_PLAN и устаревший комментарий в src/render.c;
      * «$.gfx.push.triangle отдаёт треугольники без режима» → режим есть восьмым
        аргументом;
      * «KEY_NAMES никем не заполняется, keyNameOf — заглушка-тождество» → ИСПРАВЛЕН
        КОД: input.js зовёт engine.keyName и кеширует имена, поэтому
        $.input.on('key') отдаёт 'Space', а не число. Проверка
        tests/agent/highlevel_keyname_test.py (Space и A по-настоящему нажимаются);
      * «сетевая игра — не мультиплеер», «нет фигурного свипа» → закрыты.
      
      Новый страж ловит ровно тот класс ошибок, что был: «в доке написано, что не
      сделано, а в коде сделано» — 21 проверка по девяти утверждениям.
      
      Проверка: полный набор qjs, агентских тестов, bsp, doc_claims_test и
      keyname_test зелёный.
      3151aa4f
    • Nikitos's avatar
      Авторитарный мультиплеер: $.net — id, владение, снапшоты с дельтой
      · c8543872
      Nikitos создал
      Задача §2 из порядка §12. Решение проекта: в `$` есть ТОЛЬКО авторитарная
      модель — один узел хост-сервер, его симуляция всегда права, клиенты присылают
      ввод и рисуют подтверждённое. Peer-to-peer и «у каждого своя правда» не
      выставляются.
      
      Сделана ВСЯ модель, кроме транспорта:
      
      * стабильные сетевые id: один узел — один номер, номера НЕ переиспользуются
        (иначе клиент сопоставит чужую сущность со своей); прямой и обратный поиск;
      * овнершип: узел принадлежит игроку, ввод применяется только к телу владельца;
        объявлять репликацию может ТОЛЬКО сервер (на клиенте отказ);
      * снапшоты с дельтой: в дельту попадают лишь изменившиеся поля, удалённые
        помечены null, первый снапшот полный; числа сравниваются с допуском, потому
        что позиции «дрожат»;
      * клиентское состояние: подтверждённое и предыдущее (для интерполяции),
        интерполяция чужих между двумя снапшотами, своё берётся как есть;
      * ввод по номерам: повтор и переупорядочивание игнорируются, пропуск
        (потерянный пакет) не догоняется — ввод уже неактуален; на клиенте приём
        авторитетного ввода отклоняется;
      * инвариант «клиент не пишет авторитетное» проверен тестом: ввод клиента не
        меняет подтверждённое состояние;
      * транспорт подключается снаружи ($.net.attach: listen/connect/send/poll/close):
        модель, снапшоты и дельта от сокетов не зависят.
      
      Тесты нашли ошибку: $.net.reset() не чистил стол авторитета — после сброса
      оставались игроки (проверка это поймала).
      
      ЧЕСТНО, чего ещё нет (записано в net.md и в §12): транспорт src/net.{h,c} на
      SDL3_net, предсказание локального игрока с откатом шага, лаг-компенсация хитов,
      шаг симуляции отдельно от кадра, агентские команды net/net-peer, сжатие.
      
      Проверка: tests/js/net_test.mjs (20 проверок), полный набор qjs, bsp и
      агентских тестов зелёный.
      c8543872
    • Nikitos's avatar
      Порт психики NPC и режиссёра рейда из audm-neko: $.alive
      · 6b2e2767
      Nikitos создал
      Последний блок порта (§12, §6.8). Оригинал почти не трогает Godot — психика это
      числа и события, поэтому перенесена целиком (alive_psyche.gd 786 строк,
      alive_raid_director.gd 149).
      
      * три слоя: статика (44 психотипа по Личко/Леонгарду, 14 черт, OCEAN выводится
        из черт, тренированность, роль), динамика (страх, пульс, подавление,
        усталость, ярость, стресс, голод, холод, глухота) и триггеры (ранение, смерть
        союзника, пролёт пули, взрыв, окружение, убийство) со спайком и затуханием ПО
        ЧЕРТАМ, а не константой;
      * роль в отряде: лидер по психотипу и тренированности; ведомый без лидера
        сыпется, с лидером остывает быстрее (Маршалл), leadership();
      * эффекты по Гроссману: пульс выше 140 сужает обзор и слух, сбивает прицел и
        мелкую моторику; барьер убийства вблизи тем сильнее, чем выше эмпатия;
        ступор лицом к лицу; вверх пульс разгоняется быстро, вниз остывает медленно;
      * срывы по Darkest Dungeon: на пороге стресса проверка даёт панику, ярость,
        ступор, сдачу или молитву плюс CSR у вымотанных; веса сдвигаются тем, что
        происходит (лидер не бежит, ведомый без лидера склонен к панике);
      * моральная травма (Шэй, Litz) с ПОЛОМ черт 30%: ангедония, утрата
        самосохранения, перенос между рейдами через carryMoralInjury; первое убийство
        переживается фазами эйфория → отрицание → раскаяние (эмпата вырвет);
      * режиссёр рейда: фазы spare/even/press, настройки mercy/let_win/focus,
        «загонщик» идёт к самому слабо вооружённому и НЕ добивает игрока при здоровье
        ниже 35%, назначения со сроком жизни;
      * $.alive.attach/of/tick связывают психику с узлом и ставят fear/pulse/stress.
      
      Тесты нашли ошибку: у психики не были выставлены геттеры breakCooldown,
      killPhaseLeft, vomitPending и time — поля менялись внутри, но снаружи их не
      было видно (проверки читали undefined).
      
      Проверка: tests/js/alive_test.mjs (31 проверка), полный набор qjs, bsp и
      агентских тестов зелёный.
      6b2e2767
    • Nikitos's avatar
      Процедурный пиксель-арт: $.proc — палитры, силуэт, свет, лист
      · a021b5d8
      Nikitos создал
      Задача §5 из порядка §12. Спрайты не рисуют руками: их выращивают из сида.
      Порт идей из audm-neko (districts_art.gd — атлас улицы, машин, промзоны).
      
      * палитры-РАМПЫ (skin/cloth/leather/metal/dark/accent; wasteland, city, forest):
        художник берёт оттенок из рампы, а не выдумывает цвет — иначе спрайт выглядит
        случайным; неизвестная палитра заменяется запасной, игра не падает;
      * силуэт по частям тела с пропорциями (normal/heavy/thin/child: доли высоты,
        ширина рук, ширина плеч);
      * детали по шансу: пояс, ремни, сумка, разгрузка, противогаз, капюшон; в руках
        ствол или сумка;
      * свет: верхняя кромка светлее, нижняя темнее; контур вокруг силуэта; зеркало
        по нечётному направлению;
      * реестр описаний, отрисовка в холст RGBA, спрайт движка и спрайт-лист всех
        направлений — через новый engine.textureFromPixels (пиксели прямо в GPU, без
        файлов);
      * тесты (19 проверок): палитры и рампы, пиксельные примитивы, силуэт внутри
        холста, детерминированность по сиду, части тела, свет, контур, зеркало,
        телосложение, предмет в руках, детали, палитра меняет цвет но не форму.
      
      Тест нашёл ошибку: сид при отрисовке зависел от направления, поэтому «вид
      сзади» оказывался ДРУГИМ персонажем, а не зеркалом переднего. Теперь рисунок
      один, направление — зеркало.
      
      Проверка: tests/js/proc_test.mjs и полный набор qjs зелёные.
      a021b5d8
    • Nikitos's avatar
      Z-буфер: глубина в рендере, тест в конвейерах, путь меша псевдо-3D
      · 9ee5a4cb
      Nikitos создал
      Первый шаг псевдо-3D (docs/TASKS.md §12.12). Без z-буфера псевдо-3D не бывает:
      плоский спрайт не может перекрываться выпуклостью, если глубины нет вовсе.
      
      * текстура глубины D32_FLOAT под размер кадра со пересозданием при смене размера;
        подключается целью глубины к проходу сцены с очисткой в 1.0 (дальняя
        плоскость, ближе — меньше);
      * тест глубины (LESS_OR_EQUAL) включён в конвейерах по умолчанию, управляется
        из игры: $.gfx.depth(on?) и engine.setDepth/engine.depth;
      * СПРАЙТЫ пишут z = 0, поэтому порядок отрисовки между ними сохраняется, а
        прежняя отрисовка выглядит ровно как раньше — это проверено сравнением
        кадров (тест highlevel_depth_test.py);
      * новая подсистема меша: свой формат вершины (позиция float3 с глубиной + uv +
        цвет, 24 байта), свой вершинный шейдер, конвейеры на каждый режим смешивания,
        свой GPU-буфер и заливка; меш рисуется ПЕРВЫМ в проходе сцены, чтобы успеть
        записать глубину до спрайтов;
      * engine.depthInfo() — диагностика (текстура, конвейер, счётчики меша): без неё
        «меш не рисуется» неотличимо от «меш не создан».
      
      Найденные ошибки:
      1) атрибут глубины с нулевым смещением пересекался с позицией, и конвейер меша
         молча не создавался (заметно только по диагностике);
      2) ранний выход заливки и ранний выход отрисовки сцены забывали про меш — кадр
         только с мешем не заливался и не рисовался вовсе;
      3) конвейеры меша создавались только в пути живых шейдеров и отсутствовали в
         базовом пути инициализации.
      
      ЧЕСТНО: сам меш пока НЕ рисуется на экране — конвейер и буфер есть, кадр не
      меняется. Это записано в docs/highlevel/depth.md и в docs/TASKS.md §12 отдельной
      задачей; z-буфер при этом полностью рабочий и проверенный.
      
      Проверка: tests/agent/highlevel_depth_test.py (6 проверок), полный набор qjs,
      bsp и агентских тестов зелёный.
      9ee5a4cb
    • Nikitos's avatar
      Порт cel-графа из reanimation: $.cels — граф кадров для псевдо-3D
      · b880f35e
      Nikitos создал
      Первый блок псевдо-3D (docs/TASKS.md §12.13, §4). В audm-neko и в видео-примерах
      псевдо-3D персонаж — это ГРАФ, который по «водителям» решает, какой кадр
      показать; сам граф не хранит ни спрайтов, ни таймингов. Порт из
      aarthificial/reanimation (Runtime/Nodes, Runtime/Common).
      
      * узлы: switch (выбор ветки по водителю), anim (лист с кадрами), cel, override
        и termination; массив узлов и строка — краткие записи;
      * водители: точное значение по модулю, доля (percentage: 0..1 → индекс),
        автоинкремент (перебор кадров), водители узла (setDrivers) вливаются до
        выбора ветки — как nextState.Merge в оригинале;
      * зеркало (mirror), путь решения (trace), fps (граф решается не чаще кадра
        анимации), защита от замыканий в графе: при разборе повторный узел
        становится termination, при решении повторно посещённый пропускается;
      * библиотека графов, привязка к узлу ($.cels.attach/state/tick), save/load.
      
      Тесты нашли четыре настоящих ошибки:
      1) set() писал в «будущее» состояние, которое тут же терялось при обмене —
         водители игрока не действовали вовсе;
      2) цикл в определении вешал РАЗБОР (Maximum call stack size exceeded) —
         счётчик глубины не спасал, стек кончался раньше;
      3) одноимённые resolveNode в модуле — ошибка QuickJS «invalid redefinition of
         global identifier» (правило: имена уникальны на уровне модуля);
      4) водители узла подменялись ДО выбора, из-за чего ветка перебивала сама себя.
      
      Проверка: tests/js/cels_test.mjs (16 проверок), полный набор qjs и агентских
      тестов зелёный.
      b880f35e
    • Nikitos's avatar
      Порт генерации рейда из audm-neko: $.raid
      · 8bd7f20e
      Nikitos создал
      Седьмой блок порта (docs/TASKS.md §12.11, §6.5). В audm-neko мир рейда был ОДНОЙ
      большой связной картой: районы слева направо (деревня → ПГТ → город → промзона →
      пойма), рельеф шумом, постройки по районам, точки интереса, выходы, а содержимое
      появлялось СТРИМИНГОМ по чанкам вокруг бойца.
      
      * план мира: районы с долями ширины, рельеф (гладкий шум, усиленный районом),
        постройки по видам застройки (изба, хрущёвка, гаражи, халупа), точки интереса,
        выходы по одному на район плюс «дальний», план тайников и NPC с капами;
      * всё — ЧИСЛА: генератор не создаёт ни спрайтов, ни тел; $.raid.fill() отдаёт
        содержимое чанков игре (onBuilding/onLoot/onNpc/onChunk);
      * стриминг: окно radius чанков вокруг игрока, load/unload, takePending() отдаёт
        чанки к наполнению РОВНО ОДИН РАЗ (лут и NPC не двоятся), chunkContent() —
        срез рельефа и содержимое чанка;
      * детерминированность от сида: create({ seed }) воспроизводим, reseed(),
        save/load (план воспроизводится из сида, в сейве только он и загруженные чанки);
      * погода: clear/overcast/rain/storm/fog с влиянием (fog, rain, wind, loud — в
        грозу шаги слышно хуже, light), одна на рейд.
      
      Тесты нашли три настоящих ошибки:
      1) сид в объекте рейда был СНИМКОМ, а не геттером — reseed() и load() меняли
         переменную, но игра видела старое значение;
      2) голый xorshift32 без размешивания сида давал первые значения ≈0 для соседних
         сидов: границы районов, первый дом и погода повторялись. Добавлен финализатор
         MurmurHash3 — соседние сиды расходятся сразу;
      3) districtAt(-10) отдавал ПОСЛЕДНИЙ район вместо первого.
      
      Проверка: tests/js/raid_test.mjs (19 проверок: детерминированность, районы без
      дыр, дома внутри районов, выходы, капы, стриминг, чанки, сейв, погода), полный
      набор qjs и агентских тестов зелёный.
      8bd7f20e
    • Nikitos's avatar
      Порт оружия и баллистики из audm-neko: $.weapons
      · c9341736
      Nikitos создал
      Шестой блок порта (docs/TASKS.md §12.10, §6.6). В audm-neko стволы жили в одной
      базе (WeaponDB.DB), а состояние — в BaseWeaponV2: магазин, темп, перезарядка по
      стилю, отдача, разброс и навесное, которое меняет характеристики.
      
      * база стволов и навесного с загрузкой из JSON; поля ствола: урон, темп, магазин,
        перезарядка, разброс, отдача, падение урона, пробитие, дробь, режимы огня,
        стиль перезарядки, слоты;
      * ствол в руках: fire() с причиной отказа (cooldown / reloading / empty /
        single), reload()/cancelReload(), tick() для темпа, перезарядки и остывания
        отдачи и перегрева, cycleMode(), save()/load2();
      * навесное: множители и добавки, слоты, замена с возвратом прежнего мода,
        пересчёт характеристик; магазин — МНОЖИТЕЛЬ (ammo 1.5 из 30 даёт 45), дробь —
        добавка;
      * перезарядка дробовика — по одному патрону со своим таймером: прежняя версия
        пыталась дополнить остаток в том же кадре, и при длинном кадре патроны
        терялись, при коротком — не заряжались вовсе;
      * баллистика: затухание урона по расстоянию с полом 40%, пробитие снимает класс
        брони (результат идёт в $.combat.hit), разброс от перегрева;
      * $.weapons.give(узел, id)/of/fire/tickAll — ствол в руках узла.
      
      Тест нашёл настоящий баг: db.get() искал только стволы, поэтому навесное не
      находилось и не устанавливалось (attach всегда отвечал «нет навесного»).
      
      Проверка: tests/js/weapons_test.mjs (22 проверки: темп, режимы, магазин,
      перезарядка по патрону, навесное, затухание, пробитие, сейв), полный набор qjs
      и агентских тестов зелёный.
      c9341736
    • Nikitos's avatar
      Порт боевой модели из audm-neko: $.combat — здоровье по зонам
      · 383dd41e
      Nikitos создал
      Пятый блок порта (docs/TASKS.md §12.10, §6.5). В audm-neko здоровье стало
      пятью зонами вместо одного числа (limb_health.gd + damage_model.gd), и главное
      решение модели — чёрную конечность поднимает ОБЫЧНАЯ аптечка, а не только
      хирургия: иначе игрок без хирургии встаёт в тупик.
      
      * зоны head 35 / chest 85 / arms 60 / stomach 70 / legs 65; ноль в голове или
        груди — смерть, ноль в конечности — «чёрная» с дебаффами (прицел ×1.6,
        перезарядка ×1.4, скорость ×0.45, прыжок ×0.5); парные конечности — один пул;
      * урон выстрела: (оружие + добавка за часть тела) × 1.35; добавки 30/10/6/6;
        слот рига переводится в зону (arm_forearm_front → arms), неизвестный слот —
        грудь: лучше попасть, чем промахнуться в пустоту;
      * кровь: попадание от 25 урона в руку, живот или ногу заводит кровотечение
        (1 HP/с); доли времени копятся шагами по 0.1 с, поэтому 60 кадров дают ровно
        1 HP; голова и грудь не кровят;
      * броня: класс 1..6 держит 15%…70%, защита падает вместе с прочностью;
        applyArmor возвращает { damage, blocked, spent };
      * $.combat.attach(узел) связывает модель с узлом: cur_hp/max_hp = сумма по
        нелетальным зонам, события combat:zone/blacked/bleed/died уходят в $.signal;
      * save/load, report() для отладки, heal() поднимает и чёрное.
      
      Тест нашёл настоящий баг: таблица добавок за часть тела была ключами СЛОТОВ
      (arm, torso), а damageFor получает ИМЕНА ЗОН (arms, legs) — попадание в руку
      считалось как в грудь (+10 вместо +6). Теперь принимаются оба написания.
      
      Проверка: tests/js/combat_test.mjs (18 проверок: зоны, смерть, кровь, дебаффы,
      лечение чёрного, броня, урон), полный набор qjs и агентских тестов зелёный.
      383dd41e
    • Nikitos's avatar
      Порт предметов и инвентаря из audm-neko: $.items и $.inv
      · 68945637
      Nikitos создал
      Четвёртый блок порта (docs/TASKS.md §12.9, §6.4). В audm-neko предмет был
      .tres-ресурсом (item_def.gd + items.gd), а списки хранили только id и штуки;
      здесь предмет — обычный объект, а контейнер (рюкзак, схрон, тайник) — сетка.
      
      * описание предмета: размер в клетках, масса, цена, вид, стопка, слот ношения,
        броня, медицина, еда, патроны, бонус переноса, иконка и текст документа;
      * контейнер: add/remove/count/has, find/fits, list/summary, mass/value,
        wear/unwear, wornList/armorClass, repack/sortByValue, save/load;
      * раскладка детерминированная (сверху вниз, слева направо), стопки доливаются
        в существующие — патроны ложатся 60 + 40, а не двумя половинками;
      * вес: масса лежащего и надетого плюс базовая масса контейнера; ПРЕДЕЛ
        (capacity) больше не смешивается с бонусом рюкзака — есть carryBonus() и
        carryLimit(), иначе setCapacity(8) давал 8+бонус;
      * wear() возвращает имя надетого предмета (раньше возвращал прежний, и успех
        выглядел как отказ), повторное надевание — не ошибка, unwear() не снимает
        предмет, если в контейнере нет места;
      * armorClass('пустой слот') даёт 0, а не класс другой надетой брони.
      
      Тесты нашли пять настоящих ошибок: normalizeItem игнорировал stack; пустая
      стопка оставалась двойником в счёте; capacity() смешивал предел и бонус;
      wear() возвращал не то; наконец, ctx.log падал без engine — из-за него
      модульные тесты падали на предупреждении «не влезло». Журнал теперь безопасен
      вне движка.
      
      Проверка: tests/js/items_test.mjs (12 проверок), полный набор qjs и агентских
      тестов зелёный.
      68945637
    • Nikitos's avatar
      Порт звука из audm-neko: банки вариаций, шаги, реплики NPC
      · c1ce6a6b
      Nikitos создал
      Третий блок порта (docs/TASKS.md §12.9, §6.3). В audm-neko ни один звук не был
      одним файлом: у шага шесть вариаций, у удара несколько, и один файл подряд
      слышится как одинаковые щелчки. Переносим три подсистемы.
      
      * $.sound.defineBank/playBank — банк вариаций: выбирает файл, не повторяя
        последние (avoids), разбрасывает высоту (±pitch) и громкость (диапазон),
        соблюдает interval. Банк не грузит звук сам, а зовёт $.sound.play, поэтому
        приглушение, панорама и каналы остаются общими;
      * $.steps — шаги по пройденному ПУТИ, а не по таймеру (иначе на быстром шаге
        слышна дробь): 78 пикселей на шаг у ходьбы, 96 у бега, медленнее 40 px/с —
        переминание. Бег громче, присед почти беззвучен, приземление звучит по
        скорости падения. Материал пола задаёт игра, есть псевдонимы и запасной;
      * $.barks — реплики NPC по ситуации, психотипу и уровню безумия: недавние не
        повторяются, «свои» для психотипа выбираются в 70% случаев, madness_min/max
        отсекают бред у вменяемых.
      
      Чистые функции (выбор файла, разброс, накопление пути, выбор реплики)
      проверяются без движка; тест нашёл настоящую ошибку: pickBankFile падал на null
      вместо возврата null.
      
      Проверка: tests/js/steps_test.mjs (14 проверок), полный набор qjs и агентских
      тестов зелёный.
      c1ce6a6b
    • Nikitos's avatar
      Порт заданий из audm-neko: $.quest
      · 043a6894
      Nikitos создал
      Второй блок порта (docs/TASKS.md §12.8, §6.2). В audm-neko задание было
      .tres-ресурсом (QuestDef + QuestObjective + QuestBook, 744 строки); здесь это
      обычный объект, который описывается кодом или пачкой из JSON, а ядро остаётся
      чистым и проверяется без движка.
      
      * цели шести видов (fetch, kill, haul, extract, spare, object) с фильтром по
        карте и своей подписью; события рейда onKill/onExtract/onSpare/onObject/onFetch
        помечают цели и возвращают список готовых к сдаче заданий;
      * статусы locked/available/active/ready/done и правила открытия: after (все),
        after_any (любое — развилка), excludes (взаимоисключение),
        requires_flags/requires_scenes, merciful_branch (ветка по числу флагов);
      * главное правило оригинала перенесено дословно: ВЗЯТОЕ задание не пропадает из
        журнала, даже если ветка-близнец сдана раньше;
      * accept/turnIn ставят флаги сценок ($.story.flags), turnIn возвращает награду
        (деньги, предметы, доверие) — раздаёт её игра;
      * save/load состояния, line() — строка журнала с прогрессом, forGiver() —
        список выдающего, loadFile() — пачка заданий из JSON.
      
      Тест поймал настоящую ошибку трактовки: у haul `count` — это ПОРОГ стоимости
      рюкзака, а не число повторов, поэтому прогресс уходил в 1500/1000 и задание не
      становилось готовым. Теперь зачёт хранится отдельной отметкой.
      
      Проверка: tests/js/quest_test.mjs (17 проверок: условия открытия, прогресс,
      события, награда, сейв), полный набор qjs и агентских тестов зелёный.
      043a6894
    • Nikitos's avatar
      Порт сценок из audm-neko: $.story и текстовый DSL
      · 14570526
      Nikitos создал
      Первый блок порта (docs/TASKS.md §12.8, §6.1). В audm-neko сценка — текстовый
      файл, который читается и правится в любом редакторе, а исполнитель был
      корутиной на `await`. Переносим то же поведение идиомами `$`.
      
      * story_script.js — разбор DSL (чистые функции): реплики с эмоцией и
        рассказчик, bubble, варианты и выборы с флагами, метки, set (=, +=, -=),
        if со сравнениями, goto/end, wait, camera, move (к маркеру или на +200 px),
        face, anim, ai, image, fade, sound, objective, @free;
      * условия: flag, !flag, ==, !=, >, <, >=, <=; неизвестный флаг — ноль,
        сравнение числа со строкой даёт false, а не исключение;
      * story.js — исполнитель генератором: `yield` отдаёт кадр, `$.task.each` крутит
        сценку до конца. Реплика ждёт нажатия, move — прихода (до 6 с), wait — времени;
      * катсцена (без @free) отбирает управление: $.story.locked(), полосы сверху и
        снизу (9% высоты), игра спрашивает locked() перед чтением ввода; @free идёт
        поверх игры;
      * актёры объявляются игрой ($.story.actor('Некотян', '#companion')), иначе
        ищется узел с таким id; player/игрок/я → #player или #hero;
      * флаги общие на игру (переживают смену сцены): чтение, set/check/resetFlags;
      * события шины: story:line, story:lock, story:objective, story:end;
      * автопилот и внешний ответ: $.story.auto/advance/choose — тем же путём, что
        нажатия игрока, поэтому сценку можно прогнать без ввода.
      
      Три настоящих бага нашлись при сборке движка:
      1) я звал ctx.update, которого нет — фрейм-хуки ставятся через $.update; из-за
         этого падала установка ВСЕГО API (движок писал «bootstrap.js не выставил
         globalThis.$»), а $.task и $.loading.run вообще не могли запуститься —
         исправлено там же;
      2) в QuickJS генераторная функция не отдаёт .next, поэтому $.task.each с
         `function* () {}` падал — теперь принимаются и функция, и итератор;
      3) локальная переменная `node` перекрывала хелпер создания узлов.
      
      Проверка: tests/js/story_test.mjs (17 проверок разбора) и
      tests/agent/highlevel_story_test.py (19 проверок: реплики, выборы, флаги,
      катсцена, возврат управления), полный набор qjs и агентских тестов зелёный.
      14570526
    • Nikitos's avatar
      Перезапуск скриптов на границе кадра: $.script
      · 6633e54a
      Nikitos создал
      Пробел из docs/TASKS.md §12.5 (неблокирующий hot reload). Раньше перезапуск
      происходил ПРЯМО В МОМЕНТ обнаружения правки или нажатия F5: старый рантайм
      уничтожался посреди обработки события, и кадр оставался недоигранным.
      
      * r2d_script_request_reload()/r2d_script_take_reload_request(): запрос копится,
        а исполняется главным циклом после того, как кадр отрисован и профайлер
        закрыт — JS-вызовов в этом кадре больше не будет;
      * F5 и слежение за файлами теперь ЗАПРАШИВАЮТ перезапуск, а не делают его
        сразу; в журнал идёт причина (правка файлов, F5, текст игры); первый запрос
        важнее последующих;
      * если новый код не загрузился, движок пишет ошибку и продолжает работать со
        старым рантаймом (r2d_script_reload возвращает false — это уже было);
      * $.script: request/pending/hotReload/count; биндинги engine.requestReload,
        engine.reloadPending, engine.hotReload.
      
      Честно задокументировано главное ограничение: перезапуск уничтожает весь
      JS-heap, поэтому состояние надо сохранять через $.save/$.fs — «частичной
      замены модуля» не появилось.
      
      Проверка: tests/agent/highlevel_reload_test.py (8 проверок: запрос ждёт
      границы кадра, в том же кадре рантайм отвечает, после кадра запрос снят и
      игра загрузилась заново), полный набор qjs и агентских тестов зелёный.
      6633e54a
    • Nikitos's avatar
      Работа кусками по кадрам: $.task и $.scene.loadAsync
      · 265d395a
      Nikitos создал
      Пробел из docs/TASKS.md §12.5: был только `$.loading.run`, который выполняет ПО
      ОДНОМУ шагу за кадр — если шаг сам длинный (генерация карты, тёплая загрузка),
      кадр всё равно замирает. Фоновой загрузки не было вовсе.
      
      * createScheduler()/createGeneratorScheduler() — чистые планировщики: работа
        делится по ВРЕМЕНИ (бюджет мс на кадр), а не по числу шагов;
      * $.task.chunked({ total, step, budget, label, done }) — шаги по индексу;
        $.task.each(generator) — генератор сам решает, когда закончил; $.task.list() —
        список по кадрам; running()/abortAll();
      * label показывает и обновляет экран загрузки; done(cancelled) сообщает об
        отмене или исключении — исключение внутри шага отменяет задачу, а не роняет
        кадр;
      * $.scene.loadAsync(name, { steps }) — экран загрузки, шаги по кадрам и только
        потом переход в сцену;
      * бюджет проверяется не на каждой итерации (вызов часов сам стоит времени), а
        раз в четыре; нулевой и отрицательный бюджет поднимаются до минимума, чтобы
        цикл не зависал.
      
      Тест нашёл настоящую ловушку QuickJS: у генераторной функции нет `.next`
      (в отличие от V8), поэтому `$.task.each(function*(){…})` падал. Теперь
      планировщик принимает и функцию, и готовый итератор.
      
      Проверка: tests/js/task_test.mjs (11 проверок: бюджет, продолжение с того же
      места, прогресс, отмена, исключение, генератор), полный набор qjs и агентских
      тестов зелёный.
      265d395a
    • Nikitos's avatar
      Кривые и градиенты: $.curve — одна плавность на весь движок
      · 5479ba90
      Nikitos создал
      Пробел из docs/TASKS.md §1.6: рампы были зашиты в частицах, градиентный
      помощник приватен в render.js, общей интерполяции не было — каждый модуль писал
      свою формулу затухания.
      
      * make()/define()/use()/names(): кривая значений — функция (t) => y с методами
        at (зажим t), range (равномерные сэмплы), points, plus;
      * четыре режима: linear, step (ступенька), smooth и spline (Catmull-Rom);
        сплайн считает по значениям, а не по индексам, поэтому неровные по x точки
        остаются плавными;
      * встроенные кривые: linear, easeIn/Out/InOut, pop, bounce, pulse, spike,
        fadeIn/fadeOut; любая принимается там, где ждут ease (проверено на $.tween);
      * gradient(): стопы строкой '#a → #b → #c' (стрелка — '→', '->' или '=>'),
        массивом цветов, объектами {at, color} и парами; интерполяция покомпонентная,
        а не в sRGB, поэтому тёмные цвета не выцветают в серое;
      * resolve(value): функция, число или имя → функция; разбор цветов
        (#rgb/#rrggbb/#rrggbbaa/rgb()/transparent/число) вынесен в чистую часть.
      
      Тест нашёл настоящий баг: gradient() принимал только массив и игнорировал
      строку со стрелками ('#a → #b'), то есть документированный способ задания
      градиента не работал.
      
      Проверка: tests/js/curve_test.mjs (13 проверок чистых функций),
      tests/agent/highlevel_curve_test.py (8 проверок: реестр, ease в твине, градиент),
      полный набор qjs и агентских тестов зелёный.
      5479ba90
    • Nikitos's avatar
      Фильтрация спрайтов: $.gfx.filter() — nearest или линейная
      · e9b77e16
      Nikitos создал
      Пробел из docs/TASKS.md §1.6: фильтр был только NEAREST. Для пиксель-арта это
      правильно, но текст и крупные спрайты при сильном масштабе «мылятся» зубцами, а
      режим нельзя было сменить вообще.
      
      * r2d_render_set_filter()/r2d_render_filter() и поле filter в команде батча:
        режим фиксируется на момент добавления, смена разрывает участок склейки,
        сэмплер выбирается при привязке фрагментной текстуры;
      * engine.setSpriteFilter(bool)/engine.spriteFilter() — биндинги;
      * $.gfx.filter(linear?) — без аргумента читает текущий режим, по умолчанию
        nearest (прежнее поведение, кадры совпадают побитово).
      
      Проверка: tests/agent/highlevel_filter_test.py (5 проверок: переключение меняет
      кадр, возврат даёт прежний кадр), полный набор qjs и агентских тестов зелёный.
      e9b77e16
    • Nikitos's avatar
      Импорт атласов из JSON: $.atlas (Aseprite, TexturePacker, свой формат)
      · 28b951c7
      Nikitos создал
      Пробел из docs/TASKS.md §1.6: лист резался только равномерной сеткой
      cols×rows×cw×ch, а художники отдают атласы с разными прямоугольниками и
      названиями анимаций.
      
      * parseAtlas()/normalizeFrame(): формат определяется по содержимому, а не по
        расширению. Aseprite (объект frames + meta.frameTags), TexturePacker/LibGDX
        (массив с filename), свой простой (карта имя → {x,y,w,h}); поддержаны
        w/h и width/height, плоский кадр и обёртка frame{};
      * теги Aseprite ссылаются на НОМЕРА кадров — собираются по порядку листа;
        direction: 'reverse' разворачивает список;
      * $.atlas.load читает JSON через $.fs или принимает data в памяти (ответ сети,
        тест), путь к картинке берётся из meta.image и считается от каталога JSON;
      * объект атласа: frame/frames/info/tag/tagSprites/tagInterval/tags/size —
        tagSprites() даёт готовый вход для $.anim.clip;
      * повёрнутые кадры пропускаются с записью в журнал, а не рисуются криво.
      
      Проверка: tests/js/atlas_test.mjs (7 проверок), полный набор qjs и агентских
      тестов зелёный.
      28b951c7
    • Nikitos's avatar
      Пивот спрайта, nine-slice и анимация тайлов
      · db8b8b59
      Nikitos создал
      Три пункта «дешёвого P1» (docs/TASKS.md §1.6): ноги персонажа должны оставаться
      на месте при вращении, рамки интерфейса — тянуться, вода и факелы — течь.
      
      * .pivot(x, y) и .pivotAt(x, y): точка вращения и масштаба. Спрайт в C
        вращается вокруг центра своего прямоугольника, поэтому движок сдвигает
        прямоугольник так, чтобы пивот оказался в точке узла. Без пивота сдвига нет —
        поведение прежнее, кадры совпадают побитово;
      * .slice(insets) — nine-slice: девять частей, углы целые, края тянутся, центр
        заполняет. Толщина 0..1 — доля стороны, больше 1 — пиксели; при нехватке
        места полосы ужимаются. Части кэшируются (regionSprite), поворот применяется
        к каждой части;
      * .tileset({ frames, fps|interval, ids, random, offset }) — анимация тайлов:
        кадры идут строками листа, время берётся из tickTilemap (игровое, поэтому
        пауза и --fixed-dt работают), геттер .tileAnimation(). Рисуются только тайлы
        в видимом диапазоне камеры, новые текстуры не создаются;
      * тест нашли баг: отрицательное время (сдвиг фазы) давало отрицательный индекс
        кадра — приведено к неотрицательному остатку.
      
      Проверка: tests/agent/highlevel_sprite_test.py (15 проверок: пивот меняет кадр
      и возвращается к исходному, nine-slice, ход времени анимации),
      tests/js/tile_anim_test.mjs (8 проверок чистых функций), полный набор qjs и
      агентских тестов зелёный.
      db8b8b59
    • Nikitos's avatar
      docs: подсистема текста, биндинги шрифтов и поправки про ImGui
      · 47754cd7
      Nikitos создал
      * docs/highlevel/text.md — новый справочник: шрифты и автозагрузка, семейство на
        узле, где живёт текст, кегль и зум, измерение, атлас, ограничения;
      * API.md — drawText с семейством, углом и масштабом; measureText с семейством;
        loadFont/fontDefault/fontList/fontStats; убран рассказ про ImGui-очередь,
        которой больше нет;
      * HIGH_LEVEL_API.md — ссылка на text.md и метод .font();
      * font.md — ограничения больше не врут про «свои файлы шрифтов задать нельзя».
      47754cd7
  3. 06.10.2026 6 коммитов
  4. 05.10.2026 2 коммита
    • Nikitos's avatar
      fix($): детерминированная тряска и мгновенный scene.load({ ms: 0 })
      · 988cbaf2
      Nikitos создал
      * Тряска камеры, узлов и тайлов берёт случайные числа из отдельного
        seeded-генератора (fxRandom), а не из Math.random(): при --seed и
        --fixed-dt прогон снова воспроизводим — проверено сравнением двух
        независимых прогонов. Игровой поток $.random при этом не затрагивается.
      * $.scene.load(name, { ms: 0 }) переключает сцену мгновенно: ноль был falsy
        и тихо превращался в длительность по умолчанию (300 мс).
      * docs: у $.ui.doc().on() явно сказано, что подписка идёт на конкретный
        элемент — на этой ошибке стояли мёртвые кнопки в трёх демо.
      * scene_test: проверка мгновенной смены по ms: 0.
      988cbaf2
    • Nikitos's avatar
      Russiano2D 0.1.0 — первый публичный релиз
      · a6768866
      Nikitos создал
      Российский 2D-игровой движок: ядро на C11, игровая логика на JavaScript,
      единая точка входа — объект `$` в стиле jQuery.
      
      Стек: SDL3 (окно, ввод, SDL_GPU: Vulkan/Metal/DirectX 12), QuickJS-ng 0.10,
      Box2D v3.1, SDL3_mixer, RmlUi 6.3, Dear ImGui, свои 2D BSP-дерево и полигоны
      видимости, Material Design Icons, опционально libcurl для $.http.
      
      Что умеет:
      - высокоуровневое API `$`: создание узлов `$('<player>', {...})`, CSS-подобные
        селекторы, цепочки, события, твины;
      - подсистемы: анимация клипами и машина состояний ($.anim), TileMap со слоями,
        автотайлом, террейнами и Y-sort ($.tilemap), CPU-частицы ($.particles),
        навигация A* и navmesh ($.nav), prefab и сериализация сцен ($.prefab),
        аудио-шины и эффекты ($.audio), канвас-слои и параллакс ($.layers),
        UI-контролы с якорями и темами ($.ui), Tween в стиле Godot ($.tween),
        зоны enter/leave ($.triggers), локализация ($.i18n), пул объектов ($.pool),
        HTTP-запросы ($.http);
      - физика: формы тел (прямоугольник, круг, капсула, полигон), односторонние
        платформы, события контакта collide/separate/hit, суставы;
      - blend-режимы alpha/add/multiply/none на узле и на кадр;
      - сборка игры в один исполняемый файл с шифрованием груза (ChaCha20-Poly1305);
      - интеграция с ИИ-агентами: агентский режим (JSON по stdin/stdout), headless,
        детерминированный прогон --fixed-dt/--seed, eval/state/screenshot,
        виртуальный ввод с текстом, $.agent/$.test, Python-клиент и раннер тестов.
      
      Проверки: 24 агентских теста (tools/run_tests.py), 18 наборов юнит-тестов
      под qjs, отдельные C-тесты JSON и криптографии. CI: .gitlab-ci.yml собирает
      Linux (gcc и clang) и Windows (MSVC), упаковывает dist/ и выпускает релиз по
      тегу v*.
      
      Документация: docs/HIGH_LEVEL_API.md, docs/API.md, docs/AGENT_API.md,
      docs/BUILD.md, docs/RELEASING.md, docs/GAP_ANALYSIS.md, docs/tutorial-first-game.md
      и справочники подсистем в docs/highlevel/.
      
      Лицензия авторская: использовать, менять, распространять и продавать свободно;
      если выпустишь игру на движке — скажи спасибо автору, можно не вслух (LICENSE).
      
      Репозиторий: https://hub.mos.ru/dem4ev48/russiano2d
      a6768866