Добавить production-состояния тел
This commit is contained in:
+2
-2
@@ -23,7 +23,7 @@
|
||||
{"_type":"issue","id":"goon-game-e12","title":"EPIC-12 Android","description":"Lifecycle, устройства, aspect ratios, graphics profiles и сборки. Разделы 41, 44–46, 49.","acceptance_criteria":"Release build стабильно работает на целевой Android-матрице.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":0,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:08Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:47Z","metadata":{"stages":"0,3,5,7","primary_stage":7},"labels":["android","epic","primary-stage-7","stage-0","stage-3","stage-5","stage-7"],"dependencies":[{"issue_id":"goon-game-e12","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:55Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-e07","title":"EPIC-07 Levels","description":"Solution graphs, greybox, прогрессия механик и производство 55–70 уровней. Разделы 19–25, 52–53, 66.","acceptance_criteria":"Каждый уровень имеет доказанное решение, DoD и место в content matrix.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":0,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:57Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:25Z","metadata":{"stages":"0,2,4,6","primary_stage":6},"labels":["content","epic","levels","primary-stage-6","stage-0","stage-2","stage-4","stage-6"],"dependencies":[{"issue_id":"goon-game-e07","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:49Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-e03","title":"EPIC-03 Physics","description":"Предсказуемая гибридная физика и бюджеты активных объектов. Разделы 15, 41–43, 65.","acceptance_criteria":"Одинаковая последовательность ввода воспроизводимо приводит к одинаковому состоянию головоломки.","notes":"Stage 5 closed the deterministic physics contract: PhysicsConfig v1 and 10 exact command traces replayed 3x with identical signatures; both EPIC children are closed.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":0,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:49Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T23:02:55Z","closed_at":"2026-08-18T23:02:55Z","close_reason":"Physics foundation and deterministic replay acceptance are complete.","metadata":{"stages":"0,5,7","primary_stage":5},"labels":["epic","physics","primary-stage-5","stage-0","stage-5","stage-7"],"dependencies":[{"issue_id":"goon-game-e03","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:44Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-e02","title":"EPIC-02 Death \u0026 Corpse","description":"Смерть как игровая механика, состояния тела, CorpseManager и respawn. Разделы 4–8.","acceptance_criteria":"Все типы смерти создают предсказуемое и пригодное для головоломки состояние тела.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":0,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:47Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:03Z","metadata":{"stages":"0,6,7","primary_stage":6},"labels":["corpse","death","epic","primary-stage-6","stage-0","stage-6","stage-7"],"dependencies":[{"issue_id":"goon-game-e02","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:43Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-e02","title":"EPIC-02 Death \u0026 Corpse","description":"Смерть как игровая механика, состояния тела, CorpseManager и respawn. Разделы 4–8.","acceptance_criteria":"Все типы смерти создают предсказуемое и пригодное для головоломки состояние тела.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":0,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:47Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T03:32:50Z","closed_at":"2026-08-19T03:32:50Z","close_reason":"All EPIC-02 children complete: death/respawn core, CorpseManager ownership, and every approved corpse state has deterministic runtime and puzzle evidence.","metadata":{"stages":"0,6,7","primary_stage":6},"labels":["corpse","death","epic","primary-stage-6","stage-0","stage-6","stage-7"],"dependencies":[{"issue_id":"goon-game-e02","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:43Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-plan","title":"Project Sacrifice: выполнить утверждённый production plan","description":"Главный bead для плана из docs/DEVELOPMENT_PLAN.md: vertical slice, production, alpha, beta и release candidate.","acceptance_criteria":"Release Candidate удовлетворяет критериям раздела 59; все дочерние эпики завершены либо явно исключены решением владельца продукта.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":0,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:23Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:51Z","started_at":"2026-08-17T21:41:28Z","metadata":{"stages":"0,1,2,3,4,5,6,7,8,9,10","primary_stage":10},"labels":["gdd","master","primary-stage-10","production","stage-0","stage-1","stage-10","stage-2","stage-3","stage-4","stage-5","stage-6","stage-7","stage-8","stage-9","tdd"],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-stage4-bot-traces","title":"Сохранять воспроизводимый журнал команд бота","description":"Добавить versioned trace contract для production-input сценариев с level/format/physics versions, simulation timestamps, inputs и ключевыми checkpoints.","design":"Trace recorder наблюдает публичный Driver/Input контракт и не управляет GameView напрямую; replay engine вне Stage 4 scope, журнал должен быть пригоден для последующего replay.","acceptance_criteria":"Каждый bot runner может выдать deterministic JSON trace; повторная сериализация идентична; timeout diagnostics указывает trace; sensitive/device-local data не записываются.","notes":"GREEN: trace v1 records simulation inputs/checkpoints/outcome plus format/physics versions; SHA-256 trace ID is included in diagnostics; generic and level-8 runners covered.","spec_id":"docs/CURRENT_ROADMAP.md","status":"closed","priority":1,"issue_type":"task","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-18T17:54:29Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:04Z","started_at":"2026-08-18T19:01:26Z","closed_at":"2026-08-18T19:01:34Z","close_reason":"Deterministic versioned traces and diagnostic IDs implemented for all bot runner families.","metadata":{"stages":4,"primary_stage":4},"labels":["bot","diagnostics","primary-stage-4","replay","stage-4","stage4"],"dependencies":[{"issue_id":"goon-game-stage4-bot-traces","depends_on_id":"goon-game-stage4-content-architecture","type":"parent-child","created_at":"2026-08-18T20:54:32Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-stage4-validator-tools","title":"Добавить build validator и desktop preview/export","description":"Создать запускаемый на desktop инструмент, который валидирует каталог до Android-сборки и экспортирует координаты в читаемый SVG/текстовый отчёт.","design":"Инструмент использует ту же pure-Java загрузку и validator, что runtime, без дублирования правил в Gradle или Python.","acceptance_criteria":"Gradle build отклоняет broken fixture до упаковки APK; CLI валидирует все levels и создаёт deterministic preview/export; команда и формат задокументированы.","notes":"GREEN: preBuild validateLevels, path-specific broken-signal rejection, deterministic 10 SVG + catalog.tsv preview.","spec_id":"docs/CURRENT_ROADMAP.md","status":"closed","priority":1,"issue_type":"task","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-18T17:54:28Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:04Z","started_at":"2026-08-18T19:01:11Z","closed_at":"2026-08-18T19:01:21Z","close_reason":"Shared build validator and deterministic desktop preview/export implemented, documented, and verified.","metadata":{"stages":4,"primary_stage":4},"labels":["primary-stage-4","stage-4","stage4","tooling","validator"],"dependencies":[{"issue_id":"goon-game-stage4-validator-tools","depends_on_id":"goon-game-stage4-content-architecture","type":"parent-child","created_at":"2026-08-18T20:54:31Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-stage4-validator-tools","depends_on_id":"goon-game-stage4-level-json","type":"blocks","created_at":"2026-08-18T20:54:33Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||
@@ -70,7 +70,7 @@
|
||||
{"_type":"issue","id":"goon-game-content-worlds","title":"Спроектировать и произвести 4 мира / 55–70 уровней","description":"Content matrix четырёх миров, challenge-уровней и финальных испытаний по разделам 2 и 23.","acceptance_criteria":"Все уровни проходят content pipeline и Definition of Done.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:58Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:26Z","metadata":{"stages":6,"primary_stage":6},"labels":["future","levels","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-content-worlds","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T00:43:21Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-content-worlds","depends_on_id":"goon-game-milestone-vslice","type":"blocks","created_at":"2026-08-18T00:41:41Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-weighted-objects","title":"Добавить общий IWeightedObject и физические объекты","description":"Добавить box, rock, ball, ice block и counterweight поверх единого контракта массы.","acceptance_criteria":"Pressure plate одинаково работает с персонажем, телом и объектами.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:43Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:26Z","metadata":{"stages":6,"primary_stage":6},"labels":["primary-stage-6","production","puzzle","stage-6"],"dependencies":[{"issue_id":"goon-game-weighted-objects","depends_on_id":"goon-game-e05","type":"parent-child","created_at":"2026-08-18T00:43:14Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-hazards-full","title":"Дополнить production-каталог опасностей","description":"Добавить crusher, fire, projectile, explosion и data-driven настройки импульса/звука/визуала.","acceptance_criteria":"Каждая опасность имеет prefab/data definition, тесты и минимум один учебный уровень.","notes":"2026-08-19: claimed after the three Stage 6 foundation slices. Will proceed test-first per hazard with data definition, deterministic behavior, teaching content, solution graph and production-input route; crusher/fire precede the dependent crushed/burned corpse states.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":2400,"created_at":"2026-08-17T21:39:37Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T02:28:37Z","started_at":"2026-08-19T01:07:11Z","closed_at":"2026-08-19T02:28:37Z","close_reason":"Crusher, fire, projectile and cyclic explosion are data-driven, validated and shipped with teaching levels 11-14, solution graphs and 3x production-input routes; clean 99 JVM/lint/APK and unfiltered Android 43/43 green.","metadata":{"stages":6,"primary_stage":6},"labels":["hazards","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-hazards-full","depends_on_id":"goon-game-e04","type":"parent-child","created_at":"2026-08-18T00:43:10Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-corpse-states","title":"Расширить состояния тел","description":"Добавить crushed, burned и остальные утверждённые состояния поверх normal/pinned/frozen/conductive.","acceptance_criteria":"Каждое состояние используется минимум одной уникальной головоломкой и проходит DoD механики.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:29Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:27Z","metadata":{"stages":6,"primary_stage":6},"labels":["corpse","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-corpse-states","depends_on_id":"goon-game-e02","type":"parent-child","created_at":"2026-08-18T00:43:05Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-corpse-states","depends_on_id":"goon-game-hazards-full","type":"blocks","created_at":"2026-08-19T04:07:09Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-corpse-states","title":"Расширить состояния тел","description":"Добавить crushed, burned и остальные утверждённые состояния поверх normal/pinned/frozen/conductive.","acceptance_criteria":"Каждое состояние используется минимум одной уникальной головоломкой и проходит DoD механики.","notes":"2026-08-19: claimed after production hazards dc2ebcb passed 99 JVM and Android 43/43. Will add explicit crushed/burned/shattered and derived corpse behavior test-first, then one teaching route/solution graph per new gameplay-relevant state and full regression.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:29Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T03:32:49Z","started_at":"2026-08-19T02:31:25Z","closed_at":"2026-08-19T03:32:49Z","close_reason":"GDD corpse states complete: existing normal/pinned/frozen/conductive routes plus new burned/crushed contracts, save v3 migration, levels 15-16 and bots. Clean 102 JVM; exact Android 46/46 in 1842.248s. IsDestroyed is a lifecycle field; Shattered is not an approved GDD state.","metadata":{"stages":6,"primary_stage":6},"labels":["corpse","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-corpse-states","depends_on_id":"goon-game-e02","type":"parent-child","created_at":"2026-08-18T00:43:05Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-corpse-states","depends_on_id":"goon-game-hazards-full","type":"blocks","created_at":"2026-08-19T04:07:09Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-e14","title":"EPIC-14 Analytics","description":"События прохождения, позиции смертей и crash reporting без лишних персональных данных. Раздел 47.","acceptance_criteria":"События и ошибки проверены в тестовом канале, схема версионирована.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:12Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:57Z","metadata":{"stages":"7,8,9,10","primary_stage":7},"labels":["analytics","epic","primary-stage-7","stage-10","stage-7","stage-8","stage-9"],"dependencies":[{"issue_id":"goon-game-e14","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:57Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-e11","title":"EPIC-11 Art","description":"Самостоятельное художественное направление, модульные персонажи и анимации. Разделы 26–28.","acceptance_criteria":"Визуальный язык утверждён, оригинален и покрывает все игровые состояния.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:05Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:44Z","metadata":{"stages":"5,6","primary_stage":6},"labels":["art","epic","primary-stage-6","stage-5","stage-6"],"dependencies":[{"issue_id":"goon-game-e11","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:54Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-e10","title":"EPIC-10 Audio","description":"Sound design, вариативность реакций и музыкальные loops. Разделы 29–30.","acceptance_criteria":"Vertical slice имеет полный audio pass без раздражающих повторов.","notes":"Stage 5 audio acceptance complete: procedural music/effects/voice variation, lifecycle safety and independent persisted controls; the EPIC child is closed.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:03Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T23:03:09Z","closed_at":"2026-08-18T23:03:09Z","close_reason":"Vertical Slice has a complete non-repeating audio pass.","metadata":{"stages":5,"primary_stage":5},"labels":["audio","epic","primary-stage-5","stage-5"],"dependencies":[{"issue_id":"goon-game-e10","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:52Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
|
||||
@@ -11,13 +11,14 @@ Project documents:
|
||||
- [`docs/STAGE5_ART_BIBLE.md`](docs/STAGE5_ART_BIBLE.md) — original visual language and articulated volunteer contract;
|
||||
- [`docs/STAGE5_PERFORMANCE_BUDGETS.md`](docs/STAGE5_PERFORMANCE_BUDGETS.md) — physics, replay, frame, memory, and complexity ceilings;
|
||||
- [`docs/STAGE5_QA_REPORT.md`](docs/STAGE5_QA_REPORT.md) — exact Vertical Slice candidate evidence;
|
||||
- [`docs/STAGE6_CORPSE_STATES.md`](docs/STAGE6_CORPSE_STATES.md) — burned/crushed body contracts and acceptance evidence;
|
||||
- [`docs/STAGE5_HUMAN_PLAYTEST.md`](docs/STAGE5_HUMAN_PLAYTEST.md) — required external acceptance protocol;
|
||||
- [`docs/STAGE_REGISTRY.md`](docs/STAGE_REGISTRY.md) — mandatory Stage 0–10 ownership for every Bead;
|
||||
- [`docs/BEADS_WORKFLOW.md`](docs/BEADS_WORKFLOW.md) — local backlog structure and daily Beads commands.
|
||||
|
||||
## Playable content
|
||||
|
||||
The APK contains ten sequential levels:
|
||||
The APK contains sixteen sequential levels:
|
||||
|
||||
1. movement and jumping;
|
||||
2. bodies covering a spike lane;
|
||||
@@ -28,7 +29,13 @@ The APK contains ten sequential levels:
|
||||
7. an electrified corpse completing a relay;
|
||||
8. a timed switch and moving platform;
|
||||
9. a two-signal door requiring weight and electricity;
|
||||
10. a mastery level combining saw, freeze, electricity, pressure plate, pit, moving lift, and final gate.
|
||||
10. a mastery level combining saw, freeze, electricity, pressure plate, pit, moving lift, and final gate;
|
||||
11. a rhythmic crusher and its safe rest phase;
|
||||
12. a fire lane with a staged jump;
|
||||
13. two state-driven projectile crossings;
|
||||
14. explosion delivery to a remote pressure door;
|
||||
15. a crushed body whose low collider becomes a required step;
|
||||
16. a burned body that stays solid but no longer powers a pressure bridge.
|
||||
|
||||
Death does not reset the level. A new volunteer respawns after a short delay while retained bodies stay in the simulation. The restart button deliberately clears the puzzle state. Progress, best time, and lowest volunteer count are stored locally.
|
||||
|
||||
@@ -72,7 +79,7 @@ Previews are written to `level-format/build/level-previews`. Invalid geometry,
|
||||
versions, moving-platform destinations, switch parameters, or signal references
|
||||
fail the build with a JSON path diagnostic.
|
||||
|
||||
All ten levels have device-level acceptance bots. They drive the production
|
||||
All sixteen levels have device-level acceptance bots. They drive the production
|
||||
`GameView` through the same movement and jump inputs as the touch controls and
|
||||
run each intended solution three times. Level 8 also has a dedicated recovery
|
||||
scenario for an expired timer on the far side of the pit. Stage 2 additionally
|
||||
@@ -112,8 +119,9 @@ mechanics slice adds data-driven crusher, fire, projectile and explosion hazards
|
||||
teaching levels 11–14, dynamic level pagination and production-input routes.
|
||||
Scope and final evidence are in `docs/STAGE6_CHARACTER_STATES.md`,
|
||||
`docs/STAGE6_CORPSE_MANAGER.md`, and `docs/STAGE6_PUZZLE_SYSTEM.md`; the latest
|
||||
hazard evidence is in `docs/STAGE6_PRODUCTION_HAZARDS.md`. The latest unfiltered
|
||||
gate is `99 JVM / 43 Android`.
|
||||
hazard evidence is in `docs/STAGE6_PRODUCTION_HAZARDS.md`, and the burned/crushed
|
||||
contract is in `docs/STAGE6_CORPSE_STATES.md`. The latest exact gate is
|
||||
`102 JVM / 46 Android` on the sixteen-level candidate.
|
||||
|
||||
The debug APK is written to `app/build/outputs/apk/debug/app-debug.apk`. It is signed with Android's standard debug key and can be installed with:
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class CorpseStateIntegrationTest {
|
||||
@Test
|
||||
public void crusherAndFireDeathsPublishDistinctPuzzleBodyStates() {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
activity.onActivity(current -> {
|
||||
GameView view = current.gameViewForAutomation();
|
||||
view.enableDeterministicAutomationClock();
|
||||
|
||||
view.startLevelForAutomation(11);
|
||||
view.setAutomationInput(false, true, false);
|
||||
view.advanceDeterministicAutomation(2_000L);
|
||||
GameView.AutomationSnapshot crushed = view.automationSnapshot();
|
||||
assertTrue(crushed.firstCorpseCrushed());
|
||||
assertFalse(crushed.firstCorpseBurned());
|
||||
assertTrue(crushed.firstCorpseActivatesPressure());
|
||||
assertTrue(crushed.firstCorpseWidth() > crushed.firstCorpseHeight());
|
||||
|
||||
view.startLevelForAutomation(12);
|
||||
view.setAutomationInput(false, true, false);
|
||||
view.advanceDeterministicAutomation(2_000L);
|
||||
GameView.AutomationSnapshot burned = view.automationSnapshot();
|
||||
assertTrue(burned.firstCorpseBurned());
|
||||
assertFalse(burned.firstCorpseCrushed());
|
||||
assertFalse(burned.firstCorpseActivatesPressure());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class CorpseStatePlaythroughTest {
|
||||
private static final int CONSECUTIVE_RUNS = 3;
|
||||
|
||||
@Test public void crushedBodyIsARequiredLowStepThreeTimes() {
|
||||
prove(CorpseStateScenarios.levelFifteen(), 45_000L);
|
||||
}
|
||||
|
||||
@Test public void burnedBodyLeavesThePressureBridgeDormantThreeTimes() {
|
||||
prove(CorpseStateScenarios.levelSixteen(), 45_000L);
|
||||
}
|
||||
|
||||
private static void prove(LevelPlaytestBot.Scenario scenario, long budget) {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
AndroidDriver driver = AndroidDriver.from(activity);
|
||||
for (int run = 1; run <= CONSECUTIVE_RUNS; run++) {
|
||||
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(driver, scenario, budget);
|
||||
assertTrue("run=" + run + "; " + result.diagnostic(), result.completed());
|
||||
assertEquals("run=" + run + "; route uses exactly one transformed body",
|
||||
1, result.deaths());
|
||||
assertEquals("run=" + run + "; transformed body must remain",
|
||||
1, result.corpseCount());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AndroidDriver implements LevelPlaytestBot.Driver {
|
||||
private final Instrumentation instrumentation;
|
||||
private final GameView gameView;
|
||||
|
||||
private AndroidDriver(Instrumentation instrumentation, GameView gameView) {
|
||||
this.instrumentation = instrumentation;
|
||||
this.gameView = gameView;
|
||||
}
|
||||
|
||||
static AndroidDriver from(ActivityScenario<MainActivity> activity) {
|
||||
GameView[] view = new GameView[1];
|
||||
activity.onActivity(current -> view[0] = current.gameViewForAutomation());
|
||||
return new AndroidDriver(InstrumentationRegistry.getInstrumentation(), view[0]);
|
||||
}
|
||||
|
||||
@Override public void startLevel(int oneBasedLevelNumber) {
|
||||
instrumentation.runOnMainSync(
|
||||
() -> gameView.startLevelForAutomation(oneBasedLevelNumber));
|
||||
}
|
||||
|
||||
@Override public void setInput(boolean moveLeft, boolean moveRight, boolean jump) {
|
||||
instrumentation.runOnMainSync(
|
||||
() -> gameView.setAutomationInput(moveLeft, moveRight, jump));
|
||||
}
|
||||
|
||||
@Override public GameView.AutomationSnapshot snapshot() {
|
||||
GameView.AutomationSnapshot[] snapshot = new GameView.AutomationSnapshot[1];
|
||||
instrumentation.runOnMainSync(() -> snapshot[0] = gameView.automationSnapshot());
|
||||
return snapshot[0];
|
||||
}
|
||||
|
||||
@Override public void waitForNextDecision(long millis) {
|
||||
try {
|
||||
Thread.sleep(Math.max(32L, millis));
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError("Corpse-state bot interrupted", interrupted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
/** Intended routes whose solution depends on the transformed corpse state. */
|
||||
final class CorpseStateScenarios {
|
||||
private CorpseStateScenarios() {}
|
||||
|
||||
static LevelPlaytestBot.Scenario levelFifteen() {
|
||||
return LevelPlaytestBot.Scenario.builder(15, "Level 15 - crushed low step")
|
||||
.step("Create one crushed body", 4_000L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
snapshot -> snapshot.deaths() == 1
|
||||
&& snapshot.corpseCount() == 1
|
||||
&& snapshot.firstCorpseCrushed()
|
||||
&& snapshot.firstCorpseWidth() > snapshot.firstCorpseHeight())
|
||||
.step("Wait for respawn", 3_000L, LevelPlaytestBot.Input.none(),
|
||||
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
|
||||
.step("Approach the press step", 2_000L, LevelPlaytestBot.Input.right(),
|
||||
LevelPlaytestBot.whileDeathsAtMost(1,
|
||||
LevelPlaytestBot.groundedAliveAtOrBeyond(520f)))
|
||||
.step("Observe a fresh press cycle", 4_000L,
|
||||
LevelPlaytestBot.Input.none(),
|
||||
GameView.AutomationSnapshot::firstHazardActive)
|
||||
.step("Wait for the full rest window", 4_000L,
|
||||
LevelPlaytestBot.Input.none(),
|
||||
snapshot -> !snapshot.firstHazardActive())
|
||||
.step("Land on the crushed low step", 2_000L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
LevelPlaytestBot.whileDeathsAtMost(1,
|
||||
snapshot -> snapshot.playerAlive()
|
||||
&& snapshot.playerOnGround()
|
||||
&& snapshot.playerX() >= 690f
|
||||
&& snapshot.playerY() <= 757f))
|
||||
.step("Jump from the step to the ledge", 2_500L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
LevelPlaytestBot.whileDeathsAtMost(1,
|
||||
snapshot -> snapshot.playerAlive()
|
||||
&& snapshot.playerOnGround()
|
||||
&& snapshot.playerX() >= 850f
|
||||
&& snapshot.playerY() <= 621f))
|
||||
.step("Reach the relic", 6_000L, LevelPlaytestBot.Input.right(),
|
||||
LevelPlaytestBot.whileDeathsAtMost(1,
|
||||
GameView.AutomationSnapshot::completed))
|
||||
.build();
|
||||
}
|
||||
|
||||
static LevelPlaytestBot.Scenario levelSixteen() {
|
||||
return LevelPlaytestBot.Scenario.builder(16, "Level 16 - burned pressure immunity")
|
||||
.step("Create one burned body on the plate", 4_000L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
snapshot -> snapshot.deaths() == 1
|
||||
&& snapshot.corpseCount() == 1
|
||||
&& snapshot.firstCorpseBurned()
|
||||
&& !snapshot.firstCorpseActivatesPressure())
|
||||
.step("Prove the burned body leaves the bridge dormant", 2_000L,
|
||||
LevelPlaytestBot.Input.none(),
|
||||
snapshot -> !snapshot.firstMovingPlatformPowered()
|
||||
&& snapshot.firstMovingPlatformY() >= 800f)
|
||||
.step("Wait for respawn", 3_000L, LevelPlaytestBot.Input.none(),
|
||||
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
|
||||
.step("Approach the flame before touching its plate", 2_000L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
LevelPlaytestBot.whileDeathsAtMost(1,
|
||||
LevelPlaytestBot.groundedAliveAtOrBeyond(535f)))
|
||||
.step("Take off over flame and plate", 1_000L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
LevelPlaytestBot.whileDeathsAtMost(1,
|
||||
snapshot -> snapshot.playerAlive()
|
||||
&& !snapshot.playerOnGround()
|
||||
&& snapshot.playerY() <= 700f))
|
||||
.step("Clear the burned plate", 2_000L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
LevelPlaytestBot.whileDeathsAtMost(1,
|
||||
snapshot -> snapshot.playerAlive()
|
||||
&& snapshot.playerX() >= 850f))
|
||||
.step("Approach the dormant bridge", 1_500L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 900f
|
||||
&& !snapshot.firstMovingPlatformPowered())
|
||||
.step("Board the dormant bridge", 2_000L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
LevelPlaytestBot.whileDeathsAtMost(1,
|
||||
GameView.AutomationSnapshot::playerOnMovingPlatform))
|
||||
.step("Cross to the far floor", 3_000L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
LevelPlaytestBot.whileDeathsAtMost(1,
|
||||
snapshot -> snapshot.playerAlive()
|
||||
&& snapshot.playerX() >= 1_450f))
|
||||
.step("Reach the relic", 5_000L, LevelPlaytestBot.Input.right(),
|
||||
LevelPlaytestBot.whileDeathsAtMost(1,
|
||||
GameView.AutomationSnapshot::completed))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public final class LevelPaginationTest {
|
||||
List<String> visible = view.visibleLocalizedTextForAutomation();
|
||||
assertEquals(1, view.levelPageIndexForAutomation());
|
||||
assertTrue(visible.contains("Steam Charge"));
|
||||
assertTrue(visible.contains("Ashen Counterweight"));
|
||||
assertFalse(visible.contains("The Volunteer"));
|
||||
assertTrue(visible.contains("Page 2 of 2"));
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@ public final class CorpseManager {
|
||||
public enum Transformation {
|
||||
PINNED,
|
||||
FROZEN,
|
||||
CONDUCTIVE
|
||||
CONDUCTIVE,
|
||||
BURNED,
|
||||
CRUSHED
|
||||
}
|
||||
|
||||
public static final class Body {
|
||||
@@ -24,6 +26,8 @@ public final class CorpseManager {
|
||||
boolean frozen;
|
||||
boolean conductive;
|
||||
boolean pinned;
|
||||
boolean burned;
|
||||
boolean crushed;
|
||||
boolean grounded;
|
||||
int supportIndex = -1;
|
||||
|
||||
@@ -45,6 +49,9 @@ public final class CorpseManager {
|
||||
public boolean frozen() { return frozen; }
|
||||
public boolean conductive() { return conductive; }
|
||||
public boolean pinned() { return pinned; }
|
||||
public boolean burned() { return burned; }
|
||||
public boolean crushed() { return crushed; }
|
||||
public boolean activatesPressure() { return !burned; }
|
||||
public boolean grounded() { return grounded; }
|
||||
public int supportIndex() { return supportIndex; }
|
||||
|
||||
@@ -148,6 +155,12 @@ public final class CorpseManager {
|
||||
case CONDUCTIVE:
|
||||
body.conductive = true;
|
||||
break;
|
||||
case BURNED:
|
||||
body.burned = true;
|
||||
break;
|
||||
case CRUSHED:
|
||||
body.crushed = true;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public final class GameSessionSnapshot {
|
||||
|
||||
public static final class BodyData {
|
||||
private final float x, y, width, height, velocityX, velocityY;
|
||||
private final boolean frozen, conductive, pinned, grounded;
|
||||
private final boolean frozen, conductive, pinned, grounded, crushed, burned;
|
||||
private final int supportIndex;
|
||||
private final String deathType;
|
||||
|
||||
@@ -51,6 +51,15 @@ public final class GameSessionSnapshot {
|
||||
float velocityX, float velocityY, boolean frozen,
|
||||
boolean conductive, boolean pinned, boolean grounded,
|
||||
int supportIndex, String deathType) {
|
||||
this(x, y, width, height, velocityX, velocityY, frozen, conductive,
|
||||
pinned, grounded, false, false, supportIndex, deathType);
|
||||
}
|
||||
|
||||
public BodyData(float x, float y, float width, float height,
|
||||
float velocityX, float velocityY, boolean frozen,
|
||||
boolean conductive, boolean pinned, boolean grounded,
|
||||
boolean crushed, boolean burned,
|
||||
int supportIndex, String deathType) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
@@ -61,6 +70,8 @@ public final class GameSessionSnapshot {
|
||||
this.conductive = conductive;
|
||||
this.pinned = pinned;
|
||||
this.grounded = grounded;
|
||||
this.crushed = crushed;
|
||||
this.burned = burned;
|
||||
this.supportIndex = supportIndex;
|
||||
this.deathType = deathType == null ? "SPIKE" : deathType;
|
||||
}
|
||||
@@ -75,6 +86,8 @@ public final class GameSessionSnapshot {
|
||||
public boolean conductive() { return conductive; }
|
||||
public boolean pinned() { return pinned; }
|
||||
public boolean grounded() { return grounded; }
|
||||
public boolean crushed() { return crushed; }
|
||||
public boolean burned() { return burned; }
|
||||
public int supportIndex() { return supportIndex; }
|
||||
public String deathType() { return deathType; }
|
||||
}
|
||||
|
||||
@@ -75,6 +75,11 @@ public final class GameView extends View implements Runnable {
|
||||
private final boolean firstHazardActive;
|
||||
private final float firstHazardX;
|
||||
private final float firstHazardY;
|
||||
private final boolean firstCorpseBurned;
|
||||
private final boolean firstCorpseCrushed;
|
||||
private final boolean firstCorpseActivatesPressure;
|
||||
private final float firstCorpseWidth;
|
||||
private final float firstCorpseHeight;
|
||||
|
||||
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
|
||||
boolean visiblyPaused, boolean inputActive, int levelNumber,
|
||||
@@ -163,6 +168,31 @@ public final class GameView extends View implements Runnable {
|
||||
boolean playerOnGround, float playerVelocityX,
|
||||
float playerVelocityY, boolean firstHazardActive,
|
||||
float firstHazardX, float firstHazardY) {
|
||||
this(completed, playerAlive, allDoorsOpen, visiblyPaused, inputActive, levelNumber,
|
||||
elapsedMilliseconds, playerX, playerY, deaths, corpseCount,
|
||||
firstMovingPlatformX, firstMovingPlatformY, playerOnMovingPlatform,
|
||||
firstMovingPlatformPowered, firstMovingPlatformMovingRight,
|
||||
firstMovingPlatformMovingDown, firstCorpseX, playerOnGround,
|
||||
playerVelocityX, playerVelocityY, firstHazardActive,
|
||||
firstHazardX, firstHazardY, false, false, false,
|
||||
Float.NaN, Float.NaN);
|
||||
}
|
||||
|
||||
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
|
||||
boolean visiblyPaused, boolean inputActive, int levelNumber,
|
||||
long elapsedMilliseconds,
|
||||
float playerX, float playerY, int deaths, int corpseCount,
|
||||
float firstMovingPlatformX, float firstMovingPlatformY,
|
||||
boolean playerOnMovingPlatform,
|
||||
boolean firstMovingPlatformPowered,
|
||||
boolean firstMovingPlatformMovingRight,
|
||||
boolean firstMovingPlatformMovingDown, float firstCorpseX,
|
||||
boolean playerOnGround, float playerVelocityX,
|
||||
float playerVelocityY, boolean firstHazardActive,
|
||||
float firstHazardX, float firstHazardY,
|
||||
boolean firstCorpseBurned, boolean firstCorpseCrushed,
|
||||
boolean firstCorpseActivatesPressure,
|
||||
float firstCorpseWidth, float firstCorpseHeight) {
|
||||
this.completed = completed;
|
||||
this.playerAlive = playerAlive;
|
||||
this.allDoorsOpen = allDoorsOpen;
|
||||
@@ -187,6 +217,11 @@ public final class GameView extends View implements Runnable {
|
||||
this.firstHazardActive = firstHazardActive;
|
||||
this.firstHazardX = firstHazardX;
|
||||
this.firstHazardY = firstHazardY;
|
||||
this.firstCorpseBurned = firstCorpseBurned;
|
||||
this.firstCorpseCrushed = firstCorpseCrushed;
|
||||
this.firstCorpseActivatesPressure = firstCorpseActivatesPressure;
|
||||
this.firstCorpseWidth = firstCorpseWidth;
|
||||
this.firstCorpseHeight = firstCorpseHeight;
|
||||
}
|
||||
|
||||
public boolean completed() { return completed; }
|
||||
@@ -213,6 +248,13 @@ public final class GameView extends View implements Runnable {
|
||||
public boolean firstHazardActive() { return firstHazardActive; }
|
||||
public float firstHazardX() { return firstHazardX; }
|
||||
public float firstHazardY() { return firstHazardY; }
|
||||
public boolean firstCorpseBurned() { return firstCorpseBurned; }
|
||||
public boolean firstCorpseCrushed() { return firstCorpseCrushed; }
|
||||
public boolean firstCorpseActivatesPressure() {
|
||||
return firstCorpseActivatesPressure;
|
||||
}
|
||||
public float firstCorpseWidth() { return firstCorpseWidth; }
|
||||
public float firstCorpseHeight() { return firstCorpseHeight; }
|
||||
}
|
||||
|
||||
private static final class Player {
|
||||
@@ -593,6 +635,7 @@ public final class GameView extends View implements Runnable {
|
||||
int mass = 0;
|
||||
if (player.alive && intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT, item.box)) mass++;
|
||||
for (Body corpse : corpses) {
|
||||
if (!corpse.activatesPressure()) continue;
|
||||
if (intersects(corpse.x, corpse.y, corpse.width, corpse.height, item.box)) mass++;
|
||||
}
|
||||
return mass >= item.requiredMass;
|
||||
@@ -868,6 +911,16 @@ public final class GameView extends View implements Runnable {
|
||||
corpse.vx = corpse.vy = 0f;
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.CONDUCTIVE);
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||
} else if (type == DeathType.FIRE) {
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.BURNED);
|
||||
} else if (type == DeathType.CRUSH && source != null) {
|
||||
corpse.width = 140f;
|
||||
corpse.height = 24f;
|
||||
corpse.x = source.box.x - 10f;
|
||||
corpse.y = source.box.bottom() - corpse.height;
|
||||
corpse.vx = corpse.vy = 0f;
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.CRUSHED);
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1305,6 +1358,19 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
|
||||
private void drawCorpse(Canvas canvas, Body corpse) {
|
||||
if (corpse.crushed) {
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE : Color.rgb(126, 93, 58));
|
||||
RectF slab = corpseRect(corpse);
|
||||
canvas.drawRoundRect(slab, 8f, 8f, paint);
|
||||
stroke.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.BLACK : Color.rgb(226, 177, 82));
|
||||
stroke.setStrokeWidth(5f);
|
||||
canvas.drawLine(slab.left + 10f, slab.centerY(),
|
||||
slab.right - 10f, slab.centerY(), stroke);
|
||||
drawWorldLabel(canvas, "CRUSHED", slab.centerX(), slab.top - 16f);
|
||||
return;
|
||||
}
|
||||
canvas.save();
|
||||
float cx = corpse.x + corpse.width * 0.5f;
|
||||
float cy = corpse.y + corpse.height * 0.5f;
|
||||
@@ -1319,6 +1385,17 @@ public final class GameView extends View implements Runnable {
|
||||
levelElapsed * 1.6f, 1, 0.82f,
|
||||
accessibilitySettings.highContrastEnabled());
|
||||
canvas.restore();
|
||||
if (corpse.burned) {
|
||||
RectF charred = corpseRect(corpse);
|
||||
paint.setColor(Color.argb(145, 28, 24, 29));
|
||||
canvas.drawRoundRect(charred, 12f, 12f, paint);
|
||||
stroke.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE : Color.rgb(255, 138, 54));
|
||||
stroke.setStrokeWidth(5f);
|
||||
canvas.drawLine(charred.left + 10f, charred.bottom - 10f,
|
||||
charred.right - 10f, charred.top + 10f, stroke);
|
||||
drawWorldLabel(canvas, "BURNED", charred.centerX(), charred.top - 16f);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawHud(Canvas canvas) {
|
||||
@@ -2494,7 +2571,8 @@ public final class GameView extends View implements Runnable {
|
||||
builder.addBody(new GameSessionSnapshot.BodyData(
|
||||
corpse.x, corpse.y, corpse.width, corpse.height,
|
||||
corpse.vx, corpse.vy, corpse.frozen, corpse.conductive,
|
||||
corpse.pinned, corpse.grounded, corpse.supportIndex,
|
||||
corpse.pinned, corpse.grounded, corpse.crushed, corpse.burned,
|
||||
corpse.supportIndex,
|
||||
corpse.deathType()));
|
||||
}
|
||||
ArrayList<Float> motionTimes = new ArrayList<>(platformStates.size());
|
||||
@@ -2579,6 +2657,12 @@ public final class GameView extends View implements Runnable {
|
||||
if (savedBody.pinned()) {
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||
}
|
||||
if (savedBody.crushed()) {
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.CRUSHED);
|
||||
}
|
||||
if (savedBody.burned()) {
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.BURNED);
|
||||
}
|
||||
}
|
||||
updateSignals(0f, false);
|
||||
|
||||
@@ -2636,6 +2720,7 @@ public final class GameView extends View implements Runnable {
|
||||
firstHazardX = firstHazardBox.x;
|
||||
firstHazardY = firstHazardBox.y;
|
||||
}
|
||||
Body firstCorpse = corpses.isEmpty() ? null : corpses.get(0);
|
||||
return new AutomationSnapshot(
|
||||
screen == Screen.COMPLETE,
|
||||
player.alive,
|
||||
@@ -2660,7 +2745,12 @@ public final class GameView extends View implements Runnable {
|
||||
player.vy,
|
||||
firstHazardActive,
|
||||
firstHazardX,
|
||||
firstHazardY
|
||||
firstHazardY,
|
||||
firstCorpse != null && firstCorpse.burned(),
|
||||
firstCorpse != null && firstCorpse.crushed(),
|
||||
firstCorpse != null && firstCorpse.activatesPressure(),
|
||||
firstCorpse == null ? Float.NaN : firstCorpse.width(),
|
||||
firstCorpse == null ? Float.NaN : firstCorpse.height()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public final class VersionedSessionCodec {
|
||||
private static final int MAX_BODIES = 16;
|
||||
private static final int MAX_PLATFORMS = 64;
|
||||
private static final int MAX_MAP_ENTRIES = 128;
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
public static final int CURRENT_VERSION = 3;
|
||||
|
||||
public enum Status { RESTORED, MIGRATED, UNSUPPORTED, CORRUPT }
|
||||
|
||||
@@ -41,6 +41,10 @@ public final class VersionedSessionCodec {
|
||||
return encodeWithVersion(snapshot, 1);
|
||||
}
|
||||
|
||||
static byte[] encodeLegacyV2(GameSessionSnapshot snapshot) {
|
||||
return encodeWithVersion(snapshot, 2);
|
||||
}
|
||||
|
||||
static byte[] encodeVersionForTest(int version) {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
@@ -75,7 +79,7 @@ public final class VersionedSessionCodec {
|
||||
.player(readPlayer(input));
|
||||
|
||||
int bodyCount = checkedCount(input.readInt(), MAX_BODIES);
|
||||
for (int i = 0; i < bodyCount; i++) builder.addBody(readBody(input));
|
||||
for (int i = 0; i < bodyCount; i++) builder.addBody(readBody(input, version));
|
||||
|
||||
int platformCount = checkedCount(input.readInt(), MAX_PLATFORMS);
|
||||
ArrayList<Float> motionTimes = new ArrayList<>(platformCount);
|
||||
@@ -108,7 +112,9 @@ public final class VersionedSessionCodec {
|
||||
output.writeFloat(snapshot.respawnSeconds());
|
||||
writePlayer(output, snapshot.player());
|
||||
output.writeInt(snapshot.bodies().size());
|
||||
for (GameSessionSnapshot.BodyData body : snapshot.bodies()) writeBody(output, body);
|
||||
for (GameSessionSnapshot.BodyData body : snapshot.bodies()) {
|
||||
writeBody(output, body, version);
|
||||
}
|
||||
output.writeInt(snapshot.platformMotionTimes().size());
|
||||
for (Float time : snapshot.platformMotionTimes()) output.writeFloat(time);
|
||||
writeBooleanMap(output, snapshot.toggleSignals());
|
||||
@@ -145,7 +151,8 @@ public final class VersionedSessionCodec {
|
||||
input.readInt(), input.readInt());
|
||||
}
|
||||
|
||||
private static void writeBody(DataOutputStream output, GameSessionSnapshot.BodyData body)
|
||||
private static void writeBody(DataOutputStream output, GameSessionSnapshot.BodyData body,
|
||||
int version)
|
||||
throws IOException {
|
||||
output.writeFloat(body.x());
|
||||
output.writeFloat(body.y());
|
||||
@@ -157,11 +164,15 @@ public final class VersionedSessionCodec {
|
||||
output.writeBoolean(body.conductive());
|
||||
output.writeBoolean(body.pinned());
|
||||
output.writeBoolean(body.grounded());
|
||||
if (version >= 3) {
|
||||
output.writeBoolean(body.crushed());
|
||||
output.writeBoolean(body.burned());
|
||||
}
|
||||
output.writeInt(body.supportIndex());
|
||||
output.writeUTF(body.deathType());
|
||||
}
|
||||
|
||||
private static GameSessionSnapshot.BodyData readBody(DataInputStream input)
|
||||
private static GameSessionSnapshot.BodyData readBody(DataInputStream input, int version)
|
||||
throws IOException {
|
||||
float x = finite(input.readFloat());
|
||||
float y = finite(input.readFloat());
|
||||
@@ -173,12 +184,14 @@ public final class VersionedSessionCodec {
|
||||
boolean conductive = input.readBoolean();
|
||||
boolean pinned = input.readBoolean();
|
||||
boolean grounded = input.readBoolean();
|
||||
boolean crushed = version >= 3 && input.readBoolean();
|
||||
boolean burned = version >= 3 && input.readBoolean();
|
||||
int support = input.readInt();
|
||||
String deathType = input.readUTF();
|
||||
if (deathType.length() > 24) throw new IllegalArgumentException("Death type too long");
|
||||
return new GameSessionSnapshot.BodyData(x, y, width, height,
|
||||
velocityX, velocityY, frozen, conductive, pinned, grounded,
|
||||
support, deathType);
|
||||
crushed, burned, support, deathType);
|
||||
}
|
||||
|
||||
private static void writeBooleanMap(DataOutputStream output, Map<String, Boolean> values)
|
||||
|
||||
@@ -68,4 +68,34 @@ public final class CorpseManagerTest {
|
||||
CorpseManager.Body foreign = new CorpseManager(1).register("SAW", 0f, 0f);
|
||||
assertFalse(manager.transform(foreign, CorpseManager.Transformation.FROZEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void burnedBodyRemainsPhysicalButCannotActivatePressureMechanisms() {
|
||||
CorpseManager manager = new CorpseManager(2);
|
||||
CorpseManager.Body body = manager.register("FIRE", 10f, 20f);
|
||||
|
||||
assertTrue(manager.transform(body, CorpseManager.Transformation.BURNED));
|
||||
|
||||
assertTrue(body.burned());
|
||||
assertFalse(body.crushed());
|
||||
assertFalse(body.activatesPressure());
|
||||
assertTrue(manager.bodies().contains(body));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void crushedBodyKeepsOtherTransformationsAndExposesItsLowProfile() {
|
||||
CorpseManager manager = new CorpseManager(2);
|
||||
CorpseManager.Body body = manager.register("CRUSH", 10f, 20f)
|
||||
.size(140f, 24f);
|
||||
|
||||
assertTrue(manager.transform(body, CorpseManager.Transformation.PINNED));
|
||||
assertTrue(manager.transform(body, CorpseManager.Transformation.CRUSHED));
|
||||
|
||||
assertTrue(body.crushed());
|
||||
assertFalse(body.burned());
|
||||
assertTrue(body.pinned());
|
||||
assertTrue(body.activatesPressure());
|
||||
assertEquals(140f, body.width(), 0f);
|
||||
assertEquals(24f, body.height(), 0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import java.util.Set;
|
||||
|
||||
public final class LevelCatalogTest {
|
||||
@Test
|
||||
public void productionCatalogContainsFourHazardTeachingLevelsAfterTheVerticalSlice() {
|
||||
public void productionCatalogContainsHazardsAndTwoCorpseStateTeachingLevels() {
|
||||
List<LevelDefinition> levels = LevelCatalog.all();
|
||||
assertEquals(14, levels.size());
|
||||
assertEquals(16, levels.size());
|
||||
Set<String> titles = new HashSet<>();
|
||||
for (int i = 0; i < levels.size(); i++) {
|
||||
LevelDefinition level = levels.get(i);
|
||||
@@ -26,6 +26,9 @@ public final class LevelCatalogTest {
|
||||
assertTrue(levels.get(11).containsMechanic(LevelDefinition.HazardType.FIRE));
|
||||
assertTrue(levels.get(12).containsMechanic(LevelDefinition.HazardType.PROJECTILE));
|
||||
assertTrue(levels.get(13).containsMechanic(LevelDefinition.HazardType.EXPLOSION));
|
||||
assertTrue(levels.get(14).containsMechanic(LevelDefinition.HazardType.CRUSHER));
|
||||
assertTrue(levels.get(15).containsMechanic(LevelDefinition.HazardType.FIRE));
|
||||
assertFalse(levels.get(15).platforms.get(1).controlId.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -8,8 +8,8 @@ import org.junit.Test;
|
||||
|
||||
public final class LevelPageTest {
|
||||
@Test
|
||||
public void fourteenLevelsUseTwoBoundedTenCardPages() {
|
||||
LevelPage page = new LevelPage(14, 10);
|
||||
public void sixteenLevelsUseTwoBoundedTenCardPages() {
|
||||
LevelPage page = new LevelPage(16, 10);
|
||||
|
||||
assertEquals(2, page.pageCount());
|
||||
assertEquals(0, page.firstIndex());
|
||||
@@ -19,7 +19,7 @@ public final class LevelPageTest {
|
||||
|
||||
page.next();
|
||||
assertEquals(10, page.firstIndex());
|
||||
assertEquals(14, page.endExclusive());
|
||||
assertEquals(16, page.endExclusive());
|
||||
assertTrue(page.hasPrevious());
|
||||
assertFalse(page.hasNext());
|
||||
|
||||
@@ -27,7 +27,7 @@ public final class LevelPageTest {
|
||||
assertEquals(1, page.pageIndex());
|
||||
page.selectIndex(3);
|
||||
assertEquals(0, page.pageIndex());
|
||||
page.selectIndex(13);
|
||||
page.selectIndex(15);
|
||||
assertEquals(1, page.pageIndex());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ public final class VersionedSessionCodecTest {
|
||||
assertEquals(2, restored.bodies().size());
|
||||
assertTrue(restored.bodies().get(0).pinned());
|
||||
assertTrue(restored.bodies().get(1).conductive());
|
||||
assertTrue(restored.bodies().get(0).crushed());
|
||||
assertTrue(restored.bodies().get(1).burned());
|
||||
assertEquals(Arrays.asList(1.25f, 3.5f), restored.platformMotionTimes());
|
||||
assertTrue(restored.toggleSignals().get("gate"));
|
||||
assertEquals(2.25f, restored.timedSignals().get("clock"), 0f);
|
||||
@@ -43,6 +45,19 @@ public final class VersionedSessionCodecTest {
|
||||
decoded.snapshot().saveVersion());
|
||||
assertTrue(decoded.snapshot().previousSwitchContacts().isEmpty());
|
||||
assertEquals(2, decoded.snapshot().bodies().size());
|
||||
assertFalse(decoded.snapshot().bodies().get(0).crushed());
|
||||
assertFalse(decoded.snapshot().bodies().get(1).burned());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyV2MigratesWithSafeDefaultCorpseStates() {
|
||||
VersionedSessionCodec.DecodeResult decoded = VersionedSessionCodec.decode(
|
||||
VersionedSessionCodec.encodeLegacyV2(sampleSnapshot()));
|
||||
|
||||
assertEquals(VersionedSessionCodec.Status.MIGRATED, decoded.status());
|
||||
assertTrue(decoded.snapshot().previousSwitchContacts().get("gate@420.0"));
|
||||
assertFalse(decoded.snapshot().bodies().get(0).crushed());
|
||||
assertFalse(decoded.snapshot().bodies().get(1).burned());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -84,10 +99,12 @@ public final class VersionedSessionCodecTest {
|
||||
0.1f, 0.08f, -1, -1))
|
||||
.addBody(new GameSessionSnapshot.BodyData(
|
||||
500f, 826f, 72f, 54f, 0f, 0f,
|
||||
false, false, true, true, -1, "SPIKE"))
|
||||
false, false, true, true, true, false,
|
||||
-1, "CRUSH"))
|
||||
.addBody(new GameSessionSnapshot.BodyData(
|
||||
720f, 826f, 72f, 54f, 0f, 0f,
|
||||
false, true, true, true, -1, "ELECTRIC"))
|
||||
false, true, true, true, false, true,
|
||||
-1, "FIRE"))
|
||||
.platformMotionTimes(Arrays.asList(1.25f, 3.5f))
|
||||
.toggleSignal("gate", true)
|
||||
.timedSignal("clock", 2.25f)
|
||||
|
||||
@@ -189,7 +189,9 @@ data-driven модель. Завершён первый mechanics slice: product
|
||||
(`crusher`, `fire`, `projectile`, `explosion`), обучающие уровни 11–14,
|
||||
динамическая пагинация каталога и 12/12 bot-прохождений. Последний gate:
|
||||
99 JVM-тестов и Android 16 matrix `43/43` (`2004.172 s`). Следующая зависимая
|
||||
итерация — новые corpse states на источниках production hazards.
|
||||
итерация завершена: `CRUSHED` и `BURNED`, save migration и обучающие уровни
|
||||
15–16 приняты clean gate `102 JVM` и unfiltered Android matrix `46/46`
|
||||
(`1,842.248 s`). Доказательства зафиксированы в `STAGE6_CORPSE_STATES.md`.
|
||||
|
||||
### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Графы решений уровней 1–10
|
||||
# Графы решений уровней 1–16
|
||||
|
||||
Документ фиксирует **авторский маршрут** каждого уровня текущего vertical slice. Он получен из `LevelCatalog`, моделей `LevelDefinition` и фактической логики `GameView`, затем проверен device-level ботами через production-команды движения и прыжка. `targetDeaths` ниже — целевое число жертв для результата уровня, но оно не доказывает минимальность решения.
|
||||
|
||||
На 18 августа 2026 года intended route всех уровней 1–10 автоматически выполнен по три раза подряд без retry masking. `EarlyLevelPlaythroughTest`, `LevelFivePlaythroughTest`, `LateLevelPlaythroughTest` и `LevelEightPlaythroughTest` управляют production `GameView` через те же флаги, что и touch-контролы. Для уровня 8 отдельно проверено восстановление после истечения таймера на дальней стороне `PIT`.
|
||||
На 19 августа 2026 года intended route всех уровней 1–16 автоматически выполнен по три раза подряд без retry masking. Device-тесты управляют production `GameView` через те же флаги, что и touch-контролы. Для уровня 8 отдельно проверено восстановление после истечения таймера на дальней стороне `PIT`.
|
||||
|
||||
## Семантика, общая для графов
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
- `TIMED` запускает таймер только на фронте контакта; удержание на переключателе не перезапускает истёкший таймер.
|
||||
- `Door.requiredSignals` использует AND-семантику: дверь открыта, только когда активны все указанные сигналы.
|
||||
- Управляемая движущаяся платформа накапливает `motionTime` только при активном `controlId`; при потере сигнала она замирает в текущей фазе `GameMath.pingPong`.
|
||||
- `SPIKES` оставляет закреплённое тело над шипами; `SAW` передаёт телу `impulseX`/`impulseY`; `FREEZER` создаёт неподвижный блок `104×104`; `ELECTRIC` оставляет закреплённое проводящее тело внутри опасности; `PIT` тела не оставляет.
|
||||
- `SPIKES` оставляет закреплённое тело над шипами; `SAW` передаёт телу `impulseX`/`impulseY`; `FREEZER` создаёт неподвижный блок `104×104`; `ELECTRIC` оставляет закреплённое проводящее тело внутри опасности; `CRUSHER` создаёт низкое `CRUSHED`-тело; `FIRE` оставляет твёрдое `BURNED`-тело без pressure mass; `PIT` тела не оставляет.
|
||||
- Тела являются твёрдыми опорами для игрока. Смерть сохраняет тела и состояния уровня, respawn возвращает нового игрока в `spawnX`/`spawnY`; restart очищает состояние.
|
||||
- Пересечение живого игрока с `goal` переводит сцену в `COMPLETE`.
|
||||
|
||||
@@ -360,6 +360,51 @@ Door требует signal "blast-gate" от удалённой PRESSURE
|
||||
доказал перелёт тела за плиту; минимальное authored значение `550` стабильно
|
||||
помещает тело в `firstCorpseX=980`, после чего `allDoorsOpen=true`.
|
||||
|
||||
## Уровень 15 — `Pressed Into Service`
|
||||
|
||||
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
|
||||
**Ожидаемые и доказанные смерти:** `1`; сохранённое тело обязано иметь состояние
|
||||
`CRUSHED`, размер `140×24` и оставаться твёрдой опорой.
|
||||
|
||||
```text
|
||||
Верхний ledge начинается на высоте y=720 и недоступен прыжком с floor
|
||||
→ первый доброволец входит в активный CRUSHER
|
||||
→ пресс превращает тело в низкую закреплённую плиту 140×24
|
||||
→ следующий доброволец наблюдает новый активный цикл и ждёт REST
|
||||
→ проходит безопасный пресс и приземляется на crushed body
|
||||
→ отдельным прыжком использует 24 px ступень для достижения ledge
|
||||
→ проходит по верхней платформе к goal
|
||||
→ COMPLETE ровно с одной смертью и одним crushed body
|
||||
```
|
||||
|
||||
**Контракт:** `CRUSHED` композиционно сохраняется в `CorpseManager`, уменьшает
|
||||
коллайдер и не удаляет физическую опору. Save version 3 сохраняет флаг; v1/v2
|
||||
мигрируют его в `false`. Snapshot отдельно публикует state, размеры и pressure
|
||||
capability, а Canvas добавляет текстовую метку `CRUSHED` помимо цвета.
|
||||
|
||||
## Уровень 16 — `Ashen Counterweight`
|
||||
|
||||
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
|
||||
**Ожидаемые и доказанные смерти:** `1`; сохранённое тело обязано иметь состояние
|
||||
`BURNED`, оставаться твёрдым и не активировать `PRESSURE`.
|
||||
|
||||
```text
|
||||
Moving bridge над PIT управляется signal "burn-weight"
|
||||
→ первый доброволец пересекает PRESSURE и погибает в FIRE над плитой
|
||||
→ burned body остаётся на плите как твёрдый объект
|
||||
→ burned body не даёт pressure mass: signal остаётся inactive
|
||||
→ мост остаётся в нижней фазе у y≈840 вместо подъёма
|
||||
→ следующий доброволец перепрыгивает FIRE и burned body
|
||||
→ садится на неподвижный мост и пересекает PIT
|
||||
→ достигает goal на правом floor
|
||||
→ COMPLETE ровно с одной смертью и одним burned body
|
||||
```
|
||||
|
||||
**Контракт:** `BURNED` сохраняет коллайдер, но `activatesPressure=false`; это
|
||||
проверяется одновременно состоянием тела, неактивным `firstMovingPlatformPowered`
|
||||
и нижней координатой моста. Save version 3 сохраняет флаг; Canvas добавляет
|
||||
текстовую метку `BURNED`, поэтому состояние не кодируется только цветом.
|
||||
|
||||
## Проверки, следующие из графов
|
||||
|
||||
1. Сохранять полный integrated device-regression: intended route принятых уровней по три раза и отдельный recovery route уровня 8.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Stage 6 production corpse states
|
||||
|
||||
Date: 19 August 2026
|
||||
|
||||
Beads task: `goon-game-corpse-states`
|
||||
|
||||
## Contract
|
||||
|
||||
The accepted `CorpseManager` transformations now include two production states:
|
||||
|
||||
- `CRUSHED`: a pinned `140×24` solid body whose changed collider creates a low
|
||||
step without becoming a full-height wall;
|
||||
- `BURNED`: a solid retained body that no longer contributes pressure mass.
|
||||
|
||||
Both transformations compose with existing body ownership and pinning, have
|
||||
distinct non-color Canvas labels, are exposed through read-only automation, and
|
||||
are serialized in active-session save version 3. Legacy version 1 and version 2
|
||||
sessions migrate both flags to `false`; progress save versioning is unchanged.
|
||||
|
||||
This is the complete set of new gameplay states approved by the GDD. `IsDestroyed`
|
||||
is a lifecycle field in the corpse data model, not a listed corpse-state variant;
|
||||
the plan does not define a separate `Shattered` state.
|
||||
|
||||
Level 15 teaches the collider difference by requiring the crushed slab to reach
|
||||
an otherwise inaccessible ledge. Level 16 teaches the mechanism difference by
|
||||
requiring a burned body to remain on a pressure plate without powering the
|
||||
bridge above a pit. Normal, pinned, frozen and conductive states retain their
|
||||
accepted teaching routes in levels 2/3/5, 6 and 7/9/10 respectively.
|
||||
|
||||
## TDD evidence
|
||||
|
||||
Focused RED cycles covered the missing transformations and behavior, legacy
|
||||
codec migration, GameView state publication, catalog growth from 14 to 16, and
|
||||
both production-input scenarios. Device RED on level 16 showed a small bridge
|
||||
offset caused by the living volunteer touching the plate before death; the
|
||||
acceptance was corrected to prove the authored behavior (`powered=false` and a
|
||||
lower bridge) without requiring an impossible exact initial coordinate.
|
||||
|
||||
The focused runtime integration test creates crushed and burned bodies through
|
||||
real level hazards. Each playthrough test runs its intended route three times,
|
||||
with no restart or direct game-state mutation, and requires exactly one death
|
||||
and one retained body.
|
||||
|
||||
## Acceptance gates
|
||||
|
||||
- JSON validator: `16/16`, format `1`, physics `1`.
|
||||
- Level 15: `3/3` GREEN in `77.563 s`.
|
||||
- Level 16: `3/3` GREEN in `32.015 s` after the state-based bridge assertion.
|
||||
- Runtime state integration: `1/1` GREEN in `1.165 s`.
|
||||
- Combined corpse-state/hazard/pagination matrix: `8/8` methods GREEN in
|
||||
`408.438 s` (18 production-input playthroughs plus state and pagination tests).
|
||||
- Clean gate: `102` JVM tests, failures/errors/skipped `0`; `lintDebug`,
|
||||
`assembleDebug` and `assembleDebugAndroidTest` GREEN.
|
||||
- Focused exact-candidate save/restore plus runtime state gate: `3/3` GREEN in
|
||||
`47.489 s`.
|
||||
- Final unfiltered Android 16 matrix on the exact clean APK/test APK: `46/46`,
|
||||
failures `0`, errors `0`, skipped `0`, `1,842.248 s`.
|
||||
- Debug APK ZIP integrity: GREEN; packaged JSON levels: `16`.
|
||||
- Package: `com.projectsacrifice.game.debug`, versionCode `9`, minSdk `23`,
|
||||
targetSdk `34`; APK Signature Scheme v1/v2 verification: GREEN.
|
||||
- Debug APK SHA-256:
|
||||
`4984f50aa62203060eb254b69dbf2bdf520ab6cd545eff852d22b10a164bf686`.
|
||||
|
||||
The dedicated AVD was stopped after the final gate.
|
||||
@@ -65,7 +65,7 @@ Snapshot на 19 августа 2026 года:
|
||||
| 3 | 5 | 0 |
|
||||
| 4 | 4 | 0 |
|
||||
| 5 | 15 | 0 |
|
||||
| 6 | 18 | 12 |
|
||||
| 6 | 18 | 10 |
|
||||
| 7 | 9 | 9 |
|
||||
| 8 | 1 | 1 |
|
||||
| 9 | 1 | 1 |
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"formatVersion":1,"physicsVersion":1,"levels":["level-01.json","level-02.json","level-03.json","level-04.json","level-05.json","level-06.json","level-07.json","level-08.json","level-09.json","level-10.json","level-11.json","level-12.json","level-13.json","level-14.json"]}
|
||||
{"formatVersion":1,"physicsVersion":1,"levels":["level-01.json","level-02.json","level-03.json","level-04.json","level-05.json","level-06.json","level-07.json","level-08.json","level-09.json","level-10.json","level-11.json","level-12.json","level-13.json","level-14.json","level-15.json","level-16.json"]}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"formatVersion":1,"physicsVersion":1,"number":15,
|
||||
"copy":{"en":{"title":"Pressed Into Service","hint":"The press leaves a low step. Use its thin profile to reach the ledge."},"ru":{"title":"Служба под прессом","hint":"Пресс оставляет низкую ступень. Используйте её профиль, чтобы достичь уступа."}},
|
||||
"world":{"width":2400,"height":1080},"spawn":{"x":130,"y":760},
|
||||
"goal":{"x":2180,"y":610,"width":86,"height":110},"targets":{"deaths":1,"seconds":40},
|
||||
"platforms":[
|
||||
{"box":{"x":0,"y":880,"width":2400,"height":200}},
|
||||
{"box":{"x":850,"y":720,"width":1550,"height":160}}
|
||||
],
|
||||
"hazards":[{"type":"CRUSHER","box":{"x":700,"y":690,"width":140,"height":190},
|
||||
"impulseX":0,"impulseY":0,"cycleSeconds":4,"activeSeconds":2.5,
|
||||
"speedX":0,"speedY":0,"visualStyle":"brass-press","audioCue":"crusher-impact"}],
|
||||
"switches":[],"doors":[]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"formatVersion":1,"physicsVersion":1,"number":16,
|
||||
"copy":{"en":{"title":"Ashen Counterweight","hint":"Ash remains solid but cannot wake the bridge. Use that difference."},"ru":{"title":"Пепельный противовес","hint":"Пепел остаётся твёрдым, но не запускает мост. Используйте это различие."}},
|
||||
"world":{"width":2400,"height":1080},"spawn":{"x":130,"y":760},
|
||||
"goal":{"x":2200,"y":770,"width":86,"height":110},"targets":{"deaths":1,"seconds":45},
|
||||
"platforms":[
|
||||
{"box":{"x":0,"y":880,"width":1000,"height":200}},
|
||||
{"box":{"x":1000,"y":840,"width":400,"height":40},"travelX":0,"travelY":-300,"periodSeconds":5,"controlId":"burn-weight"},
|
||||
{"box":{"x":1400,"y":880,"width":1000,"height":200}}
|
||||
],
|
||||
"hazards":[
|
||||
{"type":"FIRE","box":{"x":700,"y":790,"width":120,"height":90},
|
||||
"impulseX":0,"impulseY":0,"cycleSeconds":0,"activeSeconds":0,
|
||||
"speedX":0,"speedY":0,"visualStyle":"blue-flame","audioCue":"fire-burst"},
|
||||
{"type":"PIT","box":{"x":1000,"y":880,"width":400,"height":200},
|
||||
"impulseX":0,"impulseY":0,"cycleSeconds":0,"activeSeconds":0,
|
||||
"speedX":0,"speedY":0,"visualStyle":"","audioCue":""}
|
||||
],
|
||||
"switches":[{"type":"PRESSURE","box":{"x":610,"y":860,"width":180,"height":20},
|
||||
"signalId":"burn-weight","durationSeconds":0,"requiredMass":1}],
|
||||
"doors":[]
|
||||
}
|
||||
Reference in New Issue
Block a user