Добавить обучение и полный экран результата
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
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.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class OnboardingAndResultFlowTest {
|
||||
@Test
|
||||
public void tutorialIsOptionalNonBlockingAndDoesNotRepeatAfterRecreation() {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
activity.onActivity(owner -> {
|
||||
GameView view = owner.gameViewForAutomation();
|
||||
view.resetOnboardingForAutomation();
|
||||
view.startLevelForAutomation(1);
|
||||
assertTrue(view.onboardingVisibleForAutomation());
|
||||
assertEquals(OnboardingFlow.Step.MOVEMENT,
|
||||
view.onboardingStepForAutomation());
|
||||
|
||||
view.setAutomationInput(false, true, false);
|
||||
assertTrue("Tutorial must not disable gameplay controls",
|
||||
view.automationSnapshot().inputActive());
|
||||
|
||||
view.advanceOnboardingForAutomation();
|
||||
view.advanceOnboardingForAutomation();
|
||||
view.advanceOnboardingForAutomation();
|
||||
assertFalse(view.onboardingVisibleForAutomation());
|
||||
});
|
||||
|
||||
activity.recreate();
|
||||
activity.onActivity(owner -> {
|
||||
GameView view = owner.gameViewForAutomation();
|
||||
view.startLevelForAutomation(1);
|
||||
assertFalse(view.onboardingVisibleForAutomation());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completionShowsRunAndSavedBestAndAllResultRoutesPreserveState() {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
activity.onActivity(owner -> {
|
||||
GameView view = owner.gameViewForAutomation();
|
||||
view.selectNextControlSizeForAutomation();
|
||||
String selectedSize = view.controlSettingsForAutomation().controlSizeLabel();
|
||||
|
||||
view.startLevelForAutomation(1);
|
||||
view.finishLevelForAutomation(2, 42_000L);
|
||||
CompletionResult result = view.completionResultForAutomation();
|
||||
assertNotNull(result);
|
||||
assertTrue(view.resultVisibleForAutomation());
|
||||
assertEquals(2, result.currentDeaths());
|
||||
assertEquals(42_000L, result.currentTimeMs());
|
||||
assertTrue(result.bestDeaths() <= result.currentDeaths());
|
||||
assertTrue(result.bestTimeMs() <= result.currentTimeMs());
|
||||
assertTrue(view.levelCompletedForAutomation(1));
|
||||
|
||||
view.replayFromResultForAutomation();
|
||||
assertEquals(1, view.automationSnapshot().levelNumber());
|
||||
assertFalse(view.automationSnapshot().completed());
|
||||
assertEquals(selectedSize, view.controlSettingsForAutomation().controlSizeLabel());
|
||||
assertTrue(view.levelCompletedForAutomation(1));
|
||||
|
||||
view.finishLevelForAutomation(1, 35_000L);
|
||||
view.nextLevelFromResultForAutomation();
|
||||
assertEquals(2, view.automationSnapshot().levelNumber());
|
||||
assertEquals(selectedSize, view.controlSettingsForAutomation().controlSizeLabel());
|
||||
assertTrue(view.levelCompletedForAutomation(1));
|
||||
|
||||
view.finishLevelForAutomation(1, 35_000L);
|
||||
view.levelsFromResultForAutomation();
|
||||
assertTrue(view.levelsVisibleForAutomation());
|
||||
assertEquals(selectedSize, view.controlSettingsForAutomation().controlSizeLabel());
|
||||
assertTrue(view.levelCompletedForAutomation(2));
|
||||
view.resetControlSettingsForAutomation();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class OnboardingStoreTest {
|
||||
@Test
|
||||
public void dismissalSurvivesStoreRecreation() {
|
||||
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
OnboardingStore first = new OnboardingStore(context);
|
||||
first.resetForAutomation();
|
||||
assertFalse(first.isComplete());
|
||||
|
||||
first.markComplete();
|
||||
|
||||
assertTrue(new OnboardingStore(context).isComplete());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
/** Immutable completion statistics used by rendering and device-level navigation tests. */
|
||||
public final class CompletionResult {
|
||||
public enum NextGoal {
|
||||
TARGET_DEATHS,
|
||||
TARGET_TIME,
|
||||
NEXT_LEVEL,
|
||||
PERSONAL_BEST
|
||||
}
|
||||
|
||||
private final int currentDeaths;
|
||||
private final long currentTimeMs;
|
||||
private final int bestDeaths;
|
||||
private final long bestTimeMs;
|
||||
private final int targetDeaths;
|
||||
private final long targetTimeMs;
|
||||
private final NextGoal nextGoal;
|
||||
private final long nextGoalValue;
|
||||
|
||||
private CompletionResult(int currentDeaths, long currentTimeMs,
|
||||
int bestDeaths, long bestTimeMs,
|
||||
int targetDeaths, long targetTimeMs,
|
||||
NextGoal nextGoal, long nextGoalValue) {
|
||||
this.currentDeaths = currentDeaths;
|
||||
this.currentTimeMs = currentTimeMs;
|
||||
this.bestDeaths = bestDeaths;
|
||||
this.bestTimeMs = bestTimeMs;
|
||||
this.targetDeaths = targetDeaths;
|
||||
this.targetTimeMs = targetTimeMs;
|
||||
this.nextGoal = nextGoal;
|
||||
this.nextGoalValue = nextGoalValue;
|
||||
}
|
||||
|
||||
public static CompletionResult create(LevelDefinition level,
|
||||
int currentDeaths, long currentTimeMs,
|
||||
int bestDeaths, long bestTimeMs,
|
||||
boolean hasNextLevel) {
|
||||
long targetTimeMs = level.targetSeconds * 1_000L;
|
||||
NextGoal goal;
|
||||
long goalValue;
|
||||
if (currentDeaths > level.targetDeaths) {
|
||||
goal = NextGoal.TARGET_DEATHS;
|
||||
goalValue = level.targetDeaths;
|
||||
} else if (currentTimeMs > targetTimeMs) {
|
||||
goal = NextGoal.TARGET_TIME;
|
||||
goalValue = targetTimeMs;
|
||||
} else if (hasNextLevel) {
|
||||
goal = NextGoal.NEXT_LEVEL;
|
||||
goalValue = level.number + 1L;
|
||||
} else {
|
||||
goal = NextGoal.PERSONAL_BEST;
|
||||
goalValue = 0L;
|
||||
}
|
||||
return new CompletionResult(currentDeaths, currentTimeMs, bestDeaths, bestTimeMs,
|
||||
level.targetDeaths, targetTimeMs, goal, goalValue);
|
||||
}
|
||||
|
||||
public int currentDeaths() { return currentDeaths; }
|
||||
public long currentTimeMs() { return currentTimeMs; }
|
||||
public int bestDeaths() { return bestDeaths; }
|
||||
public long bestTimeMs() { return bestTimeMs; }
|
||||
public int targetDeaths() { return targetDeaths; }
|
||||
public long targetTimeMs() { return targetTimeMs; }
|
||||
public NextGoal nextGoal() { return nextGoal; }
|
||||
public long nextGoalValue() { return nextGoalValue; }
|
||||
}
|
||||
@@ -161,6 +161,7 @@ public final class GameView extends View implements Runnable {
|
||||
0, 0, 0, VIEW_HEIGHT, Color.rgb(20, 29, 55), Color.rgb(110, 81, 91), Shader.TileMode.CLAMP);
|
||||
private final ProgressStore progress;
|
||||
private final ControlSettingsStore controlSettingsStore;
|
||||
private final OnboardingStore onboardingStore;
|
||||
private final SparseArray<PointF> pointers = new SparseArray<>();
|
||||
private final Player player = new Player();
|
||||
private final ArrayList<Corpse> corpses = new ArrayList<>();
|
||||
@@ -187,12 +188,16 @@ public final class GameView extends View implements Runnable {
|
||||
private float canvasOffsetX;
|
||||
private float canvasOffsetY;
|
||||
private ControlSettings controlSettings;
|
||||
private OnboardingFlow onboardingFlow;
|
||||
private CompletionResult completionResult;
|
||||
|
||||
public GameView(Context context) {
|
||||
super(context);
|
||||
progress = new ProgressStore(context);
|
||||
controlSettingsStore = new ControlSettingsStore(context);
|
||||
controlSettings = controlSettingsStore.load();
|
||||
onboardingStore = new OnboardingStore(context);
|
||||
onboardingFlow = OnboardingFlow.firstRun(onboardingStore.isComplete());
|
||||
paint.setTypeface(android.graphics.Typeface.create("sans", android.graphics.Typeface.NORMAL));
|
||||
stroke.setStyle(Paint.Style.STROKE);
|
||||
stroke.setStrokeCap(Paint.Cap.ROUND);
|
||||
@@ -236,7 +241,7 @@ public final class GameView extends View implements Runnable {
|
||||
return true;
|
||||
}
|
||||
if (screen == Screen.COMPLETE) {
|
||||
screen = Screen.LEVELS;
|
||||
levelsFromResult();
|
||||
return true;
|
||||
}
|
||||
if (userPaused) {
|
||||
@@ -265,6 +270,7 @@ public final class GameView extends View implements Runnable {
|
||||
toggleSignals.clear();
|
||||
timedSignals.clear();
|
||||
previousSwitchContact.clear();
|
||||
completionResult = null;
|
||||
for (LevelDefinition.Platform item : level.platforms) platformStates.add(new PlatformState(item));
|
||||
spawnPlayer();
|
||||
lastFrameNanos = System.nanoTime();
|
||||
@@ -622,7 +628,22 @@ public final class GameView extends View implements Runnable {
|
||||
|
||||
private void checkGoal() {
|
||||
if (!intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT, level.goal)) return;
|
||||
progress.recordCompletion(level.number, deaths, (long) (levelElapsed * 1000f));
|
||||
finishLevel(deaths, (long) (levelElapsed * 1000f));
|
||||
}
|
||||
|
||||
private void finishLevel(int completedDeaths, long completedTimeMs) {
|
||||
long safeTimeMs = Math.max(0L, completedTimeMs);
|
||||
deaths = Math.max(0, completedDeaths);
|
||||
levelElapsed = safeTimeMs / 1_000f;
|
||||
progress.recordCompletion(level.number, deaths, safeTimeMs);
|
||||
completionResult = CompletionResult.create(
|
||||
level,
|
||||
deaths,
|
||||
safeTimeMs,
|
||||
progress.bestDeaths(level.number),
|
||||
progress.bestTimeMs(level.number),
|
||||
levelIndex + 1 < LevelCatalog.all().size()
|
||||
);
|
||||
player.vx = player.vy = 0f;
|
||||
screen = Screen.COMPLETE;
|
||||
pointers.clear();
|
||||
@@ -664,6 +685,7 @@ public final class GameView extends View implements Runnable {
|
||||
drawGame(canvas);
|
||||
if (screen == Screen.COMPLETE) drawComplete(canvas);
|
||||
else if (userPaused) drawPause(canvas);
|
||||
else if (onboardingFlow.isVisible()) drawOnboarding(canvas);
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
@@ -1017,22 +1039,84 @@ public final class GameView extends View implements Runnable {
|
||||
private void drawComplete(Canvas canvas) {
|
||||
paint.setColor(Color.argb(218, 8, 12, 23));
|
||||
canvas.drawRect(0, 0, VIEW_WIDTH, VIEW_HEIGHT, paint);
|
||||
drawCenteredText(canvas, "RELIC CLAIMED", VIEW_WIDTH * 0.5f, 270, 64, Color.rgb(250, 211, 111));
|
||||
drawCenteredText(canvas, level.title, VIEW_WIDTH * 0.5f, 335, 34, Color.WHITE);
|
||||
drawCenteredText(canvas, "RELIC CLAIMED", VIEW_WIDTH * 0.5f, 205, 64, Color.rgb(250, 211, 111));
|
||||
drawCenteredText(canvas, level.title, VIEW_WIDTH * 0.5f, 263, 34, Color.WHITE);
|
||||
|
||||
boolean deathStar = deaths <= level.targetDeaths;
|
||||
boolean timeStar = levelElapsed <= level.targetSeconds;
|
||||
drawCenteredText(canvas, "Completion ★", VIEW_WIDTH * 0.5f, 470, 38, Color.rgb(250, 211, 111));
|
||||
drawCenteredText(canvas, (deathStar ? "★" : "☆") + " " + deaths + " / " + level.targetDeaths + " volunteers",
|
||||
VIEW_WIDTH * 0.5f, 535, 31, deathStar ? Color.rgb(128, 229, 153) : Color.rgb(210, 211, 218));
|
||||
drawCenteredText(canvas, (timeStar ? "★" : "☆") + " " + GameMath.formatTime((long) (levelElapsed * 1000)) + " / " + GameMath.formatTime(level.targetSeconds * 1000L),
|
||||
VIEW_WIDTH * 0.5f, 588, 31, timeStar ? Color.rgb(128, 229, 153) : Color.rgb(210, 211, 218));
|
||||
CompletionResult result = completionResult;
|
||||
if (result != null) {
|
||||
drawCenteredText(canvas,
|
||||
"This run · " + result.currentDeaths() + " volunteers · "
|
||||
+ GameMath.formatTime(result.currentTimeMs()),
|
||||
VIEW_WIDTH * 0.5f, 380, 34, Color.WHITE);
|
||||
drawCenteredText(canvas,
|
||||
"Saved best · " + result.bestDeaths() + " volunteers · "
|
||||
+ GameMath.formatTime(result.bestTimeMs()),
|
||||
VIEW_WIDTH * 0.5f, 437, 31, Color.rgb(128, 229, 153));
|
||||
drawCenteredText(canvas,
|
||||
"Targets · " + result.targetDeaths() + " volunteers · "
|
||||
+ GameMath.formatTime(result.targetTimeMs()),
|
||||
VIEW_WIDTH * 0.5f, 510, 28, Color.rgb(210, 211, 218));
|
||||
drawCenteredText(canvas, nextGoalText(result), VIEW_WIDTH * 0.5f, 585, 31,
|
||||
Color.rgb(250, 211, 111));
|
||||
}
|
||||
|
||||
drawHudButton(canvas, new RectF(345, 735, 755, 850), "Level select", false);
|
||||
drawHudButton(canvas, new RectF(775, 735, 1145, 850), "Replay", false);
|
||||
if (levelIndex + 1 < LevelCatalog.all().size()) drawHudButton(canvas, new RectF(1165, 735, 1575, 850), "Next level", true);
|
||||
}
|
||||
|
||||
private String nextGoalText(CompletionResult result) {
|
||||
switch (result.nextGoal()) {
|
||||
case TARGET_DEATHS:
|
||||
return "Next goal · finish with " + result.nextGoalValue() + " volunteers";
|
||||
case TARGET_TIME:
|
||||
return "Next goal · finish in " + GameMath.formatTime(result.nextGoalValue());
|
||||
case NEXT_LEVEL:
|
||||
return "Next goal · enter level " + result.nextGoalValue();
|
||||
case PERSONAL_BEST:
|
||||
default:
|
||||
return "Next goal · improve your personal best";
|
||||
}
|
||||
}
|
||||
|
||||
private void drawOnboarding(Canvas canvas) {
|
||||
RectF card = new RectF(535, 175, 1385, 375);
|
||||
paint.setColor(Color.argb(232, 16, 22, 37));
|
||||
canvas.drawRoundRect(card, 24, 24, paint);
|
||||
stroke.setColor(Color.rgb(226, 169, 63));
|
||||
stroke.setStrokeWidth(4f);
|
||||
canvas.drawRoundRect(card, 24, 24, stroke);
|
||||
|
||||
String title;
|
||||
String message;
|
||||
String progressLabel;
|
||||
switch (onboardingFlow.step()) {
|
||||
case MOVEMENT:
|
||||
title = "MOVE";
|
||||
message = "Hold the left or right arrow to explore.";
|
||||
progressLabel = "1 / 3";
|
||||
break;
|
||||
case MULTITOUCH_JUMP:
|
||||
title = "MOVE + JUMP";
|
||||
message = "Use two fingers: hold a direction and tap Jump.";
|
||||
progressLabel = "2 / 3";
|
||||
break;
|
||||
case CORPSES_ARE_TOOLS:
|
||||
default:
|
||||
title = "DEATH LEAVES A TOOL";
|
||||
message = "A fallen volunteer remains. Use the body to open a path.";
|
||||
progressLabel = "3 / 3";
|
||||
break;
|
||||
}
|
||||
drawText(canvas, title, 575, 230, 30, Color.rgb(250, 211, 111), true);
|
||||
drawText(canvas, message, 575, 278, 27, Color.WHITE, false);
|
||||
drawText(canvas, progressLabel, 575, 337, 22, Color.rgb(180, 185, 198), false);
|
||||
drawHudButton(canvas, new RectF(965, 298, 1125, 355), "Skip", false);
|
||||
drawHudButton(canvas, new RectF(1145, 298, 1345, 355),
|
||||
onboardingFlow.step() == OnboardingFlow.Step.CORPSES_ARE_TOOLS ? "Done" : "Next",
|
||||
true);
|
||||
}
|
||||
|
||||
private void drawPause(Canvas canvas) {
|
||||
paint.setColor(Color.argb(218, 8, 12, 23));
|
||||
canvas.drawRect(0, 0, VIEW_WIDTH, VIEW_HEIGHT, paint);
|
||||
@@ -1112,6 +1196,14 @@ public final class GameView extends View implements Runnable {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (onboardingFlow.isVisible() && action == MotionEvent.ACTION_UP
|
||||
&& handleOnboardingTap(point.x, point.y)) {
|
||||
performClick();
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
|
||||
if (contains(1608, 26, 1770, 112, point.x, point.y)) {
|
||||
restartLevel();
|
||||
@@ -1188,9 +1280,46 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
|
||||
private void handleCompleteTap(float x, float y) {
|
||||
if (contains(345, 735, 755, 850, x, y)) screen = Screen.LEVELS;
|
||||
else if (contains(775, 735, 1145, 850, x, y)) restartLevel();
|
||||
else if (contains(1165, 735, 1575, 850, x, y) && levelIndex + 1 < LevelCatalog.all().size()) startLevel(levelIndex + 1);
|
||||
if (contains(345, 735, 755, 850, x, y)) levelsFromResult();
|
||||
else if (contains(775, 735, 1145, 850, x, y)) replayFromResult();
|
||||
else if (contains(1165, 735, 1575, 850, x, y)) nextLevelFromResult();
|
||||
}
|
||||
|
||||
private boolean handleOnboardingTap(float x, float y) {
|
||||
if (contains(965, 298, 1125, 355, x, y)) {
|
||||
onboardingFlow.skip();
|
||||
onboardingStore.markComplete();
|
||||
invalidate();
|
||||
return true;
|
||||
}
|
||||
if (contains(1145, 298, 1345, 355, x, y)) {
|
||||
advanceOnboarding();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void advanceOnboarding() {
|
||||
onboardingFlow.next();
|
||||
if (onboardingFlow.isComplete()) onboardingStore.markComplete();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void levelsFromResult() {
|
||||
if (screen != Screen.COMPLETE) return;
|
||||
screen = Screen.LEVELS;
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
}
|
||||
|
||||
private void replayFromResult() {
|
||||
if (screen == Screen.COMPLETE) restartLevel();
|
||||
}
|
||||
|
||||
private void nextLevelFromResult() {
|
||||
if (screen == Screen.COMPLETE && levelIndex + 1 < LevelCatalog.all().size()) {
|
||||
startLevel(levelIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePauseTap(float x, float y) {
|
||||
@@ -1254,6 +1383,34 @@ public final class GameView extends View implements Runnable {
|
||||
return screen == Screen.SETTINGS;
|
||||
}
|
||||
|
||||
public boolean levelsVisibleForAutomation() {
|
||||
return screen == Screen.LEVELS;
|
||||
}
|
||||
|
||||
public boolean onboardingVisibleForAutomation() {
|
||||
return screen == Screen.PLAYING && onboardingFlow.isVisible();
|
||||
}
|
||||
|
||||
public OnboardingFlow.Step onboardingStepForAutomation() {
|
||||
return onboardingFlow.step();
|
||||
}
|
||||
|
||||
public void resetOnboardingForAutomation() {
|
||||
onboardingStore.resetForAutomation();
|
||||
onboardingFlow = OnboardingFlow.firstRun(false);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void advanceOnboardingForAutomation() {
|
||||
advanceOnboarding();
|
||||
}
|
||||
|
||||
public void skipOnboardingForAutomation() {
|
||||
onboardingFlow.skip();
|
||||
onboardingStore.markComplete();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void resetControlSettingsForAutomation() {
|
||||
updateControlSettings(ControlSettings.defaults());
|
||||
}
|
||||
@@ -1270,6 +1427,37 @@ public final class GameView extends View implements Runnable {
|
||||
updateControlSettings(controlSettings.toggleHaptics());
|
||||
}
|
||||
|
||||
public void finishLevelForAutomation(int completedDeaths, long completedTimeMs) {
|
||||
if (screen != Screen.PLAYING || level == null) {
|
||||
throw new IllegalStateException("A level must be playing before it can finish");
|
||||
}
|
||||
finishLevel(completedDeaths, completedTimeMs);
|
||||
}
|
||||
|
||||
public CompletionResult completionResultForAutomation() {
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
public boolean resultVisibleForAutomation() {
|
||||
return screen == Screen.COMPLETE;
|
||||
}
|
||||
|
||||
public boolean levelCompletedForAutomation(int oneBasedLevelNumber) {
|
||||
return progress.isCompleted(oneBasedLevelNumber);
|
||||
}
|
||||
|
||||
public void levelsFromResultForAutomation() {
|
||||
levelsFromResult();
|
||||
}
|
||||
|
||||
public void replayFromResultForAutomation() {
|
||||
replayFromResult();
|
||||
}
|
||||
|
||||
public void nextLevelFromResultForAutomation() {
|
||||
nextLevelFromResult();
|
||||
}
|
||||
|
||||
private void updateControlSettings(ControlSettings updated) {
|
||||
controlSettings = updated;
|
||||
controlSettingsStore.save(updated);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
/** Small state machine for the optional, first-run gameplay hints. */
|
||||
public final class OnboardingFlow {
|
||||
public enum Step {
|
||||
MOVEMENT,
|
||||
MULTITOUCH_JUMP,
|
||||
CORPSES_ARE_TOOLS
|
||||
}
|
||||
|
||||
private Step step;
|
||||
private boolean complete;
|
||||
|
||||
private OnboardingFlow(boolean complete) {
|
||||
this.complete = complete;
|
||||
step = Step.MOVEMENT;
|
||||
}
|
||||
|
||||
public static OnboardingFlow firstRun(boolean alreadyComplete) {
|
||||
return new OnboardingFlow(alreadyComplete);
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return !complete;
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return complete;
|
||||
}
|
||||
|
||||
public Step step() {
|
||||
return step;
|
||||
}
|
||||
|
||||
public void next() {
|
||||
if (complete) return;
|
||||
if (step == Step.MOVEMENT) step = Step.MULTITOUCH_JUMP;
|
||||
else if (step == Step.MULTITOUCH_JUMP) step = Step.CORPSES_ARE_TOOLS;
|
||||
else complete = true;
|
||||
}
|
||||
|
||||
public void skip() {
|
||||
complete = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/** Versioned preference boundary for the optional first-run tutorial. */
|
||||
public final class OnboardingStore {
|
||||
private static final String FILE = "project_sacrifice_onboarding_v1";
|
||||
private static final String KEY_COMPLETE = "complete";
|
||||
private static final String KEY_SCHEMA = "schema";
|
||||
private static final int SCHEMA_VERSION = 1;
|
||||
|
||||
private final SharedPreferences preferences;
|
||||
|
||||
public OnboardingStore(Context context) {
|
||||
preferences = context.getSharedPreferences(FILE, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return preferences.getBoolean(KEY_COMPLETE, false);
|
||||
}
|
||||
|
||||
public void markComplete() {
|
||||
preferences.edit()
|
||||
.putInt(KEY_SCHEMA, SCHEMA_VERSION)
|
||||
.putBoolean(KEY_COMPLETE, true)
|
||||
.apply();
|
||||
}
|
||||
|
||||
public void resetForAutomation() {
|
||||
preferences.edit().clear().commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public final class CompletionResultTest {
|
||||
@Test
|
||||
public void resultKeepsThisRunAndSavedBestSeparateAndSuggestsTheMissingTarget() {
|
||||
LevelDefinition level = LevelCatalog.get(0);
|
||||
|
||||
CompletionResult result = CompletionResult.create(
|
||||
level,
|
||||
2,
|
||||
42_000L,
|
||||
0,
|
||||
25_000L,
|
||||
true
|
||||
);
|
||||
|
||||
assertEquals(2, result.currentDeaths());
|
||||
assertEquals(42_000L, result.currentTimeMs());
|
||||
assertEquals(0, result.bestDeaths());
|
||||
assertEquals(25_000L, result.bestTimeMs());
|
||||
assertEquals(CompletionResult.NextGoal.TARGET_DEATHS, result.nextGoal());
|
||||
assertEquals(0, result.nextGoalValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void masteredLevelPointsAtTheNextLevelOrPersonalBest() {
|
||||
LevelDefinition first = LevelCatalog.get(0);
|
||||
CompletionResult timeTarget = CompletionResult.create(first, 0, 31_000L,
|
||||
0, 25_000L, true);
|
||||
assertEquals(CompletionResult.NextGoal.TARGET_TIME, timeTarget.nextGoal());
|
||||
assertEquals(30_000L, timeTarget.nextGoalValue());
|
||||
|
||||
CompletionResult next = CompletionResult.create(first, 0, 25_000L,
|
||||
0, 25_000L, true);
|
||||
assertEquals(CompletionResult.NextGoal.NEXT_LEVEL, next.nextGoal());
|
||||
assertEquals(2, next.nextGoalValue());
|
||||
|
||||
LevelDefinition last = LevelCatalog.get(LevelCatalog.all().size() - 1);
|
||||
CompletionResult lastResult = CompletionResult.create(last,
|
||||
last.targetDeaths, last.targetSeconds * 900L,
|
||||
last.targetDeaths, last.targetSeconds * 900L, false);
|
||||
assertEquals(CompletionResult.NextGoal.PERSONAL_BEST, lastResult.nextGoal());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class OnboardingFlowTest {
|
||||
@Test
|
||||
public void firstRunExplainsControlsAndCorpsesThenStaysDismissed() {
|
||||
OnboardingFlow flow = OnboardingFlow.firstRun(false);
|
||||
|
||||
assertTrue(flow.isVisible());
|
||||
assertEquals(OnboardingFlow.Step.MOVEMENT, flow.step());
|
||||
|
||||
flow.next();
|
||||
assertEquals(OnboardingFlow.Step.MULTITOUCH_JUMP, flow.step());
|
||||
|
||||
flow.next();
|
||||
assertEquals(OnboardingFlow.Step.CORPSES_ARE_TOOLS, flow.step());
|
||||
|
||||
flow.next();
|
||||
assertFalse(flow.isVisible());
|
||||
assertTrue(flow.isComplete());
|
||||
assertFalse(OnboardingFlow.firstRun(flow.isComplete()).isVisible());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void firstRunCanBeSkippedImmediately() {
|
||||
OnboardingFlow flow = OnboardingFlow.firstRun(false);
|
||||
|
||||
flow.skip();
|
||||
|
||||
assertFalse(flow.isVisible());
|
||||
assertTrue(flow.isComplete());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user