Выделить event-driven PuzzleSystem

This commit is contained in:
2026-08-19 04:07:29 +03:00
parent cc3b5902cc
commit cc7f487cf7
9 changed files with 763 additions and 68 deletions
@@ -21,6 +21,7 @@ import android.view.accessibility.AccessibilityEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -228,9 +229,11 @@ 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 Map<String, Boolean> toggleSignals = new HashMap<>();
private final Map<String, Float> timedSignals = new HashMap<>();
private final Map<String, Boolean> previousSwitchContact = new HashMap<>();
private final Map<LevelDefinition.Door, DoorController> doorControllers =
new IdentityHashMap<>();
private final Map<String, LevelDefinition.Door> doorsById = new HashMap<>();
private PuzzleSystem puzzleSystem = new PuzzleSystem();
private final AccessibilityStateCache accessibilityStateCache =
new AccessibilityStateCache();
private final VolunteerCanvasRenderer volunteerRenderer = new VolunteerCanvasRenderer();
@@ -266,7 +269,6 @@ public final class GameView extends View implements Runnable {
private AccessibilitySettings accessibilitySettings;
private AudioSettings audioSettings;
private float lastFootstepAt = -10f;
private boolean previousAllDoorsOpen;
private boolean deterministicAutomationClock;
private double deterministicAccumulatorSeconds;
private float lastDrawDurationMs;
@@ -385,7 +387,6 @@ public final class GameView extends View implements Runnable {
cameraShakeY = 0f;
cameraController.reset();
lastFootstepAt = -10f;
previousAllDoorsOpen = false;
performanceMonitor.reset();
performanceFrameCounter = 0;
userPaused = false;
@@ -393,9 +394,8 @@ public final class GameView extends View implements Runnable {
deriveControls();
corpseManager.reset();
platformStates.clear();
toggleSignals.clear();
timedSignals.clear();
previousSwitchContact.clear();
configurePuzzleSystem();
completionResult = null;
configureAccessibilitySignals();
invalidateHudTextCache();
@@ -451,17 +451,6 @@ public final class GameView extends View implements Runnable {
lastFootstepAt = levelElapsed;
audioEngine.play(AudioCue.FOOTSTEP);
}
boolean allDoorsOpen = allDoorsOpenNow();
if (allDoorsOpen && !previousAllDoorsOpen) {
audioEngine.play(AudioCue.DOOR);
if (!level.doors.isEmpty()) {
LevelDefinition.Door door = level.doors.get(0);
cameraController.requestFocus(door.box.x + door.box.width * 0.5f,
door.box.y + door.box.height * 0.5f, 0.35f);
}
}
previousAllDoorsOpen = allDoorsOpen;
refreshAccessibilityDescriptionIfStateChanged();
CameraController.Frame camera = cameraController.update(dt,
@@ -473,30 +462,85 @@ public final class GameView extends View implements Runnable {
cameraShakeY = camera.shakeY();
}
private void updateSignals(float dt) {
for (Map.Entry<String, Float> item : new ArrayList<>(timedSignals.entrySet())) {
timedSignals.put(item.getKey(), Math.max(0f, item.getValue() - dt));
private void configurePuzzleSystem() {
doorControllers.clear();
doorsById.clear();
ArrayList<DoorController> controllers = new ArrayList<>(level.doors.size());
for (int index = 0; index < level.doors.size(); index++) {
LevelDefinition.Door door = level.doors.get(index);
String id = "door-" + index;
DoorController controller = new DoorController(id, door.requiredSignals);
controllers.add(controller);
doorControllers.put(door, controller);
doorsById.put(id, door);
}
puzzleSystem = new PuzzleSystem(controllers);
}
private void updateSignals(float dt) {
updateSignals(dt, true);
}
private void updateSignals(float dt, boolean feedback) {
ArrayList<PuzzleSystem.Input> inputs = new ArrayList<>(
level.switches.size() + level.hazards.size());
for (LevelDefinition.Switch item : level.switches) {
boolean contact = switchContact(item);
String contactKey = item.signalId + "@" + item.box.x;
boolean previous = Boolean.TRUE.equals(previousSwitchContact.get(contactKey));
if (item.type == LevelDefinition.SwitchType.TOGGLE && contact && !previous) {
toggleSignals.put(item.signalId, !Boolean.TRUE.equals(toggleSignals.get(item.signalId)));
switch (item.type) {
case PRESSURE:
inputs.add(PuzzleSystem.Input.pressure(
contactKey, item.signalId, contact));
break;
case TOGGLE:
inputs.add(PuzzleSystem.Input.toggle(
contactKey, item.signalId, contact));
break;
case TIMED:
inputs.add(PuzzleSystem.Input.timed(
contactKey, item.signalId, contact, item.durationSeconds));
break;
}
if (feedback && contact && !previous
&& item.type != LevelDefinition.SwitchType.PRESSURE) {
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
}
if (item.type == LevelDefinition.SwitchType.TIMED && contact && !previous) {
timedSignals.put(item.signalId, item.durationSeconds);
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
}
if (contact && !previous) audioEngine.play(AudioCue.SWITCH);
if (contact && !previous && player.alive
if (feedback && contact && !previous) audioEngine.play(AudioCue.SWITCH);
if (feedback && contact && !previous && player.alive
&& intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT, item.box)) {
characterStateMachine.beginInteraction();
}
previousSwitchContact.put(contactKey, contact);
}
for (int index = 0; index < level.hazards.size(); index++) {
LevelDefinition.Hazard hazard = level.hazards.get(index);
if (hazard.relayId == null || hazard.relayId.isEmpty()) continue;
boolean active = false;
for (Body corpse : corpses) {
if (corpse.conductive
&& intersects(corpse.x, corpse.y, corpse.width, corpse.height,
hazard.box)) {
active = true;
break;
}
}
inputs.add(PuzzleSystem.Input.relay(
"relay-" + index, hazard.relayId, active));
}
puzzleSystem.update(dt, inputs);
if (!feedback) return;
for (PuzzleSystem.Event event : puzzleSystem.events()) {
if (event.kind() != PuzzleSystem.Event.Kind.DOOR_OPENED) continue;
audioEngine.play(AudioCue.DOOR);
LevelDefinition.Door door = doorsById.get(event.targetId());
if (door != null) {
cameraController.requestFocus(door.box.x + door.box.width * 0.5f,
door.box.y + door.box.height * 0.5f, 0.35f);
}
}
}
private boolean allDoorsOpenNow() {
@@ -515,28 +559,12 @@ public final class GameView extends View implements Runnable {
}
private boolean signalActive(String id) {
if (id == null || id.isEmpty()) return true;
if (Boolean.TRUE.equals(toggleSignals.get(id))) return true;
Float time = timedSignals.get(id);
if (time != null && time > 0f) return true;
for (LevelDefinition.Switch item : level.switches) {
if (item.signalId.equals(id) && item.type == LevelDefinition.SwitchType.PRESSURE && switchContact(item)) return true;
}
for (LevelDefinition.Hazard hazard : level.hazards) {
if (id.equals(hazard.relayId)) {
for (Body corpse : corpses) {
if (corpse.conductive && intersects(corpse.x, corpse.y, corpse.width, corpse.height, hazard.box)) return true;
}
}
}
return false;
return puzzleSystem.signalActive(id);
}
private boolean doorOpen(LevelDefinition.Door door) {
for (String signal : door.requiredSignals) {
if (!signalActive(signal)) return false;
}
return true;
DoorController controller = doorControllers.get(door);
return controller != null && controller.open();
}
private void updatePlatforms(float dt) {
@@ -1003,8 +1031,9 @@ public final class GameView extends View implements Runnable {
stroke.setStrokeWidth(7f);
canvas.drawLine(box.centerX(), box.centerY(), box.centerX() + (active ? 20 : -20), box.top - 28, stroke);
if (item.type == LevelDefinition.SwitchType.TIMED && active) {
Float remaining = timedSignals.get(item.signalId);
drawWorldLabel(canvas, String.format(Locale.US, "%.1f", remaining == null ? 0f : remaining), box.centerX(), box.top - 52);
float remaining = puzzleSystem.remainingSeconds(item.signalId);
drawWorldLabel(canvas, String.format(Locale.US, "%.1f", remaining),
box.centerX(), box.top - 52);
}
drawWorldLabel(canvas, active ? "ON" : "OFF", box.centerX(), box.bottom + 32);
}
@@ -2282,10 +2311,11 @@ public final class GameView extends View implements Runnable {
ArrayList<Float> motionTimes = new ArrayList<>(platformStates.size());
for (PlatformState state : platformStates) motionTimes.add(state.motionTime);
builder.platformMotionTimes(motionTimes);
for (Map.Entry<String, Boolean> entry : toggleSignals.entrySet()) {
PuzzleSystem.State puzzleState = puzzleSystem.snapshot();
for (Map.Entry<String, Boolean> entry : puzzleState.toggleSignals().entrySet()) {
builder.toggleSignal(entry.getKey(), Boolean.TRUE.equals(entry.getValue()));
}
for (Map.Entry<String, Float> entry : timedSignals.entrySet()) {
for (Map.Entry<String, Float> entry : puzzleState.timedSignals().entrySet()) {
builder.timedSignal(entry.getKey(), Math.max(0f, entry.getValue()));
}
for (Map.Entry<String, Boolean> entry : previousSwitchContact.entrySet()) {
@@ -2316,12 +2346,10 @@ public final class GameView extends View implements Runnable {
levelElapsed = snapshot.elapsedSeconds();
deaths = snapshot.deaths();
respawnTimer = snapshot.respawnSeconds();
toggleSignals.clear();
toggleSignals.putAll(snapshot.toggleSignals());
timedSignals.clear();
timedSignals.putAll(snapshot.timedSignals());
previousSwitchContact.clear();
previousSwitchContact.putAll(snapshot.previousSwitchContacts());
puzzleSystem.restore(snapshot.toggleSignals(), snapshot.timedSignals(),
snapshot.previousSwitchContacts());
for (int i = 0; i < platformStates.size(); i++) {
PlatformState state = platformStates.get(i);
state.motionTime = snapshot.platformMotionTimes().get(i);
@@ -2362,6 +2390,7 @@ public final class GameView extends View implements Runnable {
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
}
}
updateSignals(0f, false);
leftPressed = false;
rightPressed = false;
@@ -2370,7 +2399,6 @@ public final class GameView extends View implements Runnable {
userPaused = true;
paused = false;
lastFootstepAt = levelElapsed;
previousAllDoorsOpen = allDoorsOpenNow();
cameraController.reset();
cameraX = cameraY = cameraShakeX = cameraShakeY = 0f;
lastFrameNanos = System.nanoTime();
@@ -2448,4 +2476,8 @@ public final class GameView extends View implements Runnable {
public CorpseManager.Diagnostics corpseDiagnosticsForAutomation() {
return corpseManager.diagnostics();
}
public PuzzleSystem.Diagnostics puzzleDiagnosticsForAutomation() {
return puzzleSystem.diagnostics();
}
}