diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ec3d136..04ad0fc 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -67,7 +67,7 @@ {"_type":"issue","id":"goon-game-analytics-contract","title":"Схема gameplay analytics","description":"Версионировать level_start/restart/death/complete/abandon/challenge и heatmap payload.","acceptance_criteria":"События документированы, минимальны и проверены на тестовом backend.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:40:36Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:35Z","metadata":{"stages":7,"primary_stage":7},"labels":["analytics","primary-stage-7","production","stage-7"],"dependencies":[{"issue_id":"goon-game-analytics-contract","depends_on_id":"goon-game-e14","type":"parent-child","created_at":"2026-08-18T00:43:42Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-modular-characters","title":"Модульные персонажи и косметика","description":"Body/helmet/face/weapon/cape/accessory, случайные имена и 30+ косметических элементов.","acceptance_criteria":"Новые жертвы визуально различаются, комбинации не ломают анимации.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":2880,"created_at":"2026-08-17T21:40:21Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:25Z","metadata":{"stages":6,"primary_stage":6},"labels":["art","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-modular-characters","depends_on_id":"goon-game-e11","type":"parent-child","created_at":"2026-08-18T00:43:33Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-challenges","title":"Challenges, collectibles и статистика","description":"Реализовать casualty/time/collectible цели и отображение трёх звёзд.","acceptance_criteria":"Расчёт покрыт unit-тестами и сохраняется по LevelID.","notes":"2026-08-19: claimed after weighted-object slice 259fcd3 passed 108 app JVM + 2 content-tool JVM and exact Android 48/48. TDD scope: stable LevelID/WorldID progression keys with legacy numeric migration, casualty/time/collectible challenge evaluation, persistent best three-star record, data-driven optional collectible, level-select/result rendering, automation and one teaching route without weakening existing gates.\nFinal post-review gate: duplicate stable LevelID now fails catalog load before progress-key collision (targeted RED then GREEN). Clean 116 app JVM + 2 content-tool JVM; final cold host-GPU AVD exact Android 52/52 in 551.773s. Verified APK SHA-256 0b70a0a1a508425c311d7de473318e3bb83407f5f27e9dd4a8e1876b2eb25344.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:40:13Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T06:24:46Z","started_at":"2026-08-19T04:41:26Z","closed_at":"2026-08-19T05:58:26Z","close_reason":"Accepted stable LevelID/WorldID challenge progression: progress v3/session v5 migrations, casualty/time/collectible stars, level 18 3/3 route, 115+2 JVM and exact Android 52/52 (553.686s), APK SHA-256 9c12cfbb15e313421d58f891d9d107af14c9e8e626228fa8fd0c74e56c293d14.","metadata":{"stages":6,"primary_stage":6},"labels":["primary-stage-6","production","progression","stage-6"],"dependencies":[{"issue_id":"goon-game-challenges","depends_on_id":"goon-game-e09","type":"parent-child","created_at":"2026-08-18T00:43:29Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"goon-game-content-worlds","title":"Спроектировать и произвести 4 мира / 55–70 уровней","description":"Content matrix четырёх миров, challenge-уровней и финальных испытаний по разделам 2 и 23.","acceptance_criteria":"Все уровни проходят content pipeline и Definition of Done.","notes":"2026-08-19: executable matrix commit 4869649 fixed release at 69 levels: workshop 1-17 plus four 13-level production worlds (52 rows). Foundations wave 1 now authors Levels 19-22; matrix prefix is 5/52 including Level 18. TDD/device REDs fixed only in new content: inaccessible L19 cache, skipped second L20 body, late L21 saw takeoff, early-moving L22 ferry; pagination acceptance updated from 2 to 3 pages. Four route methods passed 12/12 playthroughs, restart DoD passed 1/1. Clean gate: 117/117 app JVM, 7/7 level-format, validators 22/authored5, lint/app/test APK; final cold unfiltered Android 57/57 in 1807.264s, APK SHA 81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef. Next bounded slice is Foundations 23-26; issue remains in_progress until Level 69.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:58Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T07:50:26Z","started_at":"2026-08-19T06:26:20Z","metadata":{"stages":6,"primary_stage":6},"labels":["future","levels","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-content-worlds","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T00:43:21Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-content-worlds","depends_on_id":"goon-game-milestone-vslice","type":"blocks","created_at":"2026-08-18T00:41:41Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"_type":"issue","id":"goon-game-content-worlds","title":"Спроектировать и произвести 4 мира / 55–70 уровней","description":"Content matrix четырёх миров, challenge-уровней и финальных испытаний по разделам 2 и 23.","acceptance_criteria":"Все уровни проходят content pipeline и Definition of Done.","notes":"2026-08-19: executable matrix commit 4869649 fixed release at 69 levels: workshop 1-17 plus four 13-level production worlds (52 rows). Foundations wave 1 now authors Levels 19-22; matrix prefix is 5/52 including Level 18. TDD/device REDs fixed only in new content: inaccessible L19 cache, skipped second L20 body, late L21 saw takeoff, early-moving L22 ferry; pagination acceptance updated from 2 to 3 pages. Four route methods passed 12/12 playthroughs, restart DoD passed 1/1. Clean gate: 117/117 app JVM, 7/7 level-format, validators 22/authored5, lint/app/test APK; final cold unfiltered Android 57/57 in 1807.264s, APK SHA 81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef. Next bounded slice is Foundations 23-26; issue remains in_progress until Level 69.\n2026-08-19: Foundations wave 2 authors Levels 23-26; matrix prefix is 9/52. Remote saw delivery, frozen-body step and conductive relay pass exact 1 death/1 corpse. Level 26 Second Hand passes original-timer and expired-timer recovery routes, both 3/3 with zero deaths; recovery explicitly observes the far Door closed before rearming the shared signal. Focused suite: 5/5 methods, 15 routes, 10.768s. Clean gate: 118/118 app JVM, 7/7 level-format, validators 26/authored9, lint/app/test APK; final cold unfiltered Android 62/62 in 1063.114s, APK SHA 67510d404cfa0704500cbf36c7845e9ba9fa05e96630079ab02bc35113e946fa. Next bounded slice is Foundations 27-30; issue remains in_progress until Level 69.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:58Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T08:24:33Z","started_at":"2026-08-19T06:26:20Z","metadata":{"stages":6,"primary_stage":6},"labels":["future","levels","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-content-worlds","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T00:43:21Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-content-worlds","depends_on_id":"goon-game-milestone-vslice","type":"blocks","created_at":"2026-08-18T00:41:41Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-weighted-objects","title":"Добавить общий IWeightedObject и физические объекты","description":"Добавить box, rock, ball, ice block и counterweight поверх единого контракта массы.","acceptance_criteria":"Pressure plate одинаково работает с персонажем, телом и объектами.","notes":"2026-08-19: claimed after corpse-state slice 19bb64e passed 102 JVM and exact Android 46/46. TDD scope: public IWeightedObject mass contract, JSON Box/Rock/Ball/IceBlock/Counterweight definitions, deterministic runtime/save/render/automation, pressure parity and one teaching level with solution bot.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:43Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T04:38:25Z","started_at":"2026-08-19T03:35:44Z","closed_at":"2026-08-19T04:38:25Z","close_reason":"Shared pressure-mass contract, five authored physical-object types, deterministic runtime/save v4/render/export, level 17 and production-input route accepted. Clean 108 app JVM + 2 format JVM; focused 8/8; exact unfiltered Android 16 matrix 48/48 in 2223.695s.","metadata":{"stages":6,"primary_stage":6},"labels":["primary-stage-6","production","puzzle","stage-6"],"dependencies":[{"issue_id":"goon-game-weighted-objects","depends_on_id":"goon-game-e05","type":"parent-child","created_at":"2026-08-18T00:43:14Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-hazards-full","title":"Дополнить production-каталог опасностей","description":"Добавить crusher, fire, projectile, explosion и data-driven настройки импульса/звука/визуала.","acceptance_criteria":"Каждая опасность имеет prefab/data definition, тесты и минимум один учебный уровень.","notes":"2026-08-19: claimed after the three Stage 6 foundation slices. Will proceed test-first per hazard with data definition, deterministic behavior, teaching content, solution graph and production-input route; crusher/fire precede the dependent crushed/burned corpse states.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":2400,"created_at":"2026-08-17T21:39:37Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T02:28:37Z","started_at":"2026-08-19T01:07:11Z","closed_at":"2026-08-19T02:28:37Z","close_reason":"Crusher, fire, projectile and cyclic explosion are data-driven, validated and shipped with teaching levels 11-14, solution graphs and 3x production-input routes; clean 99 JVM/lint/APK and unfiltered Android 43/43 green.","metadata":{"stages":6,"primary_stage":6},"labels":["hazards","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-hazards-full","depends_on_id":"goon-game-e04","type":"parent-child","created_at":"2026-08-18T00:43:10Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-corpse-states","title":"Расширить состояния тел","description":"Добавить crushed, burned и остальные утверждённые состояния поверх normal/pinned/frozen/conductive.","acceptance_criteria":"Каждое состояние используется минимум одной уникальной головоломкой и проходит DoD механики.","notes":"2026-08-19: claimed after production hazards dc2ebcb passed 99 JVM and Android 43/43. Will add explicit crushed/burned/shattered and derived corpse behavior test-first, then one teaching route/solution graph per new gameplay-relevant state and full regression.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:29Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T03:32:49Z","started_at":"2026-08-19T02:31:25Z","closed_at":"2026-08-19T03:32:49Z","close_reason":"GDD corpse states complete: existing normal/pinned/frozen/conductive routes plus new burned/crushed contracts, save v3 migration, levels 15-16 and bots. Clean 102 JVM; exact Android 46/46 in 1842.248s. IsDestroyed is a lifecycle field; Shattered is not an approved GDD state.","metadata":{"stages":6,"primary_stage":6},"labels":["corpse","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-corpse-states","depends_on_id":"goon-game-e02","type":"parent-child","created_at":"2026-08-18T00:43:05Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-corpse-states","depends_on_id":"goon-game-hazards-full","type":"blocks","created_at":"2026-08-19T04:07:09Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} diff --git a/README.md b/README.md index 900677d..3094b94 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,10 @@ The first bounded Foundations authoring wave adds levels 19–22: a two-jump movement route, a two-body spike bridge, a retained pressure-door body and a toggle-controlled horizontal ferry. All four routes pass three consecutive runs, and restart clears the stateful bridge/ferry setups. +The second wave adds levels 23–26: remote saw delivery, a frozen-body step, a +conductive-body relay and a timed ferry with an explicit far-side recovery relay. +Five route methods prove all four levels three times, including both the original +timer and expired-timer recovery routes for level 26. Scope and final evidence are in `docs/STAGE6_CHARACTER_STATES.md`, `docs/STAGE6_CORPSE_MANAGER.md`, and `docs/STAGE6_PUZZLE_SYSTEM.md`; the latest hazard evidence is in `docs/STAGE6_PRODUCTION_HAZARDS.md`, and the burned/crushed @@ -138,10 +142,11 @@ contract is in `docs/STAGE6_CORPSE_STATES.md`. Weighted-object evidence is in 69 levels: 17 accepted workshop levels plus 52 production slots in Foundations, Physics, States and Mastery. Its contract and Level DoD pipeline are documented in `docs/STAGE6_CONTENT_MATRIX.md`; wave evidence is in -`docs/STAGE6_FOUNDATIONS_WAVE1.md`. The latest exact gate is `117 app JVM + 7 -content-tool JVM / 57 Android` on the twenty-two-level candidate (`57/57`, -1807.264 seconds). The verified debug APK SHA-256 is -`81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef`. +`docs/STAGE6_FOUNDATIONS_WAVE1.md` and +`docs/STAGE6_FOUNDATIONS_WAVE2.md`. The latest exact gate is `118 app JVM + 7 +content-tool JVM / 62 Android` on the twenty-six-level candidate (`62/62`, +1063.114 seconds). The verified debug APK SHA-256 is +`67510d404cfa0704500cbf36c7845e9ba9fa05e96630079ab02bc35113e946fa`. 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: diff --git a/app/src/androidTest/java/com/projectsacrifice/game/FoundationsAdvancedPlaythroughTest.java b/app/src/androidTest/java/com/projectsacrifice/game/FoundationsAdvancedPlaythroughTest.java new file mode 100644 index 0000000..1315ff0 --- /dev/null +++ b/app/src/androidTest/java/com/projectsacrifice/game/FoundationsAdvancedPlaythroughTest.java @@ -0,0 +1,88 @@ +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 FoundationsAdvancedPlaythroughTest { + @Test public void remoteVolunteerUsesOneDeliveredBodyThreeTimes() { + runThree(AdvancedFoundationsScenarios.levelTwentyThree(), 1, 1); + } + + @Test public void frozenStepUsesOneFrozenBodyThreeTimes() { + runThree(AdvancedFoundationsScenarios.levelTwentyFour(), 1, 1); + } + + @Test public void livingWireUsesOneConductiveBodyThreeTimes() { + runThree(AdvancedFoundationsScenarios.levelTwentyFive(), 1, 1); + } + + @Test public void secondHandCompletesOnTheOriginalTimerThreeTimes() { + runThree(AdvancedFoundationsScenarios.levelTwentySixIntended(), 0, 0); + } + + @Test public void secondHandRecoversAfterTheFarGateClosesThreeTimes() { + runThree(AdvancedFoundationsScenarios.levelTwentySixRecovery(), 0, 0); + } + + private static void runThree(LevelPlaytestBot.Scenario scenario, + int expectedDeaths, int expectedCorpses) { + try (ActivityScenario activity = ActivityScenario.launch(MainActivity.class)) { + Driver driver = Driver.from(activity); + for (int run = 1; run <= 3; run++) { + LevelPlaytestBot.Result result = new LevelPlaytestBot().run(driver, scenario, 45_000L); + assertTrue("run=" + run + "; " + result.diagnostic(), result.completed()); + assertEquals("run=" + run, expectedDeaths, result.deaths()); + assertEquals("run=" + run, expectedCorpses, result.corpseCount()); + assertTrue("run=" + run + "; collectible", driver.snapshot().collectibleFound()); + } + } + } + + private static final class Driver implements LevelPlaytestBot.Driver { + private final Instrumentation instrumentation; + private final GameView view; + + private Driver(Instrumentation instrumentation, GameView view) { + this.instrumentation = instrumentation; + this.view = view; + } + + static Driver from(ActivityScenario activity) { + GameView[] target = new GameView[1]; + activity.onActivity(current -> target[0] = current.gameViewForAutomation()); + return new Driver(InstrumentationRegistry.getInstrumentation(), target[0]); + } + + @Override public void startLevel(int oneBasedLevelNumber) { + instrumentation.runOnMainSync(() -> { + view.enableDeterministicAutomationClock(); + view.startLevelForAutomation(oneBasedLevelNumber); + }); + } + + @Override public void setInput(boolean left, boolean right, boolean jump) { + instrumentation.runOnMainSync(() -> view.setAutomationInput(left, right, jump)); + } + + @Override public GameView.AutomationSnapshot snapshot() { + GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1]; + instrumentation.runOnMainSync(() -> value[0] = view.automationSnapshot()); + return value[0]; + } + + @Override public void waitForNextDecision(long millis) { + instrumentation.runOnMainSync(() -> + view.advanceDeterministicAutomation(Math.max(16L, millis))); + } + } +} diff --git a/app/src/androidTest/java/com/projectsacrifice/game/LevelPaginationTest.java b/app/src/androidTest/java/com/projectsacrifice/game/LevelPaginationTest.java index 08be3be..ec1e161 100644 --- a/app/src/androidTest/java/com/projectsacrifice/game/LevelPaginationTest.java +++ b/app/src/androidTest/java/com/projectsacrifice/game/LevelPaginationTest.java @@ -53,6 +53,10 @@ public final class LevelPaginationTest { assertEquals(2, view.levelPageIndexForAutomation()); assertTrue(visible.contains("Held Open")); assertTrue(visible.contains("Clockwork Step")); + assertTrue(visible.contains("Remote Volunteer")); + assertTrue(visible.contains("Frozen Step")); + assertTrue(visible.contains("Living Wire")); + assertTrue(visible.contains("Second Hand")); assertFalse(visible.contains("Covered Path")); assertTrue(visible.contains("Page 3 of 3")); }); diff --git a/app/src/main/java/com/projectsacrifice/game/AdvancedFoundationsScenarios.java b/app/src/main/java/com/projectsacrifice/game/AdvancedFoundationsScenarios.java new file mode 100644 index 0000000..34d7557 --- /dev/null +++ b/app/src/main/java/com/projectsacrifice/game/AdvancedFoundationsScenarios.java @@ -0,0 +1,172 @@ +package com.projectsacrifice.game; + +/** Production-input routes for Foundations levels 23-26. */ +public final class AdvancedFoundationsScenarios { + private AdvancedFoundationsScenarios() {} + + public static LevelPlaytestBot.Scenario levelTwentyThree() { + return LevelPlaytestBot.Scenario.builder(23, "Level 23 - remote volunteer") + .step("Launch one volunteer onto the remote plate", 4_000L, + LevelPlaytestBot.Input.right(), + atMost(1, snapshot -> snapshot.deaths() == 1 + && snapshot.corpseCount() == 1)) + .step("Observe the delivered body open the door", 2_500L, + LevelPlaytestBot.Input.none(), + atMost(1, GameView.AutomationSnapshot::allDoorsOpen)) + .step("Wait for the crossing volunteer", 2_000L, + LevelPlaytestBot.Input.none(), atMost(1, AdvancedFoundationsScenarios::respawned)) + .step("Stage before the launch saw", 2_500L, + LevelPlaytestBot.Input.right(), + atMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(420f))) + .step("Take off before the launch saw", 1_500L, + LevelPlaytestBot.Input.rightJump(), + atMost(1, snapshot -> snapshot.playerAlive() + && !snapshot.playerOnGround() && snapshot.playerY() <= 620f)) + .step("Clear the launch saw", 2_000L, LevelPlaytestBot.Input.right(), + atMost(1, snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 740f)) + .step("Approach the delivered body", 1_500L, LevelPlaytestBot.Input.right(), + atMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(780f))) + .step("Jump over the body and collect the cache", 3_000L, + LevelPlaytestBot.Input.rightJump(), + atMost(1, snapshot -> snapshot.playerAlive() + && snapshot.collectibleFound())) + .step("Pass the remote gate", 3_000L, LevelPlaytestBot.Input.right(), + atMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(1_650f))) + .step("Reach the relic", 3_000L, LevelPlaytestBot.Input.right(), + atMost(1, GameView.AutomationSnapshot::completed)) + .build(); + } + + public static LevelPlaytestBot.Scenario levelTwentyFour() { + return LevelPlaytestBot.Scenario.builder(24, "Level 24 - frozen step") + .step("Freeze one volunteer", 4_000L, LevelPlaytestBot.Input.right(), + atMost(1, snapshot -> snapshot.deaths() == 1 + && snapshot.corpseCount() == 1)) + .step("Wait for the climbing volunteer", 2_000L, + LevelPlaytestBot.Input.none(), atMost(1, AdvancedFoundationsScenarios::respawned)) + .step("Approach the frozen block", 2_500L, LevelPlaytestBot.Input.right(), + atMost(1, snapshot -> snapshot.playerX() >= 420f)) + .step("Jump toward the frozen block", 1_500L, + LevelPlaytestBot.Input.rightJump(), + atMost(1, snapshot -> snapshot.playerX() >= 670f)) + .step("Settle on the frozen block", 2_000L, LevelPlaytestBot.Input.none(), + atMost(1, snapshot -> snapshot.playerAlive() + && snapshot.playerOnGround() + && snapshot.playerY() >= 668f && snapshot.playerY() <= 690f)) + .step("Jump to the upper route", 3_000L, LevelPlaytestBot.Input.rightJump(), + atMost(1, snapshot -> snapshot.playerAlive() + && snapshot.playerOnGround() + && snapshot.playerX() >= 850f && snapshot.playerY() <= 500f)) + .step("Cross the ledge and collect the cache", 3_000L, + LevelPlaytestBot.Input.right(), + atMost(1, snapshot -> snapshot.playerAlive() + && snapshot.collectibleFound() && snapshot.playerX() >= 1_300f)) + .step("Jump to the final ledge", 2_500L, LevelPlaytestBot.Input.rightJump(), + atMost(1, snapshot -> snapshot.playerAlive() + && snapshot.playerX() >= 1_480f && snapshot.playerY() <= 520f)) + .step("Reach the upper relic", 4_000L, LevelPlaytestBot.Input.right(), + atMost(1, GameView.AutomationSnapshot::completed)) + .build(); + } + + public static LevelPlaytestBot.Scenario levelTwentyFive() { + return LevelPlaytestBot.Scenario.builder(25, "Level 25 - living wire") + .step("Power the relay with one volunteer", 4_000L, + LevelPlaytestBot.Input.right(), + atMost(1, snapshot -> snapshot.deaths() == 1 + && snapshot.corpseCount() == 1)) + .step("Observe the conductive body open the door", 2_000L, + LevelPlaytestBot.Input.none(), + atMost(1, GameView.AutomationSnapshot::allDoorsOpen)) + .step("Wait for the crossing volunteer", 2_000L, + LevelPlaytestBot.Input.none(), atMost(1, AdvancedFoundationsScenarios::respawned)) + .step("Stage before the live relay", 2_500L, LevelPlaytestBot.Input.right(), + atMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(520f))) + .step("Take off before the live relay", 1_500L, + LevelPlaytestBot.Input.rightJump(), + atMost(1, snapshot -> snapshot.playerAlive() + && !snapshot.playerOnGround() && snapshot.playerY() <= 680f)) + .step("Clear the conductive body and collect the cache", 3_000L, + LevelPlaytestBot.Input.right(), + atMost(1, snapshot -> snapshot.playerAlive() + && snapshot.playerX() >= 1_080f && snapshot.collectibleFound())) + .step("Pass the powered door", 3_000L, LevelPlaytestBot.Input.right(), + atMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(1_470f))) + .step("Reach the relic", 3_000L, LevelPlaytestBot.Input.right(), + atMost(1, GameView.AutomationSnapshot::completed)) + .build(); + } + + public static LevelPlaytestBot.Scenario levelTwentySixIntended() { + return timedRoute(false); + } + + public static LevelPlaytestBot.Scenario levelTwentySixRecovery() { + return timedRoute(true); + } + + private static LevelPlaytestBot.Scenario timedRoute(boolean recovery) { + LevelPlaytestBot.Scenario.Builder route = LevelPlaytestBot.Scenario.builder(26, + recovery ? "Level 26 - far-side recovery" : "Level 26 - original timer"); + route.step("Start the second-hand timer", 3_000L, LevelPlaytestBot.Input.right(), + noDeaths(snapshot -> snapshot.allDoorsOpen() + && snapshot.firstMovingPlatformPowered())) + .step("Approach the timed ferry", 2_000L, LevelPlaytestBot.Input.right(), + noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(800f))) + .step("Wait for the outbound boarding phase", 3_500L, + LevelPlaytestBot.Input.none(), + noDeaths(snapshot -> MovingPlatformLaunchPolicy.readyToBoard(snapshot, 1_000f))) + .step("Board the timed ferry", 2_500L, LevelPlaytestBot.Input.rightJump(), + noDeaths(snapshot -> snapshot.playerAlive() + && snapshot.playerOnGround() + && snapshot.playerOnMovingPlatform())) + .step("Ride to the far launch phase", 4_000L, LevelPlaytestBot.Input.none(), + noDeaths(snapshot -> MovingPlatformLaunchPolicy.ready(snapshot, 1_250f))) + .step("Jump to the far floor", 3_000L, LevelPlaytestBot.Input.rightJump(), + noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(1_600f))); + if (recovery) { + route.step("Move beneath the high recovery relay", 2_000L, + LevelPlaytestBot.Input.right(), + noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(1_650f))) + .step("Wait for the far gate to close", 10_000L, + LevelPlaytestBot.Input.none(), + noDeaths(snapshot -> !snapshot.allDoorsOpen())) + .step("Jump into the recovery relay", 2_500L, + LevelPlaytestBot.Input.rightJump(), + noDeaths(GameView.AutomationSnapshot::allDoorsOpen)); + } else { + route.step("Pass the door on the original timer", 3_000L, + LevelPlaytestBot.Input.right(), + noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(2_030f))); + } + return route.step("Stage before the upper route", 2_000L, + LevelPlaytestBot.Input.right(), + noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(2_050f))) + .step("Jump onto the upper route", 3_000L, + LevelPlaytestBot.Input.rightJump(), + noDeaths(snapshot -> snapshot.playerAlive() + && snapshot.playerOnGround() + && snapshot.playerX() >= 2_250f + && snapshot.playerY() <= 610f)) + .step("Collect the upper cache", 2_000L, + LevelPlaytestBot.Input.right(), + noDeaths(snapshot -> snapshot.playerAlive() && snapshot.collectibleFound())) + .step("Reach the upper relic", 4_000L, LevelPlaytestBot.Input.right(), + noDeaths(GameView.AutomationSnapshot::completed)) + .build(); + } + + private static LevelPlaytestBot.SnapshotCondition atMost( + int deaths, LevelPlaytestBot.SnapshotCondition condition) { + return LevelPlaytestBot.whileDeathsAtMost(deaths, condition); + } + + private static LevelPlaytestBot.SnapshotCondition noDeaths( + LevelPlaytestBot.SnapshotCondition condition) { + return atMost(0, condition); + } + + private static boolean respawned(GameView.AutomationSnapshot snapshot) { + return snapshot.playerAlive() && snapshot.playerX() < 250f; + } +} diff --git a/app/src/test/java/com/projectsacrifice/game/FoundationsAdvancedContentTest.java b/app/src/test/java/com/projectsacrifice/game/FoundationsAdvancedContentTest.java new file mode 100644 index 0000000..1ce5d88 --- /dev/null +++ b/app/src/test/java/com/projectsacrifice/game/FoundationsAdvancedContentTest.java @@ -0,0 +1,58 @@ +package com.projectsacrifice.game; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public final class FoundationsAdvancedContentTest { + @Test + public void secondFoundationsWaveMatchesMatrixAndPreservesRecoveryContracts() { + List levels = LevelCatalog.all(); + assertEquals(26, levels.size()); + + LevelDefinition remote = levels.get(22); + assertIdentity(remote, 23, "foundations.remote-volunteer", "Remote Volunteer", 1, 50); + assertTrue(remote.containsMechanic(LevelDefinition.HazardType.SAW)); + assertTrue(remote.containsMechanic(LevelDefinition.SwitchType.PRESSURE)); + assertFalse(remote.doors.isEmpty()); + + LevelDefinition frozen = levels.get(23); + assertIdentity(frozen, 24, "foundations.frozen-step", "Frozen Step", 1, 48); + assertTrue(frozen.containsMechanic(LevelDefinition.HazardType.FREEZER)); + assertTrue(frozen.goal.y < 700f); + + LevelDefinition live = levels.get(24); + assertIdentity(live, 25, "foundations.living-wire", "Living Wire", 1, 52); + assertTrue(live.containsMechanic(LevelDefinition.HazardType.ELECTRIC)); + assertFalse(live.doors.isEmpty()); + + LevelDefinition timed = levels.get(25); + assertIdentity(timed, 26, "foundations.borrowed-time", "Second Hand", 0, 55); + assertTrue(timed.containsMechanic(LevelDefinition.SwitchType.TIMED)); + assertTrue(timed.containsMechanic(LevelDefinition.HazardType.PIT)); + assertTrue(timed.hasMovingPlatform()); + assertFalse(timed.doors.isEmpty()); + assertEquals(2, timed.switches.size()); + Set signals = new HashSet<>(); + for (LevelDefinition.Switch item : timed.switches) signals.add(item.signalId); + assertEquals(Set.of(timed.platforms.get(3).controlId), signals); + } + + private static void assertIdentity(LevelDefinition level, int number, String id, String title, + int targetDeaths, int targetSeconds) { + assertEquals(number, level.number); + assertEquals(id, level.levelId); + assertEquals("foundations", level.worldId); + assertEquals(title, level.title); + assertEquals(targetDeaths, level.targetDeaths); + assertEquals(targetSeconds, level.targetSeconds); + assertNotNull(level.collectible); + } +} diff --git a/app/src/test/java/com/projectsacrifice/game/FoundationsContentTest.java b/app/src/test/java/com/projectsacrifice/game/FoundationsContentTest.java index 89b73f9..730deb0 100644 --- a/app/src/test/java/com/projectsacrifice/game/FoundationsContentTest.java +++ b/app/src/test/java/com/projectsacrifice/game/FoundationsContentTest.java @@ -13,7 +13,7 @@ public final class FoundationsContentTest { @Test public void firstFoundationsWaveMatchesTheApprovedMatrixAndTeachesOneIdeaAtATime() { List levels = LevelCatalog.all(); - assertEquals(22, levels.size()); + assertTrue(levels.size() >= 22); LevelDefinition firstReturn = levels.get(18); assertIdentity(firstReturn, 19, "foundations.first-return", "First Return", 0, 28); diff --git a/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java b/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java index 8da2ca4..0630d66 100644 --- a/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java +++ b/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java @@ -15,7 +15,7 @@ public final class LevelCatalogTest { @Test public void productionCatalogContainsMechanicsAndChallengeTeachingLevels() { List levels = LevelCatalog.all(); - assertEquals(22, levels.size()); + assertEquals(26, levels.size()); Set titles = new HashSet<>(); for (int i = 0; i < levels.size(); i++) { LevelDefinition level = levels.get(i); diff --git a/docs/CURRENT_ROADMAP.md b/docs/CURRENT_ROADMAP.md index 8fb6198..b42f533 100644 --- a/docs/CURRENT_ROADMAP.md +++ b/docs/CURRENT_ROADMAP.md @@ -217,7 +217,14 @@ Android matrix `52/52` (`551.773 s`). Evidence — `STAGE6_CHALLENGES.md`. spikes, retained pressure Door и toggle ferry имеют solution graphs, collectibles, restart proof и 12/12 production-input playthroughs. Cold unfiltered candidate прошёл Android `57/57` за `1807.264 s`, app JVM `117/117`, content-tool JVM `7/7`. -Evidence — `STAGE6_FOUNDATIONS_WAVE1.md`; следующий slice — Levels 23–26. +Evidence — `STAGE6_FOUNDATIONS_WAVE1.md`. +Второй bounded wave Levels 23–26 также принят: remote saw delivery, frozen step, +conductive relay и timed ferry с дальним recovery relay имеют solution graphs, +collectibles и 15/15 последовательных production-input playthroughs. Cold +unfiltered candidate прошёл Android `62/62` за `1063.114 s`, app JVM `118/118`, +content-tool JVM `7/7`; APK содержит все 26 level JSON. Evidence — +`STAGE6_FOUNDATIONS_WAVE2.md`; следующий slice — Levels 27–30, включая Foundations +challenge и finale. ### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней diff --git a/docs/LEVEL_SOLUTION_GRAPHS.md b/docs/LEVEL_SOLUTION_GRAPHS.md index 59c8943..d3add8d 100644 --- a/docs/LEVEL_SOLUTION_GRAPHS.md +++ b/docs/LEVEL_SOLUTION_GRAPHS.md @@ -535,6 +535,78 @@ LevelID="foundations.clockwork-step", WorldID="foundations" 160 px launch overlap до движения и 20 px far overlap в конечной фазе; bot ждёт реальную поддержку, а не единичное пересечение в воздухе. +## Уровень 23 — `Remote Volunteer` + +**Статус:** доказан production-input ботом, 3/3 последовательных прохождения. +**Ожидаемые и доказанные смерти:** `1`; доставленное тело удерживает Door. + +```text +LevelID="foundations.remote-volunteer", WorldID="foundations" +→ первый доброволец входит в saw x=600..696 и получает impulse (550,-700) +→ тело попадает на raised pressure plate x=1030..1200 и открывает remote-gate +→ следующий доброволец стартует прыжок до saw и остаётся жив +→ перепрыгивает доставленное тело, забирает cache x=1180 +→ проходит Door x=1540..1622 и достигает goal с deaths=1, corpses=1 +``` + +**Контракт:** дистанционно доставленное тело является единственным источником +pressure-сигнала. Маршрут не маскирует повторную смерть и ограничен максимум одной +жертвой на каждом шаге. + +## Уровень 24 — `Frozen Step` + +**Статус:** доказан production-input ботом, 3/3 последовательных прохождения. +**Ожидаемые и доказанные смерти:** `1`; одно замороженное тело становится ступенью. + +```text +LevelID="foundations.frozen-step", WorldID="foundations" +→ первый доброволец замерзает в FREEZER x=720..820 +→ следующий прыгает и стабилизируется на frozen body на y≈680 +→ отдельным jump edge поднимается на ledge x=850..1410, y=590 +→ забирает cache x=1280 и перепрыгивает на ledge x=1500..2200 +→ достигает верхнего goal с deaths=1, corpses=1 +``` + +**Контракт:** без сохранённого frozen body высота первого уступа недостижима +доказанным маршрутом; бот отдельно наблюдает устойчивую опору на теле. + +## Уровень 25 — `Living Wire` + +**Статус:** доказан production-input ботом, 3/3 последовательных прохождения. +**Ожидаемые и доказанные смерти:** `1`; проводящее тело постоянно питает Door. + +```text +LevelID="foundations.living-wire", WorldID="foundations" +→ первый доброволец погибает в ELECTRIC x=720..830 +→ conductive corpse активирует relayId="living-power" и открывает Door +→ следующий стартует прыжок до live relay +→ перелетает тело, забирает cache x=1100 на безопасном полу +→ проходит Door x=1350..1434 и достигает goal с deaths=1, corpses=1 +``` + +**Контракт:** Door открывается только живым relay-сигналом сохранённого conductive +body; каждый шаг fail-fast запрещает вторую смерть. + +## Уровень 26 — `Second Hand` + +**Статус:** доказаны два production-input маршрута, каждый 3/3. +**Ожидаемые и доказанные смерти:** `0` для original timer и recovery. + +```text +LevelID="foundations.borrowed-time", WorldID="foundations" +→ TIMED switch x=760 запускает second-hand на 9 s и outbound ferry +→ игрок ждёт boarding phase, садится на platform x=940..1300 +→ едет до platformX≥1250 и прыгает на far floor x=1580..2700 +→ intended: проходит Door x=1920 на исходном таймере +→ recovery: ждёт закрытия Door и прыгает в высокий relay x=1700,y=650 на 4 s +→ оба маршрута поднимаются на ledge x=2230..2700, забирают cache x=2350 +→ достигают верхнего goal без смерти +``` + +**Контракт:** recovery relay использует тот же `second-hand`, но расположен над +обычным беговым маршрутом. Отдельный тест сначала наблюдает реально закрытую Door, +затем её повторное открытие — softlock после истечения исходного таймера исключён. + ## Проверки, следующие из графов 1. Сохранять полный integrated device-regression: intended route принятых уровней по три раза и отдельный recovery route уровня 8. diff --git a/docs/STAGE6_CONTENT_MATRIX.md b/docs/STAGE6_CONTENT_MATRIX.md index ca0028d..196f394 100644 --- a/docs/STAGE6_CONTENT_MATRIX.md +++ b/docs/STAGE6_CONTENT_MATRIX.md @@ -49,7 +49,7 @@ finale не вводят новую механику: они проверяют Android `preBuild` зависит от этого gate. Текущее ожидаемое сообщение: ```text -Validated production content matrix: worlds=4; planned=52; authored=5; releaseTotal=69 +Validated production content matrix: worlds=4; planned=52; authored=9; releaseTotal=69 ``` ## Content pipeline и Level DoD @@ -64,29 +64,30 @@ JSON-уровень добавляется только ведущим преф 5. restart, blocker-softlock и collectible/challenge проверки; 6. camera, sound, art, optimization и QA regression gates из Level DoD. -Следующий authoring wave — Foundations 19–30. Уровни добавляются небольшими -вертикальными срезами; незавершённый уровень не скрывается в общем счётчике -production-контента. +Foundations 19–26 уже авторизованы через этот pipeline. Следующий bounded slice — +Levels 27–30, включая challenge и finale мира; незавершённый уровень не +скрывается в общем счётчике production-контента. -## TDD и gate первого среза +## TDD и текущий gate RED сначала зафиксировал отсутствующий `ProductionContentMatrix` (15 compile errors), затем отсутствующую CLI-команду `validate-matrix` (exit `2`). После GREEN добавлен отрицательный regression: изменение planned title Level 18 отклоняется как drift реального JSON-префикса. -Текущий gate после первого Foundations wave на 19 августа 2026 года: +Текущий gate после второго Foundations wave на 19 августа 2026 года: - `ProductionContentMatrixTest` + `LevelContentToolTest`: `7/7`; -- app JVM regression: `117/117`; -- `validateLevels`: `22` authored levels; -- `validateContentMatrix`: `4` worlds, `52` planned, `5` authored production rows, +- app JVM regression: `118/118`; +- `validateLevels`: `26` authored levels; +- `validateContentMatrix`: `4` worlds, `52` planned, `9` authored production rows, release total `69`; - `lintDebug`, `assembleDebug`, `assembleDebugAndroidTest`: GREEN; -- APK ZIP integrity: no errors; matrix и `level-18.json` присутствуют в package; -- cold unfiltered Android: `57/57`, `1807.264 s`; +- APK ZIP integrity: no errors; `26` level JSON присутствуют в package; +- cold unfiltered Android: `62/62`, failures/errors `0/0`, `1063.114 s`; - debug APK SHA-256: - `81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef`. + `67510d404cfa0704500cbf36c7845e9ba9fa05e96630079ab02bc35113e946fa`. -Матрица не подменяет device evidence: Levels 19–22 прошли отдельные routes, -restart DoD и полный unfiltered gate; детали — `STAGE6_FOUNDATIONS_WAVE1.md`. +Матрица не подменяет device evidence: Levels 19–26 прошли отдельные routes и +полный unfiltered gate; детали — `STAGE6_FOUNDATIONS_WAVE1.md` и +`STAGE6_FOUNDATIONS_WAVE2.md`. diff --git a/docs/STAGE6_FOUNDATIONS_WAVE2.md b/docs/STAGE6_FOUNDATIONS_WAVE2.md new file mode 100644 index 0000000..bdaaf4d --- /dev/null +++ b/docs/STAGE6_FOUNDATIONS_WAVE2.md @@ -0,0 +1,58 @@ +# Stage 6: Foundations wave 2 + +## Принятый результат + +Второй bounded authoring wave расширяет каталог с 22 до 26 уровней: + +- 23 `Remote Volunteer` — saw доставляет одно тело на поднятую pressure plate; +- 24 `Frozen Step` — одно frozen body становится обязательной ступенью; +- 25 `Living Wire` — одно conductive body постоянно питает relay и Door; +- 26 `Second Hand` — timed ferry над PIT и высокий far-side recovery relay. + +Имя Level 26 изменено с дублирующего `Borrowed Time` на уникальное `Second Hand`; +stable ID `foundations.borrowed-time` сохранён, поэтому identity и будущие saves +не зависят от редактируемого display title. + +Все уровни имеют RU/EN copy, stable LevelID/WorldID, casualty/time targets, +collectible и solution graph. Пять Android methods выполняют по три +последовательных production-input route: по одному для Levels 23–25 и два для +Level 26 — исходный таймер и восстановление после его истечения. + +## TDD и найденные ошибки + +RED сначала зафиксировал каталог `22 != 26`, затем отсутствие +`AdvancedFoundationsScenarios`. Device-прогоны обнаружили конкретные ошибки +контента и choreography: + +1. Level 23 останавливался перед raised platform (`x=838`); подход к обязательному + прыжку перенесён раньше без изменения saw/physics. +2. Level 25 проходил ниже слишком высокого cache; cache перенесён на доказанный + безопасный ground route. +3. Оба маршрута Level 26 падали в PIT около `x=1128`; TIMED switch перенесён к + launch zone, а ferry получила достаточный boarding overlap и state-driven + launch policy. +4. После пересечения PIT Level 26 проходил под cache (`x=2638`); финальный путь + разделён на наблюдаемую посадку на верхний ledge и отдельный сбор cache, а + ledge сдвинут так, чтобы требовать этот подъём. + +Финальный focused suite прошёл `5/5` methods — `15` route executions за +`10.768 s`. Recovery route трижды ждёт `allDoorsOpen=false` на дальней стороне, +активирует высокий relay и только затем проходит вновь открытую Door. Retry, +teleport и прямая мутация completion отсутствуют. + +## Level DoD evidence + +- JSON validator: `26` levels, format/physics version `1`; +- content matrix: `4` worlds, `52` planned, `9` authored production rows, + release total `69`; +- focused routes: `5/5` methods, `15` playthroughs, `10.768 s`; +- app JVM: `118/118`; level-format JVM: `7/7`; +- clean `lintDebug`, app APK и AndroidTest APK: GREEN; +- final cold unfiltered Android 16: `62/62`, failures/errors `0/0`, + instrumentation time `1063.114 s` (`BUILD SUCCESSFUL` in `17m55s`); +- APK ZIP: no errors; `26` level JSON packaged; +- debug APK SHA-256: + `67510d404cfa0704500cbf36c7845e9ba9fa05e96630079ab02bc35113e946fa`. + +Wave не меняет глобальную physics/runtime модель. Следующий bounded slice — +Foundations 27–30, включая challenge и finale этого мира. diff --git a/level-format/src/main/resources/content/production-content-matrix.tsv b/level-format/src/main/resources/content/production-content-matrix.tsv index 44be74a..bcaacb3 100644 --- a/level-format/src/main/resources/content/production-content-matrix.tsv +++ b/level-format/src/main/resources/content/production-content-matrix.tsv @@ -7,7 +7,7 @@ number worldId levelId role title introducedMechanic mechanics targetDeaths targ 23 foundations foundations.remote-volunteer MAIN Remote Volunteer saw-launch movement,jump,death,saw-launch,pressure,door 1 50 true 24 foundations foundations.frozen-step MAIN Frozen Step frozen-body movement,jump,death,frozen-body 1 48 true 25 foundations foundations.living-wire MAIN Living Wire conductive-body movement,jump,death,conductive-body,door 1 52 true -26 foundations foundations.borrowed-time MAIN Borrowed Time timed-switch movement,jump,timed-switch,moving-platform,pit 0 55 true +26 foundations foundations.borrowed-time MAIN Second Hand timed-switch movement,jump,timed-switch,moving-platform,pit 0 55 true 27 foundations foundations.two-keys MAIN Two Keys signal-and movement,jump,death,pressure,conductive-body,signal-and,door 2 65 true 28 foundations foundations.clean-restart MAIN Clean Restart - movement,jump,death,saw-launch,frozen-body,conductive-body 3 75 true 29 foundations foundations.minimum-cost CHALLENGE Minimum Cost - movement,jump,death,corpse-bridge,pressure,timed-switch 2 65 true diff --git a/level-format/src/main/resources/levels/index.json b/level-format/src/main/resources/levels/index.json index 113e27f..d5a1283 100644 --- a/level-format/src/main/resources/levels/index.json +++ b/level-format/src/main/resources/levels/index.json @@ -1 +1 @@ -{"formatVersion":1,"physicsVersion":1,"levels":["level-01.json","level-02.json","level-03.json","level-04.json","level-05.json","level-06.json","level-07.json","level-08.json","level-09.json","level-10.json","level-11.json","level-12.json","level-13.json","level-14.json","level-15.json","level-16.json","level-17.json","level-18.json","level-19.json","level-20.json","level-21.json","level-22.json"]} +{"formatVersion":1,"physicsVersion":1,"levels":["level-01.json","level-02.json","level-03.json","level-04.json","level-05.json","level-06.json","level-07.json","level-08.json","level-09.json","level-10.json","level-11.json","level-12.json","level-13.json","level-14.json","level-15.json","level-16.json","level-17.json","level-18.json","level-19.json","level-20.json","level-21.json","level-22.json","level-23.json","level-24.json","level-25.json","level-26.json"]} diff --git a/level-format/src/main/resources/levels/level-23.json b/level-format/src/main/resources/levels/level-23.json new file mode 100644 index 0000000..5165e72 --- /dev/null +++ b/level-format/src/main/resources/levels/level-23.json @@ -0,0 +1,16 @@ +{ + "formatVersion":1,"physicsVersion":1,"number":23, + "id":"foundations.remote-volunteer","worldId":"foundations", + "copy":{"en":{"title":"Remote Volunteer","hint":"Let the saw deliver one volunteer to the raised plate, then clear the retained body."},"ru":{"title":"Удалённый доброволец","hint":"Пусть пила доставит добровольца на поднятую плиту, затем перепрыгните оставшееся тело."}}, + "world":{"width":2450,"height":1080},"spawn":{"x":130,"y":760}, + "goal":{"x":2220,"y":770,"width":86,"height":110}, + "collectible":{"x":1180,"y":650,"width":58,"height":58}, + "targets":{"deaths":1,"seconds":50}, + "platforms":[ + {"box":{"x":0,"y":880,"width":2450,"height":200}}, + {"box":{"x":900,"y":800,"width":400,"height":34}} + ], + "hazards":[{"type":"SAW","box":{"x":600,"y":760,"width":96,"height":96},"impulseX":550,"impulseY":-700}], + "switches":[{"type":"PRESSURE","box":{"x":1030,"y":780,"width":170,"height":20},"signalId":"remote-gate","durationSeconds":0,"requiredMass":1}], + "doors":[{"box":{"x":1540,"y":560,"width":82,"height":320},"requiredSignals":["remote-gate"]}] +} diff --git a/level-format/src/main/resources/levels/level-24.json b/level-format/src/main/resources/levels/level-24.json new file mode 100644 index 0000000..15f2814 --- /dev/null +++ b/level-format/src/main/resources/levels/level-24.json @@ -0,0 +1,16 @@ +{ + "formatVersion":1,"physicsVersion":1,"number":24, + "id":"foundations.frozen-step","worldId":"foundations", + "copy":{"en":{"title":"Frozen Step","hint":"Freeze one volunteer, stand on the block, and jump into the upper route."},"ru":{"title":"Замёрзшая ступень","hint":"Заморозьте добровольца, встаньте на блок и прыгните на верхний маршрут."}}, + "world":{"width":2350,"height":1080},"spawn":{"x":130,"y":760}, + "goal":{"x":2110,"y":500,"width":86,"height":110}, + "collectible":{"x":1280,"y":500,"width":58,"height":58}, + "targets":{"deaths":1,"seconds":48}, + "platforms":[ + {"box":{"x":0,"y":880,"width":1260,"height":200}}, + {"box":{"x":850,"y":590,"width":560,"height":34}}, + {"box":{"x":1500,"y":610,"width":700,"height":34}} + ], + "hazards":[{"type":"FREEZER","box":{"x":720,"y":776,"width":100,"height":104}}], + "switches":[],"doors":[] +} diff --git a/level-format/src/main/resources/levels/level-25.json b/level-format/src/main/resources/levels/level-25.json new file mode 100644 index 0000000..58aaf32 --- /dev/null +++ b/level-format/src/main/resources/levels/level-25.json @@ -0,0 +1,13 @@ +{ + "formatVersion":1,"physicsVersion":1,"number":25, + "id":"foundations.living-wire","worldId":"foundations", + "copy":{"en":{"title":"Living Wire","hint":"One conductive body keeps the relay alive. Jump over it to use the open route."},"ru":{"title":"Живой проводник","hint":"Одно проводящее тело удерживает реле. Перепрыгните его и используйте открытый маршрут."}}, + "world":{"width":2350,"height":1080},"spawn":{"x":130,"y":760}, + "goal":{"x":2130,"y":770,"width":86,"height":110}, + "collectible":{"x":1100,"y":790,"width":58,"height":58}, + "targets":{"deaths":1,"seconds":52}, + "platforms":[{"box":{"x":0,"y":880,"width":2350,"height":200}}], + "hazards":[{"type":"ELECTRIC","box":{"x":720,"y":780,"width":110,"height":100},"relayId":"living-power"}], + "switches":[], + "doors":[{"box":{"x":1350,"y":560,"width":84,"height":320},"requiredSignals":["living-power"]}] +} diff --git a/level-format/src/main/resources/levels/level-26.json b/level-format/src/main/resources/levels/level-26.json new file mode 100644 index 0000000..e81a2bd --- /dev/null +++ b/level-format/src/main/resources/levels/level-26.json @@ -0,0 +1,21 @@ +{ + "formatVersion":1,"physicsVersion":1,"number":26, + "id":"foundations.borrowed-time","worldId":"foundations", + "copy":{"en":{"title":"Second Hand","hint":"Start the clock, ride across, and use the high far relay if the gate closes."},"ru":{"title":"Секундная стрелка","hint":"Запустите часы, переедьте пропасть и используйте высокое дальнее реле, если ворота закроются."}}, + "world":{"width":2700,"height":1080},"spawn":{"x":130,"y":760}, + "goal":{"x":2470,"y":600,"width":86,"height":110}, + "collectible":{"x":2350,"y":580,"width":58,"height":58}, + "targets":{"deaths":0,"seconds":55}, + "platforms":[ + {"box":{"x":0,"y":880,"width":1100,"height":200}}, + {"box":{"x":1580,"y":880,"width":1120,"height":200}}, + {"box":{"x":2230,"y":710,"width":470,"height":34}}, + {"box":{"x":940,"y":820,"width":360,"height":34},"travelX":380,"travelY":0,"periodSeconds":4.4,"controlId":"second-hand"} + ], + "hazards":[{"type":"PIT","box":{"x":1100,"y":880,"width":480,"height":200}}], + "switches":[ + {"type":"TIMED","box":{"x":760,"y":810,"width":90,"height":70},"signalId":"second-hand","durationSeconds":9.0,"requiredMass":1}, + {"type":"TIMED","box":{"x":1700,"y":650,"width":90,"height":70},"signalId":"second-hand","durationSeconds":4.0,"requiredMass":1} + ], + "doors":[{"box":{"x":1920,"y":560,"width":82,"height":320},"requiredSignals":["second-hand"]}] +} diff --git a/level-format/src/test/java/com/projectsacrifice/game/LevelContentToolTest.java b/level-format/src/test/java/com/projectsacrifice/game/LevelContentToolTest.java index 870629a..e84752a 100644 --- a/level-format/src/test/java/com/projectsacrifice/game/LevelContentToolTest.java +++ b/level-format/src/test/java/com/projectsacrifice/game/LevelContentToolTest.java @@ -85,7 +85,7 @@ public final class LevelContentToolTest { assertEquals(0, exitCode); assertEquals("Validated production content matrix: worlds=4; planned=52; " - + "authored=5; releaseTotal=69\n", + + "authored=9; releaseTotal=69\n", output.toString(StandardCharsets.UTF_8)); assertEquals("", errors.toString(StandardCharsets.UTF_8)); }