Завершить state machine персонажа
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class CharacterStateIntegrationTest {
|
||||
@Test
|
||||
public void gameViewPublishesMovementAndCommandStates() throws Exception {
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
try (ActivityScenario<MainActivity> activity =
|
||||
ActivityScenario.launch(MainActivity.class)) {
|
||||
GameView[] view = new GameView[1];
|
||||
activity.onActivity(current -> view[0] = current.gameViewForAutomation());
|
||||
instrumentation.runOnMainSync(() -> view[0].startLevelForAutomation(1));
|
||||
|
||||
awaitState(instrumentation, view[0], CharacterStateMachine.State.IDLE, 3_000L);
|
||||
instrumentation.runOnMainSync(
|
||||
() -> view[0].setAutomationInput(false, true, false));
|
||||
awaitState(instrumentation, view[0], CharacterStateMachine.State.RUN, 3_000L);
|
||||
|
||||
instrumentation.runOnMainSync(
|
||||
() -> view[0].setAutomationInput(true, false, false));
|
||||
awaitState(instrumentation, view[0], CharacterStateMachine.State.TURN, 3_000L);
|
||||
instrumentation.runOnMainSync(
|
||||
() -> view[0].setAutomationInput(false, true, true));
|
||||
awaitState(instrumentation, view[0], CharacterStateMachine.State.JUMP, 3_000L);
|
||||
instrumentation.runOnMainSync(
|
||||
() -> view[0].setAutomationInput(false, true, false));
|
||||
awaitState(instrumentation, view[0], CharacterStateMachine.State.FALL, 3_000L);
|
||||
awaitState(instrumentation, view[0], CharacterStateMachine.State.LAND, 3_000L);
|
||||
|
||||
instrumentation.runOnMainSync(view[0]::triggerInteractionForAutomation);
|
||||
assertEquals(CharacterStateMachine.State.INTERACT,
|
||||
state(instrumentation, view[0]));
|
||||
instrumentation.runOnMainSync(view[0]::triggerStunForAutomation);
|
||||
assertEquals(CharacterStateMachine.State.STUNNED,
|
||||
state(instrumentation, view[0]));
|
||||
}
|
||||
}
|
||||
|
||||
private static void awaitState(Instrumentation instrumentation, GameView view,
|
||||
CharacterStateMachine.State expected,
|
||||
long timeoutMillis) throws Exception {
|
||||
long deadline = System.currentTimeMillis() + timeoutMillis;
|
||||
CharacterStateMachine.State current = state(instrumentation, view);
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
current = state(instrumentation, view);
|
||||
if (current == expected) return;
|
||||
Thread.sleep(24L);
|
||||
}
|
||||
throw new AssertionError("Expected " + expected + " but was " + current);
|
||||
}
|
||||
|
||||
private static CharacterStateMachine.State state(
|
||||
Instrumentation instrumentation, GameView view) {
|
||||
CharacterStateMachine.State[] result = new CharacterStateMachine.State[1];
|
||||
instrumentation.runOnMainSync(
|
||||
() -> result[0] = view.characterStateForAutomation());
|
||||
return result[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
/** Deterministic animation state derived from the character's public physics facts. */
|
||||
public final class CharacterStateMachine {
|
||||
public enum State {
|
||||
IDLE,
|
||||
RUN,
|
||||
JUMP,
|
||||
FALL,
|
||||
TURN,
|
||||
LAND,
|
||||
INTERACT,
|
||||
STUNNED,
|
||||
DEATH
|
||||
}
|
||||
|
||||
private final PhysicsConfig physics;
|
||||
private State state = State.IDLE;
|
||||
private float transientSeconds;
|
||||
private int facing = 1;
|
||||
private boolean wasOnGround;
|
||||
|
||||
public CharacterStateMachine(PhysicsConfig physics) {
|
||||
if (physics == null) throw new IllegalArgumentException("physics is required");
|
||||
this.physics = physics;
|
||||
}
|
||||
|
||||
public void reset(boolean alive, boolean onGround, float velocityX,
|
||||
float velocityY, int facing) {
|
||||
state = baseState(alive, onGround, velocityX, velocityY);
|
||||
transientSeconds = 0f;
|
||||
this.facing = normalizeFacing(facing);
|
||||
wasOnGround = onGround;
|
||||
}
|
||||
|
||||
public void observe(float deltaSeconds, boolean alive, boolean onGround,
|
||||
float velocityX, float velocityY, int facing) {
|
||||
float safeDelta = Math.max(0f, deltaSeconds);
|
||||
int normalizedFacing = normalizeFacing(facing);
|
||||
if (!alive) {
|
||||
state = State.DEATH;
|
||||
transientSeconds = 0f;
|
||||
this.facing = normalizedFacing;
|
||||
wasOnGround = onGround;
|
||||
return;
|
||||
}
|
||||
if ((state == State.TURN || state == State.LAND || state == State.INTERACT
|
||||
|| state == State.STUNNED)
|
||||
&& transientSeconds > 0f) {
|
||||
transientSeconds = Math.max(0f, transientSeconds - safeDelta);
|
||||
this.facing = normalizedFacing;
|
||||
wasOnGround = onGround;
|
||||
if (transientSeconds > 0f && onGround) return;
|
||||
}
|
||||
if (onGround && !wasOnGround) {
|
||||
state = State.LAND;
|
||||
transientSeconds = physics.landSeconds();
|
||||
this.facing = normalizedFacing;
|
||||
wasOnGround = true;
|
||||
return;
|
||||
}
|
||||
if (onGround && normalizedFacing != this.facing) {
|
||||
state = State.TURN;
|
||||
transientSeconds = physics.turnSeconds();
|
||||
this.facing = normalizedFacing;
|
||||
wasOnGround = true;
|
||||
return;
|
||||
}
|
||||
this.facing = normalizedFacing;
|
||||
wasOnGround = onGround;
|
||||
state = baseState(alive, onGround, velocityX, velocityY);
|
||||
}
|
||||
|
||||
public State state() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void beginInteraction() {
|
||||
if (state == State.DEATH || state == State.STUNNED) return;
|
||||
state = State.INTERACT;
|
||||
transientSeconds = physics.interactSeconds();
|
||||
}
|
||||
|
||||
public void stun() {
|
||||
if (state == State.DEATH) return;
|
||||
state = State.STUNNED;
|
||||
transientSeconds = physics.stunnedSeconds();
|
||||
}
|
||||
|
||||
private static State baseState(boolean alive, boolean onGround,
|
||||
float velocityX, float velocityY) {
|
||||
if (!alive) return State.DEATH;
|
||||
if (!onGround) return velocityY < 0f ? State.JUMP : State.FALL;
|
||||
return Math.abs(velocityX) >= 30f ? State.RUN : State.IDLE;
|
||||
}
|
||||
|
||||
private static int normalizeFacing(int value) {
|
||||
return value < 0 ? -1 : 1;
|
||||
}
|
||||
}
|
||||
@@ -233,6 +233,8 @@ public final class GameView extends View implements Runnable {
|
||||
private final ProceduralAudioEngine audioEngine;
|
||||
private final SparseArray<PointF> pointers = new SparseArray<>();
|
||||
private final Player player = new Player();
|
||||
private final CharacterStateMachine characterStateMachine =
|
||||
new CharacterStateMachine(PHYSICS);
|
||||
private final ArrayList<Corpse> corpses = new ArrayList<>();
|
||||
private final ArrayList<PlatformState> platformStates = new ArrayList<>();
|
||||
private final Map<String, Boolean> toggleSignals = new HashMap<>();
|
||||
@@ -427,6 +429,8 @@ public final class GameView extends View implements Runnable {
|
||||
player.jumpBuffer = 0f;
|
||||
player.supportIndex = -1;
|
||||
player.facing = 1;
|
||||
characterStateMachine.reset(player.alive, player.onGround,
|
||||
player.vx, player.vy, player.facing);
|
||||
if (deaths > 0) audioEngine.play(AudioCue.RESPAWN_VOICE);
|
||||
}
|
||||
|
||||
@@ -496,6 +500,10 @@ public final class GameView extends View implements Runnable {
|
||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
}
|
||||
if (contact && !previous) audioEngine.play(AudioCue.SWITCH);
|
||||
if (contact && !previous && player.alive
|
||||
&& intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT, item.box)) {
|
||||
characterStateMachine.beginInteraction();
|
||||
}
|
||||
previousSwitchContact.put(contactKey, contact);
|
||||
}
|
||||
}
|
||||
@@ -594,6 +602,8 @@ public final class GameView extends View implements Runnable {
|
||||
movePlayerVertical(player.vy * dt);
|
||||
player.x = GameMath.clamp(player.x, 0f, level.worldWidth - PLAYER_WIDTH);
|
||||
if (player.y > VIEW_HEIGHT + 180f) kill(DeathType.PIT, null);
|
||||
characterStateMachine.observe(dt, player.alive, player.onGround,
|
||||
player.vx, player.vy, player.facing);
|
||||
}
|
||||
|
||||
private void movePlayerHorizontal(float amount) {
|
||||
@@ -757,6 +767,8 @@ public final class GameView extends View implements Runnable {
|
||||
player.alive = false;
|
||||
player.vx = player.vy = 0f;
|
||||
player.supportIndex = -1;
|
||||
characterStateMachine.observe(0f, false, player.onGround,
|
||||
player.vx, player.vy, player.facing);
|
||||
deaths++;
|
||||
respawnTimer = RESPAWN_SECONDS;
|
||||
emitHaptic(Build.VERSION.SDK_INT >= 27
|
||||
@@ -893,8 +905,7 @@ public final class GameView extends View implements Runnable {
|
||||
for (Corpse corpse : corpses) drawCorpse(canvas, corpse);
|
||||
VolunteerRig.Skin skin = VolunteerRig.Skin.forLevel(level.number);
|
||||
if (player.alive) {
|
||||
VolunteerRig.Motion motion = VolunteerRig.aliveMotion(
|
||||
player.onGround, player.vx, player.vy);
|
||||
VolunteerRig.Motion motion = VolunteerRig.motionFor(characterStateMachine.state());
|
||||
volunteerRenderer.draw(canvas, paint, stroke, path,
|
||||
player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT,
|
||||
skin, motion, volunteerPhase(motion), player.facing, 1f,
|
||||
@@ -2339,6 +2350,8 @@ public final class GameView extends View implements Runnable {
|
||||
player.jumpBuffer = savedPlayer.jumpBufferSeconds();
|
||||
player.supportIndex = savedPlayer.supportIndex();
|
||||
player.facing = savedPlayer.facing();
|
||||
characterStateMachine.reset(player.alive, player.onGround,
|
||||
player.vx, player.vy, player.facing);
|
||||
|
||||
corpses.clear();
|
||||
for (GameSessionSnapshot.BodyData savedBody : snapshot.bodies()) {
|
||||
@@ -2427,4 +2440,16 @@ public final class GameView extends View implements Runnable {
|
||||
player.vy
|
||||
);
|
||||
}
|
||||
|
||||
public CharacterStateMachine.State characterStateForAutomation() {
|
||||
return characterStateMachine.state();
|
||||
}
|
||||
|
||||
public void triggerInteractionForAutomation() {
|
||||
characterStateMachine.beginInteraction();
|
||||
}
|
||||
|
||||
public void triggerStunForAutomation() {
|
||||
characterStateMachine.stun();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ package com.projectsacrifice.game;
|
||||
public final class PhysicsConfig {
|
||||
private static final PhysicsConfig RELEASE = new PhysicsConfig(
|
||||
1, 1f / 60f, 1_800f, 370f, 880f,
|
||||
0.13f, 0.14f, 0.78f, 16);
|
||||
0.13f, 0.14f, 0.78f, 0.10f, 0.12f, 0.24f, 0.32f, 16);
|
||||
|
||||
private final int version;
|
||||
private final float fixedStepSeconds;
|
||||
@@ -14,12 +14,17 @@ public final class PhysicsConfig {
|
||||
private final float coyoteSeconds;
|
||||
private final float jumpBufferSeconds;
|
||||
private final float respawnSeconds;
|
||||
private final float turnSeconds;
|
||||
private final float landSeconds;
|
||||
private final float interactSeconds;
|
||||
private final float stunnedSeconds;
|
||||
private final int maximumBodies;
|
||||
|
||||
private PhysicsConfig(int version, float fixedStepSeconds, float gravity,
|
||||
float runSpeed, float jumpSpeed, float coyoteSeconds,
|
||||
float jumpBufferSeconds, float respawnSeconds,
|
||||
int maximumBodies) {
|
||||
float turnSeconds, float landSeconds, float interactSeconds,
|
||||
float stunnedSeconds, int maximumBodies) {
|
||||
this.version = version;
|
||||
this.fixedStepSeconds = fixedStepSeconds;
|
||||
this.gravity = gravity;
|
||||
@@ -28,6 +33,10 @@ public final class PhysicsConfig {
|
||||
this.coyoteSeconds = coyoteSeconds;
|
||||
this.jumpBufferSeconds = jumpBufferSeconds;
|
||||
this.respawnSeconds = respawnSeconds;
|
||||
this.turnSeconds = turnSeconds;
|
||||
this.landSeconds = landSeconds;
|
||||
this.interactSeconds = interactSeconds;
|
||||
this.stunnedSeconds = stunnedSeconds;
|
||||
this.maximumBodies = maximumBodies;
|
||||
}
|
||||
|
||||
@@ -40,5 +49,9 @@ public final class PhysicsConfig {
|
||||
public float coyoteSeconds() { return coyoteSeconds; }
|
||||
public float jumpBufferSeconds() { return jumpBufferSeconds; }
|
||||
public float respawnSeconds() { return respawnSeconds; }
|
||||
public float turnSeconds() { return turnSeconds; }
|
||||
public float landSeconds() { return landSeconds; }
|
||||
public float interactSeconds() { return interactSeconds; }
|
||||
public float stunnedSeconds() { return stunnedSeconds; }
|
||||
public int maximumBodies() { return maximumBodies; }
|
||||
}
|
||||
|
||||
@@ -49,8 +49,13 @@ public final class VolunteerRig {
|
||||
public enum Motion {
|
||||
IDLE,
|
||||
RUN,
|
||||
TURN,
|
||||
JUMP,
|
||||
FALL,
|
||||
LAND,
|
||||
INTERACT,
|
||||
STUNNED,
|
||||
DEATH,
|
||||
DEAD,
|
||||
PINNED,
|
||||
FROZEN,
|
||||
@@ -117,6 +122,12 @@ public final class VolunteerRig {
|
||||
return Math.abs(horizontalVelocity) >= 30f ? Motion.RUN : Motion.IDLE;
|
||||
}
|
||||
|
||||
public static Motion motionFor(CharacterStateMachine.State state) {
|
||||
CharacterStateMachine.State safeState = state == null
|
||||
? CharacterStateMachine.State.IDLE : state;
|
||||
return Motion.valueOf(safeState.name());
|
||||
}
|
||||
|
||||
public static Motion bodyMotion(boolean frozen, boolean conductive, boolean pinned) {
|
||||
if (frozen) return Motion.FROZEN;
|
||||
if (conductive) return Motion.CONDUCTIVE;
|
||||
@@ -156,6 +167,14 @@ public final class VolunteerRig {
|
||||
xs[Joint.RIGHT_FOOT.ordinal()] += swing * 0.18f;
|
||||
ys[Joint.ROOT.ordinal()] -= Math.abs(swing) * 0.025f;
|
||||
break;
|
||||
case TURN:
|
||||
xs[Joint.LEFT_HAND.ordinal()] = 0.72f;
|
||||
xs[Joint.RIGHT_HAND.ordinal()] = 0.28f;
|
||||
ys[Joint.LEFT_HAND.ordinal()] = 0.55f;
|
||||
ys[Joint.RIGHT_HAND.ordinal()] = 0.55f;
|
||||
xs[Joint.LEFT_KNEE.ordinal()] = 0.48f;
|
||||
xs[Joint.RIGHT_KNEE.ordinal()] = 0.52f;
|
||||
break;
|
||||
case JUMP:
|
||||
xs[Joint.LEFT_HAND.ordinal()] = 0.16f;
|
||||
xs[Joint.RIGHT_HAND.ordinal()] = 0.84f;
|
||||
@@ -174,6 +193,28 @@ public final class VolunteerRig {
|
||||
xs[Joint.LEFT_FOOT.ordinal()] = 0.23f;
|
||||
xs[Joint.RIGHT_FOOT.ordinal()] = 0.77f;
|
||||
break;
|
||||
case LAND:
|
||||
ys[Joint.ROOT.ordinal()] = 0.91f;
|
||||
ys[Joint.HIPS.ordinal()] = 0.74f;
|
||||
ys[Joint.LEFT_KNEE.ordinal()] = 0.87f;
|
||||
ys[Joint.RIGHT_KNEE.ordinal()] = 0.87f;
|
||||
xs[Joint.LEFT_HAND.ordinal()] = 0.18f;
|
||||
xs[Joint.RIGHT_HAND.ordinal()] = 0.82f;
|
||||
break;
|
||||
case INTERACT:
|
||||
xs[Joint.RIGHT_HAND.ordinal()] = 0.98f;
|
||||
ys[Joint.RIGHT_HAND.ordinal()] = 0.47f;
|
||||
xs[Joint.LEFT_HAND.ordinal()] = 0.45f;
|
||||
ys[Joint.LEFT_HAND.ordinal()] = 0.54f;
|
||||
break;
|
||||
case STUNNED:
|
||||
xs[Joint.LEFT_HAND.ordinal()] = 0.36f;
|
||||
xs[Joint.RIGHT_HAND.ordinal()] = 0.64f;
|
||||
ys[Joint.LEFT_HAND.ordinal()] = 0.22f;
|
||||
ys[Joint.RIGHT_HAND.ordinal()] = 0.22f;
|
||||
xs[Joint.HEAD.ordinal()] += swing * 0.04f;
|
||||
break;
|
||||
case DEATH:
|
||||
case DEAD:
|
||||
case PINNED:
|
||||
case FROZEN:
|
||||
@@ -196,7 +237,8 @@ public final class VolunteerRig {
|
||||
if (facing < 0) {
|
||||
for (int i = 0; i < xs.length; i++) xs[i] = 1f - xs[i];
|
||||
}
|
||||
boolean visorLit = safeMotion != Motion.DEAD
|
||||
boolean visorLit = safeMotion != Motion.DEATH
|
||||
&& safeMotion != Motion.DEAD
|
||||
&& safeMotion != Motion.PINNED
|
||||
&& safeMotion != Motion.FROZEN
|
||||
&& safeMotion != Motion.CONDUCTIVE;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public final class CharacterStateMachineTest {
|
||||
@Test
|
||||
public void groundedVelocitySelectsIdleAndRunDeterministically() {
|
||||
CharacterStateMachine machine = new CharacterStateMachine(PhysicsConfig.release());
|
||||
|
||||
machine.reset(true, true, 0f, 0f, 1);
|
||||
assertEquals(CharacterStateMachine.State.IDLE, machine.state());
|
||||
|
||||
machine.observe(1f / 60f, true, true, 370f, 0f, 1);
|
||||
assertEquals(CharacterStateMachine.State.RUN, machine.state());
|
||||
|
||||
machine.observe(1f / 60f, true, true, 0f, 0f, 1);
|
||||
assertEquals(CharacterStateMachine.State.IDLE, machine.state());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verticalMotionSelectsJumpFallAndDeathHasPriority() {
|
||||
CharacterStateMachine machine = new CharacterStateMachine(PhysicsConfig.release());
|
||||
|
||||
machine.reset(true, false, 120f, -300f, 1);
|
||||
assertEquals(CharacterStateMachine.State.JUMP, machine.state());
|
||||
|
||||
machine.observe(1f / 60f, true, false, 120f, 80f, 1);
|
||||
assertEquals(CharacterStateMachine.State.FALL, machine.state());
|
||||
|
||||
machine.observe(1f / 60f, false, true, 0f, 0f, 1);
|
||||
assertEquals(CharacterStateMachine.State.DEATH, machine.state());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changingFacingEmitsOneConfiguredTurnWindow() {
|
||||
PhysicsConfig physics = PhysicsConfig.release();
|
||||
CharacterStateMachine machine = new CharacterStateMachine(physics);
|
||||
machine.reset(true, true, physics.runSpeed(), 0f, 1);
|
||||
|
||||
machine.observe(physics.fixedStepSeconds(), true, true,
|
||||
-physics.runSpeed(), 0f, -1);
|
||||
assertEquals(CharacterStateMachine.State.TURN, machine.state());
|
||||
|
||||
machine.observe(physics.turnSeconds() + physics.fixedStepSeconds(),
|
||||
true, true, -physics.runSpeed(), 0f, -1);
|
||||
assertEquals(CharacterStateMachine.State.RUN, machine.state());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void firstGroundedFrameAfterFallingEmitsConfiguredLandWindow() {
|
||||
PhysicsConfig physics = PhysicsConfig.release();
|
||||
CharacterStateMachine machine = new CharacterStateMachine(physics);
|
||||
machine.reset(true, false, 0f, 300f, 1);
|
||||
|
||||
machine.observe(physics.fixedStepSeconds(), true, true, 0f, 0f, 1);
|
||||
assertEquals(CharacterStateMachine.State.LAND, machine.state());
|
||||
|
||||
machine.observe(physics.landSeconds() + physics.fixedStepSeconds(),
|
||||
true, true, 0f, 0f, 1);
|
||||
assertEquals(CharacterStateMachine.State.IDLE, machine.state());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interactionIsAConfiguredCommandAndDeathCancelsIt() {
|
||||
PhysicsConfig physics = PhysicsConfig.release();
|
||||
CharacterStateMachine machine = new CharacterStateMachine(physics);
|
||||
machine.reset(true, true, 0f, 0f, 1);
|
||||
|
||||
machine.beginInteraction();
|
||||
assertEquals(CharacterStateMachine.State.INTERACT, machine.state());
|
||||
machine.observe(physics.interactSeconds() * 0.5f,
|
||||
true, true, 0f, 0f, 1);
|
||||
assertEquals(CharacterStateMachine.State.INTERACT, machine.state());
|
||||
|
||||
machine.observe(physics.fixedStepSeconds(), false, true, 0f, 0f, 1);
|
||||
assertEquals(CharacterStateMachine.State.DEATH, machine.state());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stunOverridesInteractionThenReturnsToObservedMovement() {
|
||||
PhysicsConfig physics = PhysicsConfig.release();
|
||||
CharacterStateMachine machine = new CharacterStateMachine(physics);
|
||||
machine.reset(true, true, physics.runSpeed(), 0f, 1);
|
||||
machine.beginInteraction();
|
||||
|
||||
machine.stun();
|
||||
assertEquals(CharacterStateMachine.State.STUNNED, machine.state());
|
||||
machine.observe(physics.stunnedSeconds() * 0.5f,
|
||||
true, true, physics.runSpeed(), 0f, 1);
|
||||
assertEquals(CharacterStateMachine.State.STUNNED, machine.state());
|
||||
|
||||
machine.observe(physics.stunnedSeconds(),
|
||||
true, true, physics.runSpeed(), 0f, 1);
|
||||
assertEquals(CharacterStateMachine.State.RUN, machine.state());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interactionCannotDowngradeAnActiveStun() {
|
||||
CharacterStateMachine machine = new CharacterStateMachine(PhysicsConfig.release());
|
||||
machine.reset(true, true, 0f, 0f, 1);
|
||||
|
||||
machine.stun();
|
||||
machine.beginInteraction();
|
||||
|
||||
assertEquals(CharacterStateMachine.State.STUNNED, machine.state());
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@ public final class PhysicsConfigTest {
|
||||
assertEquals(0.13f, physics.coyoteSeconds(), 0f);
|
||||
assertEquals(0.14f, physics.jumpBufferSeconds(), 0f);
|
||||
assertEquals(0.78f, physics.respawnSeconds(), 0f);
|
||||
assertEquals(0.10f, physics.turnSeconds(), 0f);
|
||||
assertEquals(0.12f, physics.landSeconds(), 0f);
|
||||
assertEquals(0.24f, physics.interactSeconds(), 0f);
|
||||
assertEquals(0.32f, physics.stunnedSeconds(), 0f);
|
||||
assertTrue(physics.maximumBodies() >= 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,4 +84,32 @@ public final class VolunteerRigTest {
|
||||
assertEquals(VolunteerRig.Motion.DEAD,
|
||||
VolunteerRig.bodyMotion(false, false, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyLivingCharacterStateHasAnExplicitAnimationMotion() {
|
||||
for (CharacterStateMachine.State state : CharacterStateMachine.State.values()) {
|
||||
assertEquals(state.name(), VolunteerRig.motionFor(state).name());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transientCharacterStatesHaveNonColorPoseSignals() {
|
||||
VolunteerRig.Skin skin = VolunteerRig.Skin.BRASS_SCOUT;
|
||||
VolunteerRig.Pose idle = VolunteerRig.pose(skin, VolunteerRig.Motion.IDLE, 0.25f, 1);
|
||||
VolunteerRig.Pose turn = VolunteerRig.pose(skin, VolunteerRig.Motion.TURN, 0.25f, 1);
|
||||
VolunteerRig.Pose land = VolunteerRig.pose(skin, VolunteerRig.Motion.LAND, 0.25f, 1);
|
||||
VolunteerRig.Pose interact = VolunteerRig.pose(
|
||||
skin, VolunteerRig.Motion.INTERACT, 0.25f, 1);
|
||||
VolunteerRig.Pose stunned = VolunteerRig.pose(
|
||||
skin, VolunteerRig.Motion.STUNNED, 0.25f, 1);
|
||||
|
||||
assertNotEquals(idle.x(VolunteerRig.Joint.LEFT_HAND),
|
||||
turn.x(VolunteerRig.Joint.LEFT_HAND), 0.001f);
|
||||
assertNotEquals(idle.y(VolunteerRig.Joint.ROOT),
|
||||
land.y(VolunteerRig.Joint.ROOT), 0.001f);
|
||||
assertNotEquals(idle.x(VolunteerRig.Joint.RIGHT_HAND),
|
||||
interact.x(VolunteerRig.Joint.RIGHT_HAND), 0.001f);
|
||||
assertNotEquals(idle.y(VolunteerRig.Joint.LEFT_HAND),
|
||||
stunned.y(VolunteerRig.Joint.LEFT_HAND), 0.001f);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user