Добавить вторую волну мира Foundations
This commit is contained in:
+1
-1
@@ -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}
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
+88
@@ -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<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
Driver driver = Driver.from(activity);
|
||||
for (int run = 1; run <= 3; run++) {
|
||||
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(driver, scenario, 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<MainActivity> activity) {
|
||||
GameView[] target = new GameView[1];
|
||||
activity.onActivity(current -> target[0] = current.gameViewForAutomation());
|
||||
return new Driver(InstrumentationRegistry.getInstrumentation(), target[0]);
|
||||
}
|
||||
|
||||
@Override public void startLevel(int oneBasedLevelNumber) {
|
||||
instrumentation.runOnMainSync(() -> {
|
||||
view.enableDeterministicAutomationClock();
|
||||
view.startLevelForAutomation(oneBasedLevelNumber);
|
||||
});
|
||||
}
|
||||
|
||||
@Override public void setInput(boolean left, boolean right, boolean jump) {
|
||||
instrumentation.runOnMainSync(() -> view.setAutomationInput(left, right, jump));
|
||||
}
|
||||
|
||||
@Override public GameView.AutomationSnapshot snapshot() {
|
||||
GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1];
|
||||
instrumentation.runOnMainSync(() -> value[0] = view.automationSnapshot());
|
||||
return value[0];
|
||||
}
|
||||
|
||||
@Override public void waitForNextDecision(long millis) {
|
||||
instrumentation.runOnMainSync(() ->
|
||||
view.advanceDeterministicAutomation(Math.max(16L, millis)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<LevelDefinition> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ public final class FoundationsContentTest {
|
||||
@Test
|
||||
public void firstFoundationsWaveMatchesTheApprovedMatrixAndTeachesOneIdeaAtATime() {
|
||||
List<LevelDefinition> 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);
|
||||
|
||||
@@ -15,7 +15,7 @@ public final class LevelCatalogTest {
|
||||
@Test
|
||||
public void productionCatalogContainsMechanicsAndChallengeTeachingLevels() {
|
||||
List<LevelDefinition> levels = LevelCatalog.all();
|
||||
assertEquals(22, levels.size());
|
||||
assertEquals(26, levels.size());
|
||||
Set<String> titles = new HashSet<>();
|
||||
for (int i = 0; i < levels.size(); i++) {
|
||||
LevelDefinition level = levels.get(i);
|
||||
|
||||
@@ -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 дней
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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 этого мира.
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"]}
|
||||
|
||||
@@ -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"]}]
|
||||
}
|
||||
@@ -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":[]
|
||||
}
|
||||
@@ -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"]}]
|
||||
}
|
||||
@@ -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"]}]
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user