Исправить паузу после возврата в игру

This commit is contained in:
2026-08-18 01:03:50 +03:00
parent 8ee8cff773
commit 20357c4c29
3 changed files with 117 additions and 0 deletions
@@ -0,0 +1,97 @@
package com.projectsacrifice.game;
import android.app.Instrumentation;
import androidx.lifecycle.Lifecycle;
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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/** Device-level contract for leaving and returning to an active puzzle. */
@RunWith(AndroidJUnit4.class)
public final class GameLifecycleTest {
@Test
public void returningFromBackgroundKeepsPuzzleVisiblePausedAndClearsHeldInput() {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
try (ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class)) {
final GameView[] view = new GameView[1];
scenario.onActivity(activity -> view[0] = activity.gameViewForAutomation());
createRetainedCorpse(instrumentation, view[0]);
runOnGame(instrumentation, view[0], false, false, true, false);
scenario.moveToState(Lifecycle.State.STARTED);
GameView.AutomationSnapshot backgroundStart = snapshot(instrumentation, view[0]);
sleep(700L);
GameView.AutomationSnapshot backgroundEnd = snapshot(instrumentation, view[0]);
assertEquals(5, backgroundEnd.levelNumber());
assertEquals(backgroundStart.deaths(), backgroundEnd.deaths());
assertEquals(backgroundStart.corpseCount(), backgroundEnd.corpseCount());
assertEquals(backgroundStart.playerX(), backgroundEnd.playerX(), 0.01f);
assertEquals(backgroundStart.elapsedMilliseconds(), backgroundEnd.elapsedMilliseconds());
scenario.moveToState(Lifecycle.State.RESUMED);
GameView.AutomationSnapshot returned = snapshot(instrumentation, view[0]);
assertTrue("Returning to an active puzzle must show the pause screen", returned.visiblyPaused());
assertFalse("Held touch input must not survive backgrounding", returned.inputActive());
assertEquals(backgroundEnd.deaths(), returned.deaths());
assertEquals(backgroundEnd.corpseCount(), returned.corpseCount());
scenario.onActivity(MainActivity::onBackPressed);
sleep(80L);
GameView.AutomationSnapshot resumed = snapshot(instrumentation, view[0]);
assertFalse("Back from the visible pause screen must resume the puzzle", resumed.visiblyPaused());
runOnGame(instrumentation, view[0], false, false, true, false);
sleep(250L);
runOnGame(instrumentation, view[0], false, false, false, false);
GameView.AutomationSnapshot moving = snapshot(instrumentation, view[0]);
assertTrue("The resumed simulation must accept movement", moving.playerX() > resumed.playerX() + 20f);
}
}
private static void createRetainedCorpse(Instrumentation instrumentation, GameView gameView) {
runOnGame(instrumentation, gameView, true, false, true, false);
long deadline = System.currentTimeMillis() + 8_000L;
boolean died = false;
while (System.currentTimeMillis() < deadline) {
GameView.AutomationSnapshot value = snapshot(instrumentation, gameView);
if (!value.playerAlive()) died = true;
if (died && value.playerAlive() && value.corpseCount() >= 1) {
runOnGame(instrumentation, gameView, false, false, false, false);
return;
}
sleep(24L);
}
throw new AssertionError("Level 5 did not produce a retained corpse before lifecycle check");
}
private static void runOnGame(Instrumentation instrumentation, GameView gameView,
boolean startLevelFive, boolean left, boolean right, boolean jump) {
instrumentation.runOnMainSync(() -> {
if (startLevelFive) gameView.startLevelForAutomation(5);
gameView.setAutomationInput(left, right, jump);
});
}
private static GameView.AutomationSnapshot snapshot(Instrumentation instrumentation, GameView gameView) {
final GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1];
instrumentation.runOnMainSync(() -> value[0] = gameView.automationSnapshot());
return value[0];
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new AssertionError("Lifecycle test interrupted", interrupted);
}
}
}
@@ -45,16 +45,26 @@ public final class GameView extends View implements Runnable {
private final boolean completed; private final boolean completed;
private final boolean playerAlive; private final boolean playerAlive;
private final boolean allDoorsOpen; private final boolean allDoorsOpen;
private final boolean visiblyPaused;
private final boolean inputActive;
private final int levelNumber;
private final long elapsedMilliseconds;
private final float playerX; private final float playerX;
private final float playerY; private final float playerY;
private final int deaths; private final int deaths;
private final int corpseCount; private final int corpseCount;
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen, AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
boolean visiblyPaused, boolean inputActive, int levelNumber,
long elapsedMilliseconds,
float playerX, float playerY, int deaths, int corpseCount) { float playerX, float playerY, int deaths, int corpseCount) {
this.completed = completed; this.completed = completed;
this.playerAlive = playerAlive; this.playerAlive = playerAlive;
this.allDoorsOpen = allDoorsOpen; this.allDoorsOpen = allDoorsOpen;
this.visiblyPaused = visiblyPaused;
this.inputActive = inputActive;
this.levelNumber = levelNumber;
this.elapsedMilliseconds = elapsedMilliseconds;
this.playerX = playerX; this.playerX = playerX;
this.playerY = playerY; this.playerY = playerY;
this.deaths = deaths; this.deaths = deaths;
@@ -64,6 +74,10 @@ public final class GameView extends View implements Runnable {
public boolean completed() { return completed; } public boolean completed() { return completed; }
public boolean playerAlive() { return playerAlive; } public boolean playerAlive() { return playerAlive; }
public boolean allDoorsOpen() { return allDoorsOpen; } public boolean allDoorsOpen() { return allDoorsOpen; }
public boolean visiblyPaused() { return visiblyPaused; }
public boolean inputActive() { return inputActive; }
public int levelNumber() { return levelNumber; }
public long elapsedMilliseconds() { return elapsedMilliseconds; }
public float playerX() { return playerX; } public float playerX() { return playerX; }
public float playerY() { return playerY; } public float playerY() { return playerY; }
public int deaths() { return deaths; } public int deaths() { return deaths; }
@@ -174,6 +188,7 @@ public final class GameView extends View implements Runnable {
public void setPaused(boolean value) { public void setPaused(boolean value) {
paused = value; paused = value;
if (value && screen == Screen.PLAYING) userPaused = true;
pointers.clear(); pointers.clear();
deriveControls(); deriveControls();
lastFrameNanos = System.nanoTime(); lastFrameNanos = System.nanoTime();
@@ -1117,6 +1132,10 @@ public final class GameView extends View implements Runnable {
screen == Screen.COMPLETE, screen == Screen.COMPLETE,
player.alive, player.alive,
allOpen, allOpen,
userPaused,
leftPressed || rightPressed || jumpPressed,
level == null ? 0 : level.number,
(long) (levelElapsed * 1000f),
player.x, player.x,
player.y, player.y,
deaths, deaths,
@@ -36,6 +36,7 @@ public final class MainActivity extends Activity {
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
if (gameView != null) gameView.setPaused(false);
hideSystemUi(); hideSystemUi();
} }