Завершить state machine персонажа
This commit is contained in:
+3
-3
@@ -52,8 +52,8 @@
|
|||||||
{"_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-тесты проходят.","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-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":"Система имеет отдельный контракт, тесты и не меняет решения существующих уровней.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:39:27Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:28Z","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.","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-character-states","title":"Завершить state machine персонажа","description":"Добавить явные Interact, Stunned, Turn, Land и animation-state переходы; вынести coyote/input buffer в конфиг.","acceptance_criteria":"Все переходы детерминированы, визуально различимы и имеют тесты.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:39:20Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:28Z","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}
|
||||||
{"_type":"issue","id":"goon-game-e09","title":"EPIC-09 Save \u0026 Progression","description":"Версионированные сохранения, unlock, challenges, collectibles и статистика. Разделы 20–22, 34.","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:01Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:33Z","metadata":{"stages":"0,5,6","primary_stage":6},"labels":["epic","primary-stage-6","progression","save","stage-0","stage-5","stage-6"],"dependencies":[{"issue_id":"goon-game-e09","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:51Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-e09","title":"EPIC-09 Save \u0026 Progression","description":"Версионированные сохранения, unlock, challenges, collectibles и статистика. Разделы 20–22, 34.","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:01Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:33Z","metadata":{"stages":"0,5,6","primary_stage":6},"labels":["epic","primary-stage-6","progression","save","stage-0","stage-5","stage-6"],"dependencies":[{"issue_id":"goon-game-e09","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:51Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
{"_type":"issue","id":"goon-game-e06","title":"EPIC-06 Camera","description":"Следящая камера, look-ahead, damping, bounds и focus targets. Раздел 18.","acceptance_criteria":"Камера показывает будущий маршрут, не дёргается и соблюдает границы уровня.","notes":"Stage 5 camera acceptance complete: bounded follow/focus/shake, level bounds and reduced motion; both EPIC children are closed.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:55Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T23:03:03Z","closed_at":"2026-08-18T23:03:03Z","close_reason":"Camera foundation and production polish acceptance are complete.","metadata":{"stages":"0,5","primary_stage":5},"labels":["camera","epic","primary-stage-5","stage-0","stage-5"],"dependencies":[{"issue_id":"goon-game-e06","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:48Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-e06","title":"EPIC-06 Camera","description":"Следящая камера, look-ahead, damping, bounds и focus targets. Раздел 18.","acceptance_criteria":"Камера показывает будущий маршрут, не дёргается и соблюдает границы уровня.","notes":"Stage 5 camera acceptance complete: bounded follow/focus/shake, level bounds and reduced motion; both EPIC children are closed.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:55Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T23:03:03Z","closed_at":"2026-08-18T23:03:03Z","close_reason":"Camera foundation and production polish acceptance are complete.","metadata":{"stages":"0,5","primary_stage":5},"labels":["camera","epic","primary-stage-5","stage-0","stage-5"],"dependencies":[{"issue_id":"goon-game-e06","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:48Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-e05","title":"EPIC-05 Puzzle Objects","description":"Кнопки, двери, платформы, весовые объекты и событийные сигналы. Разделы 11–14, 37.","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:38:53Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:15Z","metadata":{"stages":"0,6","primary_stage":6},"labels":["epic","primary-stage-6","puzzle","stage-0","stage-6"],"dependencies":[{"issue_id":"goon-game-e05","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:47Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-e05","title":"EPIC-05 Puzzle Objects","description":"Кнопки, двери, платформы, весовые объекты и событийные сигналы. Разделы 11–14, 37.","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:38:53Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:15Z","metadata":{"stages":"0,6","primary_stage":6},"labels":["epic","primary-stage-6","puzzle","stage-0","stage-6"],"dependencies":[{"issue_id":"goon-game-e05","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:47Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-e04","title":"EPIC-04 Hazards","description":"Каталог опасностей и универсальный контракт источника смерти. Разделы 5, 10, 38.","acceptance_criteria":"Опасности data-driven, задокументированы и проходят Android/QA проверки.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:51Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:11Z","metadata":{"stages":"0,6","primary_stage":6},"labels":["epic","hazards","primary-stage-6","stage-0","stage-6"],"dependencies":[{"issue_id":"goon-game-e04","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:45Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-e04","title":"EPIC-04 Hazards","description":"Каталог опасностей и универсальный контракт источника смерти. Разделы 5, 10, 38.","acceptance_criteria":"Опасности data-driven, задокументированы и проходят Android/QA проверки.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:51Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:11Z","metadata":{"stages":"0,6","primary_stage":6},"labels":["epic","hazards","primary-stage-6","stage-0","stage-6"],"dependencies":[{"issue_id":"goon-game-e04","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:45Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-e01","title":"EPIC-01 Character","description":"Персонаж, state machine, движение и platforming. Разделы 3, 17, 28, 36.","acceptance_criteria":"Персонаж соответствует утверждённому набору состояний и стабильно управляется на Android.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:45Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:56Z","metadata":{"stages":"0,6","primary_stage":6},"labels":["character","epic","primary-stage-6","stage-0","stage-6"],"dependencies":[{"issue_id":"goon-game-e01","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:42Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"goon-game-e01","title":"EPIC-01 Character","description":"Персонаж, state machine, движение и platforming. Разделы 3, 17, 28, 36.","acceptance_criteria":"Персонаж соответствует утверждённому набору состояний и стабильно управляется на Android.","notes":"2026-08-19: baseline controller and production state-machine children are closed; exact Android 16 36/36 confirms stable controls and routes.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:45Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T00:19:58Z","closed_at":"2026-08-19T00:19:58Z","close_reason":"Character controller and complete production animation-state contract are implemented and Android-verified.","metadata":{"stages":"0,6","primary_stage":6},"labels":["character","epic","primary-stage-6","stage-0","stage-6"],"dependencies":[{"issue_id":"goon-game-e01","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:42Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-milestone-alpha","title":"Milestone: Alpha","description":"Вся игра проходима от первого меню до финала; контент существует.","acceptance_criteria":"End-to-end прохождение завершено на release-like build.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:41:05Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:39Z","metadata":{"stages":8,"primary_stage":8},"labels":["alpha","milestone","primary-stage-8","stage-8"],"dependencies":[{"issue_id":"goon-game-milestone-alpha","depends_on_id":"goon-game-crash-reporting","type":"blocks","created_at":"2026-08-18T00:44:28Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-alpha","depends_on_id":"goon-game-device-matrix","type":"blocks","created_at":"2026-08-18T00:44:27Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-alpha","depends_on_id":"goon-game-e16","type":"parent-child","created_at":"2026-08-18T00:43:57Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-alpha","depends_on_id":"goon-game-milestone-production","type":"blocks","created_at":"2026-08-18T00:44:26Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0}
|
{"_type":"issue","id":"goon-game-milestone-alpha","title":"Milestone: Alpha","description":"Вся игра проходима от первого меню до финала; контент существует.","acceptance_criteria":"End-to-end прохождение завершено на release-like build.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:41:05Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:39Z","metadata":{"stages":8,"primary_stage":8},"labels":["alpha","milestone","primary-stage-8","stage-8"],"dependencies":[{"issue_id":"goon-game-milestone-alpha","depends_on_id":"goon-game-crash-reporting","type":"blocks","created_at":"2026-08-18T00:44:28Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-alpha","depends_on_id":"goon-game-device-matrix","type":"blocks","created_at":"2026-08-18T00:44:27Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-alpha","depends_on_id":"goon-game-e16","type":"parent-child","created_at":"2026-08-18T00:43:57Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-alpha","depends_on_id":"goon-game-milestone-production","type":"blocks","created_at":"2026-08-18T00:44:26Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0}
|
||||||
{"_type":"issue","id":"goon-game-milestone-production","title":"Milestone: Production","description":"Массовое производство уровней параллельными потоками после утверждения vertical slice.","acceptance_criteria":"Полный content matrix реализован и прошёл content pipeline.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:41:02Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:25Z","metadata":{"stages":6,"primary_stage":6},"labels":["milestone","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-milestone-production","depends_on_id":"goon-game-content-worlds","type":"blocks","created_at":"2026-08-18T00:44:25Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-production","depends_on_id":"goon-game-e16","type":"parent-child","created_at":"2026-08-18T00:43:56Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-production","depends_on_id":"goon-game-milestone-vslice","type":"blocks","created_at":"2026-08-18T00:44:24Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0}
|
{"_type":"issue","id":"goon-game-milestone-production","title":"Milestone: Production","description":"Массовое производство уровней параллельными потоками после утверждения vertical slice.","acceptance_criteria":"Полный content matrix реализован и прошёл content pipeline.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:41:02Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:25Z","metadata":{"stages":6,"primary_stage":6},"labels":["milestone","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-milestone-production","depends_on_id":"goon-game-content-worlds","type":"blocks","created_at":"2026-08-18T00:44:25Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-production","depends_on_id":"goon-game-e16","type":"parent-child","created_at":"2026-08-18T00:43:56Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-production","depends_on_id":"goon-game-milestone-vslice","type":"blocks","created_at":"2026-08-18T00:44:24Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":2,"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-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}
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ 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
|
||||||
|
deterministic character state machine with explicit transient and command states;
|
||||||
|
scope and final `81 JVM / 36 Android` evidence are in
|
||||||
|
`docs/STAGE6_CHARACTER_STATES.md`.
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.projectsacrifice.game;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
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 CharacterStateIntegrationTest {
|
||||||
|
@Test
|
||||||
|
public void gameViewPublishesMovementAndCommandStates() 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(1));
|
||||||
|
|
||||||
|
awaitState(instrumentation, view[0], CharacterStateMachine.State.IDLE, 3_000L);
|
||||||
|
instrumentation.runOnMainSync(
|
||||||
|
() -> view[0].setAutomationInput(false, true, false));
|
||||||
|
awaitState(instrumentation, view[0], CharacterStateMachine.State.RUN, 3_000L);
|
||||||
|
|
||||||
|
instrumentation.runOnMainSync(
|
||||||
|
() -> view[0].setAutomationInput(true, false, false));
|
||||||
|
awaitState(instrumentation, view[0], CharacterStateMachine.State.TURN, 3_000L);
|
||||||
|
instrumentation.runOnMainSync(
|
||||||
|
() -> view[0].setAutomationInput(false, true, true));
|
||||||
|
awaitState(instrumentation, view[0], CharacterStateMachine.State.JUMP, 3_000L);
|
||||||
|
instrumentation.runOnMainSync(
|
||||||
|
() -> view[0].setAutomationInput(false, true, false));
|
||||||
|
awaitState(instrumentation, view[0], CharacterStateMachine.State.FALL, 3_000L);
|
||||||
|
awaitState(instrumentation, view[0], CharacterStateMachine.State.LAND, 3_000L);
|
||||||
|
|
||||||
|
instrumentation.runOnMainSync(view[0]::triggerInteractionForAutomation);
|
||||||
|
assertEquals(CharacterStateMachine.State.INTERACT,
|
||||||
|
state(instrumentation, view[0]));
|
||||||
|
instrumentation.runOnMainSync(view[0]::triggerStunForAutomation);
|
||||||
|
assertEquals(CharacterStateMachine.State.STUNNED,
|
||||||
|
state(instrumentation, view[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void awaitState(Instrumentation instrumentation, GameView view,
|
||||||
|
CharacterStateMachine.State expected,
|
||||||
|
long timeoutMillis) throws Exception {
|
||||||
|
long deadline = System.currentTimeMillis() + timeoutMillis;
|
||||||
|
CharacterStateMachine.State current = state(instrumentation, view);
|
||||||
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
current = state(instrumentation, view);
|
||||||
|
if (current == expected) return;
|
||||||
|
Thread.sleep(24L);
|
||||||
|
}
|
||||||
|
throw new AssertionError("Expected " + expected + " but was " + current);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CharacterStateMachine.State state(
|
||||||
|
Instrumentation instrumentation, GameView view) {
|
||||||
|
CharacterStateMachine.State[] result = new CharacterStateMachine.State[1];
|
||||||
|
instrumentation.runOnMainSync(
|
||||||
|
() -> result[0] = view.characterStateForAutomation());
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.projectsacrifice.game;
|
||||||
|
|
||||||
|
/** Deterministic animation state derived from the character's public physics facts. */
|
||||||
|
public final class CharacterStateMachine {
|
||||||
|
public enum State {
|
||||||
|
IDLE,
|
||||||
|
RUN,
|
||||||
|
JUMP,
|
||||||
|
FALL,
|
||||||
|
TURN,
|
||||||
|
LAND,
|
||||||
|
INTERACT,
|
||||||
|
STUNNED,
|
||||||
|
DEATH
|
||||||
|
}
|
||||||
|
|
||||||
|
private final PhysicsConfig physics;
|
||||||
|
private State state = State.IDLE;
|
||||||
|
private float transientSeconds;
|
||||||
|
private int facing = 1;
|
||||||
|
private boolean wasOnGround;
|
||||||
|
|
||||||
|
public CharacterStateMachine(PhysicsConfig physics) {
|
||||||
|
if (physics == null) throw new IllegalArgumentException("physics is required");
|
||||||
|
this.physics = physics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset(boolean alive, boolean onGround, float velocityX,
|
||||||
|
float velocityY, int facing) {
|
||||||
|
state = baseState(alive, onGround, velocityX, velocityY);
|
||||||
|
transientSeconds = 0f;
|
||||||
|
this.facing = normalizeFacing(facing);
|
||||||
|
wasOnGround = onGround;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void observe(float deltaSeconds, boolean alive, boolean onGround,
|
||||||
|
float velocityX, float velocityY, int facing) {
|
||||||
|
float safeDelta = Math.max(0f, deltaSeconds);
|
||||||
|
int normalizedFacing = normalizeFacing(facing);
|
||||||
|
if (!alive) {
|
||||||
|
state = State.DEATH;
|
||||||
|
transientSeconds = 0f;
|
||||||
|
this.facing = normalizedFacing;
|
||||||
|
wasOnGround = onGround;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((state == State.TURN || state == State.LAND || state == State.INTERACT
|
||||||
|
|| state == State.STUNNED)
|
||||||
|
&& transientSeconds > 0f) {
|
||||||
|
transientSeconds = Math.max(0f, transientSeconds - safeDelta);
|
||||||
|
this.facing = normalizedFacing;
|
||||||
|
wasOnGround = onGround;
|
||||||
|
if (transientSeconds > 0f && onGround) return;
|
||||||
|
}
|
||||||
|
if (onGround && !wasOnGround) {
|
||||||
|
state = State.LAND;
|
||||||
|
transientSeconds = physics.landSeconds();
|
||||||
|
this.facing = normalizedFacing;
|
||||||
|
wasOnGround = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onGround && normalizedFacing != this.facing) {
|
||||||
|
state = State.TURN;
|
||||||
|
transientSeconds = physics.turnSeconds();
|
||||||
|
this.facing = normalizedFacing;
|
||||||
|
wasOnGround = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.facing = normalizedFacing;
|
||||||
|
wasOnGround = onGround;
|
||||||
|
state = baseState(alive, onGround, velocityX, velocityY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public State state() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void beginInteraction() {
|
||||||
|
if (state == State.DEATH || state == State.STUNNED) return;
|
||||||
|
state = State.INTERACT;
|
||||||
|
transientSeconds = physics.interactSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stun() {
|
||||||
|
if (state == State.DEATH) return;
|
||||||
|
state = State.STUNNED;
|
||||||
|
transientSeconds = physics.stunnedSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static State baseState(boolean alive, boolean onGround,
|
||||||
|
float velocityX, float velocityY) {
|
||||||
|
if (!alive) return State.DEATH;
|
||||||
|
if (!onGround) return velocityY < 0f ? State.JUMP : State.FALL;
|
||||||
|
return Math.abs(velocityX) >= 30f ? State.RUN : State.IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int normalizeFacing(int value) {
|
||||||
|
return value < 0 ? -1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -233,6 +233,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
private final ProceduralAudioEngine audioEngine;
|
private final ProceduralAudioEngine audioEngine;
|
||||||
private final SparseArray<PointF> pointers = new SparseArray<>();
|
private final SparseArray<PointF> pointers = new SparseArray<>();
|
||||||
private final Player player = new Player();
|
private final Player player = new Player();
|
||||||
|
private final CharacterStateMachine characterStateMachine =
|
||||||
|
new CharacterStateMachine(PHYSICS);
|
||||||
private final ArrayList<Corpse> corpses = new ArrayList<>();
|
private final ArrayList<Corpse> corpses = new ArrayList<>();
|
||||||
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<>();
|
||||||
@@ -427,6 +429,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
player.jumpBuffer = 0f;
|
player.jumpBuffer = 0f;
|
||||||
player.supportIndex = -1;
|
player.supportIndex = -1;
|
||||||
player.facing = 1;
|
player.facing = 1;
|
||||||
|
characterStateMachine.reset(player.alive, player.onGround,
|
||||||
|
player.vx, player.vy, player.facing);
|
||||||
if (deaths > 0) audioEngine.play(AudioCue.RESPAWN_VOICE);
|
if (deaths > 0) audioEngine.play(AudioCue.RESPAWN_VOICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,6 +500,10 @@ public final class GameView extends View implements Runnable {
|
|||||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||||
}
|
}
|
||||||
if (contact && !previous) audioEngine.play(AudioCue.SWITCH);
|
if (contact && !previous) audioEngine.play(AudioCue.SWITCH);
|
||||||
|
if (contact && !previous && player.alive
|
||||||
|
&& intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT, item.box)) {
|
||||||
|
characterStateMachine.beginInteraction();
|
||||||
|
}
|
||||||
previousSwitchContact.put(contactKey, contact);
|
previousSwitchContact.put(contactKey, contact);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -594,6 +602,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
movePlayerVertical(player.vy * dt);
|
movePlayerVertical(player.vy * dt);
|
||||||
player.x = GameMath.clamp(player.x, 0f, level.worldWidth - PLAYER_WIDTH);
|
player.x = GameMath.clamp(player.x, 0f, level.worldWidth - PLAYER_WIDTH);
|
||||||
if (player.y > VIEW_HEIGHT + 180f) kill(DeathType.PIT, null);
|
if (player.y > VIEW_HEIGHT + 180f) kill(DeathType.PIT, null);
|
||||||
|
characterStateMachine.observe(dt, player.alive, player.onGround,
|
||||||
|
player.vx, player.vy, player.facing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void movePlayerHorizontal(float amount) {
|
private void movePlayerHorizontal(float amount) {
|
||||||
@@ -757,6 +767,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
player.alive = false;
|
player.alive = false;
|
||||||
player.vx = player.vy = 0f;
|
player.vx = player.vy = 0f;
|
||||||
player.supportIndex = -1;
|
player.supportIndex = -1;
|
||||||
|
characterStateMachine.observe(0f, false, player.onGround,
|
||||||
|
player.vx, player.vy, player.facing);
|
||||||
deaths++;
|
deaths++;
|
||||||
respawnTimer = RESPAWN_SECONDS;
|
respawnTimer = RESPAWN_SECONDS;
|
||||||
emitHaptic(Build.VERSION.SDK_INT >= 27
|
emitHaptic(Build.VERSION.SDK_INT >= 27
|
||||||
@@ -893,8 +905,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
for (Corpse corpse : corpses) drawCorpse(canvas, corpse);
|
for (Corpse 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.aliveMotion(
|
VolunteerRig.Motion motion = VolunteerRig.motionFor(characterStateMachine.state());
|
||||||
player.onGround, player.vx, player.vy);
|
|
||||||
volunteerRenderer.draw(canvas, paint, stroke, path,
|
volunteerRenderer.draw(canvas, paint, stroke, path,
|
||||||
player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT,
|
player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT,
|
||||||
skin, motion, volunteerPhase(motion), player.facing, 1f,
|
skin, motion, volunteerPhase(motion), player.facing, 1f,
|
||||||
@@ -2339,6 +2350,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
player.jumpBuffer = savedPlayer.jumpBufferSeconds();
|
player.jumpBuffer = savedPlayer.jumpBufferSeconds();
|
||||||
player.supportIndex = savedPlayer.supportIndex();
|
player.supportIndex = savedPlayer.supportIndex();
|
||||||
player.facing = savedPlayer.facing();
|
player.facing = savedPlayer.facing();
|
||||||
|
characterStateMachine.reset(player.alive, player.onGround,
|
||||||
|
player.vx, player.vy, player.facing);
|
||||||
|
|
||||||
corpses.clear();
|
corpses.clear();
|
||||||
for (GameSessionSnapshot.BodyData savedBody : snapshot.bodies()) {
|
for (GameSessionSnapshot.BodyData savedBody : snapshot.bodies()) {
|
||||||
@@ -2427,4 +2440,16 @@ public final class GameView extends View implements Runnable {
|
|||||||
player.vy
|
player.vy
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CharacterStateMachine.State characterStateForAutomation() {
|
||||||
|
return characterStateMachine.state();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void triggerInteractionForAutomation() {
|
||||||
|
characterStateMachine.beginInteraction();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void triggerStunForAutomation() {
|
||||||
|
characterStateMachine.stun();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ package com.projectsacrifice.game;
|
|||||||
public final class PhysicsConfig {
|
public final class PhysicsConfig {
|
||||||
private static final PhysicsConfig RELEASE = new PhysicsConfig(
|
private static final PhysicsConfig RELEASE = new PhysicsConfig(
|
||||||
1, 1f / 60f, 1_800f, 370f, 880f,
|
1, 1f / 60f, 1_800f, 370f, 880f,
|
||||||
0.13f, 0.14f, 0.78f, 16);
|
0.13f, 0.14f, 0.78f, 0.10f, 0.12f, 0.24f, 0.32f, 16);
|
||||||
|
|
||||||
private final int version;
|
private final int version;
|
||||||
private final float fixedStepSeconds;
|
private final float fixedStepSeconds;
|
||||||
@@ -14,12 +14,17 @@ public final class PhysicsConfig {
|
|||||||
private final float coyoteSeconds;
|
private final float coyoteSeconds;
|
||||||
private final float jumpBufferSeconds;
|
private final float jumpBufferSeconds;
|
||||||
private final float respawnSeconds;
|
private final float respawnSeconds;
|
||||||
|
private final float turnSeconds;
|
||||||
|
private final float landSeconds;
|
||||||
|
private final float interactSeconds;
|
||||||
|
private final float stunnedSeconds;
|
||||||
private final int maximumBodies;
|
private final int maximumBodies;
|
||||||
|
|
||||||
private PhysicsConfig(int version, float fixedStepSeconds, float gravity,
|
private PhysicsConfig(int version, float fixedStepSeconds, float gravity,
|
||||||
float runSpeed, float jumpSpeed, float coyoteSeconds,
|
float runSpeed, float jumpSpeed, float coyoteSeconds,
|
||||||
float jumpBufferSeconds, float respawnSeconds,
|
float jumpBufferSeconds, float respawnSeconds,
|
||||||
int maximumBodies) {
|
float turnSeconds, float landSeconds, float interactSeconds,
|
||||||
|
float stunnedSeconds, int maximumBodies) {
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.fixedStepSeconds = fixedStepSeconds;
|
this.fixedStepSeconds = fixedStepSeconds;
|
||||||
this.gravity = gravity;
|
this.gravity = gravity;
|
||||||
@@ -28,6 +33,10 @@ public final class PhysicsConfig {
|
|||||||
this.coyoteSeconds = coyoteSeconds;
|
this.coyoteSeconds = coyoteSeconds;
|
||||||
this.jumpBufferSeconds = jumpBufferSeconds;
|
this.jumpBufferSeconds = jumpBufferSeconds;
|
||||||
this.respawnSeconds = respawnSeconds;
|
this.respawnSeconds = respawnSeconds;
|
||||||
|
this.turnSeconds = turnSeconds;
|
||||||
|
this.landSeconds = landSeconds;
|
||||||
|
this.interactSeconds = interactSeconds;
|
||||||
|
this.stunnedSeconds = stunnedSeconds;
|
||||||
this.maximumBodies = maximumBodies;
|
this.maximumBodies = maximumBodies;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,5 +49,9 @@ public final class PhysicsConfig {
|
|||||||
public float coyoteSeconds() { return coyoteSeconds; }
|
public float coyoteSeconds() { return coyoteSeconds; }
|
||||||
public float jumpBufferSeconds() { return jumpBufferSeconds; }
|
public float jumpBufferSeconds() { return jumpBufferSeconds; }
|
||||||
public float respawnSeconds() { return respawnSeconds; }
|
public float respawnSeconds() { return respawnSeconds; }
|
||||||
|
public float turnSeconds() { return turnSeconds; }
|
||||||
|
public float landSeconds() { return landSeconds; }
|
||||||
|
public float interactSeconds() { return interactSeconds; }
|
||||||
|
public float stunnedSeconds() { return stunnedSeconds; }
|
||||||
public int maximumBodies() { return maximumBodies; }
|
public int maximumBodies() { return maximumBodies; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,13 @@ public final class VolunteerRig {
|
|||||||
public enum Motion {
|
public enum Motion {
|
||||||
IDLE,
|
IDLE,
|
||||||
RUN,
|
RUN,
|
||||||
|
TURN,
|
||||||
JUMP,
|
JUMP,
|
||||||
FALL,
|
FALL,
|
||||||
|
LAND,
|
||||||
|
INTERACT,
|
||||||
|
STUNNED,
|
||||||
|
DEATH,
|
||||||
DEAD,
|
DEAD,
|
||||||
PINNED,
|
PINNED,
|
||||||
FROZEN,
|
FROZEN,
|
||||||
@@ -117,6 +122,12 @@ public final class VolunteerRig {
|
|||||||
return Math.abs(horizontalVelocity) >= 30f ? Motion.RUN : Motion.IDLE;
|
return Math.abs(horizontalVelocity) >= 30f ? Motion.RUN : Motion.IDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Motion motionFor(CharacterStateMachine.State state) {
|
||||||
|
CharacterStateMachine.State safeState = state == null
|
||||||
|
? CharacterStateMachine.State.IDLE : state;
|
||||||
|
return Motion.valueOf(safeState.name());
|
||||||
|
}
|
||||||
|
|
||||||
public static Motion bodyMotion(boolean frozen, boolean conductive, boolean pinned) {
|
public static Motion bodyMotion(boolean frozen, boolean conductive, boolean pinned) {
|
||||||
if (frozen) return Motion.FROZEN;
|
if (frozen) return Motion.FROZEN;
|
||||||
if (conductive) return Motion.CONDUCTIVE;
|
if (conductive) return Motion.CONDUCTIVE;
|
||||||
@@ -156,6 +167,14 @@ public final class VolunteerRig {
|
|||||||
xs[Joint.RIGHT_FOOT.ordinal()] += swing * 0.18f;
|
xs[Joint.RIGHT_FOOT.ordinal()] += swing * 0.18f;
|
||||||
ys[Joint.ROOT.ordinal()] -= Math.abs(swing) * 0.025f;
|
ys[Joint.ROOT.ordinal()] -= Math.abs(swing) * 0.025f;
|
||||||
break;
|
break;
|
||||||
|
case TURN:
|
||||||
|
xs[Joint.LEFT_HAND.ordinal()] = 0.72f;
|
||||||
|
xs[Joint.RIGHT_HAND.ordinal()] = 0.28f;
|
||||||
|
ys[Joint.LEFT_HAND.ordinal()] = 0.55f;
|
||||||
|
ys[Joint.RIGHT_HAND.ordinal()] = 0.55f;
|
||||||
|
xs[Joint.LEFT_KNEE.ordinal()] = 0.48f;
|
||||||
|
xs[Joint.RIGHT_KNEE.ordinal()] = 0.52f;
|
||||||
|
break;
|
||||||
case JUMP:
|
case JUMP:
|
||||||
xs[Joint.LEFT_HAND.ordinal()] = 0.16f;
|
xs[Joint.LEFT_HAND.ordinal()] = 0.16f;
|
||||||
xs[Joint.RIGHT_HAND.ordinal()] = 0.84f;
|
xs[Joint.RIGHT_HAND.ordinal()] = 0.84f;
|
||||||
@@ -174,6 +193,28 @@ public final class VolunteerRig {
|
|||||||
xs[Joint.LEFT_FOOT.ordinal()] = 0.23f;
|
xs[Joint.LEFT_FOOT.ordinal()] = 0.23f;
|
||||||
xs[Joint.RIGHT_FOOT.ordinal()] = 0.77f;
|
xs[Joint.RIGHT_FOOT.ordinal()] = 0.77f;
|
||||||
break;
|
break;
|
||||||
|
case LAND:
|
||||||
|
ys[Joint.ROOT.ordinal()] = 0.91f;
|
||||||
|
ys[Joint.HIPS.ordinal()] = 0.74f;
|
||||||
|
ys[Joint.LEFT_KNEE.ordinal()] = 0.87f;
|
||||||
|
ys[Joint.RIGHT_KNEE.ordinal()] = 0.87f;
|
||||||
|
xs[Joint.LEFT_HAND.ordinal()] = 0.18f;
|
||||||
|
xs[Joint.RIGHT_HAND.ordinal()] = 0.82f;
|
||||||
|
break;
|
||||||
|
case INTERACT:
|
||||||
|
xs[Joint.RIGHT_HAND.ordinal()] = 0.98f;
|
||||||
|
ys[Joint.RIGHT_HAND.ordinal()] = 0.47f;
|
||||||
|
xs[Joint.LEFT_HAND.ordinal()] = 0.45f;
|
||||||
|
ys[Joint.LEFT_HAND.ordinal()] = 0.54f;
|
||||||
|
break;
|
||||||
|
case STUNNED:
|
||||||
|
xs[Joint.LEFT_HAND.ordinal()] = 0.36f;
|
||||||
|
xs[Joint.RIGHT_HAND.ordinal()] = 0.64f;
|
||||||
|
ys[Joint.LEFT_HAND.ordinal()] = 0.22f;
|
||||||
|
ys[Joint.RIGHT_HAND.ordinal()] = 0.22f;
|
||||||
|
xs[Joint.HEAD.ordinal()] += swing * 0.04f;
|
||||||
|
break;
|
||||||
|
case DEATH:
|
||||||
case DEAD:
|
case DEAD:
|
||||||
case PINNED:
|
case PINNED:
|
||||||
case FROZEN:
|
case FROZEN:
|
||||||
@@ -196,7 +237,8 @@ public final class VolunteerRig {
|
|||||||
if (facing < 0) {
|
if (facing < 0) {
|
||||||
for (int i = 0; i < xs.length; i++) xs[i] = 1f - xs[i];
|
for (int i = 0; i < xs.length; i++) xs[i] = 1f - xs[i];
|
||||||
}
|
}
|
||||||
boolean visorLit = safeMotion != Motion.DEAD
|
boolean visorLit = safeMotion != Motion.DEATH
|
||||||
|
&& safeMotion != Motion.DEAD
|
||||||
&& safeMotion != Motion.PINNED
|
&& safeMotion != Motion.PINNED
|
||||||
&& safeMotion != Motion.FROZEN
|
&& safeMotion != Motion.FROZEN
|
||||||
&& safeMotion != Motion.CONDUCTIVE;
|
&& safeMotion != Motion.CONDUCTIVE;
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.projectsacrifice.game;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public final class CharacterStateMachineTest {
|
||||||
|
@Test
|
||||||
|
public void groundedVelocitySelectsIdleAndRunDeterministically() {
|
||||||
|
CharacterStateMachine machine = new CharacterStateMachine(PhysicsConfig.release());
|
||||||
|
|
||||||
|
machine.reset(true, true, 0f, 0f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.IDLE, machine.state());
|
||||||
|
|
||||||
|
machine.observe(1f / 60f, true, true, 370f, 0f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.RUN, machine.state());
|
||||||
|
|
||||||
|
machine.observe(1f / 60f, true, true, 0f, 0f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.IDLE, machine.state());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verticalMotionSelectsJumpFallAndDeathHasPriority() {
|
||||||
|
CharacterStateMachine machine = new CharacterStateMachine(PhysicsConfig.release());
|
||||||
|
|
||||||
|
machine.reset(true, false, 120f, -300f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.JUMP, machine.state());
|
||||||
|
|
||||||
|
machine.observe(1f / 60f, true, false, 120f, 80f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.FALL, machine.state());
|
||||||
|
|
||||||
|
machine.observe(1f / 60f, false, true, 0f, 0f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.DEATH, machine.state());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changingFacingEmitsOneConfiguredTurnWindow() {
|
||||||
|
PhysicsConfig physics = PhysicsConfig.release();
|
||||||
|
CharacterStateMachine machine = new CharacterStateMachine(physics);
|
||||||
|
machine.reset(true, true, physics.runSpeed(), 0f, 1);
|
||||||
|
|
||||||
|
machine.observe(physics.fixedStepSeconds(), true, true,
|
||||||
|
-physics.runSpeed(), 0f, -1);
|
||||||
|
assertEquals(CharacterStateMachine.State.TURN, machine.state());
|
||||||
|
|
||||||
|
machine.observe(physics.turnSeconds() + physics.fixedStepSeconds(),
|
||||||
|
true, true, -physics.runSpeed(), 0f, -1);
|
||||||
|
assertEquals(CharacterStateMachine.State.RUN, machine.state());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstGroundedFrameAfterFallingEmitsConfiguredLandWindow() {
|
||||||
|
PhysicsConfig physics = PhysicsConfig.release();
|
||||||
|
CharacterStateMachine machine = new CharacterStateMachine(physics);
|
||||||
|
machine.reset(true, false, 0f, 300f, 1);
|
||||||
|
|
||||||
|
machine.observe(physics.fixedStepSeconds(), true, true, 0f, 0f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.LAND, machine.state());
|
||||||
|
|
||||||
|
machine.observe(physics.landSeconds() + physics.fixedStepSeconds(),
|
||||||
|
true, true, 0f, 0f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.IDLE, machine.state());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void interactionIsAConfiguredCommandAndDeathCancelsIt() {
|
||||||
|
PhysicsConfig physics = PhysicsConfig.release();
|
||||||
|
CharacterStateMachine machine = new CharacterStateMachine(physics);
|
||||||
|
machine.reset(true, true, 0f, 0f, 1);
|
||||||
|
|
||||||
|
machine.beginInteraction();
|
||||||
|
assertEquals(CharacterStateMachine.State.INTERACT, machine.state());
|
||||||
|
machine.observe(physics.interactSeconds() * 0.5f,
|
||||||
|
true, true, 0f, 0f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.INTERACT, machine.state());
|
||||||
|
|
||||||
|
machine.observe(physics.fixedStepSeconds(), false, true, 0f, 0f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.DEATH, machine.state());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void stunOverridesInteractionThenReturnsToObservedMovement() {
|
||||||
|
PhysicsConfig physics = PhysicsConfig.release();
|
||||||
|
CharacterStateMachine machine = new CharacterStateMachine(physics);
|
||||||
|
machine.reset(true, true, physics.runSpeed(), 0f, 1);
|
||||||
|
machine.beginInteraction();
|
||||||
|
|
||||||
|
machine.stun();
|
||||||
|
assertEquals(CharacterStateMachine.State.STUNNED, machine.state());
|
||||||
|
machine.observe(physics.stunnedSeconds() * 0.5f,
|
||||||
|
true, true, physics.runSpeed(), 0f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.STUNNED, machine.state());
|
||||||
|
|
||||||
|
machine.observe(physics.stunnedSeconds(),
|
||||||
|
true, true, physics.runSpeed(), 0f, 1);
|
||||||
|
assertEquals(CharacterStateMachine.State.RUN, machine.state());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void interactionCannotDowngradeAnActiveStun() {
|
||||||
|
CharacterStateMachine machine = new CharacterStateMachine(PhysicsConfig.release());
|
||||||
|
machine.reset(true, true, 0f, 0f, 1);
|
||||||
|
|
||||||
|
machine.stun();
|
||||||
|
machine.beginInteraction();
|
||||||
|
|
||||||
|
assertEquals(CharacterStateMachine.State.STUNNED, machine.state());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ public final class PhysicsConfigTest {
|
|||||||
assertEquals(0.13f, physics.coyoteSeconds(), 0f);
|
assertEquals(0.13f, physics.coyoteSeconds(), 0f);
|
||||||
assertEquals(0.14f, physics.jumpBufferSeconds(), 0f);
|
assertEquals(0.14f, physics.jumpBufferSeconds(), 0f);
|
||||||
assertEquals(0.78f, physics.respawnSeconds(), 0f);
|
assertEquals(0.78f, physics.respawnSeconds(), 0f);
|
||||||
|
assertEquals(0.10f, physics.turnSeconds(), 0f);
|
||||||
|
assertEquals(0.12f, physics.landSeconds(), 0f);
|
||||||
|
assertEquals(0.24f, physics.interactSeconds(), 0f);
|
||||||
|
assertEquals(0.32f, physics.stunnedSeconds(), 0f);
|
||||||
assertTrue(physics.maximumBodies() >= 10);
|
assertTrue(physics.maximumBodies() >= 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,4 +84,32 @@ public final class VolunteerRigTest {
|
|||||||
assertEquals(VolunteerRig.Motion.DEAD,
|
assertEquals(VolunteerRig.Motion.DEAD,
|
||||||
VolunteerRig.bodyMotion(false, false, false));
|
VolunteerRig.bodyMotion(false, false, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void everyLivingCharacterStateHasAnExplicitAnimationMotion() {
|
||||||
|
for (CharacterStateMachine.State state : CharacterStateMachine.State.values()) {
|
||||||
|
assertEquals(state.name(), VolunteerRig.motionFor(state).name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void transientCharacterStatesHaveNonColorPoseSignals() {
|
||||||
|
VolunteerRig.Skin skin = VolunteerRig.Skin.BRASS_SCOUT;
|
||||||
|
VolunteerRig.Pose idle = VolunteerRig.pose(skin, VolunteerRig.Motion.IDLE, 0.25f, 1);
|
||||||
|
VolunteerRig.Pose turn = VolunteerRig.pose(skin, VolunteerRig.Motion.TURN, 0.25f, 1);
|
||||||
|
VolunteerRig.Pose land = VolunteerRig.pose(skin, VolunteerRig.Motion.LAND, 0.25f, 1);
|
||||||
|
VolunteerRig.Pose interact = VolunteerRig.pose(
|
||||||
|
skin, VolunteerRig.Motion.INTERACT, 0.25f, 1);
|
||||||
|
VolunteerRig.Pose stunned = VolunteerRig.pose(
|
||||||
|
skin, VolunteerRig.Motion.STUNNED, 0.25f, 1);
|
||||||
|
|
||||||
|
assertNotEquals(idle.x(VolunteerRig.Joint.LEFT_HAND),
|
||||||
|
turn.x(VolunteerRig.Joint.LEFT_HAND), 0.001f);
|
||||||
|
assertNotEquals(idle.y(VolunteerRig.Joint.ROOT),
|
||||||
|
land.y(VolunteerRig.Joint.ROOT), 0.001f);
|
||||||
|
assertNotEquals(idle.x(VolunteerRig.Joint.RIGHT_HAND),
|
||||||
|
interact.x(VolunteerRig.Joint.RIGHT_HAND), 0.001f);
|
||||||
|
assertNotEquals(idle.y(VolunteerRig.Joint.LEFT_HAND),
|
||||||
|
stunned.y(VolunteerRig.Joint.LEFT_HAND), 0.001f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,6 +178,12 @@ baseline-задачи также имели явного владельца по
|
|||||||
- content не содержит blocker-softlocks и обязательных невосстановимых объектов;
|
- content не содержит blocker-softlocks и обязательных невосстановимых объектов;
|
||||||
- принят `goon-game-milestone-production`.
|
- принят `goon-game-milestone-production`.
|
||||||
|
|
||||||
|
Статус на 19 августа 2026: Stage 6 начат в изолированном production stream.
|
||||||
|
Первый foundation slice `goon-game-character-states` завершён: явные
|
||||||
|
Idle/Run/Turn/Jump/Fall/Land/Interact/Stunned/Death transitions, configured
|
||||||
|
timings и отдельные non-color animation poses прошли 81 JVM-тест и финальную
|
||||||
|
Android 16 matrix `36/36`. Следующий foundation slice — `CorpseManager`.
|
||||||
|
|
||||||
### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней
|
### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней
|
||||||
|
|
||||||
Задачи:
|
Задачи:
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Stage 6 character state machine
|
||||||
|
|
||||||
|
Date: 19 August 2026
|
||||||
|
|
||||||
|
Beads task: `goon-game-character-states`
|
||||||
|
|
||||||
|
## Contract
|
||||||
|
|
||||||
|
`CharacterStateMachine` is a deterministic public model driven by the physics
|
||||||
|
facts already owned by `GameView`: alive, grounded, velocity and facing. It
|
||||||
|
publishes:
|
||||||
|
|
||||||
|
- `IDLE`, `RUN`, `JUMP`, `FALL`, `DEATH` base states;
|
||||||
|
- configured transient `TURN` and `LAND` states;
|
||||||
|
- configured command states `INTERACT` and `STUNNED`;
|
||||||
|
- priority `DEATH > STUNNED > INTERACT > locomotion`.
|
||||||
|
|
||||||
|
The model does not own collision or movement and therefore does not change the
|
||||||
|
accepted Stage 5 physics. Switch contact drives the production `INTERACT`
|
||||||
|
transition. `STUNNED` has a public command and a tested animation contract; its
|
||||||
|
production hazard source belongs to the Stage 6 hazards work.
|
||||||
|
|
||||||
|
`PhysicsConfig` remains the single source for movement, coyote/input buffer,
|
||||||
|
respawn and the new turn/land/interact/stunned durations. The config/level
|
||||||
|
physics version remains `1` because no movement or collision constant changed.
|
||||||
|
|
||||||
|
## Visual contract
|
||||||
|
|
||||||
|
Every living state maps explicitly to a `VolunteerRig.Motion`. `TURN`, `LAND`,
|
||||||
|
`INTERACT` and `STUNNED` change skeleton joints, so the states remain distinct
|
||||||
|
without relying on color. Collision dimensions stay `62 x 100`.
|
||||||
|
|
||||||
|
## TDD evidence
|
||||||
|
|
||||||
|
Separate RED cycles covered the missing state model, airborne/death states,
|
||||||
|
configured turn, configured land, interaction, stun priority, visual mapping,
|
||||||
|
GameView automation integration and the regression where interaction could
|
||||||
|
downgrade an active stun. Each RED was followed by its focused GREEN before the
|
||||||
|
next behavior was introduced.
|
||||||
|
|
||||||
|
## Final gates
|
||||||
|
|
||||||
|
- JSON validator: `10/10`, format `1`, physics `1`.
|
||||||
|
- JVM: `81`, failures `0`, errors `0`.
|
||||||
|
- `lintDebug`, `assembleDebug`, `assembleDebugAndroidTest`: GREEN.
|
||||||
|
- focused Android 16 state integration: `1/1` GREEN.
|
||||||
|
- final unfiltered Android 16 matrix: `36/36`, failures `0`, errors `0`,
|
||||||
|
`720.578 s`, on the exact final APK/test APK.
|
||||||
|
- debug APK ZIP integrity: `unzip -t` GREEN.
|
||||||
|
- debug APK SHA-256:
|
||||||
|
`d25b7dd0a06cead0d514539773acd0a84ed92bf7c2510b9b880ded2f66508581`.
|
||||||
|
|
||||||
|
The AVD was stopped after the final gate.
|
||||||
Reference in New Issue
Block a user