Добавить первую волну мира Physics

This commit is contained in:
2026-08-19 15:53:13 +03:00
parent f53e068cb1
commit 243da9185e
21 changed files with 647 additions and 28 deletions
+1 -1
View File
@@ -67,7 +67,7 @@
{"_type":"issue","id":"goon-game-analytics-contract","title":"Схема gameplay analytics","description":"Версионировать level_start/restart/death/complete/abandon/challenge и heatmap payload.","acceptance_criteria":"События документированы, минимальны и проверены на тестовом backend.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:40:36Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:35Z","metadata":{"stages":7,"primary_stage":7},"labels":["analytics","primary-stage-7","production","stage-7"],"dependencies":[{"issue_id":"goon-game-analytics-contract","depends_on_id":"goon-game-e14","type":"parent-child","created_at":"2026-08-18T00:43:42Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-analytics-contract","title":"Схема gameplay analytics","description":"Версионировать level_start/restart/death/complete/abandon/challenge и heatmap payload.","acceptance_criteria":"События документированы, минимальны и проверены на тестовом backend.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:40:36Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:35Z","metadata":{"stages":7,"primary_stage":7},"labels":["analytics","primary-stage-7","production","stage-7"],"dependencies":[{"issue_id":"goon-game-analytics-contract","depends_on_id":"goon-game-e14","type":"parent-child","created_at":"2026-08-18T00:43:42Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"goon-game-modular-characters","title":"Модульные персонажи и косметика","description":"Body/helmet/face/weapon/cape/accessory, случайные имена и 30+ косметических элементов.","acceptance_criteria":"Новые жертвы визуально различаются, комбинации не ломают анимации.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":2880,"created_at":"2026-08-17T21:40:21Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:25Z","metadata":{"stages":6,"primary_stage":6},"labels":["art","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-modular-characters","depends_on_id":"goon-game-e11","type":"parent-child","created_at":"2026-08-18T00:43:33Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-modular-characters","title":"Модульные персонажи и косметика","description":"Body/helmet/face/weapon/cape/accessory, случайные имена и 30+ косметических элементов.","acceptance_criteria":"Новые жертвы визуально различаются, комбинации не ломают анимации.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":2880,"created_at":"2026-08-17T21:40:21Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:25Z","metadata":{"stages":6,"primary_stage":6},"labels":["art","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-modular-characters","depends_on_id":"goon-game-e11","type":"parent-child","created_at":"2026-08-18T00:43:33Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"goon-game-challenges","title":"Challenges, collectibles и статистика","description":"Реализовать casualty/time/collectible цели и отображение трёх звёзд.","acceptance_criteria":"Расчёт покрыт unit-тестами и сохраняется по LevelID.","notes":"2026-08-19: claimed after weighted-object slice 259fcd3 passed 108 app JVM + 2 content-tool JVM and exact Android 48/48. TDD scope: stable LevelID/WorldID progression keys with legacy numeric migration, casualty/time/collectible challenge evaluation, persistent best three-star record, data-driven optional collectible, level-select/result rendering, automation and one teaching route without weakening existing gates.\nFinal post-review gate: duplicate stable LevelID now fails catalog load before progress-key collision (targeted RED then GREEN). Clean 116 app JVM + 2 content-tool JVM; final cold host-GPU AVD exact Android 52/52 in 551.773s. Verified APK SHA-256 0b70a0a1a508425c311d7de473318e3bb83407f5f27e9dd4a8e1876b2eb25344.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:40:13Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T06:24:46Z","started_at":"2026-08-19T04:41:26Z","closed_at":"2026-08-19T05:58:26Z","close_reason":"Accepted stable LevelID/WorldID challenge progression: progress v3/session v5 migrations, casualty/time/collectible stars, level 18 3/3 route, 115+2 JVM and exact Android 52/52 (553.686s), APK SHA-256 9c12cfbb15e313421d58f891d9d107af14c9e8e626228fa8fd0c74e56c293d14.","metadata":{"stages":6,"primary_stage":6},"labels":["primary-stage-6","production","progression","stage-6"],"dependencies":[{"issue_id":"goon-game-challenges","depends_on_id":"goon-game-e09","type":"parent-child","created_at":"2026-08-18T00:43:29Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-challenges","title":"Challenges, collectibles и статистика","description":"Реализовать casualty/time/collectible цели и отображение трёх звёзд.","acceptance_criteria":"Расчёт покрыт unit-тестами и сохраняется по LevelID.","notes":"2026-08-19: claimed after weighted-object slice 259fcd3 passed 108 app JVM + 2 content-tool JVM and exact Android 48/48. TDD scope: stable LevelID/WorldID progression keys with legacy numeric migration, casualty/time/collectible challenge evaluation, persistent best three-star record, data-driven optional collectible, level-select/result rendering, automation and one teaching route without weakening existing gates.\nFinal post-review gate: duplicate stable LevelID now fails catalog load before progress-key collision (targeted RED then GREEN). Clean 116 app JVM + 2 content-tool JVM; final cold host-GPU AVD exact Android 52/52 in 551.773s. Verified APK SHA-256 0b70a0a1a508425c311d7de473318e3bb83407f5f27e9dd4a8e1876b2eb25344.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:40:13Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T06:24:46Z","started_at":"2026-08-19T04:41:26Z","closed_at":"2026-08-19T05:58:26Z","close_reason":"Accepted stable LevelID/WorldID challenge progression: progress v3/session v5 migrations, casualty/time/collectible stars, level 18 3/3 route, 115+2 JVM and exact Android 52/52 (553.686s), APK SHA-256 9c12cfbb15e313421d58f891d9d107af14c9e8e626228fa8fd0c74e56c293d14.","metadata":{"stages":6,"primary_stage":6},"labels":["primary-stage-6","production","progression","stage-6"],"dependencies":[{"issue_id":"goon-game-challenges","depends_on_id":"goon-game-e09","type":"parent-child","created_at":"2026-08-18T00:43:29Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"goon-game-content-worlds","title":"Спроектировать и произвести 4 мира / 55–70 уровней","description":"Content matrix четырёх миров, challenge-уровней и финальных испытаний по разделам 2 и 23.","acceptance_criteria":"Все уровни проходят content pipeline и Definition of Done.","notes":"2026-08-19: executable matrix commit 4869649 fixed release at 69 levels: workshop 1-17 plus four 13-level production worlds (52 rows). Foundations wave 1 now authors Levels 19-22; matrix prefix is 5/52 including Level 18. TDD/device REDs fixed only in new content: inaccessible L19 cache, skipped second L20 body, late L21 saw takeoff, early-moving L22 ferry; pagination acceptance updated from 2 to 3 pages. Four route methods passed 12/12 playthroughs, restart DoD passed 1/1. Clean gate: 117/117 app JVM, 7/7 level-format, validators 22/authored5, lint/app/test APK; final cold unfiltered Android 57/57 in 1807.264s, APK SHA 81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef. Next bounded slice is Foundations 23-26; issue remains in_progress until Level 69.\n2026-08-19: Foundations wave 2 authors Levels 23-26; matrix prefix is 9/52. Remote saw delivery, frozen-body step and conductive relay pass exact 1 death/1 corpse. Level 26 Second Hand passes original-timer and expired-timer recovery routes, both 3/3 with zero deaths; recovery explicitly observes the far Door closed before rearming the shared signal. Focused suite: 5/5 methods, 15 routes, 10.768s. Clean gate: 118/118 app JVM, 7/7 level-format, validators 26/authored9, lint/app/test APK; final cold unfiltered Android 62/62 in 1063.114s, APK SHA 67510d404cfa0704500cbf36c7845e9ba9fa05e96630079ab02bc35113e946fa. Next bounded slice is Foundations 27-30; issue remains in_progress until Level 69.\n2026-08-19: Foundations wave 3 authors Levels 27-30 and closes the world at 13/13 matrix rows (authored production prefix 13/52). Level 27 proves two retained-body AND; Level 28 proves delivered/frozen/conductive exact 3-body route plus clean restart; Level 29 CHALLENGE proves exact two bodies; Level 30 FINALE combines bridge, pressure, toggle/timed ferry, PIT and Door at exact 3 bodies. Focused suite: 4/4 methods, 12 routes, 35.188s. Late-load REDs led only to deterministic test-clock hardening and an AVD-specific performance evidence boundary; physical hardware retains full 34ms frame gate. Final clean gate: 119/119 app JVM, 7/7 level-format, validators 30/authored13, lint/app/test APK; cold unfiltered Android 66/66 in 6749.094s, APK SHA d47ba3baffcaf3c946b77965472b8eba26e77b535e35d1dd1ca67211950e5315. Next bounded slice is Physics 31-34; issue remains in_progress until Level 69.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:58Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T12:25:30Z","started_at":"2026-08-19T06:26:20Z","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-content-worlds","title":"Спроектировать и произвести 4 мира / 55–70 уровней","description":"Content matrix четырёх миров, challenge-уровней и финальных испытаний по разделам 2 и 23.","acceptance_criteria":"Все уровни проходят content pipeline и Definition of Done.","notes":"2026-08-19: executable matrix commit 4869649 fixed release at 69 levels: workshop 1-17 plus four 13-level production worlds (52 rows). Foundations wave 1 now authors Levels 19-22; matrix prefix is 5/52 including Level 18. TDD/device REDs fixed only in new content: inaccessible L19 cache, skipped second L20 body, late L21 saw takeoff, early-moving L22 ferry; pagination acceptance updated from 2 to 3 pages. Four route methods passed 12/12 playthroughs, restart DoD passed 1/1. Clean gate: 117/117 app JVM, 7/7 level-format, validators 22/authored5, lint/app/test APK; final cold unfiltered Android 57/57 in 1807.264s, APK SHA 81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef. Next bounded slice is Foundations 23-26; issue remains in_progress until Level 69.\n2026-08-19: Foundations wave 2 authors Levels 23-26; matrix prefix is 9/52. Remote saw delivery, frozen-body step and conductive relay pass exact 1 death/1 corpse. Level 26 Second Hand passes original-timer and expired-timer recovery routes, both 3/3 with zero deaths; recovery explicitly observes the far Door closed before rearming the shared signal. Focused suite: 5/5 methods, 15 routes, 10.768s. Clean gate: 118/118 app JVM, 7/7 level-format, validators 26/authored9, lint/app/test APK; final cold unfiltered Android 62/62 in 1063.114s, APK SHA 67510d404cfa0704500cbf36c7845e9ba9fa05e96630079ab02bc35113e946fa. Next bounded slice is Foundations 27-30; issue remains in_progress until Level 69.\n2026-08-19: Foundations wave 3 authors Levels 27-30 and closes the world at 13/13 matrix rows (authored production prefix 13/52). Level 27 proves two retained-body AND; Level 28 proves delivered/frozen/conductive exact 3-body route plus clean restart; Level 29 CHALLENGE proves exact two bodies; Level 30 FINALE combines bridge, pressure, toggle/timed ferry, PIT and Door at exact 3 bodies. Focused suite: 4/4 methods, 12 routes, 35.188s. Late-load REDs led only to deterministic test-clock hardening and an AVD-specific performance evidence boundary; physical hardware retains full 34ms frame gate. Final clean gate: 119/119 app JVM, 7/7 level-format, validators 30/authored13, lint/app/test APK; cold unfiltered Android 66/66 in 6749.094s, APK SHA d47ba3baffcaf3c946b77965472b8eba26e77b535e35d1dd1ca67211950e5315. Next bounded slice is Physics 31-34; issue remains in_progress until Level 69.\n2026-08-19: Physics wave 1 authors Levels 31-34; authored production prefix is 17/52. Weight Class proves mass-3 object pressure with clean restart; Stack Order adds deterministic falling object-on-object support; Launch Window proves exact one saw body plus vertical lift; Press Cycle proves exact one crushed retained body plus active/rest crusher timing. Focused suite: 4/4 methods, 12 routes; first run 10.003s, restart-proof rerun green. TDD REDs were missing PhysicalObjectStackResolver and PhysicsWaveOneScenarios. Clean gate: 122/122 app JVM, 7/7 level-format, validators 34/authored17, lint/app/test APK; cold unfiltered Android 70/70 in 611.991s, APK SHA 1d608168c5f44bc85aabb7bbe23e8e45cd4bb2b2611d6b5c2af5fa4451c63bd3. Next bounded slice is Physics 35-38; issue remains in_progress until Level 69.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:58Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T12:52:14Z","started_at":"2026-08-19T06:26:20Z","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 одинаково работает с персонажем, телом и объектами.","notes":"2026-08-19: claimed after corpse-state slice 19bb64e passed 102 JVM and exact Android 46/46. TDD scope: public IWeightedObject mass contract, JSON Box/Rock/Ball/IceBlock/Counterweight definitions, deterministic runtime/save/render/automation, pressure parity and one teaching level with solution bot.","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:43Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T04:38:25Z","started_at":"2026-08-19T03:35:44Z","closed_at":"2026-08-19T04:38:25Z","close_reason":"Shared pressure-mass contract, five authored physical-object types, deterministic runtime/save v4/render/export, level 17 and production-input route accepted. Clean 108 app JVM + 2 format JVM; focused 8/8; exact unfiltered Android 16 matrix 48/48 in 2223.695s.","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-weighted-objects","title":"Добавить общий IWeightedObject и физические объекты","description":"Добавить box, rock, ball, ice block и counterweight поверх единого контракта массы.","acceptance_criteria":"Pressure plate одинаково работает с персонажем, телом и объектами.","notes":"2026-08-19: claimed after corpse-state slice 19bb64e passed 102 JVM and exact Android 46/46. TDD scope: public IWeightedObject mass contract, JSON Box/Rock/Ball/IceBlock/Counterweight definitions, deterministic runtime/save/render/automation, pressure parity and one teaching level with solution bot.","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:43Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T04:38:25Z","started_at":"2026-08-19T03:35:44Z","closed_at":"2026-08-19T04:38:25Z","close_reason":"Shared pressure-mass contract, five authored physical-object types, deterministic runtime/save v4/render/export, level 17 and production-input route accepted. Clean 108 app JVM + 2 format JVM; focused 8/8; exact unfiltered Android 16 matrix 48/48 in 2223.695s.","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-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 механики.","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-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}
+9 -4
View File
@@ -137,6 +137,10 @@ The final Foundations wave adds levels 27–30: a two-body AND gate, a three-sta
body sequence with clean restart, an exact-two-death challenge and a finale that body sequence with clean restart, an exact-two-death challenge and a finale that
combines corpse bridge, pressure, toggle/timer ferry and Door. The full combines corpse bridge, pressure, toggle/timer ferry and Door. The full
Foundations world now occupies its accepted 18–30 matrix range. Foundations world now occupies its accepted 18–30 matrix range.
The first Physics wave adds levels 31–34: object-held mass, a real three-object
support stack, a saw-delivery lift route and a crusher/pressure cycle. Vertical
object-on-object support is deterministic and covered independently from player
movement; all four routes pass three consecutive production-input runs.
Scope and final evidence are in `docs/STAGE6_CHARACTER_STATES.md`, Scope and final evidence are in `docs/STAGE6_CHARACTER_STATES.md`,
`docs/STAGE6_CORPSE_MANAGER.md`, and `docs/STAGE6_PUZZLE_SYSTEM.md`; the latest `docs/STAGE6_CORPSE_MANAGER.md`, and `docs/STAGE6_PUZZLE_SYSTEM.md`; the latest
hazard evidence is in `docs/STAGE6_PRODUCTION_HAZARDS.md`, and the burned/crushed hazard evidence is in `docs/STAGE6_PRODUCTION_HAZARDS.md`, and the burned/crushed
@@ -148,10 +152,11 @@ Physics, States and Mastery. Its contract and Level DoD pipeline are documented
in `docs/STAGE6_CONTENT_MATRIX.md`; wave evidence is in in `docs/STAGE6_CONTENT_MATRIX.md`; wave evidence is in
`docs/STAGE6_FOUNDATIONS_WAVE1.md` and `docs/STAGE6_FOUNDATIONS_WAVE1.md` and
`docs/STAGE6_FOUNDATIONS_WAVE2.md`; the completed world evidence is in `docs/STAGE6_FOUNDATIONS_WAVE2.md`; the completed world evidence is in
`docs/STAGE6_FOUNDATIONS_WAVE3.md`. The latest exact gate is `119 app JVM + 7 `docs/STAGE6_FOUNDATIONS_WAVE3.md`. Physics evidence starts in
content-tool JVM / 66 Android` on the thirty-level candidate (`66/66`, `docs/STAGE6_PHYSICS_WAVE1.md`. The latest exact gate is `122 app JVM + 7
6749.094 seconds). The verified debug APK SHA-256 is content-tool JVM / 70 Android` on the thirty-four-level candidate (`70/70`,
`d47ba3baffcaf3c946b77965472b8eba26e77b535e35d1dd1ca67211950e5315`. 611.991 seconds). The verified debug APK SHA-256 is
`1d608168c5f44bc85aabb7bbe23e8e45cd4bb2b2611d6b5c2af5fa4451c63bd3`.
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: 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:
@@ -24,7 +24,7 @@ public final class LevelPaginationTest {
GameView view = current.gameViewForAutomation(); GameView view = current.gameViewForAutomation();
view.setLanguageForAutomation(GameLanguage.EN); view.setLanguageForAutomation(GameLanguage.EN);
assertEquals(0, view.levelPageIndexForAutomation()); assertEquals(0, view.levelPageIndexForAutomation());
assertEquals(3, view.levelPageCountForAutomation()); assertEquals(4, view.levelPageCountForAutomation());
assertTrue(view.visibleLocalizedTextForAutomation().contains("The Volunteer")); assertTrue(view.visibleLocalizedTextForAutomation().contains("The Volunteer"));
assertFalse(view.visibleLocalizedTextForAutomation().contains("Steam Charge")); assertFalse(view.visibleLocalizedTextForAutomation().contains("Steam Charge"));
}); });
@@ -43,7 +43,7 @@ public final class LevelPaginationTest {
assertTrue(visible.contains("Covered Path")); assertTrue(visible.contains("Covered Path"));
assertFalse(visible.contains("Held Open")); assertFalse(visible.contains("Held Open"));
assertFalse(visible.contains("The Volunteer")); assertFalse(visible.contains("The Volunteer"));
assertTrue(visible.contains("Page 2 of 3")); assertTrue(visible.contains("Page 2 of 4"));
}); });
tap(activity, 1_730f, 965f); tap(activity, 1_730f, 965f);
@@ -62,9 +62,25 @@ public final class LevelPaginationTest {
assertTrue(visible.contains("Minimum Cost")); assertTrue(visible.contains("Minimum Cost"));
assertTrue(visible.contains("Foundation Relay")); assertTrue(visible.contains("Foundation Relay"));
assertFalse(visible.contains("Covered Path")); assertFalse(visible.contains("Covered Path"));
assertTrue(visible.contains("Page 3 of 3")); assertTrue(visible.contains("Page 3 of 4"));
}); });
tap(activity, 1_730f, 965f);
activity.onActivity(current -> {
GameView view = current.gameViewForAutomation();
List<String> visible = view.visibleLocalizedTextForAutomation();
assertEquals(3, view.levelPageIndexForAutomation());
assertTrue(visible.contains("Weight Class"));
assertTrue(visible.contains("Stack Order"));
assertTrue(visible.contains("Launch Window"));
assertTrue(visible.contains("Press Cycle"));
assertFalse(visible.contains("Foundation Relay"));
assertTrue(visible.contains("Page 4 of 4"));
});
tap(activity, 190f, 965f);
activity.onActivity(current ->
assertEquals(2, current.gameViewForAutomation().levelPageIndexForAutomation()));
tap(activity, 190f, 965f); tap(activity, 190f, 965f);
activity.onActivity(current -> activity.onActivity(current ->
assertEquals(1, current.gameViewForAutomation().levelPageIndexForAutomation())); assertEquals(1, current.gameViewForAutomation().levelPageIndexForAutomation()));
@@ -0,0 +1,111 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
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 PhysicsWaveOnePlaythroughTest {
@Test public void weightClassOpensFromObjectMassThreeTimes() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
Driver driver = Driver.from(activity);
for (int run = 1; run <= 3; run++) {
assertAccepted(run, new LevelPlaytestBot().run(
driver, PhysicsWaveOneScenarios.levelThirtyOne(), 55_000L),
driver, 0, 0);
}
driver.restart();
GameView.AutomationSnapshot restarted = driver.snapshot();
assertEquals(31, restarted.levelNumber());
assertEquals(650f, restarted.firstPhysicalObjectX(), 0f);
assertEquals(780f, restarted.firstPhysicalObjectY(), 0f);
assertEquals(0, restarted.deaths());
assertEquals(0, restarted.corpseCount());
assertFalse(restarted.allDoorsOpen());
assertFalse(restarted.collectibleFound());
assertFalse(restarted.inputActive());
}
}
@Test public void stackOrderClimbsSupportedObjectsThreeTimes() {
runThree(PhysicsWaveOneScenarios.levelThirtyTwo(), 0, 0);
}
@Test public void launchWindowUsesOneSawVolunteerAndLiftThreeTimes() {
runThree(PhysicsWaveOneScenarios.levelThirtyThree(), 1, 1);
}
@Test public void pressCycleLeavesOneCrushedBodyThreeTimes() {
runThree(PhysicsWaveOneScenarios.levelThirtyFour(), 1, 1);
}
private static void runThree(LevelPlaytestBot.Scenario scenario,
int expectedDeaths, int expectedCorpses) {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
Driver driver = Driver.from(activity);
for (int run = 1; run <= 3; run++) {
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(driver, scenario, 55_000L);
assertAccepted(run, result, driver, expectedDeaths, expectedCorpses);
}
}
}
private static void assertAccepted(int run, LevelPlaytestBot.Result result, Driver driver,
int expectedDeaths, int expectedCorpses) {
assertTrue("run=" + run + "; " + result.diagnostic(), result.completed());
assertEquals("run=" + run, expectedDeaths, result.deaths());
assertEquals("run=" + run, expectedCorpses, result.corpseCount());
assertTrue("run=" + run + "; collectible", driver.snapshot().collectibleFound());
}
private static final class Driver implements LevelPlaytestBot.Driver {
private final Instrumentation instrumentation;
private final GameView view;
private Driver(Instrumentation instrumentation, GameView view) {
this.instrumentation = instrumentation;
this.view = view;
}
static Driver from(ActivityScenario<MainActivity> activity) {
GameView[] target = new GameView[1];
activity.onActivity(current -> target[0] = current.gameViewForAutomation());
return new Driver(InstrumentationRegistry.getInstrumentation(), target[0]);
}
@Override public void startLevel(int oneBasedLevelNumber) {
instrumentation.runOnMainSync(() -> {
view.enableDeterministicAutomationClock();
view.startLevelForAutomation(oneBasedLevelNumber);
});
}
@Override public void setInput(boolean left, boolean right, boolean jump) {
instrumentation.runOnMainSync(() -> view.setAutomationInput(left, right, jump));
}
@Override public GameView.AutomationSnapshot snapshot() {
GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1];
instrumentation.runOnMainSync(() -> value[0] = view.automationSnapshot());
return value[0];
}
@Override public void waitForNextDecision(long millis) {
instrumentation.runOnMainSync(() ->
view.advanceDeterministicAutomation(Math.max(16L, millis)));
}
void restart() {
instrumentation.runOnMainSync(view::restartLevelForAutomation);
}
}
}
@@ -906,8 +906,9 @@ public final class GameView extends View implements Runnable {
object.vy += GRAVITY * dt; object.vy += GRAVITY * dt;
object.grounded = false; object.grounded = false;
if (object.vx != 0f) movePhysicalObjectHorizontal(object, object.vx * dt); if (object.vx != 0f) movePhysicalObjectHorizontal(object, object.vx * dt);
float previousY = object.y;
object.y += object.vy * dt; object.y += object.vy * dt;
resolvePhysicalObjectVertical(object); resolvePhysicalObjectVertical(object, previousY);
if (object.grounded) object.vx *= (float) Math.pow(0.04, dt); if (object.grounded) object.vx *= (float) Math.pow(0.04, dt);
if (Math.abs(object.vx) < 3f) object.vx = 0f; if (Math.abs(object.vx) < 3f) object.vx = 0f;
} }
@@ -949,7 +950,7 @@ public final class GameView extends View implements Runnable {
return Math.abs(object.x - before) + 0.01f >= Math.abs(amount); return Math.abs(object.x - before) + 0.01f >= Math.abs(amount);
} }
private void resolvePhysicalObjectVertical(PhysicalObjectState object) { private void resolvePhysicalObjectVertical(PhysicalObjectState object, float previousY) {
object.supportIndex = -1; object.supportIndex = -1;
RectF current = physicalObjectRect(object); RectF current = physicalObjectRect(object);
for (int i = 0; i < platformStates.size(); i++) { for (int i = 0; i < platformStates.size(); i++) {
@@ -987,6 +988,7 @@ public final class GameView extends View implements Runnable {
object.vy = 0f; object.vy = 0f;
current = physicalObjectRect(object); current = physicalObjectRect(object);
} }
PhysicalObjectStackResolver.settleFalling(object, previousY, physicalObjects);
} }
private void updateCorpses(float dt) { private void updateCorpses(float dt) {
@@ -0,0 +1,35 @@
package com.projectsacrifice.game;
/** Resolves a falling authored object onto the first crossed object beneath it. */
final class PhysicalObjectStackResolver {
private static final float CONTACT_EPSILON = 1.5f;
private PhysicalObjectStackResolver() {}
static boolean settleFalling(PhysicalObjectState moving, float previousY,
Iterable<PhysicalObjectState> objects) {
if (moving == null || objects == null || moving.vy < 0f) return false;
float previousBottom = previousY + moving.height;
float currentBottom = moving.y + moving.height;
PhysicalObjectState nearest = null;
float nearestTop = Float.POSITIVE_INFINITY;
for (PhysicalObjectState support : objects) {
if (support == null || support == moving) continue;
if (moving.x >= support.x + support.width
|| moving.x + moving.width <= support.x) continue;
if (previousBottom > support.y + CONTACT_EPSILON) continue;
if (currentBottom + CONTACT_EPSILON < support.y) continue;
if (moving.y >= support.y) continue;
if (support.y < nearestTop) {
nearest = support;
nearestTop = support.y;
}
}
if (nearest == null) return false;
moving.y = nearest.y - moving.height;
moving.vy = 0f;
moving.grounded = true;
moving.supportIndex = -1;
return true;
}
}
@@ -0,0 +1,144 @@
package com.projectsacrifice.game;
/** Production-input routes for the first Physics-world authoring wave. */
public final class PhysicsWaveOneScenarios {
private PhysicsWaveOneScenarios() {}
public static LevelPlaytestBot.Scenario levelThirtyOne() {
return LevelPlaytestBot.Scenario.builder(31, "Level 31 - weight class")
.step("Observe the mass-three object", 1_000L, LevelPlaytestBot.Input.none(),
snapshot -> snapshot.physicalObjectCount() == 1
&& snapshot.firstPhysicalObjectMass() == 3
&& !snapshot.allDoorsOpen())
.step("Approach the counterweight", 2_500L, LevelPlaytestBot.Input.right(),
aliveAtMost(0, LevelPlaytestBot.groundedAliveAtOrBeyond(520f)))
.step("Push mass onto the plate", 3_000L, LevelPlaytestBot.Input.right(),
aliveAtMost(0, snapshot -> snapshot.firstPhysicalObjectX() >= 920f
&& snapshot.allDoorsOpen()))
.step("Release while mass holds the door", 1_000L,
LevelPlaytestBot.Input.none(),
aliveAtMost(0, GameView.AutomationSnapshot::allDoorsOpen))
.step("Take off over the weight", 1_500L,
LevelPlaytestBot.Input.rightJump(),
aliveAtMost(0, snapshot -> !snapshot.playerOnGround()
&& snapshot.playerY() <= 690f))
.step("Land beyond the plate", 2_500L, LevelPlaytestBot.Input.right(),
aliveAtMost(0, LevelPlaytestBot.groundedAliveAtOrBeyond(1_200f)))
.step("Collect the cache beyond the door", 3_000L,
LevelPlaytestBot.Input.right(),
aliveAtMost(0, snapshot -> snapshot.collectibleFound()
&& snapshot.playerX() >= 1_550f))
.step("Reach the relic", 4_000L, LevelPlaytestBot.Input.right(),
aliveAtMost(0, GameView.AutomationSnapshot::completed))
.build();
}
public static LevelPlaytestBot.Scenario levelThirtyTwo() {
return LevelPlaytestBot.Scenario.builder(32, "Level 32 - stack order")
.step("Observe the supported three-object stack", 1_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.physicalObjectCount() == 3
&& snapshot.firstPhysicalObjectY() >= 799f
&& snapshot.firstPhysicalObjectY() <= 801f)
.step("Stage before the stack", 2_500L, LevelPlaytestBot.Input.right(),
aliveAtMost(0, LevelPlaytestBot.groundedAliveAtOrBeyond(480f)))
.step("Jump onto the lower object", 2_500L,
LevelPlaytestBot.Input.rightJump(),
aliveAtMost(0, snapshot -> snapshot.playerOnGround()
&& snapshot.playerX() >= 600f && snapshot.playerY() <= 700f))
.step("Climb to the top object", 3_000L,
LevelPlaytestBot.Input.rightJump(),
aliveAtMost(0, snapshot -> snapshot.playerOnGround()
&& snapshot.playerX() >= 720f && snapshot.playerY() <= 550f))
.step("Jump from the stack to the gallery", 3_000L,
LevelPlaytestBot.Input.rightJump(),
aliveAtMost(0, snapshot -> snapshot.playerOnGround()
&& snapshot.playerX() >= 1_030f && snapshot.playerY() <= 500f
&& snapshot.collectibleFound()))
.step("Cross the high gallery", 5_000L, LevelPlaytestBot.Input.right(),
aliveAtMost(0, GameView.AutomationSnapshot::completed))
.build();
}
public static LevelPlaytestBot.Scenario levelThirtyThree() {
return LevelPlaytestBot.Scenario.builder(33, "Level 33 - launch window")
.step("Read one saw launch", 4_000L, LevelPlaytestBot.Input.right(),
aliveAtMost(1, snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1))
.step("Observe the delivered body", 3_000L, LevelPlaytestBot.Input.none(),
aliveAtMost(1, snapshot -> snapshot.firstCorpseX() >= 900f))
.step("Wait for the lift runner", 2_000L, LevelPlaytestBot.Input.none(),
aliveAtMost(1, PhysicsWaveOneScenarios::respawned))
.step("Stage before the launch saw", 2_500L, LevelPlaytestBot.Input.right(),
aliveAtMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(420f)))
.step("Take off before the launch saw", 1_500L,
LevelPlaytestBot.Input.rightJump(),
aliveAtMost(1, snapshot -> !snapshot.playerOnGround()
&& snapshot.playerY() <= 620f))
.step("Clear the launch saw", 2_000L, LevelPlaytestBot.Input.right(),
aliveAtMost(1, snapshot -> snapshot.playerX() >= 760f))
.step("Stage before the delivered body", 1_500L,
LevelPlaytestBot.Input.right(),
aliveAtMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(820f)))
.step("Clear the delivered body", 3_000L,
LevelPlaytestBot.Input.rightJump(),
aliveAtMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(1_200f)))
.step("Approach the lift", 1_500L, LevelPlaytestBot.Input.right(),
aliveAtMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(1_240f)))
.step("Wait for the low lift phase", 4_000L, LevelPlaytestBot.Input.none(),
aliveAtMost(1, snapshot -> snapshot.firstMovingPlatformY() >= 770f))
.step("Board the lift", 2_500L, LevelPlaytestBot.Input.rightJump(),
aliveAtMost(1, snapshot -> snapshot.playerOnGround()
&& snapshot.playerOnMovingPlatform()))
.step("Ride to the launch window", 5_000L, LevelPlaytestBot.Input.none(),
aliveAtMost(1, snapshot -> snapshot.playerOnGround()
&& snapshot.playerOnMovingPlatform()
&& snapshot.firstMovingPlatformY() <= 430f))
.step("Jump onto the high gallery", 3_000L,
LevelPlaytestBot.Input.rightJump(),
aliveAtMost(1, snapshot -> snapshot.playerOnGround()
&& snapshot.playerX() >= 1_750f && snapshot.playerY() <= 400f))
.step("Collect the upper cache", 2_500L, LevelPlaytestBot.Input.right(),
aliveAtMost(1, snapshot -> snapshot.collectibleFound()))
.step("Reach the upper relic", 4_000L, LevelPlaytestBot.Input.right(),
aliveAtMost(1, GameView.AutomationSnapshot::completed))
.build();
}
public static LevelPlaytestBot.Scenario levelThirtyFour() {
return LevelPlaytestBot.Scenario.builder(34, "Level 34 - press cycle")
.step("Leave one volunteer in the press", 4_000L,
LevelPlaytestBot.Input.right(),
aliveAtMost(1, snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1
&& snapshot.firstCorpseCrushed()
&& snapshot.firstCorpseActivatesPressure()))
.step("Wait for the cycle runner", 2_000L, LevelPlaytestBot.Input.none(),
aliveAtMost(1, PhysicsWaveOneScenarios::respawned))
.step("Approach the occupied press", 2_500L,
LevelPlaytestBot.Input.right(),
aliveAtMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(500f)))
.step("Observe the press close", 5_000L, LevelPlaytestBot.Input.none(),
aliveAtMost(1, GameView.AutomationSnapshot::firstHazardActive))
.step("Wait for the full rest", 5_000L, LevelPlaytestBot.Input.none(),
aliveAtMost(1, snapshot -> !snapshot.firstHazardActive()))
.step("Jump through the resting press", 2_500L,
LevelPlaytestBot.Input.rightJump(),
aliveAtMost(1, snapshot -> snapshot.playerX() >= 930f))
.step("Collect the press cache", 3_000L, LevelPlaytestBot.Input.right(),
aliveAtMost(1, snapshot -> snapshot.collectibleFound()))
.step("Reach the relic", 5_000L, LevelPlaytestBot.Input.right(),
aliveAtMost(1, GameView.AutomationSnapshot::completed))
.build();
}
private static LevelPlaytestBot.SnapshotCondition aliveAtMost(
int deaths, LevelPlaytestBot.SnapshotCondition condition) {
return LevelPlaytestBot.whileDeathsAtMost(deaths,
snapshot -> snapshot.playerAlive() && condition.reached(snapshot));
}
private static boolean respawned(GameView.AutomationSnapshot snapshot) {
return snapshot.playerAlive() && snapshot.playerX() < 250f;
}
}
@@ -13,7 +13,7 @@ public final class FoundationsFinaleContentTest {
@Test @Test
public void finalFoundationsWaveMatchesTheApprovedMatrixAndClosesTheWorld() { public void finalFoundationsWaveMatchesTheApprovedMatrixAndClosesTheWorld() {
List<LevelDefinition> levels = LevelCatalog.all(); List<LevelDefinition> levels = LevelCatalog.all();
assertEquals(30, levels.size()); assertTrue(levels.size() >= 30);
LevelDefinition keys = levels.get(26); LevelDefinition keys = levels.get(26);
assertIdentity(keys, 27, "foundations.two-keys", "Two Keys", 2, 65); assertIdentity(keys, 27, "foundations.two-keys", "Two Keys", 2, 65);
@@ -15,7 +15,7 @@ public final class LevelCatalogTest {
@Test @Test
public void productionCatalogContainsMechanicsAndChallengeTeachingLevels() { public void productionCatalogContainsMechanicsAndChallengeTeachingLevels() {
List<LevelDefinition> levels = LevelCatalog.all(); List<LevelDefinition> levels = LevelCatalog.all();
assertEquals(30, levels.size()); assertEquals(34, levels.size());
Set<String> titles = new HashSet<>(); Set<String> titles = new HashSet<>();
for (int i = 0; i < levels.size(); i++) { for (int i = 0; i < levels.size(); i++) {
LevelDefinition level = levels.get(i); LevelDefinition level = levels.get(i);
@@ -0,0 +1,47 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.util.Arrays;
public final class PhysicalObjectStackResolverTest {
@Test
public void fallingObjectSettlesOnTheHighestCrossedObject() {
PhysicalObjectState lower = object(650f, 800f, 160f, 80f);
PhysicalObjectState middle = object(690f, 721f, 140f, 80f)
.velocity(0f, 60f);
PhysicalObjectState distant = object(1_200f, 700f, 100f, 100f);
boolean settled = PhysicalObjectStackResolver.settleFalling(
middle, 720f, Arrays.asList(lower, middle, distant));
assertTrue(settled);
assertEquals(720f, middle.weightedY(), 0f);
assertEquals(0f, middle.velocityY(), 0f);
assertTrue(middle.grounded());
}
@Test
public void risingOrNonCrossingObjectIsNotTeleportedOntoAStack() {
PhysicalObjectState lower = object(650f, 800f, 160f, 80f);
PhysicalObjectState rising = object(690f, 700f, 120f, 80f)
.velocity(0f, -40f);
PhysicalObjectState beside = object(300f, 721f, 100f, 80f)
.velocity(0f, 60f);
assertFalse(PhysicalObjectStackResolver.settleFalling(
rising, 704f, Arrays.asList(lower, rising)));
assertFalse(PhysicalObjectStackResolver.settleFalling(
beside, 720f, Arrays.asList(lower, beside)));
}
private static PhysicalObjectState object(float x, float y, float width, float height) {
return new PhysicalObjectState(new LevelDefinition.PhysicalObject(
LevelDefinition.PhysicalObjectType.BOX,
new LevelDefinition.Box(x, y, width, height), 1));
}
}
@@ -0,0 +1,64 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.util.List;
public final class PhysicsWaveOneContentTest {
@Test
public void firstPhysicsWaveMatchesTheExecutableMatrix() {
List<LevelDefinition> levels = LevelCatalog.all();
assertEquals(34, levels.size());
LevelDefinition mass = levels.get(30);
assertIdentity(mass, 31, "physics.mass-basics", "Weight Class", 0, 45);
assertFalse(mass.objects.isEmpty());
assertTrue(mass.containsMechanic(LevelDefinition.SwitchType.PRESSURE));
assertFalse(mass.doors.isEmpty());
LevelDefinition stack = levels.get(31);
assertIdentity(stack, 32, "physics.stack-order", "Stack Order", 0, 50);
assertTrue(stack.objects.size() >= 3);
assertTrue("The authored staircase must contain an object supported by another",
hasAuthoredObjectStack(stack));
LevelDefinition launch = levels.get(32);
assertIdentity(launch, 33, "physics.launch-window", "Launch Window", 1, 55);
assertTrue(launch.containsMechanic(LevelDefinition.HazardType.SAW));
assertTrue(launch.hasMovingPlatform());
LevelDefinition crusher = levels.get(33);
assertIdentity(crusher, 34, "physics.crusher-rhythm", "Press Cycle", 1, 55);
assertTrue(crusher.containsMechanic(LevelDefinition.HazardType.CRUSHER));
assertTrue(crusher.containsMechanic(LevelDefinition.SwitchType.PRESSURE));
}
private static boolean hasAuthoredObjectStack(LevelDefinition level) {
for (LevelDefinition.PhysicalObject upper : level.objects) {
for (LevelDefinition.PhysicalObject lower : level.objects) {
if (upper == lower) continue;
boolean horizontalOverlap = upper.box.x < lower.box.right()
&& upper.box.right() > lower.box.x;
if (horizontalOverlap
&& Math.abs(upper.box.bottom() - lower.box.y) <= 0.01f) return true;
}
}
return false;
}
private static void assertIdentity(LevelDefinition level, int number, String id, String title,
int targetDeaths, int targetSeconds) {
assertEquals(number, level.number);
assertEquals(id, level.levelId);
assertEquals("physics", level.worldId);
assertEquals(title, level.title);
assertEquals(targetDeaths, level.targetDeaths);
assertEquals(targetSeconds, level.targetSeconds);
assertNotNull(level.collectible);
}
}
+8
View File
@@ -231,6 +231,14 @@ bridge → pressure → toggle/timed ferry → Door имеют solution graphs,
`66/66` за `6749.094 s`, app JVM `119/119`, content-tool JVM `7/7`; APK содержит `66/66` за `6749.094 s`, app JVM `119/119`, content-tool JVM `7/7`; APK содержит
30 level JSON. Evidence — `STAGE6_FOUNDATIONS_WAVE3.md`. Мир Foundations 18–30 30 level JSON. Evidence — `STAGE6_FOUNDATIONS_WAVE3.md`. Мир Foundations 18–30
закрыт; следующий bounded slice — Physics 31–34. закрыт; следующий bounded slice — Physics 31–34.
Первый Physics wave Levels 31–34 принят: mass-three object удерживает Door,
трёхобъектная лестница использует новый детерминированный object/object support,
saw-delivery соединён с вертикальным lift, crushed body сохраняется на pressure
во время полного crusher cycle. Четыре метода доказали 12/12 production-input
routes и restart сдвинутого противовеса. Cold unfiltered candidate прошёл Android
`70/70` за `611.991 s`, app JVM `122/122`, content-tool JVM `7/7`; APK содержит
34 level JSON. Evidence — `STAGE6_PHYSICS_WAVE1.md`. Следующий bounded slice —
Physics 35–38.
### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней ### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней
+72
View File
@@ -683,6 +683,78 @@ LevelID="foundations.foundation-final", WorldID="foundations"
corpse bridge, pressure, toggle-controlled ferry, timed AND-door и collectible; corpse bridge, pressure, toggle-controlled ferry, timed AND-door и collectible;
каждая смерть и реальная опора на moving platform наблюдаются ботом. каждая смерть и реальная опора на moving platform наблюдаются ботом.
## Уровень 31 — `Weight Class`
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения;
после них restart возвращает сдвинутый объект и Door в исходное состояние.
**Ожидаемые и доказанные смерти:** `0`.
```text
LevelID="physics.mass-basics", WorldID="physics"
→ игрок толкает COUNTERWEIGHT mass=3 с x=650 на pressure x=940..1190
→ requiredMass=3 открывает mass-door без присутствия игрока на плите
→ input отпускается, Door остаётся открытой только массой объекта
→ игрок перепрыгивает противовес, забирает cache x=1580
→ проходит Door x=1370 и достигает goal без смертей
→ restart возвращает object x=650,y=780, закрытую Door и чистый input/cache
```
**Контракт:** это первый production-уровень Physics и отдельное обучение
`weighted-object`; pressure resolver не различает player/corpse/object по типу.
## Уровень 32 — `Stack Order`
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
**Ожидаемые и доказанные смерти:** `0`.
```text
LevelID="physics.stack-order", WorldID="physics"
→ BOX x=650,y=800 поддерживает ROCK x=700,y=720
→ ROCK поддерживает ICE_BLOCK x=760,y=640; solver сохраняет обе опоры
→ игрок отдельными jump edge поднимается на нижний и верхний объекты
→ со стопки прыгает на gallery x=1030,y=600 и забирает cache x=900,y=555
→ по верхней галерее достигает goal x=2260,y=490 без смертей
```
**Контракт:** vertical object/object contact выбирает первую пересечённую опору
снизу, не телепортирует восходящие или не пересекающиеся объекты и не меняет
существующую player physics.
## Уровень 33 — `Launch Window`
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
**Ожидаемые и доказанные смерти:** `1`; saw launch сохраняет одно тело.
```text
LevelID="physics.launch-window", WorldID="physics"
→ первый доброволец входит в saw x=600 и получает impulse (700,-420)
→ следующий начинает прыжок до saw и отдельным edge очищает доставленное тело
→ у нижней фазы садится на lift x=1350,y≈790
→ snapshot подтверждает реальную moving-platform support
→ едет до y≤430 и прыгает на gallery x=1750,y=500
→ забирает cache x=2050 и достигает верхнего goal с deaths=1, corpses=1
```
**Контракт:** уровень сочетает уже обученный saw-launch с вертикальной движущейся
опорой; бот наблюдает смерть, доставленное тело, посадку и верхнюю фазу отдельно.
## Уровень 34 — `Press Cycle`
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
**Ожидаемые и доказанные смерти:** `1`; crushed body сохраняется на pressure.
```text
LevelID="physics.crusher-rhythm", WorldID="physics"
→ первый доброволец погибает в CRUSHER x=680..850
→ crushed body становится низкой retained mass на pressure x=650..880
→ следующий наблюдает active и inactive фазы полного 4.5 s цикла
→ одним новым jump edge пересекает resting press без второй смерти
→ забирает cache x=1320 и достигает goal с deaths=1, corpses=1
```
**Контракт:** pressure фиксирует результат жертвы, а timing остаётся обязательным:
каждый шаг fail-fast отклоняет вторую смерть и не перезапускает цикл.
## Проверки, следующие из графов ## Проверки, следующие из графов
1. Сохранять полный integrated device-regression: intended route принятых уровней по три раза и отдельный recovery route уровня 8. 1. Сохранять полный integrated device-regression: intended route принятых уровней по три раза и отдельный recovery route уровня 8.
+15 -14
View File
@@ -49,7 +49,7 @@ finale не вводят новую механику: они проверяют
Android `preBuild` зависит от этого gate. Текущее ожидаемое сообщение: Android `preBuild` зависит от этого gate. Текущее ожидаемое сообщение:
```text ```text
Validated production content matrix: worlds=4; planned=52; authored=13; releaseTotal=69 Validated production content matrix: worlds=4; planned=52; authored=17; releaseTotal=69
``` ```
## Content pipeline и Level DoD ## Content pipeline и Level DoD
@@ -64,9 +64,9 @@ JSON-уровень добавляется только ведущим преф
5. restart, blocker-softlock и collectible/challenge проверки; 5. restart, blocker-softlock и collectible/challenge проверки;
6. camera, sound, art, optimization и QA regression gates из Level DoD. 6. camera, sound, art, optimization и QA regression gates из Level DoD.
Foundations 18–30 полностью авторизованы через этот pipeline. Следующий bounded Foundations 18–30 и Physics 31–34 полностью авторизованы через этот pipeline.
slice — Physics 31–34; незавершённый уровень не скрывается в общем счётчике Следующий bounded slice — Physics 35–38; незавершённый уровень не скрывается в
production-контента. общем счётчике production-контента.
## TDD и текущий gate ## TDD и текущий gate
@@ -75,19 +75,20 @@ errors), затем отсутствующую CLI-команду `validate-matr
добавлен отрицательный regression: изменение planned title Level 18 отклоняется добавлен отрицательный regression: изменение planned title Level 18 отклоняется
как drift реального JSON-префикса. как drift реального JSON-префикса.
Текущий gate после завершения мира Foundations на 19 августа 2026 года: Текущий gate после первого Physics wave на 19 августа 2026 года:
- `ProductionContentMatrixTest` + `LevelContentToolTest`: `7/7`; - `ProductionContentMatrixTest` + `LevelContentToolTest`: `7/7`;
- app JVM regression: `119/119`; - app JVM regression: `122/122`;
- `validateLevels`: `30` authored levels; - `validateLevels`: `34` authored levels;
- `validateContentMatrix`: `4` worlds, `52` planned, `13` authored production rows, - `validateContentMatrix`: `4` worlds, `52` planned, `17` authored production rows,
release total `69`; release total `69`;
- `lintDebug`, `assembleDebug`, `assembleDebugAndroidTest`: GREEN; - `lintDebug`, `assembleDebug`, `assembleDebugAndroidTest`: GREEN;
- APK ZIP integrity: no errors; `30` level JSON присутствуют в package; - APK ZIP integrity: no errors; `34` level JSON присутствуют в package;
- cold unfiltered Android: `66/66`, failures/errors/skipped `0/0/0`, `6749.094 s`; - cold unfiltered Android: `70/70`, failures/errors/skipped `0/0/0`, `611.991 s`;
- debug APK SHA-256: - debug APK SHA-256:
`d47ba3baffcaf3c946b77965472b8eba26e77b535e35d1dd1ca67211950e5315`. `1d608168c5f44bc85aabb7bbe23e8e45cd4bb2b2611d6b5c2af5fa4451c63bd3`.
Матрица не подменяет device evidence: Foundations 18–30 прошёл отдельные routes Матрица не подменяет device evidence: Foundations 18–30 и Physics 31–34 прошли
и полный unfiltered gate; детали — `STAGE6_FOUNDATIONS_WAVE1.md`, отдельные routes и полный unfiltered gate; детали — `STAGE6_FOUNDATIONS_WAVE1.md`,
`STAGE6_FOUNDATIONS_WAVE2.md` и `STAGE6_FOUNDATIONS_WAVE3.md`. `STAGE6_FOUNDATIONS_WAVE2.md`, `STAGE6_FOUNDATIONS_WAVE3.md` и
`STAGE6_PHYSICS_WAVE1.md`.
+48
View File
@@ -0,0 +1,48 @@
# Stage 6: Physics wave 1
## Принятый результат
Первый bounded authoring wave мира Physics добавляет уровни 31–34:
- 31 `Weight Class` — COUNTERWEIGHT mass 3 самостоятельно удерживает pressure Door;
- 32 `Stack Order` — три authored objects образуют реальную вертикальную опору;
- 33 `Launch Window` — одна saw-жертва, доставленное тело и вертикальный lift;
- 34 `Press Cycle` — crushed body остаётся на pressure, следующий игрок читает
active/rest фазы crusher.
Все четыре уровня совпадают с executable matrix по stable LevelID/WorldID,
RU/EN copy, targets и collectible. Четыре Android methods выполняют по три
последовательных production-input route: `12/12`, без retry или прямой мутации.
Level 31 после трёх маршрутов дополнительно доказывает restart: объект возвращён
в `(650,780)`, Door закрыта, deaths/corpses/cache/input очищены.
## Object-stacking TDD
Первый RED был точным compile failure: `PhysicalObjectStackResolver` отсутствовал.
Второй RED зафиксировал отсутствующий `PhysicsWaveOneScenarios`. Минимальная GREEN
реализация добавляет только vertical object/object contact:
- falling object выбирает верхнюю первую пересечённую опору;
- rising object, объект сбоку и не пересёкший верх опоры не телепортируются;
- support обнуляет vertical velocity и публикует grounded state;
- player movement, run/jump constants и прежняя geometry не менялись.
`physicsVersion` остаётся `1`: существующие уровни не авторизуют вертикально
пересекающиеся objects, поэтому принятые trace/geometry не меняются; новый контакт
активируется только новой stacked geometry Level 32.
## Device evidence
- targeted first run: `4/4` methods, `12` routes, `10.003 s`;
- targeted rerun после restart-proof: `4/4`, GREEN;
- cold unfiltered Android 16: `70/70`, failures/errors/skipped `0/0/0`,
instrumentation `611.991 s`, `BUILD SUCCESSFUL in 10m23s`;
- app JVM: `122/122`; level-format JVM: `7/7`;
- clean `lintDebug`, app APK и AndroidTest APK: GREEN;
- validator: `34` levels; matrix `17/52` authored production rows;
- APK ZIP: no errors; `34` level JSON packaged;
- debug APK SHA-256:
`1d608168c5f44bc85aabb7bbe23e8e45cd4bb2b2611d6b5c2af5fa4451c63bd3`.
Следующий bounded slice — Physics 35–38: moving hazard, returning projectile,
blast delivery и moving load.
@@ -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","level-15.json","level-16.json","level-17.json","level-18.json","level-19.json","level-20.json","level-21.json","level-22.json","level-23.json","level-24.json","level-25.json","level-26.json","level-27.json","level-28.json","level-29.json","level-30.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","level-17.json","level-18.json","level-19.json","level-20.json","level-21.json","level-22.json","level-23.json","level-24.json","level-25.json","level-26.json","level-27.json","level-28.json","level-29.json","level-30.json","level-31.json","level-32.json","level-33.json","level-34.json"]}
@@ -0,0 +1,14 @@
{
"formatVersion":1,"physicsVersion":1,"number":31,
"id":"physics.mass-basics","worldId":"physics",
"copy":{"en":{"title":"Weight Class","hint":"Push the marked counterweight onto the plate; mass, not the player, keeps the door open."},"ru":{"title":"Весовая категория","hint":"Затолкните отмеченный противовес на плиту: дверь удерживает масса, а не игрок."}},
"world":{"width":2300,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2070,"y":770,"width":86,"height":110},
"collectible":{"x":1580,"y":790,"width":58,"height":58},
"targets":{"deaths":0,"seconds":45},
"objects":[{"type":"COUNTERWEIGHT","box":{"x":650,"y":780,"width":90,"height":100},"mass":3}],
"platforms":[{"box":{"x":0,"y":880,"width":2300,"height":200}}],
"hazards":[],
"switches":[{"type":"PRESSURE","box":{"x":940,"y":860,"width":250,"height":20},"signalId":"mass-door","durationSeconds":0,"requiredMass":3}],
"doors":[{"box":{"x":1370,"y":560,"width":84,"height":320},"requiredSignals":["mass-door"]}]
}
@@ -0,0 +1,19 @@
{
"formatVersion":1,"physicsVersion":1,"number":32,
"id":"physics.stack-order","worldId":"physics",
"copy":{"en":{"title":"Stack Order","hint":"Climb the supported object staircase in order, then jump onto the high gallery."},"ru":{"title":"Порядок стопки","hint":"Поднимитесь по опирающимся друг на друга объектам и прыгните на высокую галерею."}},
"world":{"width":2500,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2260,"y":490,"width":86,"height":110},
"collectible":{"x":900,"y":555,"width":58,"height":58},
"targets":{"deaths":0,"seconds":50},
"objects":[
{"type":"BOX","box":{"x":650,"y":800,"width":160,"height":80},"mass":1},
{"type":"ROCK","box":{"x":700,"y":720,"width":140,"height":80},"mass":2},
{"type":"ICE_BLOCK","box":{"x":760,"y":640,"width":120,"height":80},"mass":2}
],
"platforms":[
{"box":{"x":0,"y":880,"width":1080,"height":200}},
{"box":{"x":1030,"y":600,"width":1470,"height":480}}
],
"hazards":[],"switches":[],"doors":[]
}
@@ -0,0 +1,18 @@
{
"formatVersion":1,"physicsVersion":1,"number":33,
"id":"physics.launch-window","worldId":"physics",
"copy":{"en":{"title":"Launch Window","hint":"Read the saw launch once, clear the delivered body, then board the moving bridge at its near edge."},"ru":{"title":"Окно запуска","hint":"Один раз изучите бросок пилы, перепрыгните доставленное тело и садитесь на мост у ближнего края."}},
"world":{"width":2900,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2660,"y":390,"width":86,"height":110},
"collectible":{"x":2050,"y":410,"width":58,"height":58},
"targets":{"deaths":1,"seconds":55},
"platforms":[
{"box":{"x":0,"y":880,"width":2900,"height":200}},
{"box":{"x":1750,"y":500,"width":1150,"height":34}},
{"box":{"x":1350,"y":790,"width":300,"height":34},"travelX":0,"travelY":-390,"periodSeconds":5.0}
],
"hazards":[
{"type":"SAW","box":{"x":600,"y":760,"width":96,"height":96},"impulseX":700,"impulseY":-420}
],
"switches":[],"doors":[]
}
@@ -0,0 +1,15 @@
{
"formatVersion":1,"physicsVersion":1,"number":34,
"id":"physics.crusher-rhythm","worldId":"physics",
"copy":{"en":{"title":"Press Cycle","hint":"Leave one low body on the plate, read the press cycle, and cross during its full rest."},"ru":{"title":"Цикл пресса","hint":"Оставьте одно низкое тело на плите, прочитайте цикл пресса и пройдите в полный покой."}},
"world":{"width":2450,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2210,"y":770,"width":86,"height":110},
"collectible":{"x":1320,"y":790,"width":58,"height":58},
"targets":{"deaths":1,"seconds":55},
"platforms":[{"box":{"x":0,"y":880,"width":2450,"height":200}}],
"hazards":[{"type":"CRUSHER","box":{"x":680,"y":690,"width":170,"height":190},
"impulseX":40,"impulseY":100,"cycleSeconds":4.5,"activeSeconds":2.4,
"speedX":0,"speedY":0,"visualStyle":"steel-press","audioCue":"crusher-impact"}],
"switches":[{"type":"PRESSURE","box":{"x":650,"y":860,"width":230,"height":20},"signalId":"pressed-mass","durationSeconds":0,"requiredMass":1}],
"doors":[]
}
@@ -85,7 +85,7 @@ public final class LevelContentToolTest {
assertEquals(0, exitCode); assertEquals(0, exitCode);
assertEquals("Validated production content matrix: worlds=4; planned=52; " assertEquals("Validated production content matrix: worlds=4; planned=52; "
+ "authored=13; releaseTotal=69\n", + "authored=17; releaseTotal=69\n",
output.toString(StandardCharsets.UTF_8)); output.toString(StandardCharsets.UTF_8));
assertEquals("", errors.toString(StandardCharsets.UTF_8)); assertEquals("", errors.toString(StandardCharsets.UTF_8));
} }