aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/java/coffee
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/main/java/coffee')
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/actor/GridBoardActor.java68
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/config/Settings.java2
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java126
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/FrameState.java16
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/GameScreenWorld.java19
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/component/GridEntityRef.java11
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/component/InterpolatedPosition.java18
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GameWorldManagementSystem.java100
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridInterpolationSystem.java46
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/InputSystem.java73
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/RenderSystem.java74
11 files changed, 446 insertions, 107 deletions
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/actor/GridBoardActor.java b/core/src/main/java/coffee/liz/abstractionengine/app/actor/GridBoardActor.java
new file mode 100644
index 0000000..3c7a82a
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/actor/GridBoardActor.java
@@ -0,0 +1,68 @@
+package coffee.liz.abstractionengine.app.actor;
+
+import coffee.liz.abstractionengine.app.Theme;
+import coffee.liz.abstractionengine.entity.EntityType;
+import coffee.liz.ecs.math.Mat2;
+import coffee.liz.ecs.math.Vec2;
+import coffee.liz.ecs.math.Vec2f;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.Batch;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+import com.badlogic.gdx.scenes.scene2d.Actor;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+public class GridBoardActor extends Actor {
+ private final ShapeRenderer shapeRenderer;
+ private final Vec2<Integer> gridSize;
+
+ @Setter
+ private Vec2<Float> cellSize;
+
+ @Setter
+ private List<RenderableEntity> entities = List.of();
+
+ @Override
+ public void draw(final Batch batch, final float parentAlpha) {
+ batch.end();
+
+ shapeRenderer.setProjectionMatrix(batch.getProjectionMatrix());
+ shapeRenderer.setTransformMatrix(batch.getTransformMatrix());
+ shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
+
+ if (cellSize != null) {
+ final float dotRadius = Math.min(cellSize.getX(), cellSize.getY()) * 0.07f;
+ shapeRenderer.setColor(Theme.BG_PATTERN);
+ Mat2.init(gridSize, pos -> {
+ final Vec2<Float> center = pos.floatValue().plus(Vec2f.builder().x(0.5f).y(0.5f).build())
+ .scale(cellSize);
+ shapeRenderer.circle(center.getX(), center.getY(), dotRadius);
+ return center;
+ });
+ }
+
+ for (final RenderableEntity entity : entities) {
+ shapeRenderer.setColor(getEntityColor(entity.entityType()));
+ shapeRenderer.rect(entity.position().getX(), entity.position().getY(), entity.size().getX(),
+ entity.size().getY());
+ }
+
+ shapeRenderer.end();
+ batch.begin();
+ }
+
+ private static Color getEntityColor(final EntityType type) {
+ return switch (type) {
+ case WALL -> Theme.MUTED;
+ case PLAYER -> Theme.PRIMARY;
+ case ABSTRACTION -> Theme.SECONDARY;
+ default -> Color.GREEN;
+ };
+ }
+
+ public record RenderableEntity(EntityType entityType, Vec2<Float> position, Vec2<Float> size) {
+ }
+}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/config/Settings.java b/core/src/main/java/coffee/liz/abstractionengine/app/config/Settings.java
index 3d8be7a..01d84a3 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/config/Settings.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/config/Settings.java
@@ -9,7 +9,7 @@ import lombok.Getter;
@Getter
public class Settings {
@Builder.Default
- private boolean skipIntro = true;
+ private boolean skipIntro = false;
@Builder.Default
private boolean playMusic = true;
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java
index f9402d1..3ddef91 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java
@@ -1,150 +1,64 @@
package coffee.liz.abstractionengine.app.screen;
-import coffee.liz.abstractionengine.AbstractionEngineGridWorld;
import coffee.liz.abstractionengine.app.AbstractionEngineGame;
-import coffee.liz.abstractionengine.app.Theme;
-import coffee.liz.abstractionengine.entity.EntityFactory;
-import coffee.liz.abstractionengine.entity.EntityType;
-import coffee.liz.abstractionengine.grid.component.GridInputState;
-import coffee.liz.abstractionengine.grid.component.GridPosition;
+import coffee.liz.abstractionengine.app.screen.game.FrameState;
+import coffee.liz.abstractionengine.app.screen.game.GameScreenWorld;
+import coffee.liz.abstractionengine.app.screen.game.system.GameWorldManagementSystem;
+import coffee.liz.abstractionengine.app.utils.FunctionUtils;
import coffee.liz.ecs.math.Vec2;
-import coffee.liz.ecs.math.Vec2f;
import coffee.liz.ecs.math.Vec2i;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
-import com.badlogic.gdx.utils.ScreenUtils;
+import com.badlogic.gdx.utils.viewport.FitViewport;
import lombok.RequiredArgsConstructor;
import java.time.Duration;
-import java.util.Set;
@RequiredArgsConstructor
public class GameScreen implements Screen {
private final AbstractionEngineGame game;
- private static final Vec2<Integer> GAME_GRID_SIZE = Vec2i.builder().x(30).y(30).build();
- private final static Duration GRID_INTERPOLATION_TIME = Duration.ofMillis(60);
+ private static final Vec2<Integer> GAME_GRID_SIZE = Vec2i.builder().x(20).y(20).build();
- private final AbstractionEngineGridWorld gameWorld = new AbstractionEngineGridWorld(GAME_GRID_SIZE);
- private Duration sinceUpdate = Duration.ZERO;
+ private GameScreenWorld outerWorld;
+ private FitViewport viewport;
- /**
- * Screen lifecycle hook.
- */
@Override
public void show() {
- EntityFactory.populateRect(GAME_GRID_SIZE, gameWorld, EntityFactory.Wall::addToWorld);
- EntityFactory.Player.addToWorld(gameWorld, Vec2i.builder().y(20).x(10).build());
- EntityFactory.Abstraction.addToWorld(gameWorld, Vec2i.builder().y(20).x(12).build());
- EntityFactory.Abstraction.addToWorld(gameWorld, Vec2i.builder().y(20).x(13).build());
+ viewport = new FitViewport(AbstractionEngineGame.WORLD_SIZE.getX(), AbstractionEngineGame.WORLD_SIZE.getY());
+ outerWorld = new GameScreenWorld(GAME_GRID_SIZE, viewport);
+ outerWorld.getSystem(GameWorldManagementSystem.class).populateLevel();
+ viewport.update(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), true);
+ render(0);
}
- /**
- * Screen render hook.
- *
- * @param delta
- * time since last frame
- */
@Override
public void render(final float delta) {
- game.viewport.apply();
- sinceUpdate = sinceUpdate.plus(Duration.ofMillis((int) (delta * 1000)));
- if (GRID_INTERPOLATION_TIME.compareTo(sinceUpdate) > 0) {
- return;
- } else {
- sinceUpdate = Duration.ZERO;
- }
-
- ScreenUtils.clear(Theme.BG);
-
- final GridInputState state = GridInputState.builder().movement(getMovementVector()).build();
- gameWorld.update(state);
-
- final Vec2<Float> cellSize = computeCellSize();
- game.shapeRenderer.setProjectionMatrix(game.batch.getProjectionMatrix());
- game.shapeRenderer.setTransformMatrix(game.batch.getTransformMatrix());
- game.shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
- gameWorld.query(Set.of(GridPosition.class, EntityType.class)).forEach(entity -> {
- final EntityType type = entity.get(EntityType.class);
- final GridPosition position = entity.get(GridPosition.class);
-
- final Vec2<Float> drawPos = position.getPosition().floatValue().scale(cellSize);
- game.shapeRenderer.setColor(getEntityColor(type));
- game.shapeRenderer.rect(drawPos.getX(), drawPos.getY(), cellSize.getX(), cellSize.getY());
- });
- game.shapeRenderer.end();
- }
-
- private Vec2<Integer> getMovementVector() {
- return game.settings.getKeyBinds().filterActiveActions(Gdx.input::isKeyPressed).stream().reduce(Vec2i.ZERO,
- (movement, action) -> {
- final Vec2<Integer> velocity = switch (action) {
- case MOVE_UP -> Vec2i.NORTH;
- case MOVE_DOWN -> Vec2i.SOUTH;
- case MOVE_LEFT -> Vec2i.WEST;
- case MOVE_RIGHT -> Vec2i.EAST;
- };
- return movement.plus(velocity);
- }, Vec2::plus);
- }
-
- private Color getEntityColor(final EntityType type) {
- return switch (type) {
- case WALL -> Theme.BORDER;
- case PLAYER -> Theme.PRIMARY;
- case ABSTRACTION -> Theme.SECONDARY;
- default -> Color.GREEN;
- };
+ viewport.apply();
+ final FrameState state = FrameState.builder().settings(game.settings).isKeyPressed(Gdx.input::isKeyPressed)
+ .build();
+ outerWorld.update(state, Duration.ofMillis((long) (delta * 1000)));
}
- /**
- * Screen resize hook.
- *
- * @param width
- * new width in pixels
- * @param height
- * new height in pixels
- */
@Override
public void resize(final int width, final int height) {
- game.viewport.update(width, height);
+ viewport.update(width, height, true);
}
- /**
- * Screen lifecycle hook.
- */
@Override
public void pause() {
-
}
- /**
- * Screen lifecycle hook.
- */
@Override
public void resume() {
-
}
- /**
- * Screen lifecycle hook.
- */
@Override
public void hide() {
-
+ dispose();
}
- /**
- * Screen cleanup hook.
- */
@Override
public void dispose() {
-
- }
-
- private Vec2<Float> computeCellSize() {
- return Vec2f.builder().x(game.viewport.getWorldWidth() / GAME_GRID_SIZE.getX())
- .y(game.viewport.getWorldHeight() / GAME_GRID_SIZE.getY()).build();
+ FunctionUtils.wrapCheckedException(outerWorld::close);
}
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/FrameState.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/FrameState.java
new file mode 100644
index 0000000..3f31530
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/FrameState.java
@@ -0,0 +1,16 @@
+package coffee.liz.abstractionengine.app.screen.game;
+
+import coffee.liz.abstractionengine.app.config.Settings;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.function.Predicate;
+
+@Builder
+@Getter
+@RequiredArgsConstructor
+public final class FrameState {
+ private final Settings settings;
+ private final Predicate<Integer> isKeyPressed;
+}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/GameScreenWorld.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/GameScreenWorld.java
new file mode 100644
index 0000000..f6385f6
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/GameScreenWorld.java
@@ -0,0 +1,19 @@
+package coffee.liz.abstractionengine.app.screen.game;
+
+import coffee.liz.abstractionengine.app.screen.game.system.GameWorldManagementSystem;
+import coffee.liz.abstractionengine.app.screen.game.system.GridInterpolationSystem;
+import coffee.liz.abstractionengine.app.screen.game.system.InputSystem;
+import coffee.liz.abstractionengine.app.screen.game.system.RenderSystem;
+import coffee.liz.ecs.DAGWorld;
+import coffee.liz.ecs.math.Vec2;
+import com.badlogic.gdx.utils.viewport.FitViewport;
+
+import java.util.Map;
+
+public class GameScreenWorld extends DAGWorld<FrameState> {
+ public GameScreenWorld(final Vec2<Integer> gridSize, final FitViewport viewport) {
+ super(Map.of(InputSystem.class, new InputSystem(), GameWorldManagementSystem.class,
+ new GameWorldManagementSystem(gridSize), GridInterpolationSystem.class, new GridInterpolationSystem(),
+ RenderSystem.class, new RenderSystem(viewport, gridSize)));
+ }
+}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/component/GridEntityRef.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/component/GridEntityRef.java
new file mode 100644
index 0000000..788d530
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/component/GridEntityRef.java
@@ -0,0 +1,11 @@
+package coffee.liz.abstractionengine.app.screen.game.component;
+
+import coffee.liz.ecs.model.Component;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public final class GridEntityRef implements Component {
+ private final int gridEntityId;
+}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/component/InterpolatedPosition.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/component/InterpolatedPosition.java
new file mode 100644
index 0000000..6700575
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/component/InterpolatedPosition.java
@@ -0,0 +1,18 @@
+package coffee.liz.abstractionengine.app.screen.game.component;
+
+import coffee.liz.ecs.math.Vec2;
+import coffee.liz.ecs.model.Component;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public final class InterpolatedPosition implements Component {
+ private final Vec2<Float> from;
+ private final Vec2<Float> to;
+ private final Vec2<Float> current;
+
+ public static InterpolatedPosition atRest(final Vec2<Float> position) {
+ return new InterpolatedPosition(position, position, position);
+ }
+}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GameWorldManagementSystem.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GameWorldManagementSystem.java
new file mode 100644
index 0000000..74355d1
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GameWorldManagementSystem.java
@@ -0,0 +1,100 @@
+package coffee.liz.abstractionengine.app.screen.game.system;
+
+import coffee.liz.abstractionengine.AbstractionEngineGridWorld;
+import coffee.liz.abstractionengine.app.screen.game.FrameState;
+import coffee.liz.abstractionengine.app.screen.game.component.GridEntityRef;
+import coffee.liz.abstractionengine.app.screen.game.component.InterpolatedPosition;
+import coffee.liz.abstractionengine.app.utils.FunctionUtils;
+import coffee.liz.abstractionengine.entity.EntityFactory;
+import coffee.liz.abstractionengine.entity.EntityType;
+import coffee.liz.abstractionengine.grid.component.GridInputState;
+import coffee.liz.abstractionengine.grid.component.GridPosition;
+import coffee.liz.ecs.math.Vec2;
+import coffee.liz.ecs.math.Vec2i;
+import coffee.liz.ecs.model.Entity;
+import coffee.liz.ecs.model.System;
+import coffee.liz.ecs.model.World;
+import lombok.Getter;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class GameWorldManagementSystem implements System<FrameState> {
+ private final AbstractionEngineGridWorld gridWorld;
+ private final Vec2<Integer> gridSize;
+ private final Map<Integer, Entity> gridToOuterMap = new HashMap<>();
+
+ @Getter
+ private boolean stepped = false;
+
+ public GameWorldManagementSystem(final Vec2<Integer> gridSize) {
+ this.gridSize = gridSize;
+ this.gridWorld = new AbstractionEngineGridWorld(gridSize);
+ }
+
+ public void populateLevel() {
+ EntityFactory.populateRect(gridSize, gridWorld, EntityFactory.Wall::addToWorld);
+ EntityFactory.Player.addToWorld(gridWorld, Vec2i.builder().y(10).x(10).build());
+ EntityFactory.Abstraction.addToWorld(gridWorld, Vec2i.builder().y(10).x(12).build());
+ EntityFactory.Abstraction.addToWorld(gridWorld, Vec2i.builder().y(10).x(13).build());
+ gridWorld.update(GridInputState.builder().movement(Vec2i.ZERO).build());
+ }
+
+ @Override
+ public Collection<Class<? extends System<FrameState>>> getDependencies() {
+ return Set.of(InputSystem.class);
+ }
+
+ @Override
+ public void update(final World<FrameState> world, final FrameState state, final Duration dt) {
+ final InputSystem inputSystem = world.getSystem(InputSystem.class);
+ final Vec2<Integer> movement = inputSystem.getStepMovement();
+
+ stepped = !movement.equals(Vec2i.ZERO);
+ if (stepped) {
+ final GridInputState gridInput = GridInputState.builder().movement(movement).build();
+ gridWorld.update(gridInput);
+ }
+
+ syncEntities(world);
+ }
+
+ private void syncEntities(final World<FrameState> outerWorld) {
+ final Set<Entity> gridEntities = gridWorld.query(Set.of(GridPosition.class, EntityType.class));
+ final Set<Integer> seenGridIds = new HashSet<>();
+
+ for (final Entity gridEntity : gridEntities) {
+ seenGridIds.add(gridEntity.getId());
+
+ if (!gridToOuterMap.containsKey(gridEntity.getId())) {
+ final Entity outer = outerWorld.createEntity();
+ final Vec2<Float> pos = gridEntity.get(GridPosition.class).getPosition().floatValue();
+ outer.add(new GridEntityRef(gridEntity.getId()));
+ outer.add(gridEntity.get(EntityType.class));
+ outer.add(InterpolatedPosition.atRest(pos));
+ gridToOuterMap.put(gridEntity.getId(), outer);
+ } else if (stepped) {
+ final Entity outer = gridToOuterMap.get(gridEntity.getId());
+ final InterpolatedPosition current = outer.get(InterpolatedPosition.class);
+ final Vec2<Float> newTarget = gridEntity.get(GridPosition.class).getPosition().floatValue();
+ outer.add(new InterpolatedPosition(current.getCurrent(), newTarget, current.getCurrent()));
+ }
+ }
+
+ final Set<Integer> removedIds = new HashSet<>(gridToOuterMap.keySet());
+ removedIds.removeAll(seenGridIds);
+ for (final int removedId : removedIds) {
+ final Entity outer = gridToOuterMap.remove(removedId);
+ outerWorld.removeEntity(outer);
+ }
+ }
+
+ @Override
+ public void close() {
+ FunctionUtils.wrapCheckedException(gridWorld::close);
+ }
+}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridInterpolationSystem.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridInterpolationSystem.java
new file mode 100644
index 0000000..2e4826d
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridInterpolationSystem.java
@@ -0,0 +1,46 @@
+package coffee.liz.abstractionengine.app.screen.game.system;
+
+import coffee.liz.abstractionengine.app.screen.game.FrameState;
+import coffee.liz.abstractionengine.app.screen.game.component.InterpolatedPosition;
+import coffee.liz.ecs.math.Vec2;
+import coffee.liz.ecs.math.Vec2f;
+import coffee.liz.ecs.model.System;
+import coffee.liz.ecs.model.World;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Set;
+
+public class GridInterpolationSystem implements System<FrameState> {
+ private static final Duration INTERPOLATION_TIME = Duration.ofMillis(50);
+
+ private Duration elapsed = Duration.ZERO;
+
+ @Override
+ public Collection<Class<? extends System<FrameState>>> getDependencies() {
+ return Set.of(GameWorldManagementSystem.class);
+ }
+
+ @Override
+ public void update(final World<FrameState> world, final FrameState state, final Duration dt) {
+ final GameWorldManagementSystem gameWorldSystem = world.getSystem(GameWorldManagementSystem.class);
+
+ if (gameWorldSystem.isStepped()) {
+ elapsed = Duration.ZERO;
+ }
+
+ elapsed = elapsed.plus(dt);
+ final float t = Math.min(1.0f, (float) elapsed.toMillis() / INTERPOLATION_TIME.toMillis());
+
+ world.query(Set.of(InterpolatedPosition.class)).forEach(entity -> {
+ final InterpolatedPosition pos = entity.get(InterpolatedPosition.class);
+ final Vec2<Float> interpolated = lerp(pos.getFrom(), pos.getTo(), t);
+ entity.add(new InterpolatedPosition(pos.getFrom(), pos.getTo(), interpolated));
+ });
+ }
+
+ private static Vec2<Float> lerp(final Vec2<Float> from, final Vec2<Float> to, final float t) {
+ return Vec2f.builder().x(from.getX() + (to.getX() - from.getX()) * t)
+ .y(from.getY() + (to.getY() - from.getY()) * t).build();
+ }
+}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/InputSystem.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/InputSystem.java
new file mode 100644
index 0000000..b3af099
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/InputSystem.java
@@ -0,0 +1,73 @@
+package coffee.liz.abstractionengine.app.screen.game.system;
+
+import coffee.liz.abstractionengine.app.config.KeyBinds;
+import coffee.liz.abstractionengine.app.screen.game.FrameState;
+import coffee.liz.ecs.math.Vec2;
+import coffee.liz.ecs.math.Vec2i;
+import coffee.liz.ecs.model.World;
+import lombok.Getter;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Set;
+
+public class InputSystem implements coffee.liz.ecs.model.System<FrameState> {
+ private static final Duration INITIAL_DELAY = Duration.ofMillis(300);
+ private static final Duration REPEAT_INTERVAL = Duration.ofMillis(100);
+
+ private KeyBinds.Action heldAction = null;
+ private Duration heldTime = Duration.ZERO;
+ private boolean pastInitialDelay = false;
+
+ @Getter
+ private Vec2<Integer> stepMovement = Vec2i.ZERO;
+
+ @Override
+ public Collection<Class<? extends coffee.liz.ecs.model.System<FrameState>>> getDependencies() {
+ return Set.of();
+ }
+
+ @Override
+ public void update(final World<FrameState> world, final FrameState state, final Duration dt) {
+ final Set<KeyBinds.Action> currentlyActive = state.getSettings().getKeyBinds()
+ .filterActiveActions(state.getIsKeyPressed());
+
+ if (currentlyActive.isEmpty()) {
+ heldAction = null;
+ stepMovement = Vec2i.ZERO;
+ return;
+ }
+
+ final KeyBinds.Action active = currentlyActive.contains(heldAction)
+ ? heldAction
+ : currentlyActive.iterator().next();
+
+ if (!active.equals(heldAction)) {
+ heldAction = active;
+ heldTime = Duration.ZERO;
+ pastInitialDelay = false;
+ stepMovement = actionToMovement(active);
+ } else {
+ heldTime = heldTime.plus(dt);
+ if (!pastInitialDelay && heldTime.compareTo(INITIAL_DELAY) >= 0) {
+ pastInitialDelay = true;
+ heldTime = heldTime.minus(INITIAL_DELAY);
+ stepMovement = actionToMovement(active);
+ } else if (pastInitialDelay && heldTime.compareTo(REPEAT_INTERVAL) >= 0) {
+ heldTime = heldTime.minus(REPEAT_INTERVAL);
+ stepMovement = actionToMovement(active);
+ } else {
+ stepMovement = Vec2i.ZERO;
+ }
+ }
+ }
+
+ private static Vec2<Integer> actionToMovement(final KeyBinds.Action action) {
+ return switch (action) {
+ case MOVE_UP -> Vec2i.NORTH;
+ case MOVE_DOWN -> Vec2i.SOUTH;
+ case MOVE_LEFT -> Vec2i.WEST;
+ case MOVE_RIGHT -> Vec2i.EAST;
+ };
+ }
+}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/RenderSystem.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/RenderSystem.java
new file mode 100644
index 0000000..344271f
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/RenderSystem.java
@@ -0,0 +1,74 @@
+package coffee.liz.abstractionengine.app.screen.game.system;
+
+import coffee.liz.abstractionengine.app.Theme;
+import coffee.liz.abstractionengine.app.actor.GridBoardActor;
+import coffee.liz.abstractionengine.app.screen.game.FrameState;
+import coffee.liz.abstractionengine.app.screen.game.component.InterpolatedPosition;
+import coffee.liz.abstractionengine.entity.EntityType;
+import coffee.liz.ecs.math.Vec2;
+import coffee.liz.ecs.math.Vec2f;
+import coffee.liz.ecs.model.System;
+import coffee.liz.ecs.model.World;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+import com.badlogic.gdx.scenes.scene2d.Stage;
+import com.badlogic.gdx.utils.ScreenUtils;
+import com.badlogic.gdx.utils.viewport.FitViewport;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+public class RenderSystem implements System<FrameState> {
+ private final FitViewport viewport;
+ private final Vec2<Integer> gridSize;
+ private final ShapeRenderer shapeRenderer;
+ private final Stage stage;
+ private final GridBoardActor boardActor;
+
+ public RenderSystem(final FitViewport viewport, final Vec2<Integer> gridSize) {
+ this.viewport = viewport;
+ this.gridSize = gridSize;
+ this.shapeRenderer = new ShapeRenderer();
+ this.stage = new Stage(viewport);
+ this.boardActor = new GridBoardActor(shapeRenderer, gridSize);
+ stage.addActor(boardActor);
+ }
+
+ @Override
+ public Collection<Class<? extends System<FrameState>>> getDependencies() {
+ return Set.of(GridInterpolationSystem.class);
+ }
+
+ @Override
+ public void update(final World<FrameState> world, final FrameState state, final Duration dt) {
+ ScreenUtils.clear(Theme.BG);
+
+ final Vec2<Float> cellSize = computeCellSize();
+ final List<GridBoardActor.RenderableEntity> renderables = world
+ .query(Set.of(InterpolatedPosition.class, EntityType.class)).stream().map(entity -> {
+ final InterpolatedPosition pos = entity.get(InterpolatedPosition.class);
+ final EntityType type = entity.get(EntityType.class);
+ final Vec2<Float> drawPos = pos.getCurrent().scale(cellSize);
+ return new GridBoardActor.RenderableEntity(type, drawPos, cellSize);
+ }).toList();
+
+ boardActor.setCellSize(cellSize);
+ boardActor.setEntities(renderables);
+
+ final float deltaSeconds = dt.toMillis() / 1000f;
+ stage.act(deltaSeconds);
+ stage.draw();
+ }
+
+ @Override
+ public void close() {
+ stage.dispose();
+ shapeRenderer.dispose();
+ }
+
+ private Vec2<Float> computeCellSize() {
+ return Vec2f.builder().x(viewport.getWorldWidth() / gridSize.getX())
+ .y(viewport.getWorldHeight() / gridSize.getY()).build();
+ }
+}