Добавить обучение и полный экран результата

This commit is contained in:
2026-08-18 16:21:25 +03:00
parent 44067abde3
commit 00323d7916
9 changed files with 604 additions and 14 deletions
@@ -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);