From f643f0afb8c7d91a7a39ff96f58b95baac985ce0 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sat, 7 Mar 2026 15:01:14 -0800 Subject: Some really good refactoring happening in here --- .../main/java/coffee/liz/dyl/actions/Action.java | 4 + .../java/coffee/liz/dyl/actions/JumpAction.java | 4 + .../java/coffee/liz/dyl/actions/MoveAction.java | 14 ++++ .../coffee/liz/dyl/components/AnimationState.java | 36 --------- .../coffee/liz/dyl/components/Controllable.java | 6 -- .../coffee/liz/dyl/components/DamageGraceTime.java | 17 ----- .../coffee/liz/dyl/components/FacingDirection.java | 12 --- .../liz/dyl/components/control/ActionQueue.java | 62 +++++++++++++++ .../liz/dyl/components/control/Controllable.java | 6 ++ .../dyl/components/control/FacingDirection.java | 15 ++++ .../liz/dyl/components/graphic/AnimationState.java | 34 +++++++++ .../liz/dyl/components/health/DamageGraceTime.java | 17 +++++ .../dyl/components/health/RelativeDamageBox.java | 10 +++ .../dyl/components/physics/CollisionContacts.java | 20 +++-- .../coffee/liz/dyl/components/physics/Jump.java | 5 +- .../liz/dyl/components/physics/Jumpable.java | 10 --- .../coffee/liz/dyl/components/physics/Solid.java | 3 +- .../coffee/liz/dyl/config/PhysicsConstants.java | 6 +- .../coffee/liz/dyl/entities/PlayerFactory.java | 19 ++--- .../coffee/liz/dyl/systems/AnimationSystem.java | 55 -------------- .../java/coffee/liz/dyl/systems/DamageSystem.java | 23 ------ .../java/coffee/liz/dyl/systems/InputSystem.java | 79 -------------------- .../java/coffee/liz/dyl/systems/JumpSystem.java | 43 ----------- .../java/coffee/liz/dyl/systems/RenderSystem.java | 52 ------------- .../systems/collision/SolidCollisionSystem.java | 58 +++++++++++++++ .../liz/dyl/systems/control/InputSystem.java | 51 +++++++++++++ .../systems/control/JumpActionConsumerSystem.java | 58 +++++++++++++++ .../control/MovementActionConsumerSystem.java | 48 ++++++++++++ .../liz/dyl/systems/graphics/AnimationSystem.java | 56 ++++++++++++++ .../liz/dyl/systems/graphics/RenderSystem.java | 52 +++++++++++++ .../liz/dyl/systems/health/DamageSystem.java | 23 ++++++ .../dyl/systems/physics/AccelerationSystem.java | 2 +- .../liz/dyl/systems/physics/CollisionGrid.java | 21 ++++-- .../liz/dyl/systems/physics/CollisionSystem.java | 85 ++++++--------------- .../java/coffee/liz/dyl/world/DylGameWorld.java | 20 +++-- core/src/main/java/coffee/liz/ecs/DAGWorld.java | 7 ++ .../main/java/coffee/liz/ecs/events/EventBus.java | 9 +-- core/src/main/java/coffee/liz/ecs/model/Query.java | 4 - core/src/main/java/coffee/liz/ecs/model/World.java | 2 + design/bugs.txt | 3 - design/design.txt | 84 --------------------- design/todo.txt | 8 -- docs/bugs.txt | 3 + docs/design.txt | 87 ++++++++++++++++++++++ docs/input.txt | 18 +++++ docs/todo.txt | 8 ++ 46 files changed, 716 insertions(+), 543 deletions(-) create mode 100644 core/src/main/java/coffee/liz/dyl/actions/Action.java create mode 100644 core/src/main/java/coffee/liz/dyl/actions/JumpAction.java create mode 100644 core/src/main/java/coffee/liz/dyl/actions/MoveAction.java delete mode 100644 core/src/main/java/coffee/liz/dyl/components/AnimationState.java delete mode 100644 core/src/main/java/coffee/liz/dyl/components/Controllable.java delete mode 100644 core/src/main/java/coffee/liz/dyl/components/DamageGraceTime.java delete mode 100644 core/src/main/java/coffee/liz/dyl/components/FacingDirection.java create mode 100644 core/src/main/java/coffee/liz/dyl/components/control/ActionQueue.java create mode 100644 core/src/main/java/coffee/liz/dyl/components/control/Controllable.java create mode 100644 core/src/main/java/coffee/liz/dyl/components/control/FacingDirection.java create mode 100644 core/src/main/java/coffee/liz/dyl/components/graphic/AnimationState.java create mode 100644 core/src/main/java/coffee/liz/dyl/components/health/DamageGraceTime.java create mode 100644 core/src/main/java/coffee/liz/dyl/components/health/RelativeDamageBox.java delete mode 100644 core/src/main/java/coffee/liz/dyl/systems/AnimationSystem.java delete mode 100644 core/src/main/java/coffee/liz/dyl/systems/DamageSystem.java delete mode 100644 core/src/main/java/coffee/liz/dyl/systems/InputSystem.java delete mode 100644 core/src/main/java/coffee/liz/dyl/systems/JumpSystem.java delete mode 100644 core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java create mode 100644 core/src/main/java/coffee/liz/dyl/systems/collision/SolidCollisionSystem.java create mode 100644 core/src/main/java/coffee/liz/dyl/systems/control/InputSystem.java create mode 100644 core/src/main/java/coffee/liz/dyl/systems/control/JumpActionConsumerSystem.java create mode 100644 core/src/main/java/coffee/liz/dyl/systems/control/MovementActionConsumerSystem.java create mode 100644 core/src/main/java/coffee/liz/dyl/systems/graphics/AnimationSystem.java create mode 100644 core/src/main/java/coffee/liz/dyl/systems/graphics/RenderSystem.java create mode 100644 core/src/main/java/coffee/liz/dyl/systems/health/DamageSystem.java delete mode 100644 design/bugs.txt delete mode 100644 design/design.txt delete mode 100644 design/todo.txt create mode 100644 docs/bugs.txt create mode 100644 docs/design.txt create mode 100644 docs/input.txt create mode 100644 docs/todo.txt diff --git a/core/src/main/java/coffee/liz/dyl/actions/Action.java b/core/src/main/java/coffee/liz/dyl/actions/Action.java new file mode 100644 index 0000000..224cfc6 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/actions/Action.java @@ -0,0 +1,4 @@ +package coffee.liz.dyl.actions; + +public interface Action { +} diff --git a/core/src/main/java/coffee/liz/dyl/actions/JumpAction.java b/core/src/main/java/coffee/liz/dyl/actions/JumpAction.java new file mode 100644 index 0000000..0d8eda5 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/actions/JumpAction.java @@ -0,0 +1,4 @@ +package coffee.liz.dyl.actions; + +public class JumpAction implements Action { +} diff --git a/core/src/main/java/coffee/liz/dyl/actions/MoveAction.java b/core/src/main/java/coffee/liz/dyl/actions/MoveAction.java new file mode 100644 index 0000000..ad56387 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/actions/MoveAction.java @@ -0,0 +1,14 @@ +package coffee.liz.dyl.actions; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class MoveAction implements Action { + private final Direction direction; + + public enum Direction { + LEFT, RIGHT + } +} diff --git a/core/src/main/java/coffee/liz/dyl/components/AnimationState.java b/core/src/main/java/coffee/liz/dyl/components/AnimationState.java deleted file mode 100644 index 04505c5..0000000 --- a/core/src/main/java/coffee/liz/dyl/components/AnimationState.java +++ /dev/null @@ -1,36 +0,0 @@ -package coffee.liz.dyl.components; - -import coffee.liz.dyl.components.graphic.AnimationGraphic; -import coffee.liz.ecs.model.Component; -import jakarta.annotation.Nullable; -import lombok.Builder; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; - -import java.util.Map; - -@Getter -@Setter -@RequiredArgsConstructor -public class AnimationState implements Component { - private State currentState; - private State transition; - - private final Map animationStates; - - @Nullable - public AnimationGraphic getTransitionAnimation() { - if (transition == null) { - return null; - } - return animationStates.get(transition); - } - - public enum State { - IDLE, - RUN, - JUMP, - DAMAGE - } -} diff --git a/core/src/main/java/coffee/liz/dyl/components/Controllable.java b/core/src/main/java/coffee/liz/dyl/components/Controllable.java deleted file mode 100644 index c476c91..0000000 --- a/core/src/main/java/coffee/liz/dyl/components/Controllable.java +++ /dev/null @@ -1,6 +0,0 @@ -package coffee.liz.dyl.components; - -import coffee.liz.ecs.model.Component; - -public class Controllable implements Component { -} diff --git a/core/src/main/java/coffee/liz/dyl/components/DamageGraceTime.java b/core/src/main/java/coffee/liz/dyl/components/DamageGraceTime.java deleted file mode 100644 index 8424639..0000000 --- a/core/src/main/java/coffee/liz/dyl/components/DamageGraceTime.java +++ /dev/null @@ -1,17 +0,0 @@ -package coffee.liz.dyl.components; - -import coffee.liz.ecs.model.Component; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class DamageGraceTime implements Component { - private float duration; - - public void decrement(final float deltaTime) { - duration -= deltaTime; - } - - public boolean isOver() { - return duration <= 0; - } -} diff --git a/core/src/main/java/coffee/liz/dyl/components/FacingDirection.java b/core/src/main/java/coffee/liz/dyl/components/FacingDirection.java deleted file mode 100644 index d517fc1..0000000 --- a/core/src/main/java/coffee/liz/dyl/components/FacingDirection.java +++ /dev/null @@ -1,12 +0,0 @@ -package coffee.liz.dyl.components; - -import coffee.liz.ecs.math.Vec2; -import coffee.liz.ecs.model.Component; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@Getter -public class FacingDirection implements Component { - private final Vec2 unitDirection; -} diff --git a/core/src/main/java/coffee/liz/dyl/components/control/ActionQueue.java b/core/src/main/java/coffee/liz/dyl/components/control/ActionQueue.java new file mode 100644 index 0000000..35b936c --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/control/ActionQueue.java @@ -0,0 +1,62 @@ +package coffee.liz.dyl.components.control; + +import coffee.liz.dyl.actions.Action; +import coffee.liz.ecs.model.Component; +import jakarta.annotation.Nullable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; + +public class ActionQueue implements Component { + private final Map, ExpiringAction> actionQueue = new HashMap<>(); + + public boolean has(final Class actionType) { + return actionQueue.containsKey(actionType); + } + + public void queue(final Action action, final float currentTime) { + queue(action, currentTime, -1f); + } + + public void queue(final Action action, final float currentTime, final float expiresAfterSeconds) { + actionQueue.put(action.getClass(), new ExpiringAction(action, currentTime, expiresAfterSeconds)); + } + + @SuppressWarnings("unchecked") + @Nullable + public T consume(final Class actionClass, final float currentTime, final Predicate consumed) { + final ExpiringAction expiringAction = actionQueue.get(actionClass); + if (expiringAction == null) { + return null; + } + if (expiringAction.isExpired(currentTime)) { + actionQueue.remove(actionClass); + return null; + } + + final Action action = expiringAction.getAction(); + if (action != null && actionClass.isAssignableFrom(action.getClass()) && consumed.test((T) action)) { + actionQueue.remove(actionClass); + return (T) action; + } + return null; + } + + @RequiredArgsConstructor + @Getter + private static class ExpiringAction { + private final Action action; + private final float queuedAt; + private final float expiresAfterSeconds; + + public boolean isExpired(final float currentTime) { + if (expiresAfterSeconds < 0f) { + return false; + } + return (currentTime - queuedAt) > expiresAfterSeconds; + } + } +} diff --git a/core/src/main/java/coffee/liz/dyl/components/control/Controllable.java b/core/src/main/java/coffee/liz/dyl/components/control/Controllable.java new file mode 100644 index 0000000..f63a1c2 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/control/Controllable.java @@ -0,0 +1,6 @@ +package coffee.liz.dyl.components.control; + +import coffee.liz.ecs.model.Component; + +public class Controllable implements Component { +} diff --git a/core/src/main/java/coffee/liz/dyl/components/control/FacingDirection.java b/core/src/main/java/coffee/liz/dyl/components/control/FacingDirection.java new file mode 100644 index 0000000..280bfb0 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/control/FacingDirection.java @@ -0,0 +1,15 @@ +package coffee.liz.dyl.components.control; + +import coffee.liz.ecs.math.Vec2; +import coffee.liz.ecs.model.Component; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@AllArgsConstructor +@Getter +@Setter +public class FacingDirection implements Component { + private Vec2 unitDirection; +} diff --git a/core/src/main/java/coffee/liz/dyl/components/graphic/AnimationState.java b/core/src/main/java/coffee/liz/dyl/components/graphic/AnimationState.java new file mode 100644 index 0000000..ad183da --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/graphic/AnimationState.java @@ -0,0 +1,34 @@ +package coffee.liz.dyl.components.graphic; + +import coffee.liz.ecs.model.Component; +import jakarta.annotation.Nullable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.Map; + +@Getter +@Setter +@RequiredArgsConstructor +public class AnimationState implements Component { + private State currentState; + private State transition; + + private final Map animationStates; + + @Nullable + public AnimationGraphic getTransitionAnimation() { + if (transition == null) { + return null; + } + return animationStates.get(transition); + } + + public enum State { + IDLE, + RUN, + JUMP, + DAMAGE + } +} diff --git a/core/src/main/java/coffee/liz/dyl/components/health/DamageGraceTime.java b/core/src/main/java/coffee/liz/dyl/components/health/DamageGraceTime.java new file mode 100644 index 0000000..ca16f63 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/health/DamageGraceTime.java @@ -0,0 +1,17 @@ +package coffee.liz.dyl.components.health; + +import coffee.liz.ecs.model.Component; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class DamageGraceTime implements Component { + private float duration; + + public void decrement(final float deltaTime) { + duration -= deltaTime; + } + + public boolean isOver() { + return duration <= 0; + } +} diff --git a/core/src/main/java/coffee/liz/dyl/components/health/RelativeDamageBox.java b/core/src/main/java/coffee/liz/dyl/components/health/RelativeDamageBox.java new file mode 100644 index 0000000..e290d4f --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/health/RelativeDamageBox.java @@ -0,0 +1,10 @@ +package coffee.liz.dyl.components.health; + +import coffee.liz.ecs.model.Component; + +public class RelativeDamageBox implements Component { + // TODO: Bounding box relative to sprite bounding box + // A damage system will check that the damaging entity collides with the physics bounding box and then that it + // also collides with this component, so we don't have to index the grid twice. + // private final Vec2 relativeDimensions; // To fill the center +} diff --git a/core/src/main/java/coffee/liz/dyl/components/physics/CollisionContacts.java b/core/src/main/java/coffee/liz/dyl/components/physics/CollisionContacts.java index 49ab7f6..033c3b6 100644 --- a/core/src/main/java/coffee/liz/dyl/components/physics/CollisionContacts.java +++ b/core/src/main/java/coffee/liz/dyl/components/physics/CollisionContacts.java @@ -12,16 +12,20 @@ import java.util.List; @Getter public class CollisionContacts implements Component { - @Getter - @RequiredArgsConstructor - public class Contact { - private final Entity contactEntity; - private final Vec2 penetrationVector; + private final List contacts = new ArrayList<>(); + + public void clear() { + contacts.clear(); } - private final List contacts = new ArrayList<>(); + public void add(final Entity entity, final Vec2 separationVector) { + contacts.add(new Contact(entity, separationVector)); + } - public void add(final Entity entity, final Vec2 penetrationVector) { - contacts.add(new Contact(entity, penetrationVector)); + @Getter + @RequiredArgsConstructor + public static class Contact { + private final Entity contactEntity; + private final Vec2 separationVector; } } diff --git a/core/src/main/java/coffee/liz/dyl/components/physics/Jump.java b/core/src/main/java/coffee/liz/dyl/components/physics/Jump.java index 79d2f78..c791040 100644 --- a/core/src/main/java/coffee/liz/dyl/components/physics/Jump.java +++ b/core/src/main/java/coffee/liz/dyl/components/physics/Jump.java @@ -4,15 +4,12 @@ import coffee.liz.ecs.model.Component; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; import lombok.Setter; -import java.time.Instant; - @NoArgsConstructor @AllArgsConstructor @Getter @Setter public class Jump implements Component { - private Instant jumpStart; + private float startTime; } diff --git a/core/src/main/java/coffee/liz/dyl/components/physics/Jumpable.java b/core/src/main/java/coffee/liz/dyl/components/physics/Jumpable.java index d35464a..4ce8e74 100644 --- a/core/src/main/java/coffee/liz/dyl/components/physics/Jumpable.java +++ b/core/src/main/java/coffee/liz/dyl/components/physics/Jumpable.java @@ -1,16 +1,6 @@ package coffee.liz.dyl.components.physics; import coffee.liz.ecs.model.Component; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -import java.time.Instant; - -@AllArgsConstructor -@Getter -@Setter public class Jumpable implements Component { - private boolean canPhysicallyJump = false; - private Instant jumpRequestTime; } diff --git a/core/src/main/java/coffee/liz/dyl/components/physics/Solid.java b/core/src/main/java/coffee/liz/dyl/components/physics/Solid.java index 57af1e3..c9d4a78 100644 --- a/core/src/main/java/coffee/liz/dyl/components/physics/Solid.java +++ b/core/src/main/java/coffee/liz/dyl/components/physics/Solid.java @@ -2,4 +2,5 @@ package coffee.liz.dyl.components.physics; import coffee.liz.ecs.model.Component; -public class Solid implements Component {} +public class Solid implements Component { +} diff --git a/core/src/main/java/coffee/liz/dyl/config/PhysicsConstants.java b/core/src/main/java/coffee/liz/dyl/config/PhysicsConstants.java index 3019160..6414629 100644 --- a/core/src/main/java/coffee/liz/dyl/config/PhysicsConstants.java +++ b/core/src/main/java/coffee/liz/dyl/config/PhysicsConstants.java @@ -1,14 +1,12 @@ package coffee.liz.dyl.config; -import java.time.Duration; - public final class PhysicsConstants { public static final float GRAVITY = 40f; public static final float ADDITIONAL_JUMP_OVER_GRAVITY = 1.05f; public static final float MOVE_SPEED = 8.0f; public static final float JUMP_INITIAL_VEL = 10.0f; - public static final Duration MAX_JUMP = Duration.ofMillis(100); - public static final Duration JUMP_BUFFER = Duration.ofMillis(400); + public static final float MAX_JUMP_SECONDS = 0.1f; + public static final float JUMP_BUFFER_SECONDS = 0.2f; public static final float JUMP_CUT_FACTOR = 0.5f; public static final float DAMAGE_TIME = 0.25f; diff --git a/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java b/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java index ffd55f6..e5e8855 100644 --- a/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java +++ b/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java @@ -1,17 +1,12 @@ package coffee.liz.dyl.entities; -import coffee.liz.dyl.components.AnimationState; -import coffee.liz.dyl.components.Controllable; -import coffee.liz.dyl.components.FacingDirection; +import coffee.liz.dyl.components.control.ActionQueue; +import coffee.liz.dyl.components.graphic.AnimationState; +import coffee.liz.dyl.components.control.Controllable; +import coffee.liz.dyl.components.control.FacingDirection; import coffee.liz.dyl.components.graphic.AnimationGraphic; -import coffee.liz.dyl.components.physics.Jumpable; +import coffee.liz.dyl.components.physics.*; import coffee.liz.dyl.config.PhysicsConstants; -import coffee.liz.dyl.components.physics.BoundingBox; -import coffee.liz.dyl.components.physics.Forces; -import coffee.liz.dyl.components.physics.Gravity; -import coffee.liz.dyl.components.physics.Jump; -import coffee.liz.dyl.components.physics.Mass; -import coffee.liz.dyl.components.physics.Velocity; import coffee.liz.ecs.math.Vec2; import coffee.liz.ecs.math.Vec2f; import coffee.liz.ecs.math.Vec2i; @@ -41,12 +36,14 @@ public class PlayerFactory { .add(animationState.getAnimationStates().get(AnimationState.State.IDLE)) .add(new Controllable()) .add(new BoundingBox(new Vec2f(1f, 1f), PlayerAssets.PLAYER_DIMS.floatValue().scale(1/6f, 1/6f))) + .add(new CollisionContacts()) + .add(new ActionQueue()) .add(new Velocity(Vec2f.ZERO)) .add(new Mass(0.8f)) .add(new Forces()) .add(new FacingDirection(Vec2f.ZERO)) .add(new Gravity(-20f)) - .add(new Jumpable(false, null)); + .add(new Jumpable()); } private static class PlayerAssets { diff --git a/core/src/main/java/coffee/liz/dyl/systems/AnimationSystem.java b/core/src/main/java/coffee/liz/dyl/systems/AnimationSystem.java deleted file mode 100644 index a932eea..0000000 --- a/core/src/main/java/coffee/liz/dyl/systems/AnimationSystem.java +++ /dev/null @@ -1,55 +0,0 @@ -package coffee.liz.dyl.systems; - -import coffee.liz.dyl.components.AnimationState; -import coffee.liz.dyl.components.FacingDirection; -import coffee.liz.dyl.components.graphic.AnimationGraphic; -import coffee.liz.dyl.components.graphic.Graphic; -import coffee.liz.dyl.components.physics.Jump; -import coffee.liz.dyl.components.physics.Velocity; -import coffee.liz.dyl.systems.physics.CollisionSystem; -import coffee.liz.ecs.model.System; -import coffee.liz.ecs.model.World; - -import java.util.Collection; -import java.util.List; - -public class AnimationSystem implements System { - @Override - public Collection> getDependencies() { - return List.of(InputSystem.class, CollisionSystem.class, DamageSystem.class); - } - - @Override - public void update(final World world, final float deltaSeconds) { - world.queryable().allOf(AnimationState.class, Velocity.class).forEach(entity -> { - final AnimationState animationState = entity.get(AnimationState.class); - final Velocity velocity = entity.get(Velocity.class); - - if (entity.has(Jump.class)) { - animationState.setTransition(AnimationState.State.JUMP); - return; - } - if (velocity.getVelocity().getX() != 0) { - animationState.setTransition(AnimationState.State.RUN); - return; - } - animationState.setTransition(AnimationState.State.IDLE); - }); - - world.queryable().allOf(AnimationState.class).forEach(entity -> { - final AnimationState animationState = entity.get(AnimationState.class); - final AnimationGraphic animation = animationState.getTransitionAnimation(); - if (animation == null) { - return; - } - if (entity.has(FacingDirection.class)) { - animation.setFacing(entity.get(FacingDirection.class).getUnitDirection()); - } - if (animationState.getTransition().equals(animationState.getCurrentState())) { - return; - } - - entity.add(animation); - }); - } -} diff --git a/core/src/main/java/coffee/liz/dyl/systems/DamageSystem.java b/core/src/main/java/coffee/liz/dyl/systems/DamageSystem.java deleted file mode 100644 index 60db760..0000000 --- a/core/src/main/java/coffee/liz/dyl/systems/DamageSystem.java +++ /dev/null @@ -1,23 +0,0 @@ -package coffee.liz.dyl.systems; - -import coffee.liz.dyl.systems.physics.CollisionSystem; -import coffee.liz.ecs.model.System; -import coffee.liz.ecs.model.World; - -import java.util.Collection; -import java.util.List; - -public class DamageSystem implements System { - - @Override - public Collection> getDependencies() { - return List.of(CollisionSystem.class); - } - - @Override - public void update(final World world, final float deltaSeconds) { -// world.queryable().allOf(CollisionContacts.class) -// world.queryable().allOf(DamageGraceTime.class).forEach() - // TODO: Set opacity - } -} diff --git a/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java b/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java deleted file mode 100644 index ea0e574..0000000 --- a/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java +++ /dev/null @@ -1,79 +0,0 @@ -package coffee.liz.dyl.systems; - -import coffee.liz.dyl.components.Controllable; -import coffee.liz.dyl.components.FacingDirection; -import coffee.liz.dyl.components.physics.Jumpable; -import coffee.liz.dyl.config.KeyBinds; -import coffee.liz.dyl.components.physics.Jump; -import coffee.liz.dyl.components.physics.Velocity; -import coffee.liz.ecs.math.Vec2f; -import coffee.liz.ecs.model.System; -import coffee.liz.ecs.model.World; -import lombok.RequiredArgsConstructor; - -import java.time.Duration; -import java.time.Instant; -import java.util.Collection; -import java.util.Set; -import java.util.function.Supplier; - -import static coffee.liz.dyl.config.PhysicsConstants.*; - -@RequiredArgsConstructor -public class InputSystem implements System { - private final Supplier> activeActions; - - @Override - public Collection> getDependencies() { - return Set.of(); - } - - @Override - public void update(final World world, final float deltaSeconds) { - final Set actions = activeActions.get(); - - world.queryable().allOf(Controllable.class, Velocity.class).forEach(entity -> { - final Velocity velocity = entity.get(Velocity.class); - - float dx = 0f; - if (actions.contains(KeyBinds.Action.MOVE_LEFT)) dx -= MOVE_SPEED; - if (actions.contains(KeyBinds.Action.MOVE_RIGHT)) dx += MOVE_SPEED; - if (dx != 0) { - entity.add(new FacingDirection(new Vec2f(dx / MOVE_SPEED, 0f))); - } - - final Instant now = Instant.now(); - final Jumpable jumpable = entity.get(Jumpable.class); - final boolean canRequestJump = jumpable.getJumpRequestTime() == null; - final boolean jumpRequested = actions.contains(KeyBinds.Action.JUMP); - final boolean hasCurrentJump = entity.has(Jump.class); - - float dy = velocity.getVelocity().getY(); - - // buffer only fires on a released press — holding never re-triggers - final boolean withinBuffer = !canRequestJump && !jumpRequested - && Duration.between(jumpable.getJumpRequestTime(), now).compareTo(JUMP_BUFFER) <= 0; - if (jumpable.isCanPhysicallyJump() && !hasCurrentJump && (canRequestJump && jumpRequested || withinBuffer)) { - entity.add(new Jump(now)); - dy = JUMP_INITIAL_VEL; - jumpable.setJumpRequestTime(now); // mark consumed; keeps canRequestJump false while held - } else if (!jumpRequested && hasCurrentJump && dy > 0) { - dy *= JUMP_CUT_FACTOR; - } - - // record an aerial press for buffering (ground presses are recorded above on initiation) - if (!jumpable.isCanPhysicallyJump() && canRequestJump && jumpRequested && !entity.has(Jump.class)) { - jumpable.setJumpRequestTime(now); - } else if (!jumpRequested && !entity.has(Jump.class)) { - if (jumpable.isCanPhysicallyJump()) { - jumpable.setJumpRequestTime(null); // released on ground — ready for next press - } else if (!canRequestJump - && Duration.between(jumpable.getJumpRequestTime(), now).compareTo(JUMP_BUFFER) > 0) { - jumpable.setJumpRequestTime(null); // aerial, buffer expired - } - } - - velocity.setVelocity(new Vec2f(dx, dy)); - }); - } -} diff --git a/core/src/main/java/coffee/liz/dyl/systems/JumpSystem.java b/core/src/main/java/coffee/liz/dyl/systems/JumpSystem.java deleted file mode 100644 index d1552f5..0000000 --- a/core/src/main/java/coffee/liz/dyl/systems/JumpSystem.java +++ /dev/null @@ -1,43 +0,0 @@ -package coffee.liz.dyl.systems; - -import coffee.liz.dyl.components.physics.CollisionContacts; -import coffee.liz.dyl.components.physics.Jump; -import coffee.liz.dyl.components.physics.Jumpable; -import coffee.liz.dyl.components.physics.Velocity; -import coffee.liz.dyl.systems.physics.CollisionSystem; -import coffee.liz.ecs.model.System; -import coffee.liz.ecs.model.World; - -import java.util.Collection; -import java.util.List; - -public class JumpSystem implements System { - @Override - public Collection> getDependencies() { - return List.of(CollisionSystem.class); - } - - @Override - public void update(final World world, final float deltaSeconds) { - world.queryable().allOf(Jumpable.class).forEach(entity -> { - entity.get(Jumpable.class).setCanPhysicallyJump(false); - }); - world.queryable().allOf(Jumpable.class, CollisionContacts.class).forEach(entity -> { - final CollisionContacts contacts = entity.get(CollisionContacts.class); - final Jumpable jumpable = entity.get(Jumpable.class); - contacts.getContacts().forEach(contact -> { - final boolean solidUnderUs = contact.getPenetrationVector().getY() <= 0; - if (!solidUnderUs) { - return; - } - jumpable.setCanPhysicallyJump(true); - - final boolean currentJumpOver = entity.has(Jump.class) && entity.has(Velocity.class) - && entity.get(Velocity.class).getVelocity().getY() <= 0; - if (currentJumpOver) { - entity.remove(Jump.class); - } - }); - }); - } -} diff --git a/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java b/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java deleted file mode 100644 index 1624d0f..0000000 --- a/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java +++ /dev/null @@ -1,52 +0,0 @@ -package coffee.liz.dyl.systems; - -import coffee.liz.dyl.components.graphic.Graphic; -import coffee.liz.dyl.components.physics.BoundingBox; -import coffee.liz.ecs.model.System; -import coffee.liz.ecs.model.World; -import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.graphics.OrthographicCamera; -import com.badlogic.gdx.graphics.g2d.Batch; -import com.badlogic.gdx.utils.viewport.Viewport; - -import java.util.Collection; -import java.util.Comparator; -import java.util.Set; - -public class RenderSystem implements System { - private final Batch batch; - private final OrthographicCamera camera; - private final Viewport viewport; - - public RenderSystem(final Batch batch, final Viewport viewport) { - this.batch = batch; - this.viewport = viewport; - this.camera = (OrthographicCamera) viewport.getCamera(); - } - - @Override - public Collection> getDependencies() { - return Set.of(AnimationSystem.class); - } - - @Override - public void update(final World world, final float deltaSeconds) { - viewport.apply(); - camera.update(); - - batch.setProjectionMatrix(camera.combined); - batch.begin(); - batch.setColor(Color.WHITE); - - world.queryable().allOf(BoundingBox.class, Graphic.class).stream() - .sorted(Comparator.comparingDouble(e -> e.get(Graphic.class).getZ())) - .forEach(e -> { - final BoundingBox boundingBox = e.get(BoundingBox.class); - final Graphic graphic = e.get(Graphic.class); - graphic.draw(batch, boundingBox); - }); - - batch.setColor(Color.WHITE); - batch.end(); - } -} diff --git a/core/src/main/java/coffee/liz/dyl/systems/collision/SolidCollisionSystem.java b/core/src/main/java/coffee/liz/dyl/systems/collision/SolidCollisionSystem.java new file mode 100644 index 0000000..6f3697f --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/collision/SolidCollisionSystem.java @@ -0,0 +1,58 @@ +package coffee.liz.dyl.systems.collision; + +import coffee.liz.dyl.components.physics.BoundingBox; +import coffee.liz.dyl.components.physics.CollisionContacts; +import coffee.liz.dyl.components.physics.Solid; +import coffee.liz.dyl.components.physics.Velocity; +import coffee.liz.dyl.systems.physics.CollisionSystem; +import coffee.liz.ecs.math.Vec2; +import coffee.liz.ecs.math.Vec2f; +import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; + +import java.util.Collection; +import java.util.List; + +public class SolidCollisionSystem implements System { + @Override + public Collection> getDependencies() { + return List.of(CollisionSystem.class); + } + + @Override + public void update(final World world, final float deltaSeconds) { + world.queryable().allOf(CollisionContacts.class, BoundingBox.class, Velocity.class).forEach(me -> { + final CollisionContacts contacts = me.get(CollisionContacts.class); + contacts.getContacts().forEach(contact -> { + final Entity them = contact.getContactEntity(); + if (!them.has(Solid.class)) { + return; + } + + applyNormalImpulse(me, contact); + }); + }); + } + + private void applyNormalImpulse(final Entity me, final CollisionContacts.Contact contact) { + final float sLen = contact.getSeparationVector().length(); + if (sLen < 0.0001f) { + return; // Just touching, no separation needed + } + + final BoundingBox meBox = me.get(BoundingBox.class); + meBox.setPosition(meBox.getPosition().plus(contact.getSeparationVector())); + final float nx = contact.getSeparationVector().getX() / sLen; + final float ny = contact.getSeparationVector().getY() / sLen; + + final Vec2 vel = me.get(Velocity.class).getVelocity(); + final float velIntoWall = -(vel.getX() * nx + vel.getY() * ny); + if (velIntoWall > 0f) { + me.get(Velocity.class).setVelocity(new Vec2f( + vel.getX() + nx * velIntoWall, + vel.getY() + ny * velIntoWall + )); + } + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/control/InputSystem.java b/core/src/main/java/coffee/liz/dyl/systems/control/InputSystem.java new file mode 100644 index 0000000..ca114ee --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/control/InputSystem.java @@ -0,0 +1,51 @@ +package coffee.liz.dyl.systems.control; + +import coffee.liz.dyl.actions.JumpAction; +import coffee.liz.dyl.actions.MoveAction; +import coffee.liz.dyl.components.control.ActionQueue; +import coffee.liz.dyl.components.control.Controllable; +import coffee.liz.dyl.components.physics.Jumpable; +import coffee.liz.dyl.config.KeyBinds; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.Set; +import java.util.function.Supplier; + +import static coffee.liz.dyl.config.PhysicsConstants.*; + +@RequiredArgsConstructor +public class InputSystem implements System { + private final Supplier> heldActions; + private final Supplier> justPressedActions; + + @Override + public Collection> getDependencies() { + return Set.of(); + } + + @Override + public void update(final World world, final float deltaSeconds) { + final float time = world.getTime(); + final Set held = heldActions.get(); + final Set justPressed = justPressedActions.get(); + + world.queryable().allOf(Controllable.class, ActionQueue.class).forEach(entity -> { + final ActionQueue actionQueue = entity.get(ActionQueue.class); + + final boolean moveLeft = held.contains(KeyBinds.Action.MOVE_LEFT); + final boolean moveRight = held.contains(KeyBinds.Action.MOVE_RIGHT); + if (moveLeft != moveRight) { + final MoveAction action = moveLeft ? new MoveAction(MoveAction.Direction.LEFT) + : new MoveAction(MoveAction.Direction.RIGHT); + actionQueue.queue(action, time); + } + + if (entity.has(Jumpable.class) && justPressed.contains(KeyBinds.Action.JUMP)) { + actionQueue.queue(new JumpAction(), time, JUMP_BUFFER_SECONDS); + } + }); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/control/JumpActionConsumerSystem.java b/core/src/main/java/coffee/liz/dyl/systems/control/JumpActionConsumerSystem.java new file mode 100644 index 0000000..eadc402 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/control/JumpActionConsumerSystem.java @@ -0,0 +1,58 @@ +package coffee.liz.dyl.systems.control; + +import coffee.liz.dyl.actions.JumpAction; +import coffee.liz.dyl.components.control.ActionQueue; +import coffee.liz.dyl.components.physics.*; +import coffee.liz.dyl.config.PhysicsConstants; +import coffee.liz.dyl.systems.collision.SolidCollisionSystem; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; + +import java.util.Collection; +import java.util.List; + +public class JumpActionConsumerSystem implements System { + @Override + public Collection> getDependencies() { + return List.of(InputSystem.class, SolidCollisionSystem.class); + } + + @Override + public void update(final World world, final float deltaSeconds) { + final float time = world.getTime(); + + world.queryable().allOf(Jumpable.class, Velocity.class, CollisionContacts.class, ActionQueue.class).forEach(entity -> { + final ActionQueue queue = entity.get(ActionQueue.class); + final Velocity velocity = entity.get(Velocity.class); + final CollisionContacts contacts = entity.get(CollisionContacts.class); + final boolean solidOverUs = contacts.getContacts().stream() + .anyMatch(contact -> contact.getSeparationVector().getY() < 0 && contact.getContactEntity().has(Solid.class)); + final boolean solidUnderUs = contacts.getContacts().stream() + .anyMatch(contact -> contact.getSeparationVector().getY() > 0 && contact.getContactEntity().has(Solid.class)); + final boolean hasCurrentJump = entity.has(Jump.class); + + final boolean bonkedIntoCeiling = solidOverUs && hasCurrentJump; + final boolean justHitFloor = solidUnderUs && hasCurrentJump; + final boolean notJumping = !hasCurrentJump || bonkedIntoCeiling || justHitFloor; + if (notJumping) { + entity.remove(Jump.class); + } + + queue.consume(JumpAction.class, time, _action -> { + if (notJumping && solidUnderUs) { + entity.add(new Jump(time)); + } + if (entity.has(Jump.class)) { + final Jump jump = entity.get(Jump.class); + final boolean inTimeWindowToApplyJump = (time - jump.getStartTime()) <= PhysicsConstants.MAX_JUMP_SECONDS; + if (inTimeWindowToApplyJump) { + velocity.setVelocity(velocity.getVelocity() + .transform(x -> x, y -> PhysicsConstants.JUMP_INITIAL_VEL)); + return true; + } + } + return false; + }); + }); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/control/MovementActionConsumerSystem.java b/core/src/main/java/coffee/liz/dyl/systems/control/MovementActionConsumerSystem.java new file mode 100644 index 0000000..9791cfb --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/control/MovementActionConsumerSystem.java @@ -0,0 +1,48 @@ +package coffee.liz.dyl.systems.control; + +import coffee.liz.dyl.actions.MoveAction; +import coffee.liz.dyl.components.control.ActionQueue; +import coffee.liz.dyl.components.control.FacingDirection; +import coffee.liz.dyl.components.physics.Velocity; +import coffee.liz.dyl.config.PhysicsConstants; +import coffee.liz.ecs.math.Vec2f; +import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; + +import java.util.Collection; +import java.util.List; + +public class MovementActionConsumerSystem implements System { + @Override + public Collection> getDependencies() { + return List.of(InputSystem.class); + } + + @Override + public void update(final World world, final float deltaSeconds) { + final float time = world.getTime(); + + world.queryable().allOf(ActionQueue.class, Velocity.class, FacingDirection.class).forEach(entity -> { + final ActionQueue actionQueue = entity.get(ActionQueue.class); + if (!actionQueue.has(MoveAction.class)) { + final Velocity velocity = entity.get(Velocity.class); + velocity.setVelocity(velocity.getVelocity().transform(_x -> 0f, y -> y)); + return; + } + actionQueue.consume(MoveAction.class, time, (a) -> consumeAction(entity, a)); + }); + } + + private boolean consumeAction(final Entity entity, final MoveAction action) { + final Velocity velocity = entity.get(Velocity.class); + final FacingDirection facingDirection = entity.get(FacingDirection.class); + final float dx = switch(action.getDirection()) { + case LEFT -> -PhysicsConstants.MOVE_SPEED; + case RIGHT -> PhysicsConstants.MOVE_SPEED; + }; + facingDirection.setUnitDirection(new Vec2f(dx / PhysicsConstants.MOVE_SPEED, 0f)); + velocity.setVelocity(velocity.getVelocity().transform(_x -> dx, y -> y)); + return true; + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/graphics/AnimationSystem.java b/core/src/main/java/coffee/liz/dyl/systems/graphics/AnimationSystem.java new file mode 100644 index 0000000..a1e4874 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/graphics/AnimationSystem.java @@ -0,0 +1,56 @@ +package coffee.liz.dyl.systems.graphics; + +import coffee.liz.dyl.components.graphic.AnimationState; +import coffee.liz.dyl.components.control.FacingDirection; +import coffee.liz.dyl.components.graphic.AnimationGraphic; +import coffee.liz.dyl.components.physics.Jump; +import coffee.liz.dyl.components.physics.Velocity; +import coffee.liz.dyl.systems.health.DamageSystem; +import coffee.liz.dyl.systems.control.InputSystem; +import coffee.liz.dyl.systems.physics.CollisionSystem; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; + +import java.util.Collection; +import java.util.List; + +public class AnimationSystem implements System { + @Override + public Collection> getDependencies() { + return List.of(InputSystem.class, CollisionSystem.class, DamageSystem.class); + } + + @Override + public void update(final World world, final float deltaSeconds) { + world.queryable().allOf(AnimationState.class, Velocity.class).forEach(entity -> { + final AnimationState animationState = entity.get(AnimationState.class); + final Velocity velocity = entity.get(Velocity.class); + + if (entity.has(Jump.class)) { + animationState.setTransition(AnimationState.State.JUMP); + return; + } + if (velocity.getVelocity().getX() != 0) { + animationState.setTransition(AnimationState.State.RUN); + return; + } + animationState.setTransition(AnimationState.State.IDLE); + }); + + world.queryable().allOf(AnimationState.class).forEach(entity -> { + final AnimationState animationState = entity.get(AnimationState.class); + final AnimationGraphic animation = animationState.getTransitionAnimation(); + if (animation == null) { + return; + } + if (entity.has(FacingDirection.class)) { + animation.setFacing(entity.get(FacingDirection.class).getUnitDirection()); + } + if (animationState.getTransition().equals(animationState.getCurrentState())) { + return; + } + + entity.add(animation); + }); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/graphics/RenderSystem.java b/core/src/main/java/coffee/liz/dyl/systems/graphics/RenderSystem.java new file mode 100644 index 0000000..5897c65 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/graphics/RenderSystem.java @@ -0,0 +1,52 @@ +package coffee.liz.dyl.systems.graphics; + +import coffee.liz.dyl.components.graphic.Graphic; +import coffee.liz.dyl.components.physics.BoundingBox; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.utils.viewport.Viewport; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Set; + +public class RenderSystem implements System { + private final Batch batch; + private final OrthographicCamera camera; + private final Viewport viewport; + + public RenderSystem(final Batch batch, final Viewport viewport) { + this.batch = batch; + this.viewport = viewport; + this.camera = (OrthographicCamera) viewport.getCamera(); + } + + @Override + public Collection> getDependencies() { + return Set.of(AnimationSystem.class); + } + + @Override + public void update(final World world, final float deltaSeconds) { + viewport.apply(); + camera.update(); + + batch.setProjectionMatrix(camera.combined); + batch.begin(); + batch.setColor(Color.WHITE); + + world.queryable().allOf(BoundingBox.class, Graphic.class).stream() + .sorted(Comparator.comparingDouble(e -> e.get(Graphic.class).getZ())) + .forEach(e -> { + final BoundingBox boundingBox = e.get(BoundingBox.class); + final Graphic graphic = e.get(Graphic.class); + graphic.draw(batch, boundingBox); + }); + + batch.setColor(Color.WHITE); + batch.end(); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/health/DamageSystem.java b/core/src/main/java/coffee/liz/dyl/systems/health/DamageSystem.java new file mode 100644 index 0000000..b8ac306 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/health/DamageSystem.java @@ -0,0 +1,23 @@ +package coffee.liz.dyl.systems.health; + +import coffee.liz.dyl.systems.physics.CollisionSystem; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; + +import java.util.Collection; +import java.util.List; + +public class DamageSystem implements System { + + @Override + public Collection> getDependencies() { + return List.of(CollisionSystem.class); + } + + @Override + public void update(final World world, final float deltaSeconds) { +// world.queryable().allOf(CollisionContacts.class) +// world.queryable().allOf(DamageGraceTime.class).forEach() + // TODO: Set opacity + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/physics/AccelerationSystem.java b/core/src/main/java/coffee/liz/dyl/systems/physics/AccelerationSystem.java index 519cc3f..c3c61d0 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/physics/AccelerationSystem.java +++ b/core/src/main/java/coffee/liz/dyl/systems/physics/AccelerationSystem.java @@ -7,7 +7,7 @@ import coffee.liz.dyl.components.physics.Jump; import coffee.liz.dyl.components.physics.Mass; import coffee.liz.dyl.components.physics.Velocity; import coffee.liz.dyl.config.PhysicsConstants; -import coffee.liz.dyl.systems.InputSystem; +import coffee.liz.dyl.systems.control.InputSystem; import coffee.liz.ecs.math.Vec2; import coffee.liz.ecs.math.Vec2f; import coffee.liz.ecs.model.System; diff --git a/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionGrid.java b/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionGrid.java index fb14b70..617fc22 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionGrid.java +++ b/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionGrid.java @@ -2,22 +2,29 @@ package coffee.liz.dyl.systems.physics; import coffee.liz.dyl.components.physics.BoundingBox; import coffee.liz.ecs.math.Vec2; +import coffee.liz.ecs.math.Vec2f; import coffee.liz.ecs.math.Vec2i; import coffee.liz.ecs.model.Entity; import lombok.Getter; -import lombok.Setter; +import lombok.RequiredArgsConstructor; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; -@Setter @Getter +@RequiredArgsConstructor public class CollisionGrid { + private final Vec2 cellSize; private final Map> cells = new HashMap<>(); private Vec2 origin; - private Vec2 cellSize; + + public void updateOrigin(final Collection> points) { + float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; + for (final Vec2 pos : points) { + minX = Math.min(minX, pos.getX()); + minY = Math.min(minY, pos.getY()); + } + this.origin = new Vec2f(minX, minY); + } public void clear() { cells.clear(); diff --git a/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionSystem.java b/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionSystem.java index fb98151..099839e 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionSystem.java +++ b/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionSystem.java @@ -2,9 +2,7 @@ package coffee.liz.dyl.systems.physics; import coffee.liz.dyl.components.physics.BoundingBox; import coffee.liz.dyl.components.physics.CollisionContacts; -import coffee.liz.dyl.components.physics.Solid; import coffee.liz.dyl.components.physics.Velocity; -import coffee.liz.dyl.systems.InputSystem; import coffee.liz.ecs.math.Vec2; import coffee.liz.ecs.math.Vec2f; import coffee.liz.ecs.model.Entity; @@ -13,12 +11,14 @@ import coffee.liz.ecs.model.World; import lombok.RequiredArgsConstructor; import java.util.Collection; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; @RequiredArgsConstructor public class CollisionSystem implements System { - private float cellSize = 3f; - private final CollisionGrid grid = new CollisionGrid(); + private static final Vec2 CELL_SIZE = new Vec2f(3f, 3f); + private final CollisionGrid grid = new CollisionGrid(CELL_SIZE); @Override public Collection> getDependencies() { @@ -27,20 +27,19 @@ public class CollisionSystem implements System { @Override public void update(final World world, final float deltaSeconds) { - world.queryable().allOf(CollisionContacts.class) - .forEach(e -> e.remove(CollisionContacts.class)); - final Set allBoxEntities = world.queryable().allOf(BoundingBox.class); if (allBoxEntities.isEmpty()) { return; } + final List allCollidingEntities = allBoxEntities.stream().filter(e -> e.has(CollisionContacts.class)).toList(); + allCollidingEntities.forEach(e -> e.get(CollisionContacts.class).clear()); grid.clear(); - final Vec2 origin = getOrigin(allBoxEntities.stream() + + final List> entityPositions = allBoxEntities.stream() .map(e -> e.get(BoundingBox.class).getPosition()) - .toList()); - grid.setOrigin(origin); - grid.setCellSize(new Vec2f(cellSize, cellSize)); + .toList(); + grid.updateOrigin(entityPositions); for (final Entity entity : allBoxEntities) { grid.insert(entity); } @@ -51,69 +50,31 @@ public class CollisionSystem implements System { if (me.equals(them)) { continue; } - if (!meBox.isCollidingWith(them.get(BoundingBox.class))) { + final BoundingBox themBox = them.get(BoundingBox.class); + if (!meBox.isCollidingWith(themBox)) { continue; } - if (them.has(Solid.class)) { - resolveCollision(me, them); + if (me.has(CollisionContacts.class)) { + me.get(CollisionContacts.class).add(them, getSeparationVector(meBox, themBox)); + } + if (them.has(CollisionContacts.class)) { + them.get(CollisionContacts.class).add(me, getSeparationVector(themBox, meBox)); } } } } - - private void resolveCollision(final Entity me, final Entity them) { - final BoundingBox meBox = me.get(BoundingBox.class); - final BoundingBox themBox = them.get(BoundingBox.class); - - final Vec2 mtv = getPenetrationVector(meBox, themBox); - contacts(me).add(them, getPenetrationVector(themBox, meBox)); - contacts(them).add(me, mtv); - meBox.setPosition(meBox.getPosition().plus(mtv)); - - // Cancel the velocity component pressing into the solid (normal impulse) - final float mtvLen = mtv.length(); - final float nx = mtv.getX() / mtvLen; - final float ny = mtv.getY() / mtvLen; - - final Vec2 vel = me.get(Velocity.class).getVelocity(); - final float velIntoWall = -(vel.getX() * nx + vel.getY() * ny); - if (velIntoWall > 0f) { - me.get(Velocity.class).setVelocity(new Vec2f( - vel.getX() + nx * velIntoWall, - vel.getY() + ny * velIntoWall - )); - } - } - - private Vec2 getPenetrationVector(final BoundingBox meBox, final BoundingBox themBox) { + private Vec2 getSeparationVector(final BoundingBox meBox, final BoundingBox themBox) { final float fromLeft = meBox.getRight() - themBox.getPosition().getX(); final float fromRight = themBox.getRight() - meBox.getPosition().getX(); final float fromBottom = meBox.getTop() - themBox.getPosition().getY(); final float fromTop = themBox.getTop() - meBox.getPosition().getY(); - final float penetrationX = fromLeft < fromRight ? -fromLeft : fromRight; - final float penetrationY = fromBottom < fromTop ? -fromBottom : fromTop; - - final Vec2 minimumTranslationVector = Math.abs(penetrationX) <= Math.abs(penetrationY) - ? new Vec2f(penetrationX, 0f) - : new Vec2f(0f, penetrationY); - return minimumTranslationVector; - } - - private CollisionContacts contacts(final Entity entity) { - if (!entity.has(CollisionContacts.class)) { - entity.add(new CollisionContacts()); - } - return entity.get(CollisionContacts.class); - } + final float separationX = fromLeft < fromRight ? -fromLeft : fromRight; + final float separationY = fromBottom < fromTop ? -fromBottom : fromTop; - private Vec2 getOrigin(final Collection> positions) { - float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; - for (final Vec2 pos : positions) { - minX = Math.min(minX, pos.getX()); - minY = Math.min(minY, pos.getY()); - } - return new Vec2f(minX, minY); + return Math.abs(separationX) <= Math.abs(separationY) + ? new Vec2f(separationX, 0f) + : new Vec2f(0f, separationY); } } diff --git a/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java index fd8e282..fb1f847 100644 --- a/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java +++ b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java @@ -4,11 +4,13 @@ import coffee.liz.dyl.DylGame; import coffee.liz.dyl.config.PhysicsConstants; import coffee.liz.dyl.entities.FloorFactory; import coffee.liz.dyl.entities.PlayerFactory; -import coffee.liz.dyl.systems.AnimationSystem; -import coffee.liz.dyl.systems.DamageSystem; -import coffee.liz.dyl.systems.InputSystem; -import coffee.liz.dyl.systems.JumpSystem; -import coffee.liz.dyl.systems.RenderSystem; +import coffee.liz.dyl.systems.collision.SolidCollisionSystem; +import coffee.liz.dyl.systems.control.MovementActionConsumerSystem; +import coffee.liz.dyl.systems.graphics.AnimationSystem; +import coffee.liz.dyl.systems.health.DamageSystem; +import coffee.liz.dyl.systems.control.InputSystem; +import coffee.liz.dyl.systems.control.JumpActionConsumerSystem; +import coffee.liz.dyl.systems.graphics.RenderSystem; import coffee.liz.ecs.DAGWorld; import coffee.liz.dyl.components.physics.BoundingBox; import coffee.liz.dyl.components.physics.Solid; @@ -21,14 +23,18 @@ import com.badlogic.gdx.Gdx; public class DylGameWorld extends DAGWorld { public DylGameWorld(final DylGame game) { super( - new InputSystem(() -> game.getSettings().getKeyBinds().filterActiveActions(Gdx.input::isKeyPressed)), + new InputSystem( + () -> game.getSettings().getKeyBinds().filterActiveActions(Gdx.input::isKeyPressed), + () -> game.getSettings().getKeyBinds().filterActiveActions(Gdx.input::isKeyJustPressed)), new AccelerationSystem(PhysicsConstants.GRAVITY), new MovementSystem(), new CollisionSystem(), + new SolidCollisionSystem(), new DamageSystem(), new AnimationSystem(), new RenderSystem(game.getBatch(), game.getViewport()), - new JumpSystem() + new JumpActionConsumerSystem(), + new MovementActionConsumerSystem() ); PlayerFactory.addTo(this); FloorFactory.addTo(this); diff --git a/core/src/main/java/coffee/liz/ecs/DAGWorld.java b/core/src/main/java/coffee/liz/ecs/DAGWorld.java index f941dba..f9801c2 100644 --- a/core/src/main/java/coffee/liz/ecs/DAGWorld.java +++ b/core/src/main/java/coffee/liz/ecs/DAGWorld.java @@ -34,6 +34,7 @@ public class DAGWorld implements World { protected final Map, System> systems; private final List systemExecutionOrder; private final QueryBuilder queryBuilder = new QueryBuilder(this); + private float elapsedTime = 0f; public DAGWorld(final System... systems) { this.systems = singletonClazzMap(systems); @@ -74,9 +75,15 @@ public class DAGWorld implements World { @Override public void update(final float deltaSeconds) { + elapsedTime += deltaSeconds; systemExecutionOrder.forEach(system -> system.update(this, deltaSeconds)); } + @Override + public float getTime() { + return elapsedTime; + } + @SuppressWarnings("unchecked") @Override public S getSystem(final Class system) { diff --git a/core/src/main/java/coffee/liz/ecs/events/EventBus.java b/core/src/main/java/coffee/liz/ecs/events/EventBus.java index d814281..1ed04c5 100644 --- a/core/src/main/java/coffee/liz/ecs/events/EventBus.java +++ b/core/src/main/java/coffee/liz/ecs/events/EventBus.java @@ -28,13 +28,10 @@ public class EventBus { /** * Remove a previously registered hook. * - * @param hook - * callback to remove - * @return true - * if the hook was removed. + * @param hook callback to remove */ - public boolean unsubscribe(final Consumer hook) { - return subscriptions.remove(hook); + public void unsubscribe(final Consumer hook) { + subscriptions.remove(hook); } /** diff --git a/core/src/main/java/coffee/liz/ecs/model/Query.java b/core/src/main/java/coffee/liz/ecs/model/Query.java index 679ea49..8b52d42 100644 --- a/core/src/main/java/coffee/liz/ecs/model/Query.java +++ b/core/src/main/java/coffee/liz/ecs/model/Query.java @@ -5,10 +5,6 @@ import java.util.Set; import java.util.stream.Collectors; public record Query(Set> queryingComponents, QueryFilter filter) { - public Query { - queryingComponents = Set.copyOf(queryingComponents); - } - @SafeVarargs public static Query allOf(final Class... components) { return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.ALL_OF); diff --git a/core/src/main/java/coffee/liz/ecs/model/World.java b/core/src/main/java/coffee/liz/ecs/model/World.java index 63335c8..88602fc 100644 --- a/core/src/main/java/coffee/liz/ecs/model/World.java +++ b/core/src/main/java/coffee/liz/ecs/model/World.java @@ -15,6 +15,8 @@ public interface World { void update(float deltaSeconds); + float getTime(); + S getSystem(Class system); default void dispose() { diff --git a/design/bugs.txt b/design/bugs.txt deleted file mode 100644 index e65b3ca..0000000 --- a/design/bugs.txt +++ /dev/null @@ -1,3 +0,0 @@ -Clipping into a solid gives double height: - -Jumping into a solid from the side has weird interactions. First, I can hold the direction where the vector goes into the solid and jump, and I'll get a "double jump", probably because the jump resets immediately on top collision. ~/Desktop/dyl-bugs/jump-into-solid.mov. diff --git a/design/design.txt b/design/design.txt deleted file mode 100644 index 74c720d..0000000 --- a/design/design.txt +++ /dev/null @@ -1,84 +0,0 @@ -=== -Don't You Leave (really draft) -=== - -This is a game inspired by A Dark Room. The game is (probably) a crafting, RTS-inspired, roguelike. A run is ~50 minutes. - -Your main goal is to keep a fire, whose fuel is cash, warm for your wife. As the fire grows in size, other civilians start to base around the flame. These do nothing but stay next to your fire and run away if they get too cold, but you can click on them to "assign" them a job. - -Your fire increasingly needs more fuel as it grows in size. To gain more money you need to kill more and more players / pillage civilizations. You're forced to continue coming back to the fire by the fact that you need to refill the storage of fuel to store for the fire. - -Other civilizations' civilians are parts of armies. But in your civilization, all civilians are "pacifists". They die if they die; they don't fight back. It is fully on you to take entire armies - those attacking and which you attack. As you kill more and more others, your wife becomes increasingly more and more dissatisfied. - -There are two ways to lose: -- You die -- Your wife dies. You kill yourself. Loss text: "You couldn't protect her. Try again?" - -Other "players" are constantly coming to your civilization to kill your wife. - -The main notification system of the game is the edges of the screen and your wife's heartbeat. It gets slower. -- Text in a marquee style "chilly" -> "cold" -> "icy" -> "hypothermic", as ice increasingly becomes opaque, takes over the edges of the screen. -- Depending on the distance to the nearest enemy, "uncomfortable" -> "scared" -> "fear" -> "terror" with purple edges. - -The game ends when all other civiliazations. At that point, all your civilians will battle against you. Once you kill all of them, your wife becomes the final boss, and it's the hardest. When you kill her, you get infinite money but "You couldn't protect her. Try again?" but instead of a sad ending it's happy because you made so much money! - -===Crafting=== - -There are resources which you need to collect to make weapons. Cash is the base resource, which is the fuel which goes to your fire that is piped into factories around the fire and stuff. - -- Wood (for sticks) -- Magic emerald (for staff) -- Iron (for swords, -- Oil (for your car) - -===Exploration=== - -You need to monitor your hunger and thirst when traveling. There are wells, abandoned farms, (tbd) oracles to visit and include into your inventory. - -Depending on the radius to your little settlement is how advanced the civilization is; mostly just due to the fact they've had time to develop (THIS WILL REQUIRE A GOOD BIT OF ARTIFICIAL INTELLIGENCE TO CRAFT I BELIEVE). - -You start with just a small pack to bring back things to craft with. But you can craft larger things to bring back larger / more items as you explore. This is justified by items' weight. - -===Combat=== - -You start from just your fists. (TODO) You can pick up a rock and punch down a tree. - -There are a few main "classes" to spec into. -Magic -Military -Science - -Strategy manifests in maintaining the order of upgrades you choose to stay ahead of the threat. For instance, a distant civilization that is really really advanced in science might take less precidence over a medium advanced military civilization close by attacking soon. - -The Oracles are key to your combat experiences. There are a few oracles. It marks on your map the positions of civilizations and when they plan on attacking. But it doesn't reveal what kind of civilization (magic, military, science, etc.) it is. Thus, you need to go scout the attackers to see what you need to plan / gather for. - -===Game=== - - -The game starts out in a cave. You have to fight through the levels of the cave to get to the floor below the overworld. -You have to get basic tools from fighting skeletons, etc. The cave should have 1 - 3 other players. If you don't kill them, -they have the chance of getting to your wife and killing her. This gives the player the incentive to explore the entire -cave system to kill each. - -Once you find the path to the overworld you can place a fire down to start attracting other players. You want to protect -the entrance to the cave for obvious reasons. - -=== Terrain === - -The game is front-facing, like paper mario. This is essential to giving the combat mechanics "height" that I think would -be hard to represent in a top-down game like stardew valley. -But there are "forks" in the road, so to speak. I imagine that these forks would be dithered or otherwise not visible. -And the forks go up or down. - -This gives the world a little bit of "depth". - - _______ - / | - / | ----------S | (height difference is two screens' worth - \ | - \_______ - -Something I still need to decide on is how the camera will work. Is every chunk of the game a -room like mega man? Or is it like Terraria where the camera centers on the player and chunks -feel seemless? The former is probably easier generation-wise etc. diff --git a/design/todo.txt b/design/todo.txt deleted file mode 100644 index 023868e..0000000 --- a/design/todo.txt +++ /dev/null @@ -1,8 +0,0 @@ -Terrain: -- [ ] Basic platforming -- [ ] First dungeon -- [ ] First overworld attempt - -Combat: -- [ ] Basic enemy -- [ ] Separate solid collision bounding box and damageable collision bounding box. Perhaps even different systems? diff --git a/docs/bugs.txt b/docs/bugs.txt new file mode 100644 index 0000000..e65b3ca --- /dev/null +++ b/docs/bugs.txt @@ -0,0 +1,3 @@ +Clipping into a solid gives double height: + +Jumping into a solid from the side has weird interactions. First, I can hold the direction where the vector goes into the solid and jump, and I'll get a "double jump", probably because the jump resets immediately on top collision. ~/Desktop/dyl-bugs/jump-into-solid.mov. diff --git a/docs/design.txt b/docs/design.txt new file mode 100644 index 0000000..d0926ad --- /dev/null +++ b/docs/design.txt @@ -0,0 +1,87 @@ +=== +Don't You Leave (really draft) +=== + +This is a game inspired by A Dark Room. The game is (probably) a crafting, RTS-inspired, roguelike. A run is ~50 minutes. + +Your main goal is to keep a fire, whose fuel is cash, warm for your wife. As the fire grows in size, other civilians start to base around the flame. These do nothing but stay next to your fire and run away if they get too cold, but you can click on them to "assign" them a job. + +Your fire increasingly needs more fuel as it grows in size. To gain more money you need to kill more and more players / pillage civilizations. You're forced to continue coming back to the fire by the fact that you need to refill the storage of fuel to store for the fire. + +Other civilizations' civilians are parts of armies. But in your civilization, all civilians are "pacifists". They die if they die; they don't fight back. It is fully on you to take entire armies - those attacking and which you attack. As you kill more and more others, your wife becomes increasingly more and more dissatisfied. + +There are two ways to lose: +- You die +- Your wife dies. You kill yourself. Loss text: "You couldn't protect her. Try again?" + +Other "players" are constantly coming to your civilization to kill your wife. + +The main notification system of the game is the edges of the screen and your wife's heartbeat. It gets slower. +- Text in a marquee style "chilly" -> "cold" -> "icy" -> "hypothermic", as ice increasingly becomes opaque, takes over the edges of the screen. +- Depending on the distance to the nearest enemy, "uncomfortable" -> "scared" -> "fear" -> "terror" with purple edges. + +The game ends when all other civiliazations. At that point, all your civilians will battle against you. Once you kill all of them, your wife becomes the final boss, and it's the hardest. When you kill her, you get infinite money but "You couldn't protect her. Try again?" but instead of a sad ending it's happy because you made so much money! + +===Crafting=== + +There are resources which you need to collect to make weapons. Cash is the base resource, which is the fuel which goes to your fire that is piped into factories around the fire and stuff. + +- Wood (for sticks) +- Magic emerald (for staff) +- Iron (for swords, +- Oil (for your car) + +===Exploration=== + +You need to monitor your hunger and thirst when traveling. There are wells, abandoned farms, (tbd) oracles to visit and include into your inventory. + +Depending on the radius to your little settlement is how advanced the civilization is; mostly just due to the fact they've had time to develop (THIS WILL REQUIRE A GOOD BIT OF ARTIFICIAL INTELLIGENCE TO CRAFT I BELIEVE). + +You start with just a small pack to bring back things to craft with. But you can craft larger things to bring back larger / more items as you explore. This is justified by items' weight. + +===Combat=== + +You start from just your fists. (TODO) You can pick up a rock and punch down a tree. + +There are a few main "classes" to spec into. +Magic +Military +Science + +Strategy manifests in maintaining the order of upgrades you choose to stay ahead of the threat. For instance, a distant civilization that is really really advanced in science might take less precidence over a medium advanced military civilization close by attacking soon. + +The Oracles are key to your combat experiences. There are a few oracles. It marks on your map the positions of civilizations and when they plan on attacking (or just "rumors"). But it doesn't reveal what kind of civilization (magic, military, science, etc.) it is. Thus, you need to go scout the attackers to see what you need to plan / gather for. + +The actual combat will be driven by using your grappling hook to swing around and dodge enemies by grappling onto +ledges or trees? + +===Game=== + + +The game starts out in a cave. You have to fight through the levels of the cave to get to the floor below the overworld. +You have to get basic tools from fighting skeletons, etc. The cave should have 1 - 3 other players. If you don't kill them, +they have the chance of getting to your wife and killing her. This gives the player the incentive to explore the entire +cave system to kill each. + +Once you find the path to the overworld you can place a fire down to start attracting other players. You want to protect +the entrance to the cave for obvious reasons. + +=== Terrain === + +The game is front-facing, like paper mario. This is essential to giving the combat mechanics "height" that I think would +be hard to represent in a top-down game like stardew valley. +But there are "forks" in the road, so to speak. I imagine that these forks would be dithered or otherwise not visible. +And the forks go up or down. + +This gives the world a little bit of "depth". + + _______ + / | + / | +---------S | (height difference is two screens' worth + \ | + \_______ + +Something I still need to decide on is how the camera will work. Is every chunk of the game a +room like mega man? Or is it like Terraria where the camera centers on the player and chunks +feel seemless? The former is probably easier generation-wise etc. diff --git a/docs/input.txt b/docs/input.txt new file mode 100644 index 0000000..d5bc1a7 --- /dev/null +++ b/docs/input.txt @@ -0,0 +1,18 @@ +Entities will have an ActionQueue component. The ActionQueue is a set of actions and durations within which those +actions may be applied. This will solve the issue of buffering inputs as well as having multiple systems control which +actions an entity might take (for a future AI system, perhaps?). + +Let's introduce the jumping mechanics of this game as an example of the system interaction with entities. + +When the player presses a key mapped to a Jumping action, a Jump action is queued and expires in 80 milliseconds. +The JumpSystem consumes the action if the entity is able to "physically" jump: +- it is in contact with a solid underneath and has zero velocity on the y axis (?) and there is no current Jump component +- or, there is a Jump component but the "availableJump" counter is greater than zero (it is decremented on consumption + of the Jump action), to account for the future of a double jump being possible. + +If the JumpSystem does not consume() the action within the allotted 80 milliseconds, the action expires and is removed +from the queue. + +The queue is an action-singleton queue. Meaning, enqueueing an action has the effect of replacing any action of the same +type whose expiration is earlier than the enqueued action. + diff --git a/docs/todo.txt b/docs/todo.txt new file mode 100644 index 0000000..023868e --- /dev/null +++ b/docs/todo.txt @@ -0,0 +1,8 @@ +Terrain: +- [ ] Basic platforming +- [ ] First dungeon +- [ ] First overworld attempt + +Combat: +- [ ] Basic enemy +- [ ] Separate solid collision bounding box and damageable collision bounding box. Perhaps even different systems? -- cgit v1.2.3-70-g09d2