Выделить CorpseManager
This commit is contained in:
+2
-2
@@ -49,10 +49,10 @@
|
|||||||
{"_type":"issue","id":"goon-game-ui-vslice","title":"Базовый HUD и multitouch controls","description":"В APK есть направления, jump, restart, pause, deaths и timer.","acceptance_criteria":"Управление работает одновременно двумя касаниями.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:40:00Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:04Z","closed_at":"2026-08-17T21:41:21Z","close_reason":"Базовый HUD и multitouch controls реализованы.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-ui-vslice","depends_on_id":"goon-game-e08","type":"parent-child","created_at":"2026-08-18T00:43:22Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-ui-vslice","title":"Базовый HUD и multitouch controls","description":"В APK есть направления, jump, restart, pause, deaths и timer.","acceptance_criteria":"Управление работает одновременно двумя касаниями.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:40:00Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:04Z","closed_at":"2026-08-17T21:41:21Z","close_reason":"Базовый HUD и multitouch controls реализованы.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-ui-vslice","depends_on_id":"goon-game-e08","type":"parent-child","created_at":"2026-08-18T00:43:22Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-camera-polish","title":"Доработать production camera controller","description":"Добавить vertical damping, room transitions, focus target и управляемый shake.","acceptance_criteria":"Камера не скрывает обязательные объекты и не вызывает дискомфорт.","notes":"Stage 5 RC: bounded horizontal/vertical damping, mechanism focus, impact shake, reduced-motion persistence; camera excluded from physics signatures; 35/35 Android gate GREEN.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:39:50Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T23:01:45Z","closed_at":"2026-08-18T23:01:45Z","close_reason":"Production camera behavior and reduced-motion acceptance are implemented and regression-tested.","metadata":{"stages":5,"primary_stage":5},"labels":["camera","primary-stage-5","stage-5","vertical-slice"],"dependencies":[{"issue_id":"goon-game-camera-polish","depends_on_id":"goon-game-e06","type":"parent-child","created_at":"2026-08-18T00:43:16Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-camera-polish","title":"Доработать production camera controller","description":"Добавить vertical damping, room transitions, focus target и управляемый shake.","acceptance_criteria":"Камера не скрывает обязательные объекты и не вызывает дискомфорт.","notes":"Stage 5 RC: bounded horizontal/vertical damping, mechanism focus, impact shake, reduced-motion persistence; camera excluded from physics signatures; 35/35 Android gate GREEN.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:39:50Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T23:01:45Z","closed_at":"2026-08-18T23:01:45Z","close_reason":"Production camera behavior and reduced-motion acceptance are implemented and regression-tested.","metadata":{"stages":5,"primary_stage":5},"labels":["camera","primary-stage-5","stage-5","vertical-slice"],"dependencies":[{"issue_id":"goon-game-camera-polish","depends_on_id":"goon-game-e06","type":"parent-child","created_at":"2026-08-18T00:43:16Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-camera-basic","title":"Базовая камера с look-ahead и bounds","description":"В текущем runtime реализовано сглаженное горизонтальное слежение.","acceptance_criteria":"Камера работает во всех 10 уровнях.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:45Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:04Z","closed_at":"2026-08-17T21:41:18Z","close_reason":"Базовое сглаженное слежение реализовано.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-camera-basic","depends_on_id":"goon-game-e06","type":"parent-child","created_at":"2026-08-18T00:43:15Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-camera-basic","title":"Базовая камера с look-ahead и bounds","description":"В текущем runtime реализовано сглаженное горизонтальное слежение.","acceptance_criteria":"Камера работает во всех 10 уровнях.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:45Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:04Z","closed_at":"2026-08-17T21:41:18Z","close_reason":"Базовое сглаженное слежение реализовано.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-camera-basic","depends_on_id":"goon-game-e06","type":"parent-child","created_at":"2026-08-18T00:43:15Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-puzzle-events","title":"Выделить event-driven PuzzleSystem","description":"Заменить прямую проверку сигналов универсальными каналами событий и явными состояниями DoorController.","acceptance_criteria":"Кнопки, двери и платформы связываются данными; integration-тесты проходят.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:41Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:27Z","metadata":{"stages":6,"primary_stage":6},"labels":["architecture","primary-stage-6","puzzle","stage-6"],"dependencies":[{"issue_id":"goon-game-puzzle-events","depends_on_id":"goon-game-e05","type":"parent-child","created_at":"2026-08-18T00:43:13Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-puzzle-events","title":"Выделить event-driven PuzzleSystem","description":"Заменить прямую проверку сигналов универсальными каналами событий и явными состояниями DoorController.","acceptance_criteria":"Кнопки, двери и платформы связываются данными; integration-тесты проходят.","notes":"2026-08-19: claimed as the next Stage 6 foundation slice after CorpseManager. Planned boundary: deterministic event/signal/door state model, data-driven bindings, GameView adapter, RED-GREEN integration, no level-solution changes.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":1,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:41Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T00:43:02Z","started_at":"2026-08-19T00:43:02Z","metadata":{"stages":6,"primary_stage":6},"labels":["architecture","primary-stage-6","puzzle","stage-6"],"dependencies":[{"issue_id":"goon-game-puzzle-events","depends_on_id":"goon-game-e05","type":"parent-child","created_at":"2026-08-18T00:43:13Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-puzzle-vslice","title":"Базовые puzzle objects vertical slice","description":"Работают pressure/toggle/timed switches, doors и moving platforms.","acceptance_criteria":"Существующие уровни используют все базовые типы.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:39Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:05Z","closed_at":"2026-08-17T21:41:17Z","close_reason":"Базовые кнопки, двери и платформы реализованы.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-puzzle-vslice","depends_on_id":"goon-game-e05","type":"parent-child","created_at":"2026-08-18T00:43:11Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-puzzle-vslice","title":"Базовые puzzle objects vertical slice","description":"Работают pressure/toggle/timed switches, doors и moving platforms.","acceptance_criteria":"Существующие уровни используют все базовые типы.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:39Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:05Z","closed_at":"2026-08-17T21:41:17Z","close_reason":"Базовые кнопки, двери и платформы реализованы.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-puzzle-vslice","depends_on_id":"goon-game-e05","type":"parent-child","created_at":"2026-08-18T00:43:11Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-hazards-vslice","title":"Опасности vertical slice","description":"Реализованы spikes, saw, pit, freeze и electricity.","acceptance_criteria":"Опасности присутствуют в каталоге 10 уровней и не ломают restart.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:35Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:05Z","closed_at":"2026-08-17T21:41:16Z","close_reason":"Базовый набор опасностей присутствует в 10 уровнях.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-hazards-vslice","depends_on_id":"goon-game-e04","type":"parent-child","created_at":"2026-08-18T00:43:09Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-hazards-vslice","title":"Опасности vertical slice","description":"Реализованы spikes, saw, pit, freeze и electricity.","acceptance_criteria":"Опасности присутствуют в каталоге 10 уровней и не ломают restart.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:35Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:05Z","closed_at":"2026-08-17T21:41:16Z","close_reason":"Базовый набор опасностей присутствует в 10 уровнях.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-hazards-vslice","depends_on_id":"goon-game-e04","type":"parent-child","created_at":"2026-08-18T00:43:09Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-corpse-manager","title":"Выделить полноценный CorpseManager","description":"Убрать управление телами из монолитного GameView; добавить register/remove/transform/reset, лимит и диагностические счётчики.","acceptance_criteria":"Система имеет отдельный контракт, тесты и не меняет решения существующих уровней.","notes":"2026-08-19: next Stage 6 foundation slice after character state machine. Planned boundary: deep public corpse lifecycle contract, GameView extraction without level-solution changes, test-first register/remove/transform/reset/limit/diagnostics.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":1,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:39:27Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T00:19:59Z","started_at":"2026-08-19T00:19:59Z","metadata":{"stages":6,"primary_stage":6},"labels":["architecture","corpse","primary-stage-6","stage-6"],"dependencies":[{"issue_id":"goon-game-corpse-manager","depends_on_id":"goon-game-e02","type":"parent-child","created_at":"2026-08-18T00:43:04Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
|
{"_type":"issue","id":"goon-game-corpse-manager","title":"Выделить полноценный CorpseManager","description":"Убрать управление телами из монолитного GameView; добавить register/remove/transform/reset, лимит и диагностические счётчики.","acceptance_criteria":"Система имеет отдельный контракт, тесты и не меняет решения существующих уровней.","notes":"2026-08-19: next Stage 6 foundation slice after character state machine. Planned boundary: deep public corpse lifecycle contract, GameView extraction without level-solution changes, test-first register/remove/transform/reset/limit/diagnostics.\n2026-08-19: completed test-first. CorpseManager now owns bounded register/remove/reset/FIFO eviction, composable pinned/frozen/conductive transformations, body save/restore and immutable diagnostics. GameView remains the physics adapter; no physics, geometry or level-signature changes. Final gates: 84 JVM tests; lintDebug/assembleDebug/assembleDebugAndroidTest GREEN; Android 16 unfiltered 37/37, 0 failed/0 skipped, 427.237s; APK ZIP valid, SHA-256 982fcf312f10bf23feb3a8794b708b61002012a3706c94ebb4320fefb36fb889. Evidence: docs/STAGE6_CORPSE_MANAGER.md.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:39:27Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T00:43:01Z","started_at":"2026-08-19T00:19:59Z","closed_at":"2026-08-19T00:43:01Z","close_reason":"Acceptance criteria выполнены: отдельный lifecycle-контракт интегрирован, существующие решения сохранены, полная Android-матрица 37/37 зелёная","metadata":{"stages":6,"primary_stage":6},"labels":["architecture","corpse","primary-stage-6","stage-6"],"dependencies":[{"issue_id":"goon-game-corpse-manager","depends_on_id":"goon-game-e02","type":"parent-child","created_at":"2026-08-18T00:43:04Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-character-states","title":"Завершить state machine персонажа","description":"Добавить явные Interact, Stunned, Turn, Land и animation-state переходы; вынести coyote/input buffer в конфиг.","acceptance_criteria":"Все переходы детерминированы, визуально различимы и имеют тесты.","notes":"2026-08-19: Stage 6 started after accepted Vertical Slice. First vertical slice: explicit deterministic character state machine (Idle/Run/Turn/Jump/Fall/Land/Interact/Stunned/Death), movement timings sourced from PhysicsConfig, GameView integration without physics changes, public automation state, JVM contract and full build gate.\n2026-08-19: TDD complete. Explicit Idle/Run/Turn/Jump/Fall/Land/Interact/Stunned/Death model, configured timings, non-color poses, GameView spawn/update/death/restore/switch/automation integration. Final exact gates: 81 JVM; lint/build; Android 16 unfiltered 36/36 in 720.578s; APK ZIP and SHA d25b7dd0a06cead0d514539773acd0a84ed92bf7c2510b9b880ded2f66508581. Evidence: docs/STAGE6_CHARACTER_STATES.md.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:39:20Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T00:19:57Z","started_at":"2026-08-18T23:20:45Z","closed_at":"2026-08-19T00:19:57Z","close_reason":"All character transitions are deterministic, visually distinct, configured and proven by JVM plus exact Android 16 integration/regression gates.","metadata":{"stages":6,"primary_stage":6},"labels":["character","future","primary-stage-6","stage-6"],"dependencies":[{"issue_id":"goon-game-character-states","depends_on_id":"goon-game-e01","type":"parent-child","created_at":"2026-08-18T00:43:02Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-character-states","title":"Завершить state machine персонажа","description":"Добавить явные Interact, Stunned, Turn, Land и animation-state переходы; вынести coyote/input buffer в конфиг.","acceptance_criteria":"Все переходы детерминированы, визуально различимы и имеют тесты.","notes":"2026-08-19: Stage 6 started after accepted Vertical Slice. First vertical slice: explicit deterministic character state machine (Idle/Run/Turn/Jump/Fall/Land/Interact/Stunned/Death), movement timings sourced from PhysicsConfig, GameView integration without physics changes, public automation state, JVM contract and full build gate.\n2026-08-19: TDD complete. Explicit Idle/Run/Turn/Jump/Fall/Land/Interact/Stunned/Death model, configured timings, non-color poses, GameView spawn/update/death/restore/switch/automation integration. Final exact gates: 81 JVM; lint/build; Android 16 unfiltered 36/36 in 720.578s; APK ZIP and SHA d25b7dd0a06cead0d514539773acd0a84ed92bf7c2510b9b880ded2f66508581. Evidence: docs/STAGE6_CHARACTER_STATES.md.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:39:20Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T00:19:57Z","started_at":"2026-08-18T23:20:45Z","closed_at":"2026-08-19T00:19:57Z","close_reason":"All character transitions are deterministic, visually distinct, configured and proven by JVM plus exact Android 16 integration/regression gates.","metadata":{"stages":6,"primary_stage":6},"labels":["character","future","primary-stage-6","stage-6"],"dependencies":[{"issue_id":"goon-game-character-states","depends_on_id":"goon-game-e01","type":"parent-child","created_at":"2026-08-18T00:43:02Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-character-basic","title":"Базовый character controller","description":"Движение, остановка, разворот, прыжок, падение и приземление в текущем Canvas runtime.","acceptance_criteria":"Функции присутствуют в APK и покрыты smoke-тестом.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:18Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:05Z","closed_at":"2026-08-17T21:41:12Z","close_reason":"Реализовано в текущем vertical slice.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-character-basic","depends_on_id":"goon-game-e01","type":"parent-child","created_at":"2026-08-18T00:43:01Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-character-basic","title":"Базовый character controller","description":"Движение, остановка, разворот, прыжок, падение и приземление в текущем Canvas runtime.","acceptance_criteria":"Функции присутствуют в APK и покрыты smoke-тестом.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:18Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:05Z","closed_at":"2026-08-17T21:41:12Z","close_reason":"Реализовано в текущем vertical slice.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-character-basic","depends_on_id":"goon-game-e01","type":"parent-child","created_at":"2026-08-18T00:43:01Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-e13","title":"EPIC-13 Optimization","description":"60 FPS target, object pooling, physics budgets и low profile. Разделы 41–44.","acceptance_criteria":"Производительность укладывается в зафиксированные бюджеты на средних и слабых устройствах.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:10Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:51Z","metadata":{"stages":"5,7","primary_stage":7},"labels":["epic","performance","primary-stage-7","stage-5","stage-7"],"dependencies":[{"issue_id":"goon-game-e13","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:56Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-e13","title":"EPIC-13 Optimization","description":"60 FPS target, object pooling, physics budgets и low profile. Разделы 41–44.","acceptance_criteria":"Производительность укладывается в зафиксированные бюджеты на средних и слабых устройствах.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:10Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:51Z","metadata":{"stages":"5,7","primary_stage":7},"labels":["epic","performance","primary-stage-7","stage-5","stage-7"],"dependencies":[{"issue_id":"goon-game-e13","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:56Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|||||||
@@ -105,10 +105,12 @@ times with identical state signatures. The product owner confirmed completion of
|
|||||||
external testing on 19 August 2026; the evidence boundary is recorded without
|
external testing on 19 August 2026; the evidence boundary is recorded without
|
||||||
invented session data in `docs/STAGE5_HUMAN_PLAYTEST_RESULT.md`. Stage 5 is accepted.
|
invented session data in `docs/STAGE5_HUMAN_PLAYTEST_RESULT.md`. Stage 5 is accepted.
|
||||||
|
|
||||||
Stage 6 production work has started. The first completed foundation slice adds a
|
Stage 6 production work has started. Its first two completed foundation slices add
|
||||||
deterministic character state machine with explicit transient and command states;
|
a deterministic character state machine and a bounded `CorpseManager` that owns
|
||||||
scope and final `81 JVM / 36 Android` evidence are in
|
body registration, transformations, eviction, reset, save and restore. Scope and
|
||||||
`docs/STAGE6_CHARACTER_STATES.md`.
|
final evidence are in `docs/STAGE6_CHARACTER_STATES.md` and
|
||||||
|
`docs/STAGE6_CORPSE_MANAGER.md`; the latest unfiltered gate is
|
||||||
|
`84 JVM / 37 Android`.
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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 CorpseManagerIntegrationTest {
|
||||||
|
@Test
|
||||||
|
public void deathRegistersAndRestartResetsTheOwnedBodyLifecycle() throws Exception {
|
||||||
|
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||||
|
try (ActivityScenario<MainActivity> activity =
|
||||||
|
ActivityScenario.launch(MainActivity.class)) {
|
||||||
|
GameView[] view = new GameView[1];
|
||||||
|
activity.onActivity(current -> view[0] = current.gameViewForAutomation());
|
||||||
|
instrumentation.runOnMainSync(() -> {
|
||||||
|
view[0].startLevelForAutomation(10);
|
||||||
|
view[0].setAutomationInput(false, true, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
GameView.AutomationSnapshot died = awaitBody(instrumentation, view[0], 8_000L);
|
||||||
|
CorpseManager.Diagnostics before = diagnostics(instrumentation, view[0]);
|
||||||
|
assertEquals(1, died.corpseCount());
|
||||||
|
assertEquals(1L, before.registered());
|
||||||
|
assertEquals(0L, before.evicted());
|
||||||
|
|
||||||
|
instrumentation.runOnMainSync(view[0]::restartLevelForAutomation);
|
||||||
|
GameView.AutomationSnapshot restarted = snapshot(instrumentation, view[0]);
|
||||||
|
CorpseManager.Diagnostics after = diagnostics(instrumentation, view[0]);
|
||||||
|
|
||||||
|
assertEquals(0, restarted.corpseCount());
|
||||||
|
assertEquals(before.resets() + 1L, after.resets());
|
||||||
|
assertEquals(before.clearedByReset() + 1L, after.clearedByReset());
|
||||||
|
assertTrue(restarted.playerAlive());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameView.AutomationSnapshot awaitBody(
|
||||||
|
Instrumentation instrumentation, GameView view, long timeoutMillis)
|
||||||
|
throws Exception {
|
||||||
|
long deadline = System.currentTimeMillis() + timeoutMillis;
|
||||||
|
GameView.AutomationSnapshot current = snapshot(instrumentation, view);
|
||||||
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
current = snapshot(instrumentation, view);
|
||||||
|
if (current.corpseCount() == 1) return current;
|
||||||
|
Thread.sleep(24L);
|
||||||
|
}
|
||||||
|
throw new AssertionError("Body was not registered; deaths=" + current.deaths()
|
||||||
|
+ ", corpses=" + current.corpseCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameView.AutomationSnapshot snapshot(
|
||||||
|
Instrumentation instrumentation, GameView view) {
|
||||||
|
GameView.AutomationSnapshot[] result = new GameView.AutomationSnapshot[1];
|
||||||
|
instrumentation.runOnMainSync(() -> result[0] = view.automationSnapshot());
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CorpseManager.Diagnostics diagnostics(
|
||||||
|
Instrumentation instrumentation, GameView view) {
|
||||||
|
CorpseManager.Diagnostics[] result = new CorpseManager.Diagnostics[1];
|
||||||
|
instrumentation.runOnMainSync(
|
||||||
|
() -> result[0] = view.corpseDiagnosticsForAutomation());
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.projectsacrifice.game;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Owns the bounded lifecycle of puzzle bodies for one active level. */
|
||||||
|
public final class CorpseManager {
|
||||||
|
public enum Transformation {
|
||||||
|
PINNED,
|
||||||
|
FROZEN,
|
||||||
|
CONDUCTIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Body {
|
||||||
|
final long id;
|
||||||
|
final String deathType;
|
||||||
|
float x;
|
||||||
|
float y;
|
||||||
|
float width = 72f;
|
||||||
|
float height = 54f;
|
||||||
|
float vx;
|
||||||
|
float vy;
|
||||||
|
boolean frozen;
|
||||||
|
boolean conductive;
|
||||||
|
boolean pinned;
|
||||||
|
boolean grounded;
|
||||||
|
int supportIndex = -1;
|
||||||
|
|
||||||
|
private Body(long id, String deathType, float x, float y) {
|
||||||
|
this.id = id;
|
||||||
|
this.deathType = deathType;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long id() { return id; }
|
||||||
|
public String deathType() { return deathType; }
|
||||||
|
public float x() { return x; }
|
||||||
|
public float y() { return y; }
|
||||||
|
public float width() { return width; }
|
||||||
|
public float height() { return height; }
|
||||||
|
public float velocityX() { return vx; }
|
||||||
|
public float velocityY() { return vy; }
|
||||||
|
public boolean frozen() { return frozen; }
|
||||||
|
public boolean conductive() { return conductive; }
|
||||||
|
public boolean pinned() { return pinned; }
|
||||||
|
public boolean grounded() { return grounded; }
|
||||||
|
public int supportIndex() { return supportIndex; }
|
||||||
|
|
||||||
|
public Body position(float x, float y) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Body size(float width, float height) {
|
||||||
|
if (width <= 0f || height <= 0f) {
|
||||||
|
throw new IllegalArgumentException("body size must be positive");
|
||||||
|
}
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Body velocity(float x, float y) {
|
||||||
|
vx = x;
|
||||||
|
vy = y;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Body grounded(boolean value) {
|
||||||
|
grounded = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Body supportIndex(int value) {
|
||||||
|
supportIndex = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Diagnostics {
|
||||||
|
private final long registered;
|
||||||
|
private final long evicted;
|
||||||
|
private final long removed;
|
||||||
|
private final long resets;
|
||||||
|
private final long clearedByReset;
|
||||||
|
|
||||||
|
private Diagnostics(long registered, long evicted, long removed,
|
||||||
|
long resets, long clearedByReset) {
|
||||||
|
this.registered = registered;
|
||||||
|
this.evicted = evicted;
|
||||||
|
this.removed = removed;
|
||||||
|
this.resets = resets;
|
||||||
|
this.clearedByReset = clearedByReset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long registered() { return registered; }
|
||||||
|
public long evicted() { return evicted; }
|
||||||
|
public long removed() { return removed; }
|
||||||
|
public long resets() { return resets; }
|
||||||
|
public long clearedByReset() { return clearedByReset; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private final int maximumBodies;
|
||||||
|
private final ArrayList<Body> bodies = new ArrayList<>();
|
||||||
|
private long nextId = 1L;
|
||||||
|
private long registered;
|
||||||
|
private long evicted;
|
||||||
|
private long removed;
|
||||||
|
private long resets;
|
||||||
|
private long clearedByReset;
|
||||||
|
|
||||||
|
public CorpseManager(int maximumBodies) {
|
||||||
|
if (maximumBodies < 1) throw new IllegalArgumentException("maximumBodies must be positive");
|
||||||
|
this.maximumBodies = maximumBodies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Body register(String deathType, float x, float y) {
|
||||||
|
Body body = new Body(nextId++, safeDeathType(deathType), x, y);
|
||||||
|
bodies.add(body);
|
||||||
|
registered++;
|
||||||
|
if (bodies.size() > maximumBodies) {
|
||||||
|
bodies.remove(0);
|
||||||
|
evicted++;
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int count() { return bodies.size(); }
|
||||||
|
|
||||||
|
public boolean remove(Body body) {
|
||||||
|
boolean changed = body != null && bodies.remove(body);
|
||||||
|
if (changed) removed++;
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean transform(Body body, Transformation transformation) {
|
||||||
|
if (body == null || transformation == null || !bodies.contains(body)) return false;
|
||||||
|
switch (transformation) {
|
||||||
|
case PINNED:
|
||||||
|
body.pinned = true;
|
||||||
|
break;
|
||||||
|
case FROZEN:
|
||||||
|
body.frozen = true;
|
||||||
|
break;
|
||||||
|
case CONDUCTIVE:
|
||||||
|
body.conductive = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
clearedByReset += bodies.size();
|
||||||
|
bodies.clear();
|
||||||
|
resets++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Body> bodies() {
|
||||||
|
return Collections.unmodifiableList(bodies);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Diagnostics diagnostics() {
|
||||||
|
return new Diagnostics(registered, evicted, removed, resets, clearedByReset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeDeathType(String value) {
|
||||||
|
return value == null || value.trim().isEmpty() ? "GENERIC" : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.projectsacrifice.game;
|
package com.projectsacrifice.game;
|
||||||
|
|
||||||
|
import com.projectsacrifice.game.CorpseManager.Body;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@@ -190,18 +192,6 @@ public final class GameView extends View implements Runnable {
|
|||||||
int facing = 1;
|
int facing = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class Corpse {
|
|
||||||
float x, y, width = 72f, height = 54f, vx, vy;
|
|
||||||
boolean frozen;
|
|
||||||
boolean conductive;
|
|
||||||
boolean pinned;
|
|
||||||
boolean grounded;
|
|
||||||
int supportIndex = -1;
|
|
||||||
DeathType type;
|
|
||||||
|
|
||||||
RectF rect() { return new RectF(x, y, x + width, y + height); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class PlatformState {
|
private static final class PlatformState {
|
||||||
float x, y, previousX, previousY, motionTime;
|
float x, y, previousX, previousY, motionTime;
|
||||||
final LevelDefinition.Platform definition;
|
final LevelDefinition.Platform definition;
|
||||||
@@ -235,7 +225,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
private final Player player = new Player();
|
private final Player player = new Player();
|
||||||
private final CharacterStateMachine characterStateMachine =
|
private final CharacterStateMachine characterStateMachine =
|
||||||
new CharacterStateMachine(PHYSICS);
|
new CharacterStateMachine(PHYSICS);
|
||||||
private final ArrayList<Corpse> corpses = new ArrayList<>();
|
private final CorpseManager corpseManager = new CorpseManager(MAX_CORPSES);
|
||||||
|
private final List<Body> corpses = corpseManager.bodies();
|
||||||
private final ArrayList<PlatformState> platformStates = new ArrayList<>();
|
private final ArrayList<PlatformState> platformStates = new ArrayList<>();
|
||||||
private final Map<String, Boolean> toggleSignals = new HashMap<>();
|
private final Map<String, Boolean> toggleSignals = new HashMap<>();
|
||||||
private final Map<String, Float> timedSignals = new HashMap<>();
|
private final Map<String, Float> timedSignals = new HashMap<>();
|
||||||
@@ -400,7 +391,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
userPaused = false;
|
userPaused = false;
|
||||||
pointers.clear();
|
pointers.clear();
|
||||||
deriveControls();
|
deriveControls();
|
||||||
corpses.clear();
|
corpseManager.reset();
|
||||||
platformStates.clear();
|
platformStates.clear();
|
||||||
toggleSignals.clear();
|
toggleSignals.clear();
|
||||||
timedSignals.clear();
|
timedSignals.clear();
|
||||||
@@ -517,7 +508,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
private boolean switchContact(LevelDefinition.Switch item) {
|
private boolean switchContact(LevelDefinition.Switch item) {
|
||||||
int mass = 0;
|
int mass = 0;
|
||||||
if (player.alive && intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT, item.box)) mass++;
|
if (player.alive && intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT, item.box)) mass++;
|
||||||
for (Corpse corpse : corpses) {
|
for (Body corpse : corpses) {
|
||||||
if (intersects(corpse.x, corpse.y, corpse.width, corpse.height, item.box)) mass++;
|
if (intersects(corpse.x, corpse.y, corpse.width, corpse.height, item.box)) mass++;
|
||||||
}
|
}
|
||||||
return mass >= item.requiredMass;
|
return mass >= item.requiredMass;
|
||||||
@@ -533,7 +524,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
}
|
}
|
||||||
for (LevelDefinition.Hazard hazard : level.hazards) {
|
for (LevelDefinition.Hazard hazard : level.hazards) {
|
||||||
if (id.equals(hazard.relayId)) {
|
if (id.equals(hazard.relayId)) {
|
||||||
for (Corpse corpse : corpses) {
|
for (Body corpse : corpses) {
|
||||||
if (corpse.conductive && intersects(corpse.x, corpse.y, corpse.width, corpse.height, hazard.box)) return true;
|
if (corpse.conductive && intersects(corpse.x, corpse.y, corpse.width, corpse.height, hazard.box)) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,7 +557,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
player.x += support.x - support.previousX;
|
player.x += support.x - support.previousX;
|
||||||
player.y += support.y - support.previousY;
|
player.y += support.y - support.previousY;
|
||||||
}
|
}
|
||||||
for (Corpse corpse : corpses) {
|
for (Body corpse : corpses) {
|
||||||
if (!corpse.frozen && corpse.supportIndex >= 0 && corpse.supportIndex < platformStates.size()) {
|
if (!corpse.frozen && corpse.supportIndex >= 0 && corpse.supportIndex < platformStates.size()) {
|
||||||
PlatformState support = platformStates.get(corpse.supportIndex);
|
PlatformState support = platformStates.get(corpse.supportIndex);
|
||||||
corpse.x += support.x - support.previousX;
|
corpse.x += support.x - support.previousX;
|
||||||
@@ -650,8 +641,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
player.vy = 0f;
|
player.vy = 0f;
|
||||||
current = playerRect();
|
current = playerRect();
|
||||||
}
|
}
|
||||||
for (Corpse corpse : corpses) {
|
for (Body corpse : corpses) {
|
||||||
RectF solid = corpse.rect();
|
RectF solid = corpseRect(corpse);
|
||||||
if (!RectF.intersects(current, solid)) continue;
|
if (!RectF.intersects(current, solid)) continue;
|
||||||
if (amount >= 0f) {
|
if (amount >= 0f) {
|
||||||
player.y = solid.top - PLAYER_HEIGHT;
|
player.y = solid.top - PLAYER_HEIGHT;
|
||||||
@@ -666,7 +657,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateCorpses(float dt) {
|
private void updateCorpses(float dt) {
|
||||||
for (Corpse corpse : corpses) {
|
for (Body corpse : corpses) {
|
||||||
if (corpse.frozen || corpse.pinned) continue;
|
if (corpse.frozen || corpse.pinned) continue;
|
||||||
corpse.vy += GRAVITY * dt;
|
corpse.vy += GRAVITY * dt;
|
||||||
corpse.grounded = false;
|
corpse.grounded = false;
|
||||||
@@ -680,7 +671,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean settleCorpseOnPressurePlate(Corpse corpse) {
|
private boolean settleCorpseOnPressurePlate(Body corpse) {
|
||||||
for (LevelDefinition.Switch item : level.switches) {
|
for (LevelDefinition.Switch item : level.switches) {
|
||||||
if (item.type != LevelDefinition.SwitchType.PRESSURE) continue;
|
if (item.type != LevelDefinition.SwitchType.PRESSURE) continue;
|
||||||
if (!intersects(corpse.x, corpse.y, corpse.width, corpse.height, item.box)) continue;
|
if (!intersects(corpse.x, corpse.y, corpse.width, corpse.height, item.box)) continue;
|
||||||
@@ -691,26 +682,26 @@ public final class GameView extends View implements Runnable {
|
|||||||
}
|
}
|
||||||
corpse.vx = 0f;
|
corpse.vx = 0f;
|
||||||
corpse.vy = 0f;
|
corpse.vy = 0f;
|
||||||
corpse.pinned = true;
|
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resolveCorpseHorizontal(Corpse corpse) {
|
private void resolveCorpseHorizontal(Body corpse) {
|
||||||
RectF current = corpse.rect();
|
RectF current = corpseRect(corpse);
|
||||||
for (RectF solid : solidRects(false)) {
|
for (RectF solid : solidRects(false)) {
|
||||||
if (!RectF.intersects(current, solid)) continue;
|
if (!RectF.intersects(current, solid)) continue;
|
||||||
if (corpse.vx > 0f) corpse.x = solid.left - corpse.width;
|
if (corpse.vx > 0f) corpse.x = solid.left - corpse.width;
|
||||||
else if (corpse.vx < 0f) corpse.x = solid.right;
|
else if (corpse.vx < 0f) corpse.x = solid.right;
|
||||||
corpse.vx *= -0.18f;
|
corpse.vx *= -0.18f;
|
||||||
current = corpse.rect();
|
current = corpseRect(corpse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resolveCorpseVertical(Corpse corpse) {
|
private void resolveCorpseVertical(Body corpse) {
|
||||||
corpse.supportIndex = -1;
|
corpse.supportIndex = -1;
|
||||||
RectF current = corpse.rect();
|
RectF current = corpseRect(corpse);
|
||||||
for (int i = 0; i < platformStates.size(); i++) {
|
for (int i = 0; i < platformStates.size(); i++) {
|
||||||
RectF solid = platformStates.get(i).rect();
|
RectF solid = platformStates.get(i).rect();
|
||||||
if (!RectF.intersects(current, solid)) continue;
|
if (!RectF.intersects(current, solid)) continue;
|
||||||
@@ -723,7 +714,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
corpse.y = solid.bottom;
|
corpse.y = solid.bottom;
|
||||||
corpse.vy *= -0.1f;
|
corpse.vy *= -0.1f;
|
||||||
}
|
}
|
||||||
current = corpse.rect();
|
current = corpseRect(corpse);
|
||||||
}
|
}
|
||||||
for (LevelDefinition.Door door : level.doors) {
|
for (LevelDefinition.Door door : level.doors) {
|
||||||
if (doorOpen(door)) continue;
|
if (doorOpen(door)) continue;
|
||||||
@@ -737,7 +728,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
corpse.y = solid.bottom;
|
corpse.y = solid.bottom;
|
||||||
corpse.vy *= -0.1f;
|
corpse.vy *= -0.1f;
|
||||||
}
|
}
|
||||||
current = corpse.rect();
|
current = corpseRect(corpse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,7 +736,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
ArrayList<RectF> result = new ArrayList<>();
|
ArrayList<RectF> result = new ArrayList<>();
|
||||||
for (PlatformState platform : platformStates) result.add(platform.rect());
|
for (PlatformState platform : platformStates) result.add(platform.rect());
|
||||||
for (LevelDefinition.Door door : level.doors) if (!doorOpen(door)) result.add(rect(door.box));
|
for (LevelDefinition.Door door : level.doors) if (!doorOpen(door)) result.add(rect(door.box));
|
||||||
if (includeCorpses) for (Corpse corpse : corpses) result.add(corpse.rect());
|
if (includeCorpses) for (Body corpse : corpses) result.add(corpseRect(corpse));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,10 +769,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
cameraController.impact(type == DeathType.PIT ? 12f : 24f);
|
cameraController.impact(type == DeathType.PIT ? 12f : 24f);
|
||||||
|
|
||||||
if (type != DeathType.PIT) {
|
if (type != DeathType.PIT) {
|
||||||
Corpse corpse = new Corpse();
|
Body corpse = corpseManager.register(type.name(), player.x,
|
||||||
corpse.type = type;
|
player.y + PLAYER_HEIGHT - 54f);
|
||||||
corpse.x = player.x;
|
|
||||||
corpse.y = player.y + PLAYER_HEIGHT - corpse.height;
|
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
corpse.vx = source.impulseX;
|
corpse.vx = source.impulseX;
|
||||||
corpse.vy = source.impulseY;
|
corpse.vy = source.impulseY;
|
||||||
@@ -789,23 +778,21 @@ public final class GameView extends View implements Runnable {
|
|||||||
if (type == DeathType.SPIKE && source != null) {
|
if (type == DeathType.SPIKE && source != null) {
|
||||||
corpse.y = source.box.y - corpse.height;
|
corpse.y = source.box.y - corpse.height;
|
||||||
corpse.vx = corpse.vy = 0f;
|
corpse.vx = corpse.vy = 0f;
|
||||||
corpse.pinned = true;
|
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||||
} else if (type == DeathType.FREEZE && source != null) {
|
} else if (type == DeathType.FREEZE && source != null) {
|
||||||
corpse.width = 104f;
|
corpse.width = 104f;
|
||||||
corpse.height = 104f;
|
corpse.height = 104f;
|
||||||
corpse.x = source.box.x - 2f;
|
corpse.x = source.box.x - 2f;
|
||||||
corpse.y = 880f - corpse.height;
|
corpse.y = 880f - corpse.height;
|
||||||
corpse.vx = corpse.vy = 0f;
|
corpse.vx = corpse.vy = 0f;
|
||||||
corpse.frozen = true;
|
corpseManager.transform(corpse, CorpseManager.Transformation.FROZEN);
|
||||||
} else if (type == DeathType.ELECTRIC && source != null) {
|
} else if (type == DeathType.ELECTRIC && source != null) {
|
||||||
corpse.conductive = true;
|
|
||||||
corpse.x = source.box.x + (source.box.width - corpse.width) * 0.5f;
|
corpse.x = source.box.x + (source.box.width - corpse.width) * 0.5f;
|
||||||
corpse.y = 880f - corpse.height;
|
corpse.y = 880f - corpse.height;
|
||||||
corpse.vx = corpse.vy = 0f;
|
corpse.vx = corpse.vy = 0f;
|
||||||
corpse.pinned = true;
|
corpseManager.transform(corpse, CorpseManager.Transformation.CONDUCTIVE);
|
||||||
|
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||||
}
|
}
|
||||||
corpses.add(corpse);
|
|
||||||
while (corpses.size() > MAX_CORPSES) corpses.remove(0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -841,6 +828,11 @@ public final class GameView extends View implements Runnable {
|
|||||||
return new RectF(player.x, player.y, player.x + PLAYER_WIDTH, player.y + PLAYER_HEIGHT);
|
return new RectF(player.x, player.y, player.x + PLAYER_WIDTH, player.y + PLAYER_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RectF corpseRect(Body corpse) {
|
||||||
|
return new RectF(corpse.x, corpse.y,
|
||||||
|
corpse.x + corpse.width, corpse.y + corpse.height);
|
||||||
|
}
|
||||||
|
|
||||||
private static RectF rect(LevelDefinition.Box box) {
|
private static RectF rect(LevelDefinition.Box box) {
|
||||||
return new RectF(box.x, box.y, box.right(), box.bottom());
|
return new RectF(box.x, box.y, box.right(), box.bottom());
|
||||||
}
|
}
|
||||||
@@ -902,7 +894,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
drawHazards(canvas);
|
drawHazards(canvas);
|
||||||
drawDoors(canvas);
|
drawDoors(canvas);
|
||||||
drawGoal(canvas);
|
drawGoal(canvas);
|
||||||
for (Corpse corpse : corpses) drawCorpse(canvas, corpse);
|
for (Body corpse : corpses) drawCorpse(canvas, corpse);
|
||||||
VolunteerRig.Skin skin = VolunteerRig.Skin.forLevel(level.number);
|
VolunteerRig.Skin skin = VolunteerRig.Skin.forLevel(level.number);
|
||||||
if (player.alive) {
|
if (player.alive) {
|
||||||
VolunteerRig.Motion motion = VolunteerRig.motionFor(characterStateMachine.state());
|
VolunteerRig.Motion motion = VolunteerRig.motionFor(characterStateMachine.state());
|
||||||
@@ -1140,7 +1132,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
canvas.drawCircle(box.centerX() - 6f, box.centerY() - 7f, 7f, paint);
|
canvas.drawCircle(box.centerX() - 6f, box.centerY() - 7f, 7f, paint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawCorpse(Canvas canvas, Corpse corpse) {
|
private void drawCorpse(Canvas canvas, Body corpse) {
|
||||||
canvas.save();
|
canvas.save();
|
||||||
float cx = corpse.x + corpse.width * 0.5f;
|
float cx = corpse.x + corpse.width * 0.5f;
|
||||||
float cy = corpse.y + corpse.height * 0.5f;
|
float cy = corpse.y + corpse.height * 0.5f;
|
||||||
@@ -2280,12 +2272,12 @@ public final class GameView extends View implements Runnable {
|
|||||||
player.x, player.y, player.vx, player.vy,
|
player.x, player.y, player.vx, player.vy,
|
||||||
player.alive, player.onGround, player.coyote,
|
player.alive, player.onGround, player.coyote,
|
||||||
player.jumpBuffer, player.supportIndex, player.facing));
|
player.jumpBuffer, player.supportIndex, player.facing));
|
||||||
for (Corpse corpse : corpses) {
|
for (Body corpse : corpses) {
|
||||||
builder.addBody(new GameSessionSnapshot.BodyData(
|
builder.addBody(new GameSessionSnapshot.BodyData(
|
||||||
corpse.x, corpse.y, corpse.width, corpse.height,
|
corpse.x, corpse.y, corpse.width, corpse.height,
|
||||||
corpse.vx, corpse.vy, corpse.frozen, corpse.conductive,
|
corpse.vx, corpse.vy, corpse.frozen, corpse.conductive,
|
||||||
corpse.pinned, corpse.grounded, corpse.supportIndex,
|
corpse.pinned, corpse.grounded, corpse.supportIndex,
|
||||||
corpse.type == null ? DeathType.SPIKE.name() : corpse.type.name()));
|
corpse.deathType()));
|
||||||
}
|
}
|
||||||
ArrayList<Float> motionTimes = new ArrayList<>(platformStates.size());
|
ArrayList<Float> motionTimes = new ArrayList<>(platformStates.size());
|
||||||
for (PlatformState state : platformStates) motionTimes.add(state.motionTime);
|
for (PlatformState state : platformStates) motionTimes.add(state.motionTime);
|
||||||
@@ -2353,22 +2345,22 @@ public final class GameView extends View implements Runnable {
|
|||||||
characterStateMachine.reset(player.alive, player.onGround,
|
characterStateMachine.reset(player.alive, player.onGround,
|
||||||
player.vx, player.vy, player.facing);
|
player.vx, player.vy, player.facing);
|
||||||
|
|
||||||
corpses.clear();
|
|
||||||
for (GameSessionSnapshot.BodyData savedBody : snapshot.bodies()) {
|
for (GameSessionSnapshot.BodyData savedBody : snapshot.bodies()) {
|
||||||
Corpse corpse = new Corpse();
|
Body corpse = corpseManager.register(savedBody.deathType(),
|
||||||
corpse.x = savedBody.x();
|
savedBody.x(), savedBody.y())
|
||||||
corpse.y = savedBody.y();
|
.size(savedBody.width(), savedBody.height())
|
||||||
corpse.width = savedBody.width();
|
.velocity(savedBody.velocityX(), savedBody.velocityY())
|
||||||
corpse.height = savedBody.height();
|
.grounded(savedBody.grounded())
|
||||||
corpse.vx = savedBody.velocityX();
|
.supportIndex(savedBody.supportIndex());
|
||||||
corpse.vy = savedBody.velocityY();
|
if (savedBody.frozen()) {
|
||||||
corpse.frozen = savedBody.frozen();
|
corpseManager.transform(corpse, CorpseManager.Transformation.FROZEN);
|
||||||
corpse.conductive = savedBody.conductive();
|
}
|
||||||
corpse.pinned = savedBody.pinned();
|
if (savedBody.conductive()) {
|
||||||
corpse.grounded = savedBody.grounded();
|
corpseManager.transform(corpse, CorpseManager.Transformation.CONDUCTIVE);
|
||||||
corpse.supportIndex = savedBody.supportIndex();
|
}
|
||||||
corpse.type = DeathType.valueOf(savedBody.deathType());
|
if (savedBody.pinned()) {
|
||||||
corpses.add(corpse);
|
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
leftPressed = false;
|
leftPressed = false;
|
||||||
@@ -2452,4 +2444,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
public void triggerStunForAutomation() {
|
public void triggerStunForAutomation() {
|
||||||
characterStateMachine.stun();
|
characterStateMachine.stun();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CorpseManager.Diagnostics corpseDiagnosticsForAutomation() {
|
||||||
|
return corpseManager.diagnostics();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.projectsacrifice.game;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public final class CorpseManagerTest {
|
||||||
|
@Test
|
||||||
|
public void registeringBeyondTheBudgetEvictsTheOldestBodyAndReportsIt() {
|
||||||
|
CorpseManager manager = new CorpseManager(2);
|
||||||
|
CorpseManager.Body first = manager.register("SPIKE", 10f, 20f);
|
||||||
|
CorpseManager.Body second = manager.register("SAW", 30f, 40f);
|
||||||
|
CorpseManager.Body third = manager.register("FREEZE", 50f, 60f);
|
||||||
|
|
||||||
|
assertEquals(2, manager.count());
|
||||||
|
assertSame(second, manager.bodies().get(0));
|
||||||
|
assertSame(third, manager.bodies().get(1));
|
||||||
|
assertEquals(3L, manager.diagnostics().registered());
|
||||||
|
assertEquals(1L, manager.diagnostics().evicted());
|
||||||
|
assertEquals(first.id() + 1L, second.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void removeAndResetAreIdempotentAndHaveSeparateDiagnostics() {
|
||||||
|
CorpseManager manager = new CorpseManager(3);
|
||||||
|
CorpseManager.Body first = manager.register("SPIKE", 10f, 20f);
|
||||||
|
manager.register("SAW", 30f, 40f);
|
||||||
|
|
||||||
|
assertTrue(manager.remove(first));
|
||||||
|
assertFalse(manager.remove(first));
|
||||||
|
manager.reset();
|
||||||
|
|
||||||
|
assertEquals(0, manager.count());
|
||||||
|
assertEquals(1L, manager.diagnostics().removed());
|
||||||
|
assertEquals(1L, manager.diagnostics().resets());
|
||||||
|
assertEquals(1L, manager.diagnostics().clearedByReset());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ownedBodiesExposePhysicsFactsAndComposableTransformations() {
|
||||||
|
CorpseManager manager = new CorpseManager(3);
|
||||||
|
CorpseManager.Body body = manager.register("ELECTRIC", 10f, 20f)
|
||||||
|
.position(30f, 40f)
|
||||||
|
.size(104f, 54f)
|
||||||
|
.velocity(12f, -8f)
|
||||||
|
.grounded(true)
|
||||||
|
.supportIndex(2);
|
||||||
|
|
||||||
|
assertTrue(manager.transform(body, CorpseManager.Transformation.FROZEN));
|
||||||
|
assertTrue(manager.transform(body, CorpseManager.Transformation.CONDUCTIVE));
|
||||||
|
assertTrue(manager.transform(body, CorpseManager.Transformation.PINNED));
|
||||||
|
|
||||||
|
assertEquals(30f, body.x(), 0f);
|
||||||
|
assertEquals(40f, body.y(), 0f);
|
||||||
|
assertEquals(104f, body.width(), 0f);
|
||||||
|
assertEquals(54f, body.height(), 0f);
|
||||||
|
assertEquals(12f, body.velocityX(), 0f);
|
||||||
|
assertEquals(-8f, body.velocityY(), 0f);
|
||||||
|
assertTrue(body.frozen());
|
||||||
|
assertTrue(body.conductive());
|
||||||
|
assertTrue(body.pinned());
|
||||||
|
assertTrue(body.grounded());
|
||||||
|
assertEquals(2, body.supportIndex());
|
||||||
|
|
||||||
|
CorpseManager.Body foreign = new CorpseManager(1).register("SAW", 0f, 0f);
|
||||||
|
assertFalse(manager.transform(foreign, CorpseManager.Transformation.FROZEN));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -179,10 +179,12 @@ baseline-задачи также имели явного владельца по
|
|||||||
- принят `goon-game-milestone-production`.
|
- принят `goon-game-milestone-production`.
|
||||||
|
|
||||||
Статус на 19 августа 2026: Stage 6 начат в изолированном production stream.
|
Статус на 19 августа 2026: Stage 6 начат в изолированном production stream.
|
||||||
Первый foundation slice `goon-game-character-states` завершён: явные
|
Завершены первые два foundation slice. `goon-game-character-states` добавил
|
||||||
Idle/Run/Turn/Jump/Fall/Land/Interact/Stunned/Death transitions, configured
|
явные Idle/Run/Turn/Jump/Fall/Land/Interact/Stunned/Death transitions.
|
||||||
timings и отдельные non-color animation poses прошли 81 JVM-тест и финальную
|
`goon-game-corpse-manager` перенёс регистрацию, transforms, FIFO budget, reset,
|
||||||
Android 16 matrix `36/36`. Следующий foundation slice — `CorpseManager`.
|
save/restore и diagnostics тел в отдельного владельца без изменения физики и
|
||||||
|
геометрии. Последний gate: 84 JVM-теста и Android 16 matrix `37/37`.
|
||||||
|
Следующий foundation slice — event-driven `PuzzleSystem`.
|
||||||
|
|
||||||
### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней
|
### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Stage 6 CorpseManager
|
||||||
|
|
||||||
|
Date: 19 August 2026
|
||||||
|
|
||||||
|
Beads task: `goon-game-corpse-manager`
|
||||||
|
|
||||||
|
## Contract
|
||||||
|
|
||||||
|
`CorpseManager` is the single owner of bodies in one active level. It provides:
|
||||||
|
|
||||||
|
- bounded registration with stable IDs and deterministic FIFO eviction;
|
||||||
|
- explicit remove and level-reset lifecycle operations;
|
||||||
|
- composable `PINNED`, `FROZEN`, and `CONDUCTIVE` transformations;
|
||||||
|
- body physics facts needed by collision, moving-platform, save, and restore code;
|
||||||
|
- immutable lifecycle diagnostics for automation and regression tests.
|
||||||
|
|
||||||
|
`GameView` remains the high-frequency physics adapter, but it no longer creates,
|
||||||
|
owns, evicts, resets, or restores bodies through an internal collection. The
|
||||||
|
manager exposes an unmodifiable live view, so callers can update owned body facts
|
||||||
|
without being able to change collection membership.
|
||||||
|
|
||||||
|
The body budget and FIFO policy preserve the accepted Vertical Slice behavior.
|
||||||
|
Restart and level changes clear active bodies through the manager; save/restore
|
||||||
|
re-registers bodies and then reapplies their stored transformations. No level
|
||||||
|
geometry, movement, collision constant, or death signature changed.
|
||||||
|
|
||||||
|
## TDD evidence
|
||||||
|
|
||||||
|
Separate RED cycles covered the missing manager contract, removal/reset
|
||||||
|
diagnostics, mutable physics facts plus composable transformations, rejection of
|
||||||
|
foreign bodies, and the missing `GameView` automation hook. Each RED was followed
|
||||||
|
by its focused GREEN before the next behavior was introduced.
|
||||||
|
|
||||||
|
The Android integration test creates a real level-10 saw body through production
|
||||||
|
input, verifies manager registration, restarts the level through the public game
|
||||||
|
surface, and verifies that manager reset owns the cleanup.
|
||||||
|
|
||||||
|
## Final gates
|
||||||
|
|
||||||
|
- JSON validator: `10/10`, format `1`, physics `1`.
|
||||||
|
- JVM: `84`, failures `0`, errors `0`, skipped `0`.
|
||||||
|
- `lintDebug`, `assembleDebug`, `assembleDebugAndroidTest`: GREEN.
|
||||||
|
- focused Android 16 lifecycle: `1/1` GREEN.
|
||||||
|
- final unfiltered Android 16 matrix: `37/37`, failures `0`, errors `0`,
|
||||||
|
skipped `0`, `427.237 s`, on the exact final APK/test APK.
|
||||||
|
- debug APK ZIP integrity: `unzip -t` GREEN.
|
||||||
|
- debug APK SHA-256:
|
||||||
|
`982fcf312f10bf23feb3a8794b708b61002012a3706c94ebb4320fefb36fb889`.
|
||||||
|
|
||||||
|
The dedicated AVD was stopped after the final gate.
|
||||||
Reference in New Issue
Block a user