Добавить production-каталог опасностей

This commit is contained in:
2026-08-19 05:30:56 +03:00
parent cc7f487cf7
commit dc2ebcbf8c
33 changed files with 1160 additions and 37 deletions
@@ -45,6 +45,15 @@ public final class DeterministicInputTapeTest {
ReplayStateSignature.of(puzzle, false, false));
}
@Test
public void productionTeachingLevelsCanOwnVersionedInputTapes() {
DeterministicInputTape tape = DeterministicInputTape.builder(14)
.record(false, true, true, 250L)
.build();
assertEquals(14, tape.levelNumber());
}
private static final class FakeTarget implements DeterministicInputTape.Target {
private int level;
private long ticks;
@@ -27,13 +27,16 @@ public final class GameStringsTest {
assertEquals("Русский", russian.languageName());
assertEquals("Рыцарь-доброволец", russian.levelTitle(1));
assertEquals("Последнее шествие", russian.levelTitle(10));
assertEquals("Паровой заряд", russian.levelTitle(14));
assertEquals("Page 2 of 2", english.pageIndicator(2, 2));
assertEquals("Страница 2 из 2", russian.pageIndicator(2, 2));
assertEquals("Добровольцы 2 Время 00:07", russian.hudStats(2, 7_000L));
assertEquals("✓ 2 добровольца · 00:07", russian.levelBestStats(2, 7_000L));
assertEquals("★ 2 / 3 добровольца", russian.completionDeaths(true, 2, 3));
assertEquals("Смерть — это инструмент", russian.tutorialWelcomeTitle());
assertFalse(russian.tutorialSacrificeBody().trim().isEmpty());
for (int level = 1; level <= 10; level++) {
for (int level = 1; level <= LevelCatalog.all().size(); level++) {
assertFalse(russian.levelTitle(level).trim().isEmpty());
assertFalse(russian.levelHint(level).trim().isEmpty());
assertNotEquals(english.levelTitle(level), russian.levelTitle(level));
@@ -0,0 +1,59 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public final class HazardRuntimeTest {
@Test
public void crusherActiveWindowRepeatsOnItsDataDrivenCycle() {
LevelDefinition.Hazard definition = new LevelDefinition.Hazard(
LevelDefinition.HazardType.CRUSHER,
new LevelDefinition.Box(620f, 690f, 150f, 190f),
null, 0f, 180f, 4f, 1.25f, 0f, 0f,
"brass-press", "crusher-impact");
HazardRuntime runtime = new HazardRuntime(definition);
assertTrue(runtime.active());
runtime.update(1.2f);
assertTrue(runtime.active());
runtime.update(0.1f);
assertFalse(runtime.active());
runtime.update(2.7f);
assertTrue(runtime.active());
runtime.update(4f);
assertTrue(runtime.active());
}
@Test
public void projectileMovesFromItsSpawnBoxAndWrapsAtTheConfiguredCycle() {
LevelDefinition.Hazard definition = new LevelDefinition.Hazard(
LevelDefinition.HazardType.PROJECTILE,
new LevelDefinition.Box(620f, 790f, 48f, 48f),
null, 420f, -120f, 3f, 0f, 360f, 0f,
"clockwork-bolt", "projectile-hit");
HazardRuntime runtime = new HazardRuntime(definition);
runtime.update(1f);
assertTrue(runtime.currentBox().x == 980f);
runtime.update(2f);
assertTrue(runtime.currentBox().x == 620f);
}
@Test
public void explosionHasADataDrivenBlastAndRestWindow() {
LevelDefinition.Hazard definition = new LevelDefinition.Hazard(
LevelDefinition.HazardType.EXPLOSION,
new LevelDefinition.Box(620f, 740f, 140f, 140f),
null, 550f, -500f, 4f, 2f, 0f, 0f,
"steam-blast", "explosion-blast");
HazardRuntime runtime = new HazardRuntime(definition);
assertTrue(runtime.active());
runtime.update(2.1f);
assertFalse(runtime.active());
runtime.update(1.9f);
assertTrue(runtime.active());
}
}
@@ -12,9 +12,9 @@ import java.util.Set;
public final class LevelCatalogTest {
@Test
public void verticalSliceContainsTenSequentialLevels() {
public void productionCatalogContainsFourHazardTeachingLevelsAfterTheVerticalSlice() {
List<LevelDefinition> levels = LevelCatalog.all();
assertEquals(10, levels.size());
assertEquals(14, levels.size());
Set<String> titles = new HashSet<>();
for (int i = 0; i < levels.size(); i++) {
LevelDefinition level = levels.get(i);
@@ -22,6 +22,10 @@ public final class LevelCatalogTest {
assertTrue(titles.add(level.title));
assertFalse(level.hint.trim().isEmpty());
}
assertTrue(levels.get(10).containsMechanic(LevelDefinition.HazardType.CRUSHER));
assertTrue(levels.get(11).containsMechanic(LevelDefinition.HazardType.FIRE));
assertTrue(levels.get(12).containsMechanic(LevelDefinition.HazardType.PROJECTILE));
assertTrue(levels.get(13).containsMechanic(LevelDefinition.HazardType.EXPLOSION));
}
@Test
@@ -126,6 +126,94 @@ public final class LevelJsonLoaderTest {
}
}
@Test
public void crusherLoadsItsCycleImpulseAudioAndVisualSettingsFromData() {
String json = validLevelJson(1).replace(
"\"hazards\":[],\"switches\":[],\"doors\":[]",
"\"hazards\":[{\"type\":\"CRUSHER\","
+ "\"box\":{\"x\":620,\"y\":690,\"width\":150,\"height\":190},"
+ "\"impulseX\":0,\"impulseY\":180,"
+ "\"cycleSeconds\":4,\"activeSeconds\":1.25,"
+ "\"speedX\":0,\"speedY\":0,"
+ "\"visualStyle\":\"brass-press\",\"audioCue\":\"crusher-impact\"}],"
+ "\"switches\":[],\"doors\":[]");
LevelDefinition.Hazard crusher = LevelJsonLoader.load(
new StringReader(json), "crusher.json").hazards.get(0);
assertEquals(LevelDefinition.HazardType.CRUSHER, crusher.type);
assertEquals(4f, crusher.cycleSeconds, 0f);
assertEquals(1.25f, crusher.activeSeconds, 0f);
assertEquals(180f, crusher.impulseY, 0f);
assertEquals("brass-press", crusher.visualStyle);
assertEquals("crusher-impact", crusher.audioCue);
}
@Test
public void fireLoadsItsCorpseImpulseAudioAndVisualSettingsFromData() {
String json = validLevelJson(1).replace(
"\"hazards\":[],\"switches\":[],\"doors\":[]",
"\"hazards\":[{\"type\":\"FIRE\","
+ "\"box\":{\"x\":700,\"y\":790,\"width\":120,\"height\":90},"
+ "\"impulseX\":160,\"impulseY\":-220,"
+ "\"visualStyle\":\"blue-flame\",\"audioCue\":\"fire-burst\"}],"
+ "\"switches\":[],\"doors\":[]");
LevelDefinition.Hazard fire = LevelJsonLoader.load(
new StringReader(json), "fire.json").hazards.get(0);
assertEquals(LevelDefinition.HazardType.FIRE, fire.type);
assertEquals(160f, fire.impulseX, 0f);
assertEquals(-220f, fire.impulseY, 0f);
assertEquals("blue-flame", fire.visualStyle);
assertEquals("fire-burst", fire.audioCue);
}
@Test
public void projectileLoadsItsVelocityCycleAudioAndVisualSettingsFromData() {
String json = validLevelJson(1).replace(
"\"hazards\":[],\"switches\":[],\"doors\":[]",
"\"hazards\":[{\"type\":\"PROJECTILE\","
+ "\"box\":{\"x\":620,\"y\":790,\"width\":48,\"height\":48},"
+ "\"impulseX\":420,\"impulseY\":-120,"
+ "\"cycleSeconds\":3,\"activeSeconds\":0,"
+ "\"speedX\":360,\"speedY\":0,"
+ "\"visualStyle\":\"clockwork-bolt\",\"audioCue\":\"projectile-hit\"}],"
+ "\"switches\":[],\"doors\":[]");
LevelDefinition.Hazard projectile = LevelJsonLoader.load(
new StringReader(json), "projectile.json").hazards.get(0);
assertEquals(LevelDefinition.HazardType.PROJECTILE, projectile.type);
assertEquals(3f, projectile.cycleSeconds, 0f);
assertEquals(360f, projectile.speedX, 0f);
assertEquals("clockwork-bolt", projectile.visualStyle);
assertEquals("projectile-hit", projectile.audioCue);
}
@Test
public void explosionLoadsItsRequiredImpulseAudioAndVisualSettingsFromData() {
String json = validLevelJson(1).replace(
"\"hazards\":[],\"switches\":[],\"doors\":[]",
"\"hazards\":[{\"type\":\"EXPLOSION\","
+ "\"box\":{\"x\":720,\"y\":740,\"width\":140,\"height\":140},"
+ "\"impulseX\":520,\"impulseY\":-380,"
+ "\"cycleSeconds\":4,\"activeSeconds\":2,"
+ "\"visualStyle\":\"steam-blast\",\"audioCue\":\"explosion-blast\"}],"
+ "\"switches\":[],\"doors\":[]");
LevelDefinition.Hazard explosion = LevelJsonLoader.load(
new StringReader(json), "explosion.json").hazards.get(0);
assertEquals(LevelDefinition.HazardType.EXPLOSION, explosion.type);
assertEquals(520f, explosion.impulseX, 0f);
assertEquals(-380f, explosion.impulseY, 0f);
assertEquals(4f, explosion.cycleSeconds, 0f);
assertEquals(2f, explosion.activeSeconds, 0f);
assertEquals("steam-blast", explosion.visualStyle);
assertEquals("explosion-blast", explosion.audioCue);
}
static String validLevelJson(int formatVersion) {
return "{"
+ "\"formatVersion\":" + formatVersion + ","
@@ -0,0 +1,33 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public final class LevelPageTest {
@Test
public void fourteenLevelsUseTwoBoundedTenCardPages() {
LevelPage page = new LevelPage(14, 10);
assertEquals(2, page.pageCount());
assertEquals(0, page.firstIndex());
assertEquals(10, page.endExclusive());
assertFalse(page.hasPrevious());
assertTrue(page.hasNext());
page.next();
assertEquals(10, page.firstIndex());
assertEquals(14, page.endExclusive());
assertTrue(page.hasPrevious());
assertFalse(page.hasNext());
page.next();
assertEquals(1, page.pageIndex());
page.selectIndex(3);
assertEquals(0, page.pageIndex());
page.selectIndex(13);
assertEquals(1, page.pageIndex());
}
}
@@ -58,6 +58,22 @@ public final class VersionedSessionCodecTest {
assertNull(future.snapshot());
}
@Test
public void productionTeachingLevelSnapshotRoundTrips() {
GameSessionSnapshot snapshot = GameSessionSnapshot.builder(14)
.elapsedSeconds(3f)
.player(new GameSessionSnapshot.PlayerData(
130f, 760f, 0f, 0f, true, true,
0f, 0f, -1, 1))
.build();
VersionedSessionCodec.DecodeResult decoded = VersionedSessionCodec.decode(
VersionedSessionCodec.encode(snapshot));
assertEquals(VersionedSessionCodec.Status.RESTORED, decoded.status());
assertEquals(14, decoded.snapshot().levelNumber());
}
private static GameSessionSnapshot sampleSnapshot() {
return GameSessionSnapshot.builder(5)
.elapsedSeconds(12.75f)