Добавить постоянную локализацию RU и EN
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
/** Player-selected language, independent from the device locale. */
|
||||
public enum GameLanguage {
|
||||
EN("en"),
|
||||
RU("ru");
|
||||
|
||||
private final String code;
|
||||
|
||||
GameLanguage(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String code() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public GameLanguage next() {
|
||||
return this == EN ? RU : EN;
|
||||
}
|
||||
|
||||
public static GameLanguage fromCode(String code) {
|
||||
if (RU.code.equalsIgnoreCase(code)) return RU;
|
||||
return EN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/** Persists the explicit in-game language without mutating progress or controls. */
|
||||
final class GameLanguageStore {
|
||||
private static final String FILE = "project_sacrifice_language_v1";
|
||||
private static final String KEY_LANGUAGE = "language";
|
||||
private final SharedPreferences preferences;
|
||||
|
||||
GameLanguageStore(Context context) {
|
||||
preferences = context.getSharedPreferences(FILE, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
GameLanguage load() {
|
||||
return GameLanguage.fromCode(preferences.getString(KEY_LANGUAGE, GameLanguage.EN.code()));
|
||||
}
|
||||
|
||||
void save(GameLanguage language) {
|
||||
preferences.edit().putString(KEY_LANGUAGE,
|
||||
language == null ? GameLanguage.EN.code() : language.code()).apply();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
/** All player-facing copy behind one typed, Android-independent interface. */
|
||||
public final class GameStrings {
|
||||
private static final String[] EN_TITLES = {
|
||||
"The Volunteer", "A Useful End", "Dead Weight", "The Long Lift",
|
||||
"Express Delivery", "Cold Comfort", "Live Wire", "Borrowed Time",
|
||||
"Two Good Reasons", "The Last Procession"
|
||||
};
|
||||
private static final String[] RU_TITLES = {
|
||||
"Рыцарь-доброволец", "Полезный конец", "Мёртвый груз", "Долгий подъём",
|
||||
"Срочная доставка", "Холодное утешение", "Живой провод", "Время взаймы",
|
||||
"Две веские причины", "Последнее шествие"
|
||||
};
|
||||
private static final String[] EN_HINTS = {
|
||||
"Reach the relic. Arrows move; the winged button jumps.",
|
||||
"The spikes keep every volunteer. Build a path from what remains.",
|
||||
"Leave a body on the brass plate so the next volunteer can pass.",
|
||||
"Touch the lever, then ride the moving platform to the upper gallery.",
|
||||
"The saw throws to the right. Deliver a volunteer to the distant plate.",
|
||||
"The frost makes a solid block. Use the frozen volunteer as a step.",
|
||||
"A fallen volunteer conducts the current and keeps the relay alive.",
|
||||
"Jump for the far blue relay if the timed gate closes.",
|
||||
"The final gate needs weight and power at the same time.",
|
||||
"Launch, freeze, conduct, ascend. Every lesson opens the last relic."
|
||||
};
|
||||
private static final String[] RU_HINTS = {
|
||||
"Доберитесь до реликвии. Стрелки двигают; кнопка с крылом — прыжок.",
|
||||
"Шипы сохраняют каждого добровольца. Проложите путь из того, что осталось.",
|
||||
"Оставьте тело на латунной плите, чтобы следующий доброволец прошёл.",
|
||||
"Коснитесь рычага, затем поднимитесь на платформе к верхней галерее.",
|
||||
"Пила бросает вправо. Доставьте добровольца на дальнюю плиту.",
|
||||
"Мороз создаёт твёрдый блок. Используйте замёрзшего добровольца как ступень.",
|
||||
"Павший доброволец проводит ток и поддерживает реле.",
|
||||
"Прыгните к дальнему синему реле, если ворота по таймеру закроются.",
|
||||
"Последним воротам одновременно нужны вес и питание.",
|
||||
"Разгонитесь, замёрзните, проведите ток, поднимитесь. Каждый урок открывает последнюю реликвию."
|
||||
};
|
||||
|
||||
private final GameLanguage language;
|
||||
|
||||
private GameStrings(GameLanguage language) {
|
||||
this.language = language == null ? GameLanguage.EN : language;
|
||||
}
|
||||
|
||||
public static GameStrings forLanguage(GameLanguage language) {
|
||||
return new GameStrings(language);
|
||||
}
|
||||
|
||||
public GameLanguage language() {
|
||||
return language;
|
||||
}
|
||||
|
||||
private boolean russian() {
|
||||
return language == GameLanguage.RU;
|
||||
}
|
||||
|
||||
public String appContentDescription() {
|
||||
return russian()
|
||||
? "Project Sacrifice. Платформенная головоломка из десяти уровней."
|
||||
: "Project Sacrifice. Ten-level puzzle platform game.";
|
||||
}
|
||||
|
||||
public String projectTitle() { return "PROJECT SACRIFICE"; }
|
||||
public String tagline() {
|
||||
return russian() ? "Каждый конец оставляет путь вперёд"
|
||||
: "Every ending leaves a way forward";
|
||||
}
|
||||
public String locked() { return russian() ? "ЗАКРЫТО" : "LOCKED"; }
|
||||
public String tapToEnter() { return russian() ? "Нажмите, чтобы войти" : "Tap to enter"; }
|
||||
public String levelSelectFooter() {
|
||||
return russian()
|
||||
? "Альбомная ориентация · мультитач · прогресс сохраняется автоматически"
|
||||
: "Landscape · multitouch · progress saves automatically";
|
||||
}
|
||||
|
||||
public String settingsButton() {
|
||||
return russian() ? "Настройки" : "Settings";
|
||||
}
|
||||
|
||||
public String settingsTitle() { return russian() ? "НАСТРОЙКИ" : "SETTINGS"; }
|
||||
public String settingsInstruction() {
|
||||
return russian() ? "Нажмите на строку, чтобы изменить. Всё сохраняется автоматически."
|
||||
: "Tap a row to change it. Changes save automatically.";
|
||||
}
|
||||
public String controlSizeSetting() { return russian() ? "Размер управления" : "Control size"; }
|
||||
public String controlOpacitySetting() { return russian() ? "Прозрачность управления" : "Control opacity"; }
|
||||
public String vibrationSetting() { return russian() ? "Вибрация" : "Vibration"; }
|
||||
public String languageSetting() { return russian() ? "Язык" : "Language"; }
|
||||
public String languageName() { return russian() ? "Русский" : "English"; }
|
||||
public String controlSizeValue(int index) {
|
||||
String[] values = russian()
|
||||
? new String[]{"Маленький", "Средний", "Большой"}
|
||||
: new String[]{"Small", "Medium", "Large"};
|
||||
return values[validPreset(index)];
|
||||
}
|
||||
public String controlOpacityValue(int index) {
|
||||
String[] values = russian()
|
||||
? new String[]{"Низкая", "Средняя", "Высокая"}
|
||||
: new String[]{"Low", "Medium", "High"};
|
||||
return values[validPreset(index)];
|
||||
}
|
||||
public String hapticsValue(boolean enabled) {
|
||||
if (russian()) return enabled ? "Вкл." : "Выкл.";
|
||||
return enabled ? "On" : "Off";
|
||||
}
|
||||
public String back() { return russian() ? "Назад" : "Back"; }
|
||||
|
||||
public String hudStats(int deaths, long elapsedMilliseconds) {
|
||||
if (russian()) {
|
||||
return "Добровольцы " + deaths + " Время " + GameMath.formatTime(elapsedMilliseconds);
|
||||
}
|
||||
return "Volunteers " + deaths + " Time " + GameMath.formatTime(elapsedMilliseconds);
|
||||
}
|
||||
public String restart() { return russian() ? "Заново" : "Restart"; }
|
||||
public String respawning() {
|
||||
return russian() ? "Следующий доброволец уже в пути…"
|
||||
: "Another volunteer is on the way…";
|
||||
}
|
||||
|
||||
public String levelBestStats(int deaths, long elapsedMilliseconds) {
|
||||
return "✓ " + deaths + " " + volunteerWord(deaths) + " · "
|
||||
+ GameMath.formatTime(elapsedMilliseconds);
|
||||
}
|
||||
public String relicClaimed() { return russian() ? "РЕЛИКВИЯ ПОЛУЧЕНА" : "RELIC CLAIMED"; }
|
||||
public String completion() { return russian() ? "Завершение ★" : "Completion ★"; }
|
||||
public String completionDeaths(boolean earned, int deaths, int targetDeaths) {
|
||||
return (earned ? "★" : "☆") + " " + deaths + " / " + targetDeaths + " "
|
||||
+ volunteerWord(deaths);
|
||||
}
|
||||
public String completionTime(boolean earned, long elapsedMilliseconds, long targetMilliseconds) {
|
||||
return (earned ? "★" : "☆") + " " + GameMath.formatTime(elapsedMilliseconds)
|
||||
+ " / " + GameMath.formatTime(targetMilliseconds);
|
||||
}
|
||||
public String levelSelect() { return russian() ? "Выбор уровня" : "Level select"; }
|
||||
public String replay() { return russian() ? "Повторить" : "Replay"; }
|
||||
public String nextLevel() { return russian() ? "Следующий уровень" : "Next level"; }
|
||||
public String paused() { return russian() ? "ПАУЗА" : "PAUSED"; }
|
||||
public String resume() { return russian() ? "Продолжить" : "Resume"; }
|
||||
|
||||
// Result-flow hooks used by the Stage 3 result screen.
|
||||
public String currentResult() { return russian() ? "Текущий результат" : "Current result"; }
|
||||
public String bestResult() { return russian() ? "Лучший результат" : "Best result"; }
|
||||
public String deathsStat() { return russian() ? "Добровольцы" : "Volunteers"; }
|
||||
public String timeStat() { return russian() ? "Время" : "Time"; }
|
||||
public String newBest() { return russian() ? "НОВЫЙ РЕКОРД" : "NEW BEST"; }
|
||||
|
||||
public String currentResultLine(int deaths, long timeMs) {
|
||||
return currentResult() + " · " + deaths + " " + volunteerWord(deaths)
|
||||
+ " · " + GameMath.formatTime(timeMs);
|
||||
}
|
||||
|
||||
public String bestResultLine(int deaths, long timeMs) {
|
||||
return bestResult() + " · " + deaths + " " + volunteerWord(deaths)
|
||||
+ " · " + GameMath.formatTime(timeMs);
|
||||
}
|
||||
|
||||
public String targetsResultLine(int deaths, long timeMs) {
|
||||
String label = russian() ? "Цели" : "Targets";
|
||||
return label + " · " + deaths + " " + volunteerWord(deaths)
|
||||
+ " · " + GameMath.formatTime(timeMs);
|
||||
}
|
||||
|
||||
public String nextGoalDeaths(long deaths) {
|
||||
return russian()
|
||||
? "Следующая цель · пройти с " + deaths + " добровольцами"
|
||||
: "Next goal · finish with " + deaths + " volunteers";
|
||||
}
|
||||
|
||||
public String nextGoalTime(long timeMs) {
|
||||
return (russian() ? "Следующая цель · пройти за " : "Next goal · finish in ")
|
||||
+ GameMath.formatTime(timeMs);
|
||||
}
|
||||
|
||||
public String nextGoalLevel(long level) {
|
||||
return (russian() ? "Следующая цель · перейти на уровень "
|
||||
: "Next goal · enter level ") + level;
|
||||
}
|
||||
|
||||
public String nextGoalPersonalBest() {
|
||||
return russian() ? "Следующая цель · улучшить личный рекорд"
|
||||
: "Next goal · improve your personal best";
|
||||
}
|
||||
|
||||
// Onboarding hooks keep new tutorial Canvas copy out of GameView conditionals.
|
||||
public String tutorialWelcomeTitle() {
|
||||
return russian() ? "Смерть — это инструмент" : "Death is a tool";
|
||||
}
|
||||
public String tutorialWelcomeBody() {
|
||||
return russian()
|
||||
? "Двигайтесь стрелками и прыгайте кнопкой с крылом. Доберитесь до реликвии."
|
||||
: "Move with the arrows and jump with the winged button. Reach the relic.";
|
||||
}
|
||||
public String tutorialSacrificeTitle() {
|
||||
return russian() ? "Оставьте путь вперёд" : "Leave a way forward";
|
||||
}
|
||||
public String tutorialSacrificeBody() {
|
||||
return russian()
|
||||
? "После смерти тело остаётся в мире: закрывает шипы, держит плиты и становится ступенью."
|
||||
: "A body stays in the world after death: cover spikes, hold plates, and make a step.";
|
||||
}
|
||||
public String tutorialSkip() { return russian() ? "Пропустить" : "Skip"; }
|
||||
public String tutorialNext() { return russian() ? "Далее" : "Next"; }
|
||||
public String tutorialStart() { return russian() ? "Начать" : "Start"; }
|
||||
public String tutorialDone() { return russian() ? "Готово" : "Done"; }
|
||||
public String tutorialMovementTitle() { return russian() ? "ДВИЖЕНИЕ" : "MOVE"; }
|
||||
public String tutorialMovementBody() {
|
||||
return russian() ? "Удерживайте левую или правую стрелку, чтобы двигаться."
|
||||
: "Hold the left or right arrow to explore.";
|
||||
}
|
||||
public String tutorialJumpTitle() { return russian() ? "ДВИЖЕНИЕ + ПРЫЖОК" : "MOVE + JUMP"; }
|
||||
public String tutorialJumpBody() {
|
||||
return russian() ? "Используйте два пальца: держите направление и нажмите Прыжок."
|
||||
: "Use two fingers: hold a direction and tap Jump.";
|
||||
}
|
||||
public String tutorialCorpseTitle() {
|
||||
return russian() ? "СМЕРТЬ ОСТАВЛЯЕТ ИНСТРУМЕНТ" : "DEATH LEAVES A TOOL";
|
||||
}
|
||||
public String tutorialCorpseBody() {
|
||||
return russian() ? "Павший доброволец остаётся. Используйте тело, чтобы открыть путь."
|
||||
: "A fallen volunteer remains. Use the body to open a path.";
|
||||
}
|
||||
public String tutorialProgress(int step, int total) {
|
||||
return step + " / " + total;
|
||||
}
|
||||
|
||||
public String levelTitle(int oneBasedLevelNumber) {
|
||||
return (russian() ? RU_TITLES : EN_TITLES)[levelIndex(oneBasedLevelNumber)];
|
||||
}
|
||||
|
||||
public String levelHint(int oneBasedLevelNumber) {
|
||||
return (russian() ? RU_HINTS : EN_HINTS)[levelIndex(oneBasedLevelNumber)];
|
||||
}
|
||||
|
||||
private String volunteerWord(int count) {
|
||||
if (!russian()) return count == 1 ? "volunteer" : "volunteers";
|
||||
int mod100 = Math.abs(count) % 100;
|
||||
int mod10 = Math.abs(count) % 10;
|
||||
if (mod100 >= 11 && mod100 <= 14) return "добровольцев";
|
||||
if (mod10 == 1) return "доброволец";
|
||||
if (mod10 >= 2 && mod10 <= 4) return "добровольца";
|
||||
return "добровольцев";
|
||||
}
|
||||
|
||||
private static int validPreset(int index) {
|
||||
return index >= 0 && index <= 2 ? index : 1;
|
||||
}
|
||||
|
||||
private static int levelIndex(int oneBasedLevelNumber) {
|
||||
int index = oneBasedLevelNumber - 1;
|
||||
return index >= 0 && index < EN_TITLES.length ? index : 0;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -162,6 +163,7 @@ public final class GameView extends View implements Runnable {
|
||||
private final ProgressStore progress;
|
||||
private final ControlSettingsStore controlSettingsStore;
|
||||
private final OnboardingStore onboardingStore;
|
||||
private final GameLanguageStore languageStore;
|
||||
private final SparseArray<PointF> pointers = new SparseArray<>();
|
||||
private final Player player = new Player();
|
||||
private final ArrayList<Corpse> corpses = new ArrayList<>();
|
||||
@@ -190,6 +192,8 @@ public final class GameView extends View implements Runnable {
|
||||
private ControlSettings controlSettings;
|
||||
private OnboardingFlow onboardingFlow;
|
||||
private CompletionResult completionResult;
|
||||
private GameLanguage language;
|
||||
private GameStrings strings;
|
||||
|
||||
public GameView(Context context) {
|
||||
super(context);
|
||||
@@ -198,12 +202,15 @@ public final class GameView extends View implements Runnable {
|
||||
controlSettings = controlSettingsStore.load();
|
||||
onboardingStore = new OnboardingStore(context);
|
||||
onboardingFlow = OnboardingFlow.firstRun(onboardingStore.isComplete());
|
||||
languageStore = new GameLanguageStore(context);
|
||||
language = languageStore.load();
|
||||
strings = GameStrings.forLanguage(language);
|
||||
paint.setTypeface(android.graphics.Typeface.create("sans", android.graphics.Typeface.NORMAL));
|
||||
stroke.setStyle(Paint.Style.STROKE);
|
||||
stroke.setStrokeCap(Paint.Cap.ROUND);
|
||||
setFocusable(true);
|
||||
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
setContentDescription("Project Sacrifice. Ten-level puzzle platform game.");
|
||||
setContentDescription(strings.appContentDescription());
|
||||
running = true;
|
||||
postOnAnimation(this);
|
||||
}
|
||||
@@ -942,11 +949,15 @@ public final class GameView extends View implements Runnable {
|
||||
private void drawHud(Canvas canvas) {
|
||||
paint.setColor(Color.argb(178, 15, 20, 34));
|
||||
canvas.drawRoundRect(new RectF(28, 26, 940, 138), 24, 24, paint);
|
||||
drawText(canvas, String.format(Locale.US, "%02d %s", level.number, level.title), 56, 72, 34, Color.rgb(248, 220, 151), false);
|
||||
drawText(canvas, level.hint, 56, 116, 25, Color.rgb(231, 231, 235), false);
|
||||
drawText(canvas, "Volunteers " + deaths + " Time " + GameMath.formatTime((long) (levelElapsed * 1000)), 1010, 82, 31, Color.WHITE, false);
|
||||
drawText(canvas, String.format(Locale.US, "%02d %s", level.number,
|
||||
strings.levelTitle(level.number)), 56, 72, 34,
|
||||
Color.rgb(248, 220, 151), false);
|
||||
drawFittedText(canvas, strings.levelHint(level.number), 56, 116, 25, 16,
|
||||
850, Color.rgb(231, 231, 235), false);
|
||||
drawText(canvas, strings.hudStats(deaths, (long) (levelElapsed * 1000)),
|
||||
1010, 82, 31, Color.WHITE, false);
|
||||
|
||||
drawHudButton(canvas, new RectF(1608, 26, 1770, 112), "Restart", false);
|
||||
drawHudButton(canvas, new RectF(1608, 26, 1770, 112), strings.restart(), false);
|
||||
drawHudButton(canvas, new RectF(1790, 26, 1892, 112), "II", false);
|
||||
|
||||
float controlScale = controlSettings.controlScale();
|
||||
@@ -954,7 +965,8 @@ public final class GameView extends View implements Runnable {
|
||||
drawControlCircle(canvas, 365, 925, 92 * controlScale, "›", rightPressed);
|
||||
drawControlCircle(canvas, 1760, 900, 116 * controlScale, "▲", jumpPressed);
|
||||
if (!player.alive) {
|
||||
drawCenteredText(canvas, "Another volunteer is on the way…", VIEW_WIDTH * 0.5f, 235, 38, Color.rgb(250, 222, 153));
|
||||
drawCenteredText(canvas, strings.respawning(), VIEW_WIDTH * 0.5f, 235, 38,
|
||||
Color.rgb(250, 222, 153));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,8 +986,10 @@ public final class GameView extends View implements Runnable {
|
||||
|
||||
private void drawLevelSelect(Canvas canvas) {
|
||||
drawSky(canvas);
|
||||
drawCenteredText(canvas, "PROJECT SACRIFICE", VIEW_WIDTH * 0.5f, 120, 64, Color.rgb(248, 212, 126));
|
||||
drawCenteredText(canvas, "Every ending leaves a way forward", VIEW_WIDTH * 0.5f, 177, 29, Color.rgb(225, 226, 233));
|
||||
drawCenteredText(canvas, strings.projectTitle(), VIEW_WIDTH * 0.5f, 120, 64,
|
||||
Color.rgb(248, 212, 126));
|
||||
drawCenteredText(canvas, strings.tagline(), VIEW_WIDTH * 0.5f, 177, 29,
|
||||
Color.rgb(225, 226, 233));
|
||||
int unlocked = progress.unlockedCount();
|
||||
float startX = 110f;
|
||||
float cardW = 310f;
|
||||
@@ -994,35 +1008,43 @@ public final class GameView extends View implements Runnable {
|
||||
stroke.setColor(complete ? Color.rgb(109, 222, 143) : (enabled ? Color.rgb(205, 164, 83) : Color.rgb(73, 77, 91)));
|
||||
stroke.setStrokeWidth(5f);
|
||||
canvas.drawRoundRect(card, 22, 22, stroke);
|
||||
drawCenteredText(canvas, enabled ? String.format(Locale.US, "%02d", i + 1) : "LOCKED", card.centerX(), top + 72, enabled ? 48 : 27,
|
||||
drawCenteredText(canvas, enabled ? String.format(Locale.US, "%02d", i + 1) : strings.locked(), card.centerX(), top + 72, enabled ? 48 : 27,
|
||||
enabled ? Color.rgb(248, 210, 118) : Color.rgb(120, 123, 135));
|
||||
LevelDefinition item = LevelCatalog.get(i);
|
||||
drawCenteredText(canvas, item.title, card.centerX(), top + 124, 28, enabled ? Color.WHITE : Color.rgb(111, 114, 125));
|
||||
drawCenteredFittedText(canvas, strings.levelTitle(item.number), card.centerX(),
|
||||
top + 124, 28, 20, cardW - 28,
|
||||
enabled ? Color.WHITE : Color.rgb(111, 114, 125));
|
||||
if (complete) {
|
||||
int bestDeaths = progress.bestDeaths(i + 1);
|
||||
long bestTime = progress.bestTimeMs(i + 1);
|
||||
drawCenteredText(canvas, "✓ " + bestDeaths + " volunteers · " + GameMath.formatTime(bestTime), card.centerX(), top + 185, 21, Color.rgb(138, 231, 163));
|
||||
drawCenteredFittedText(canvas, strings.levelBestStats(bestDeaths, bestTime),
|
||||
card.centerX(), top + 185, 21, 17, cardW - 24,
|
||||
Color.rgb(138, 231, 163));
|
||||
} else if (enabled) {
|
||||
drawCenteredText(canvas, "Tap to enter", card.centerX(), top + 190, 22, Color.rgb(202, 204, 214));
|
||||
drawCenteredText(canvas, strings.tapToEnter(), card.centerX(), top + 190,
|
||||
22, Color.rgb(202, 204, 214));
|
||||
}
|
||||
}
|
||||
drawHudButton(canvas, new RectF(1560, 70, 1850, 160), "Settings", false);
|
||||
drawCenteredText(canvas, "Landscape · multitouch · progress saves automatically", VIEW_WIDTH * 0.5f, 980, 24, Color.argb(190, 230, 230, 236));
|
||||
drawHudButton(canvas, new RectF(1560, 70, 1850, 160), strings.settingsButton(), false);
|
||||
drawCenteredText(canvas, strings.levelSelectFooter(), VIEW_WIDTH * 0.5f, 980, 24,
|
||||
Color.argb(190, 230, 230, 236));
|
||||
}
|
||||
|
||||
private void drawSettings(Canvas canvas) {
|
||||
drawSky(canvas);
|
||||
drawCenteredText(canvas, "SETTINGS", VIEW_WIDTH * 0.5f, 145, 64,
|
||||
drawCenteredText(canvas, strings.settingsTitle(), VIEW_WIDTH * 0.5f, 145, 64,
|
||||
Color.rgb(248, 212, 126));
|
||||
drawCenteredText(canvas, "Tap a row to change it. Changes save automatically.",
|
||||
drawCenteredText(canvas, strings.settingsInstruction(),
|
||||
VIEW_WIDTH * 0.5f, 205, 27, Color.rgb(225, 226, 233));
|
||||
drawSettingRow(canvas, new RectF(420, 280, 1500, 410), "Control size",
|
||||
controlSettings.controlSizeLabel());
|
||||
drawSettingRow(canvas, new RectF(420, 450, 1500, 580), "Control opacity",
|
||||
controlSettings.controlOpacityLabel());
|
||||
drawSettingRow(canvas, new RectF(420, 620, 1500, 750), "Vibration",
|
||||
controlSettings.hapticsLabel());
|
||||
drawHudButton(canvas, new RectF(710, 850, 1210, 960), "Back", true);
|
||||
drawText(canvas, strings.languageSetting(), 1_370f, 126f, 27f, Color.WHITE, true);
|
||||
drawHudButton(canvas, new RectF(1580, 70, 1850, 160), strings.languageName(), false);
|
||||
drawSettingRow(canvas, new RectF(420, 280, 1500, 410), strings.controlSizeSetting(),
|
||||
strings.controlSizeValue(controlSettings.controlSizeIndex()));
|
||||
drawSettingRow(canvas, new RectF(420, 450, 1500, 580), strings.controlOpacitySetting(),
|
||||
strings.controlOpacityValue(controlSettings.controlOpacityIndex()));
|
||||
drawSettingRow(canvas, new RectF(420, 620, 1500, 750), strings.vibrationSetting(),
|
||||
strings.hapticsValue(controlSettings.hapticsEnabled()));
|
||||
drawHudButton(canvas, new RectF(710, 850, 1210, 960), strings.back(), true);
|
||||
}
|
||||
|
||||
private void drawSettingRow(Canvas canvas, RectF box, String label, String value) {
|
||||
@@ -1039,43 +1061,44 @@ 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, 205, 64, Color.rgb(250, 211, 111));
|
||||
drawCenteredText(canvas, level.title, VIEW_WIDTH * 0.5f, 263, 34, Color.WHITE);
|
||||
drawCenteredText(canvas, strings.relicClaimed(), VIEW_WIDTH * 0.5f, 205, 64,
|
||||
Color.rgb(250, 211, 111));
|
||||
drawCenteredText(canvas, strings.levelTitle(level.number), VIEW_WIDTH * 0.5f, 263,
|
||||
34, Color.WHITE);
|
||||
|
||||
CompletionResult result = completionResult;
|
||||
if (result != null) {
|
||||
drawCenteredText(canvas,
|
||||
"This run · " + result.currentDeaths() + " volunteers · "
|
||||
+ GameMath.formatTime(result.currentTimeMs()),
|
||||
drawCenteredText(canvas, strings.currentResultLine(result.currentDeaths(),
|
||||
result.currentTimeMs()),
|
||||
VIEW_WIDTH * 0.5f, 380, 34, Color.WHITE);
|
||||
drawCenteredText(canvas,
|
||||
"Saved best · " + result.bestDeaths() + " volunteers · "
|
||||
+ GameMath.formatTime(result.bestTimeMs()),
|
||||
drawCenteredText(canvas, strings.bestResultLine(result.bestDeaths(),
|
||||
result.bestTimeMs()),
|
||||
VIEW_WIDTH * 0.5f, 437, 31, Color.rgb(128, 229, 153));
|
||||
drawCenteredText(canvas,
|
||||
"Targets · " + result.targetDeaths() + " volunteers · "
|
||||
+ GameMath.formatTime(result.targetTimeMs()),
|
||||
drawCenteredText(canvas, strings.targetsResultLine(result.targetDeaths(),
|
||||
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);
|
||||
drawHudButton(canvas, new RectF(345, 735, 755, 850), strings.levelSelect(), false);
|
||||
drawHudButton(canvas, new RectF(775, 735, 1145, 850), strings.replay(), false);
|
||||
if (levelIndex + 1 < LevelCatalog.all().size()) {
|
||||
drawHudButton(canvas, new RectF(1165, 735, 1575, 850), strings.nextLevel(), true);
|
||||
}
|
||||
}
|
||||
|
||||
private String nextGoalText(CompletionResult result) {
|
||||
switch (result.nextGoal()) {
|
||||
case TARGET_DEATHS:
|
||||
return "Next goal · finish with " + result.nextGoalValue() + " volunteers";
|
||||
return strings.nextGoalDeaths(result.nextGoalValue());
|
||||
case TARGET_TIME:
|
||||
return "Next goal · finish in " + GameMath.formatTime(result.nextGoalValue());
|
||||
return strings.nextGoalTime(result.nextGoalValue());
|
||||
case NEXT_LEVEL:
|
||||
return "Next goal · enter level " + result.nextGoalValue();
|
||||
return strings.nextGoalLevel(result.nextGoalValue());
|
||||
case PERSONAL_BEST:
|
||||
default:
|
||||
return "Next goal · improve your personal best";
|
||||
return strings.nextGoalPersonalBest();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1092,37 +1115,64 @@ public final class GameView extends View implements Runnable {
|
||||
String progressLabel;
|
||||
switch (onboardingFlow.step()) {
|
||||
case MOVEMENT:
|
||||
title = "MOVE";
|
||||
message = "Hold the left or right arrow to explore.";
|
||||
progressLabel = "1 / 3";
|
||||
title = strings.tutorialMovementTitle();
|
||||
message = strings.tutorialMovementBody();
|
||||
progressLabel = strings.tutorialProgress(1, 3);
|
||||
break;
|
||||
case MULTITOUCH_JUMP:
|
||||
title = "MOVE + JUMP";
|
||||
message = "Use two fingers: hold a direction and tap Jump.";
|
||||
progressLabel = "2 / 3";
|
||||
title = strings.tutorialJumpTitle();
|
||||
message = strings.tutorialJumpBody();
|
||||
progressLabel = strings.tutorialProgress(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";
|
||||
title = strings.tutorialCorpseTitle();
|
||||
message = strings.tutorialCorpseBody();
|
||||
progressLabel = strings.tutorialProgress(3, 3);
|
||||
break;
|
||||
}
|
||||
drawText(canvas, title, 575, 230, 30, Color.rgb(250, 211, 111), true);
|
||||
drawText(canvas, message, 575, 278, 27, Color.WHITE, false);
|
||||
drawFittedText(canvas, title, 575, 230, 30, 21, 740,
|
||||
Color.rgb(250, 211, 111), true);
|
||||
drawFittedText(canvas, message, 575, 278, 27, 18, 740, 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(965, 298, 1125, 355), strings.tutorialSkip(), false);
|
||||
drawHudButton(canvas, new RectF(1145, 298, 1345, 355),
|
||||
onboardingFlow.step() == OnboardingFlow.Step.CORPSES_ARE_TOOLS ? "Done" : "Next",
|
||||
onboardingFlow.step() == OnboardingFlow.Step.CORPSES_ARE_TOOLS
|
||||
? strings.tutorialDone() : strings.tutorialNext(),
|
||||
true);
|
||||
}
|
||||
|
||||
private String onboardingTitle() {
|
||||
switch (onboardingFlow.step()) {
|
||||
case MOVEMENT:
|
||||
return strings.tutorialMovementTitle();
|
||||
case MULTITOUCH_JUMP:
|
||||
return strings.tutorialJumpTitle();
|
||||
case CORPSES_ARE_TOOLS:
|
||||
default:
|
||||
return strings.tutorialCorpseTitle();
|
||||
}
|
||||
}
|
||||
|
||||
private String onboardingBody() {
|
||||
switch (onboardingFlow.step()) {
|
||||
case MOVEMENT:
|
||||
return strings.tutorialMovementBody();
|
||||
case MULTITOUCH_JUMP:
|
||||
return strings.tutorialJumpBody();
|
||||
case CORPSES_ARE_TOOLS:
|
||||
default:
|
||||
return strings.tutorialCorpseBody();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawPause(Canvas canvas) {
|
||||
paint.setColor(Color.argb(218, 8, 12, 23));
|
||||
canvas.drawRect(0, 0, VIEW_WIDTH, VIEW_HEIGHT, paint);
|
||||
drawCenteredText(canvas, "PAUSED", VIEW_WIDTH * 0.5f, 350, 68, Color.rgb(248, 212, 126));
|
||||
drawHudButton(canvas, new RectF(610, 485, 1310, 610), "Resume", true);
|
||||
drawHudButton(canvas, new RectF(610, 650, 1310, 775), "Level select", false);
|
||||
drawCenteredText(canvas, strings.paused(), VIEW_WIDTH * 0.5f, 350, 68,
|
||||
Color.rgb(248, 212, 126));
|
||||
drawHudButton(canvas, new RectF(610, 485, 1310, 610), strings.resume(), true);
|
||||
drawHudButton(canvas, new RectF(610, 650, 1310, 775), strings.levelSelect(), false);
|
||||
}
|
||||
|
||||
private void drawHudButton(Canvas canvas, RectF box, String label, boolean primary) {
|
||||
@@ -1148,6 +1198,30 @@ public final class GameView extends View implements Runnable {
|
||||
canvas.drawText(text, x, baseline, paint);
|
||||
}
|
||||
|
||||
private void drawFittedText(Canvas canvas, String text, float x, float baseline,
|
||||
float preferredSize, float minimumSize, float maximumWidth,
|
||||
int color, boolean bold) {
|
||||
float size = fittedTextSize(text, preferredSize, minimumSize, maximumWidth, bold);
|
||||
drawText(canvas, text, x, baseline, size, color, bold);
|
||||
}
|
||||
|
||||
private void drawCenteredFittedText(Canvas canvas, String text, float x, float baseline,
|
||||
float preferredSize, float minimumSize,
|
||||
float maximumWidth, int color) {
|
||||
float size = fittedTextSize(text, preferredSize, minimumSize, maximumWidth, true);
|
||||
drawCenteredText(canvas, text, x, baseline, size, color);
|
||||
}
|
||||
|
||||
private float fittedTextSize(String text, float preferredSize, float minimumSize,
|
||||
float maximumWidth, boolean bold) {
|
||||
paint.setTextSize(preferredSize);
|
||||
paint.setTypeface(android.graphics.Typeface.create("sans",
|
||||
bold ? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL));
|
||||
float measured = paint.measureText(text);
|
||||
if (measured <= maximumWidth || measured <= 0f) return preferredSize;
|
||||
return Math.max(minimumSize, preferredSize * maximumWidth / measured);
|
||||
}
|
||||
|
||||
private void drawCenteredText(Canvas canvas, String text, float x, float baseline, float size, int color) {
|
||||
paint.setShader(null);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
@@ -1331,7 +1405,10 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
|
||||
private void handleSettingsTap(float x, float y) {
|
||||
if (contains(420, 280, 1500, 410, x, y)) {
|
||||
if (contains(1580, 70, 1850, 160, x, y)) {
|
||||
updateLanguage(language.next());
|
||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
} else if (contains(420, 280, 1500, 410, x, y)) {
|
||||
updateControlSettings(controlSettings.nextControlSize());
|
||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
} else if (contains(420, 450, 1500, 580, x, y)) {
|
||||
@@ -1458,6 +1535,98 @@ public final class GameView extends View implements Runnable {
|
||||
nextLevelFromResult();
|
||||
}
|
||||
|
||||
public GameLanguage languageForAutomation() {
|
||||
return language;
|
||||
}
|
||||
|
||||
public void resetLanguageForAutomation() {
|
||||
updateLanguage(GameLanguage.EN);
|
||||
}
|
||||
|
||||
public void selectNextLanguageForAutomation() {
|
||||
updateLanguage(language.next());
|
||||
}
|
||||
|
||||
public void setLanguageForAutomation(GameLanguage updated) {
|
||||
updateLanguage(updated);
|
||||
}
|
||||
|
||||
/** Exact localized copy sources that the current Canvas state renders. */
|
||||
public List<String> visibleLocalizedTextForAutomation() {
|
||||
ArrayList<String> visible = new ArrayList<>();
|
||||
if (screen == Screen.LEVELS) {
|
||||
visible.add(strings.projectTitle());
|
||||
visible.add(strings.tagline());
|
||||
for (int i = 0; i < LevelCatalog.all().size(); i++) {
|
||||
boolean enabled = i < progress.unlockedCount();
|
||||
visible.add(enabled ? String.format(Locale.US, "%02d", i + 1) : strings.locked());
|
||||
visible.add(strings.levelTitle(i + 1));
|
||||
if (progress.isCompleted(i + 1)) {
|
||||
visible.add(strings.levelBestStats(progress.bestDeaths(i + 1),
|
||||
progress.bestTimeMs(i + 1)));
|
||||
} else if (enabled) {
|
||||
visible.add(strings.tapToEnter());
|
||||
}
|
||||
}
|
||||
visible.add(strings.settingsButton());
|
||||
visible.add(strings.levelSelectFooter());
|
||||
} else if (screen == Screen.SETTINGS) {
|
||||
visible.add(strings.settingsTitle());
|
||||
visible.add(strings.settingsInstruction());
|
||||
visible.add(strings.languageSetting());
|
||||
visible.add(strings.languageName());
|
||||
visible.add(strings.controlSizeSetting());
|
||||
visible.add(strings.controlSizeValue(controlSettings.controlSizeIndex()));
|
||||
visible.add(strings.controlOpacitySetting());
|
||||
visible.add(strings.controlOpacityValue(controlSettings.controlOpacityIndex()));
|
||||
visible.add(strings.vibrationSetting());
|
||||
visible.add(strings.hapticsValue(controlSettings.hapticsEnabled()));
|
||||
visible.add(strings.back());
|
||||
} else if (level != null) {
|
||||
visible.add(String.format(Locale.US, "%02d %s", level.number,
|
||||
strings.levelTitle(level.number)));
|
||||
visible.add(strings.levelHint(level.number));
|
||||
visible.add(strings.hudStats(deaths, (long) (levelElapsed * 1000)));
|
||||
visible.add(strings.restart());
|
||||
if (!player.alive) visible.add(strings.respawning());
|
||||
if (screen == Screen.COMPLETE) {
|
||||
visible.add(strings.relicClaimed());
|
||||
visible.add(strings.levelTitle(level.number));
|
||||
if (completionResult != null) {
|
||||
visible.add(strings.currentResultLine(completionResult.currentDeaths(),
|
||||
completionResult.currentTimeMs()));
|
||||
visible.add(strings.bestResultLine(completionResult.bestDeaths(),
|
||||
completionResult.bestTimeMs()));
|
||||
visible.add(strings.targetsResultLine(completionResult.targetDeaths(),
|
||||
completionResult.targetTimeMs()));
|
||||
visible.add(nextGoalText(completionResult));
|
||||
}
|
||||
visible.add(strings.levelSelect());
|
||||
visible.add(strings.replay());
|
||||
if (levelIndex + 1 < LevelCatalog.all().size()) visible.add(strings.nextLevel());
|
||||
} else if (userPaused) {
|
||||
visible.add(strings.paused());
|
||||
visible.add(strings.resume());
|
||||
visible.add(strings.levelSelect());
|
||||
} else if (onboardingFlow.isVisible()) {
|
||||
visible.add(onboardingTitle());
|
||||
visible.add(onboardingBody());
|
||||
visible.add(strings.tutorialSkip());
|
||||
visible.add(onboardingFlow.step() == OnboardingFlow.Step.CORPSES_ARE_TOOLS
|
||||
? strings.tutorialDone() : strings.tutorialNext());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableList(visible);
|
||||
}
|
||||
|
||||
private void updateLanguage(GameLanguage updated) {
|
||||
language = updated == null ? GameLanguage.EN : updated;
|
||||
strings = GameStrings.forLanguage(language);
|
||||
languageStore.save(language);
|
||||
setContentDescription(strings.appContentDescription());
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void updateControlSettings(ControlSettings updated) {
|
||||
controlSettings = updated;
|
||||
controlSettingsStore.save(updated);
|
||||
|
||||
Reference in New Issue
Block a user