- 07.10.2026 25 коммитов
-
-
Nikitos создал
Фазы 6-8: утверждения $.expect, реактивные запросы $.watch, DevTools $.devtools на RmlUi, engine.ui.loadMarkup v0.1.16
-
Nikitos создал
Запросы в радиусе и within(), команды query/inspect/profile, запись и воспроизведение ввода (--record/--replay), законы движка и правила агентов, сверка доков с кодом v0.1.15
-
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, быстрый набор — зелёные.
-
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 — зелёные. -
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 зелёные. -
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 зелёный. -
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 и агентских тестов зелёный. -
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 и агентских тестов зелёный.
-
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 зелёные.
-
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 и агентских тестов зелёный.
-
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 и агентских тестов зелёный.
-
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 и агентских тестов зелёный. -
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 и агентских тестов зелёный.
-
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 и агентских тестов зелёный. -
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 и агентских тестов зелёный. -
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 и агентских тестов зелёный.
-
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 и агентских тестов зелёный.
-
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 и агентских тестов зелёный. -
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 и агентских тестов зелёный.
-
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 и агентских тестов зелёный. -
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 и агентских тестов зелёный. -
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 и агентских тестов зелёный.
-
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 и агентских тестов зелёный. -
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 и агентских тестов зелёный. -
Nikitos создал
* docs/highlevel/text.md — новый справочник: шрифты и автозагрузка, семейство на узле, где живёт текст, кегль и зум, измерение, атлас, ограничения; * API.md — drawText с семейством, углом и масштабом; measureText с семейством; loadFont/fontDefault/fontList/fontStats; убран рассказ про ImGui-очередь, которой больше нет; * HIGH_LEVEL_API.md — ссылка на text.md и метод .font(); * font.md — ограничения больше не врут про «свои файлы шрифтов задать нельзя».
-
- 06.10.2026 6 коммитов
-
-
Nikitos создал
-
Nikitos создал
Честный bloom, шейдеры узлов, render target, DSP-эффекты и реверб-шины; закрыто 11 из 12 пунктов §28 v0.1.10
-
Nikitos создал
-
Nikitos создал
-
Nikitos создал
-
Nikitos создал
- src/profile.{h,c}: зоны кадра (логика, сборка батча, физика, загрузка VB/IB, draw, интерфейс, прочее), среднее и пик за 120 кадров. - engine.now() — монотонные миллисекунды для внутрикадровых замеров. - $.loading: экран загрузки с полосой прогресса и шагами по кадрам. - Курсор в скриптах: engine.setCursor/cursorVisible, $.input.cursor(name). - $.ui.setIcon(селектор, имя) — иконка отдельной меткой. - r2d_sprite_create: полтексельный отступ UV — соседний кадр атласа больше не затягивается на границе ячейки.
-
- 05.10.2026 2 коммита
-
-
Nikitos создал
* Тряска камеры, узлов и тайлов берёт случайные числа из отдельного seeded-генератора (fxRandom), а не из Math.random(): при --seed и --fixed-dt прогон снова воспроизводим — проверено сравнением двух независимых прогонов. Игровой поток $.random при этом не затрагивается. * $.scene.load(name, { ms: 0 }) переключает сцену мгновенно: ноль был falsy и тихо превращался в длительность по умолчанию (300 мс). * docs: у $.ui.doc().on() явно сказано, что подписка идёт на конкретный элемент — на этой ошибке стояли мёртвые кнопки в трёх демо. * scene_test: проверка мгновенной смены по ms: 0. -
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
-