Добавить production-каталог опасностей
This commit is contained in:
@@ -43,9 +43,12 @@ public final class GameView extends View implements Runnable {
|
||||
private static final float JUMP_BUFFER_SECONDS = PHYSICS.jumpBufferSeconds();
|
||||
private static final float RESPAWN_SECONDS = PHYSICS.respawnSeconds();
|
||||
private static final int MAX_CORPSES = PHYSICS.maximumBodies();
|
||||
private static final int LEVELS_PER_PAGE = 10;
|
||||
|
||||
private enum Screen { LEVELS, SETTINGS, PLAYING, COMPLETE }
|
||||
private enum DeathType { SPIKE, SAW, FREEZE, ELECTRIC, PIT }
|
||||
private enum DeathType {
|
||||
SPIKE, SAW, FREEZE, ELECTRIC, PIT, CRUSH, FIRE, PROJECTILE, EXPLOSION
|
||||
}
|
||||
|
||||
public static final class AutomationSnapshot {
|
||||
private final boolean completed;
|
||||
@@ -69,6 +72,9 @@ public final class GameView extends View implements Runnable {
|
||||
private final boolean playerOnGround;
|
||||
private final float playerVelocityX;
|
||||
private final float playerVelocityY;
|
||||
private final boolean firstHazardActive;
|
||||
private final float firstHazardX;
|
||||
private final float firstHazardY;
|
||||
|
||||
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
|
||||
boolean visiblyPaused, boolean inputActive, int levelNumber,
|
||||
@@ -137,6 +143,26 @@ public final class GameView extends View implements Runnable {
|
||||
boolean firstMovingPlatformMovingDown, float firstCorpseX,
|
||||
boolean playerOnGround, float playerVelocityX,
|
||||
float playerVelocityY) {
|
||||
this(completed, playerAlive, allDoorsOpen, visiblyPaused, inputActive, levelNumber,
|
||||
elapsedMilliseconds, playerX, playerY, deaths, corpseCount,
|
||||
firstMovingPlatformX, firstMovingPlatformY, playerOnMovingPlatform,
|
||||
firstMovingPlatformPowered, firstMovingPlatformMovingRight,
|
||||
firstMovingPlatformMovingDown, firstCorpseX, playerOnGround,
|
||||
playerVelocityX, playerVelocityY, false, Float.NaN, Float.NaN);
|
||||
}
|
||||
|
||||
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
|
||||
boolean visiblyPaused, boolean inputActive, int levelNumber,
|
||||
long elapsedMilliseconds,
|
||||
float playerX, float playerY, int deaths, int corpseCount,
|
||||
float firstMovingPlatformX, float firstMovingPlatformY,
|
||||
boolean playerOnMovingPlatform,
|
||||
boolean firstMovingPlatformPowered,
|
||||
boolean firstMovingPlatformMovingRight,
|
||||
boolean firstMovingPlatformMovingDown, float firstCorpseX,
|
||||
boolean playerOnGround, float playerVelocityX,
|
||||
float playerVelocityY, boolean firstHazardActive,
|
||||
float firstHazardX, float firstHazardY) {
|
||||
this.completed = completed;
|
||||
this.playerAlive = playerAlive;
|
||||
this.allDoorsOpen = allDoorsOpen;
|
||||
@@ -158,6 +184,9 @@ public final class GameView extends View implements Runnable {
|
||||
this.playerOnGround = playerOnGround;
|
||||
this.playerVelocityX = playerVelocityX;
|
||||
this.playerVelocityY = playerVelocityY;
|
||||
this.firstHazardActive = firstHazardActive;
|
||||
this.firstHazardX = firstHazardX;
|
||||
this.firstHazardY = firstHazardY;
|
||||
}
|
||||
|
||||
public boolean completed() { return completed; }
|
||||
@@ -181,6 +210,9 @@ public final class GameView extends View implements Runnable {
|
||||
public boolean playerOnGround() { return playerOnGround; }
|
||||
public float playerVelocityX() { return playerVelocityX; }
|
||||
public float playerVelocityY() { return playerVelocityY; }
|
||||
public boolean firstHazardActive() { return firstHazardActive; }
|
||||
public float firstHazardX() { return firstHazardX; }
|
||||
public float firstHazardY() { return firstHazardY; }
|
||||
}
|
||||
|
||||
private static final class Player {
|
||||
@@ -229,6 +261,7 @@ public final class GameView extends View implements Runnable {
|
||||
private final CorpseManager corpseManager = new CorpseManager(MAX_CORPSES);
|
||||
private final List<Body> corpses = corpseManager.bodies();
|
||||
private final ArrayList<PlatformState> platformStates = new ArrayList<>();
|
||||
private final ArrayList<HazardRuntime> hazardRuntimes = new ArrayList<>();
|
||||
private final Map<String, Boolean> previousSwitchContact = new HashMap<>();
|
||||
private final Map<LevelDefinition.Door, DoorController> doorControllers =
|
||||
new IdentityHashMap<>();
|
||||
@@ -240,6 +273,8 @@ public final class GameView extends View implements Runnable {
|
||||
private final CameraController cameraController =
|
||||
new CameraController(VIEW_WIDTH, VIEW_HEIGHT);
|
||||
private final PerformanceMonitor performanceMonitor = new PerformanceMonitor(240);
|
||||
private final LevelPage levelPage =
|
||||
new LevelPage(LevelCatalog.all().size(), LEVELS_PER_PAGE);
|
||||
|
||||
private Screen screen = Screen.LEVELS;
|
||||
private LevelDefinition level;
|
||||
@@ -394,12 +429,16 @@ public final class GameView extends View implements Runnable {
|
||||
deriveControls();
|
||||
corpseManager.reset();
|
||||
platformStates.clear();
|
||||
hazardRuntimes.clear();
|
||||
previousSwitchContact.clear();
|
||||
configurePuzzleSystem();
|
||||
completionResult = null;
|
||||
configureAccessibilitySignals();
|
||||
invalidateHudTextCache();
|
||||
for (LevelDefinition.Platform item : level.platforms) platformStates.add(new PlatformState(item));
|
||||
for (LevelDefinition.Hazard item : level.hazards) {
|
||||
hazardRuntimes.add(new HazardRuntime(item));
|
||||
}
|
||||
spawnPlayer();
|
||||
lastFrameNanos = System.nanoTime();
|
||||
refreshAccessibilityDescription(true);
|
||||
@@ -428,6 +467,7 @@ public final class GameView extends View implements Runnable {
|
||||
private void update(float dt) {
|
||||
levelElapsed += dt;
|
||||
audioEngine.updateMusic(levelElapsed, level.number);
|
||||
for (HazardRuntime runtime : hazardRuntimes) runtime.update(dt);
|
||||
updateSignals(dt);
|
||||
updatePlatforms(dt);
|
||||
|
||||
@@ -769,14 +809,22 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
|
||||
private void checkHazards() {
|
||||
for (LevelDefinition.Hazard hazard : level.hazards) {
|
||||
if (!intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT, hazard.box)) continue;
|
||||
for (int index = 0; index < level.hazards.size(); index++) {
|
||||
LevelDefinition.Hazard hazard = level.hazards.get(index);
|
||||
HazardRuntime runtime = hazardRuntimes.get(index);
|
||||
if (!runtime.active()) continue;
|
||||
if (!intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT,
|
||||
runtime.currentBox())) continue;
|
||||
switch (hazard.type) {
|
||||
case SPIKES: kill(DeathType.SPIKE, hazard); return;
|
||||
case SAW: kill(DeathType.SAW, hazard); return;
|
||||
case FREEZER: kill(DeathType.FREEZE, hazard); return;
|
||||
case ELECTRIC: kill(DeathType.ELECTRIC, hazard); return;
|
||||
case PIT: kill(DeathType.PIT, hazard); return;
|
||||
case CRUSHER: kill(DeathType.CRUSH, hazard); return;
|
||||
case FIRE: kill(DeathType.FIRE, hazard); return;
|
||||
case PROJECTILE: kill(DeathType.PROJECTILE, hazard); return;
|
||||
case EXPLOSION: kill(DeathType.EXPLOSION, hazard); return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -792,7 +840,7 @@ public final class GameView extends View implements Runnable {
|
||||
respawnTimer = RESPAWN_SECONDS;
|
||||
emitHaptic(Build.VERSION.SDK_INT >= 27
|
||||
? HapticFeedbackConstants.LONG_PRESS : HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
audioEngine.play(AudioCue.DEATH_IMPACT);
|
||||
audioEngine.play(hazardAudioCue(source));
|
||||
audioEngine.play(AudioCue.DEATH_VOICE);
|
||||
cameraController.impact(type == DeathType.PIT ? 12f : 24f);
|
||||
|
||||
@@ -824,6 +872,22 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private static AudioCue hazardAudioCue(LevelDefinition.Hazard source) {
|
||||
if (source != null && "crusher-impact".equals(source.audioCue)) {
|
||||
return AudioCue.CRUSHER_IMPACT;
|
||||
}
|
||||
if (source != null && "fire-burst".equals(source.audioCue)) {
|
||||
return AudioCue.FIRE_BURST;
|
||||
}
|
||||
if (source != null && "projectile-hit".equals(source.audioCue)) {
|
||||
return AudioCue.PROJECTILE_HIT;
|
||||
}
|
||||
if (source != null && "explosion-blast".equals(source.audioCue)) {
|
||||
return AudioCue.EXPLOSION_BLAST;
|
||||
}
|
||||
return AudioCue.DEATH_IMPACT;
|
||||
}
|
||||
|
||||
private void checkGoal() {
|
||||
if (!intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT, level.goal)) return;
|
||||
finishLevel(deaths, (long) (levelElapsed * 1000f));
|
||||
@@ -1041,8 +1105,9 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
|
||||
private void drawHazards(Canvas canvas) {
|
||||
for (LevelDefinition.Hazard hazard : level.hazards) {
|
||||
RectF box = rect(hazard.box);
|
||||
for (int index = 0; index < level.hazards.size(); index++) {
|
||||
LevelDefinition.Hazard hazard = level.hazards.get(index);
|
||||
RectF box = rect(hazardRuntimes.get(index).currentBox());
|
||||
switch (hazard.type) {
|
||||
case SPIKES:
|
||||
paint.setColor(Color.rgb(194, 201, 211));
|
||||
@@ -1088,10 +1153,88 @@ public final class GameView extends View implements Runnable {
|
||||
paint.setColor(Color.argb(80, 132, 87, 170));
|
||||
for (float x = box.left + 30; x < box.right; x += 80) canvas.drawOval(new RectF(x, box.top + 35, x + 30, box.top + 80), paint);
|
||||
break;
|
||||
case CRUSHER:
|
||||
drawCrusher(canvas, box, hazardRuntimes.get(index).active(),
|
||||
hazard.visualStyle);
|
||||
break;
|
||||
case FIRE:
|
||||
drawFire(canvas, box, hazard.visualStyle);
|
||||
break;
|
||||
case PROJECTILE:
|
||||
drawProjectile(canvas, box, hazard.visualStyle);
|
||||
break;
|
||||
case EXPLOSION:
|
||||
drawExplosion(canvas, box, hazardRuntimes.get(index).active(),
|
||||
hazard.visualStyle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawCrusher(Canvas canvas, RectF box, boolean active, String visualStyle) {
|
||||
boolean brass = "brass-press".equals(visualStyle);
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.BLACK : (brass ? Color.rgb(91, 66, 38) : Color.rgb(72, 76, 88)));
|
||||
canvas.drawRoundRect(box, 12f, 12f, paint);
|
||||
float jawHeight = active ? box.height() * 0.48f : box.height() * 0.18f;
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE : (brass ? Color.rgb(211, 157, 67) : Color.rgb(174, 180, 192)));
|
||||
canvas.drawRect(box.left + 8f, box.top + 8f,
|
||||
box.right - 8f, box.top + jawHeight, paint);
|
||||
canvas.drawRect(box.left + 8f, box.bottom - jawHeight,
|
||||
box.right - 8f, box.bottom - 8f, paint);
|
||||
drawWorldLabel(canvas, active ? "CRUSH" : "WAIT", box.centerX(), box.top - 20f);
|
||||
}
|
||||
|
||||
private void drawFire(Canvas canvas, RectF box, String visualStyle) {
|
||||
boolean blue = "blue-flame".equals(visualStyle);
|
||||
paint.setColor(Color.argb(110, blue ? 54 : 236, blue ? 182 : 90,
|
||||
blue ? 255 : 45));
|
||||
canvas.drawRoundRect(box, 16f, 16f, paint);
|
||||
float wave = accessibilitySettings.reducedMotionEnabled()
|
||||
? 0f : (float) Math.sin(levelElapsed * 7f) * 10f;
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE : (blue ? Color.rgb(116, 229, 255) : Color.rgb(255, 190, 64)));
|
||||
for (float x = box.left + 18f; x < box.right; x += 34f) {
|
||||
path.reset();
|
||||
path.moveTo(x - 14f, box.bottom - 6f);
|
||||
path.lineTo(x + wave * 0.25f, box.top + 12f);
|
||||
path.lineTo(x + 14f, box.bottom - 6f);
|
||||
path.close();
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawProjectile(Canvas canvas, RectF box, String visualStyle) {
|
||||
boolean clockwork = "clockwork-bolt".equals(visualStyle);
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE : (clockwork ? Color.rgb(233, 181, 76) : Color.rgb(204, 211, 221)));
|
||||
canvas.drawCircle(box.centerX(), box.centerY(), box.width() * 0.46f, paint);
|
||||
stroke.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.BLACK : Color.rgb(72, 56, 42));
|
||||
stroke.setStrokeWidth(5f);
|
||||
canvas.drawLine(box.left + 8f, box.centerY(), box.right - 8f, box.centerY(), stroke);
|
||||
canvas.drawLine(box.centerX(), box.top + 8f, box.centerX(), box.bottom - 8f, stroke);
|
||||
}
|
||||
|
||||
private void drawExplosion(Canvas canvas, RectF box, boolean active, String visualStyle) {
|
||||
boolean steam = "steam-blast".equals(visualStyle);
|
||||
float pulse = accessibilitySettings.reducedMotionEnabled()
|
||||
? 0f : (float) Math.sin(levelElapsed * 5f) * 8f;
|
||||
paint.setColor(Color.argb(active ? 95 : 28, steam ? 195 : 255, steam ? 220 : 124,
|
||||
steam ? 231 : 52));
|
||||
canvas.drawCircle(box.centerX(), box.centerY(),
|
||||
Math.max(12f, box.width() * (active ? 0.47f : 0.2f) + pulse), paint);
|
||||
stroke.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE : Color.rgb(255, 218, 118));
|
||||
stroke.setStrokeWidth(7f);
|
||||
canvas.drawLine(box.left + 12f, box.top + 12f,
|
||||
box.right - 12f, box.bottom - 12f, stroke);
|
||||
canvas.drawLine(box.right - 12f, box.top + 12f,
|
||||
box.left + 12f, box.bottom - 12f, stroke);
|
||||
drawWorldLabel(canvas, active ? "BLAST" : "REST", box.centerX(), box.top - 20f);
|
||||
}
|
||||
|
||||
|
||||
private void drawSaw(Canvas canvas, RectF box) {
|
||||
float angle = levelElapsed * 4.5f;
|
||||
@@ -1241,9 +1384,10 @@ public final class GameView extends View implements Runnable {
|
||||
float cardW = 310f;
|
||||
float cardH = 250f;
|
||||
float gapX = 65f;
|
||||
for (int i = 0; i < LevelCatalog.all().size(); i++) {
|
||||
int col = i % 5;
|
||||
int row = i / 5;
|
||||
for (int i = levelPage.firstIndex(); i < levelPage.endExclusive(); i++) {
|
||||
int localIndex = i - levelPage.firstIndex();
|
||||
int col = localIndex % 5;
|
||||
int row = localIndex / 5;
|
||||
float left = startX + col * (cardW + gapX);
|
||||
float top = 260f + row * 310f;
|
||||
RectF card = new RectF(left, top, left + cardW, top + cardH);
|
||||
@@ -1278,7 +1422,16 @@ public final class GameView extends View implements Runnable {
|
||||
canvas.restore();
|
||||
}
|
||||
drawHudButton(canvas, new RectF(1560, 70, 1850, 160), strings.settingsButton(), false);
|
||||
drawCenteredText(canvas, strings.levelSelectFooter(), VIEW_WIDTH * 0.5f, 980, 24,
|
||||
if (levelPage.hasPrevious()) {
|
||||
drawHudButton(canvas, new RectF(50, 920, 330, 1010), strings.previousPage(), false);
|
||||
}
|
||||
if (levelPage.hasNext()) {
|
||||
drawHudButton(canvas, new RectF(1590, 920, 1870, 1010), strings.nextPage(), false);
|
||||
}
|
||||
drawCenteredText(canvas, strings.pageIndicator(levelPage.pageIndex() + 1,
|
||||
levelPage.pageCount()), VIEW_WIDTH * 0.5f, 965, 25,
|
||||
Color.rgb(248, 212, 126));
|
||||
drawCenteredText(canvas, strings.levelSelectFooter(), VIEW_WIDTH * 0.5f, 1042, 22,
|
||||
Color.argb(190, 230, 230, 236));
|
||||
}
|
||||
|
||||
@@ -1651,13 +1804,26 @@ public final class GameView extends View implements Runnable {
|
||||
refreshAccessibilityDescription(true);
|
||||
return;
|
||||
}
|
||||
if (levelPage.hasPrevious() && contains(50, 920, 330, 1010, x, y)) {
|
||||
levelPage.previous();
|
||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
refreshAccessibilityDescription(true);
|
||||
return;
|
||||
}
|
||||
if (levelPage.hasNext() && contains(1590, 920, 1870, 1010, x, y)) {
|
||||
levelPage.next();
|
||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
refreshAccessibilityDescription(true);
|
||||
return;
|
||||
}
|
||||
float startX = 110f;
|
||||
float cardW = 310f;
|
||||
float cardH = 250f;
|
||||
float gapX = 65f;
|
||||
for (int i = 0; i < LevelCatalog.all().size(); i++) {
|
||||
int col = i % 5;
|
||||
int row = i / 5;
|
||||
for (int i = levelPage.firstIndex(); i < levelPage.endExclusive(); i++) {
|
||||
int localIndex = i - levelPage.firstIndex();
|
||||
int col = localIndex % 5;
|
||||
int row = localIndex / 5;
|
||||
float left = startX + col * (cardW + gapX);
|
||||
float top = 260f + row * 310f;
|
||||
if (contains(left, top, left + cardW, top + cardH, x, y) && i < progress.unlockedCount()) {
|
||||
@@ -1695,6 +1861,7 @@ public final class GameView extends View implements Runnable {
|
||||
|
||||
private void levelsFromResult() {
|
||||
if (screen != Screen.COMPLETE) return;
|
||||
levelPage.selectIndex(levelIndex);
|
||||
screen = Screen.LEVELS;
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
@@ -1906,6 +2073,24 @@ public final class GameView extends View implements Runnable {
|
||||
levelsFromResult();
|
||||
}
|
||||
|
||||
public int levelPageIndexForAutomation() {
|
||||
return levelPage.pageIndex();
|
||||
}
|
||||
|
||||
public int levelPageCountForAutomation() {
|
||||
return levelPage.pageCount();
|
||||
}
|
||||
|
||||
public void nextLevelPageForAutomation() {
|
||||
levelPage.next();
|
||||
refreshAccessibilityDescription(true);
|
||||
}
|
||||
|
||||
public void previousLevelPageForAutomation() {
|
||||
levelPage.previous();
|
||||
refreshAccessibilityDescription(true);
|
||||
}
|
||||
|
||||
public void replayFromResultForAutomation() {
|
||||
replayFromResult();
|
||||
}
|
||||
@@ -1936,7 +2121,7 @@ public final class GameView extends View implements Runnable {
|
||||
if (screen == Screen.LEVELS) {
|
||||
visible.add(strings.projectTitle());
|
||||
visible.add(strings.tagline());
|
||||
for (int i = 0; i < LevelCatalog.all().size(); i++) {
|
||||
for (int i = levelPage.firstIndex(); i < levelPage.endExclusive(); i++) {
|
||||
boolean enabled = i < progress.unlockedCount();
|
||||
visible.add(enabled ? String.format(Locale.US, "%02d", i + 1) : strings.locked());
|
||||
visible.add(strings.levelTitle(i + 1));
|
||||
@@ -1948,6 +2133,10 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
}
|
||||
visible.add(strings.settingsButton());
|
||||
if (levelPage.hasPrevious()) visible.add(strings.previousPage());
|
||||
if (levelPage.hasNext()) visible.add(strings.nextPage());
|
||||
visible.add(strings.pageIndicator(levelPage.pageIndex() + 1,
|
||||
levelPage.pageCount()));
|
||||
visible.add(strings.levelSelectFooter());
|
||||
} else if (screen == Screen.SETTINGS) {
|
||||
visible.add(strings.settingsTitle());
|
||||
@@ -2344,6 +2533,7 @@ public final class GameView extends View implements Runnable {
|
||||
|
||||
startLevel(index);
|
||||
levelElapsed = snapshot.elapsedSeconds();
|
||||
for (HazardRuntime runtime : hazardRuntimes) runtime.update(levelElapsed);
|
||||
deaths = snapshot.deaths();
|
||||
respawnTimer = snapshot.respawnSeconds();
|
||||
previousSwitchContact.clear();
|
||||
@@ -2436,6 +2626,16 @@ public final class GameView extends View implements Runnable {
|
||||
&& player.supportIndex >= 0
|
||||
&& player.supportIndex < platformStates.size()
|
||||
&& platformStates.get(player.supportIndex).definition.isMoving();
|
||||
boolean firstHazardActive = false;
|
||||
float firstHazardX = Float.NaN;
|
||||
float firstHazardY = Float.NaN;
|
||||
if (!hazardRuntimes.isEmpty()) {
|
||||
HazardRuntime firstHazard = hazardRuntimes.get(0);
|
||||
LevelDefinition.Box firstHazardBox = firstHazard.currentBox();
|
||||
firstHazardActive = firstHazard.active();
|
||||
firstHazardX = firstHazardBox.x;
|
||||
firstHazardY = firstHazardBox.y;
|
||||
}
|
||||
return new AutomationSnapshot(
|
||||
screen == Screen.COMPLETE,
|
||||
player.alive,
|
||||
@@ -2457,7 +2657,10 @@ public final class GameView extends View implements Runnable {
|
||||
corpses.isEmpty() ? Float.NaN : corpses.get(0).x,
|
||||
player.onGround,
|
||||
player.vx,
|
||||
player.vy
|
||||
player.vy,
|
||||
firstHazardActive,
|
||||
firstHazardX,
|
||||
firstHazardY
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user