summaryrefslogtreecommitdiff
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/dyl/DylGame.java53
-rw-r--r--core/src/main/java/coffee/liz/dyl/FirstScreen.java45
-rw-r--r--core/src/main/java/coffee/liz/dyl/FrameState.java16
-rw-r--r--core/src/main/java/coffee/liz/dyl/Main.java11
-rw-r--r--core/src/main/java/coffee/liz/dyl/components/Controllable.java6
-rw-r--r--core/src/main/java/coffee/liz/dyl/components/Transform.java18
-rw-r--r--core/src/main/java/coffee/liz/dyl/components/Velocity.java16
-rw-r--r--core/src/main/java/coffee/liz/dyl/components/graphic/Graphic.java13
-rw-r--r--core/src/main/java/coffee/liz/dyl/components/graphic/TextureGraphic.java21
-rw-r--r--core/src/main/java/coffee/liz/dyl/config/KeyBinds.java41
-rw-r--r--core/src/main/java/coffee/liz/dyl/config/Settings.java16
-rw-r--r--core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java31
-rw-r--r--core/src/main/java/coffee/liz/dyl/screen/GameScreen.java51
-rw-r--r--core/src/main/java/coffee/liz/dyl/systems/InputSystem.java41
-rw-r--r--core/src/main/java/coffee/liz/dyl/systems/IntegrationSystem.java30
-rw-r--r--core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java53
-rw-r--r--core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java23
-rw-r--r--core/src/main/java/coffee/liz/ecs/DAGWorld.java207
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/Component.java8
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/Entity.java105
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/Query.java30
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java25
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/System.java32
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/World.java64
-rw-r--r--core/src/main/java/coffee/liz/ecs/utils/FunctionUtils.java28
25 files changed, 928 insertions, 56 deletions
diff --git a/core/src/main/java/coffee/liz/dyl/DylGame.java b/core/src/main/java/coffee/liz/dyl/DylGame.java
new file mode 100644
index 0000000..aa25617
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/DylGame.java
@@ -0,0 +1,53 @@
+package coffee.liz.dyl;
+
+import coffee.liz.dyl.config.Settings;
+import coffee.liz.dyl.screen.GameScreen;
+import com.badlogic.gdx.Game;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.InputMultiplexer;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.g2d.Batch;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+import com.badlogic.gdx.utils.viewport.FitViewport;
+import com.badlogic.gdx.utils.viewport.Viewport;
+import lombok.Getter;
+
+@Getter
+public class DylGame extends Game {
+ public static final float WORLD_WIDTH = 10f;
+ public static final float WORLD_HEIGHT = 10f;
+ public static final float UNIT_SCALE = 1/10f;
+
+ private InputMultiplexer inputMultiplexer;
+ private Batch batch;
+ private OrthographicCamera camera;
+ private Viewport viewport;
+ private Settings settings = Settings.builder().build();
+
+ @Override
+ public void create() {
+ inputMultiplexer = new InputMultiplexer();
+ Gdx.input.setInputProcessor(inputMultiplexer);
+
+ batch = new SpriteBatch();
+ camera = new OrthographicCamera();
+ viewport = new FitViewport(WORLD_WIDTH, WORLD_HEIGHT, camera);
+
+ setScreen(new GameScreen(this));
+ }
+
+ @Override
+ public void render() {
+ Gdx.gl.glClearColor(0, 0, 0, 1);
+ Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+
+ super.render();
+ }
+
+ @Override
+ public void resize(final int width, final int height) {
+ viewport.update(width, height, true);
+ super.resize(width, height);
+ }
+}
diff --git a/core/src/main/java/coffee/liz/dyl/FirstScreen.java b/core/src/main/java/coffee/liz/dyl/FirstScreen.java
deleted file mode 100644
index 4901283..0000000
--- a/core/src/main/java/coffee/liz/dyl/FirstScreen.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package coffee.liz.dyl;
-
-import com.badlogic.gdx.Screen;
-
-/** First screen of the application. Displayed after the application is created. */
-public class FirstScreen implements Screen {
- @Override
- public void show() {
- // Prepare your screen here.
- }
-
- @Override
- public void render(float delta) {
- // Draw your screen here. "delta" is the time since last render in seconds.
- }
-
- @Override
- public void resize(int width, int height) {
- // If the window is minimized on a desktop (LWJGL3) platform, width and height are 0, which causes problems.
- // In that case, we don't resize anything, and wait for the window to be a normal size before updating.
- if(width <= 0 || height <= 0) return;
-
- // Resize your screen here. The parameters represent the new window size.
- }
-
- @Override
- public void pause() {
- // Invoked when your application is paused.
- }
-
- @Override
- public void resume() {
- // Invoked when your application is resumed after pause.
- }
-
- @Override
- public void hide() {
- // This method is called when another screen replaces this one.
- }
-
- @Override
- public void dispose() {
- // Destroy screen's assets here.
- }
-} \ No newline at end of file
diff --git a/core/src/main/java/coffee/liz/dyl/FrameState.java b/core/src/main/java/coffee/liz/dyl/FrameState.java
new file mode 100644
index 0000000..7d70cde
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/FrameState.java
@@ -0,0 +1,16 @@
+package coffee.liz.dyl;
+
+import coffee.liz.dyl.config.Settings;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.function.Predicate;
+
+@Builder
+@Getter
+@RequiredArgsConstructor
+public class FrameState {
+ private final Settings settings;
+ private final Predicate<Integer> isKeyPressed;
+}
diff --git a/core/src/main/java/coffee/liz/dyl/Main.java b/core/src/main/java/coffee/liz/dyl/Main.java
deleted file mode 100644
index 24fa3de..0000000
--- a/core/src/main/java/coffee/liz/dyl/Main.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package coffee.liz.dyl;
-
-import com.badlogic.gdx.Game;
-
-/** {@link com.badlogic.gdx.ApplicationListener} implementation shared by all platforms. */
-public class Main extends Game {
- @Override
- public void create() {
- setScreen(new FirstScreen());
- }
-} \ No newline at end of file
diff --git a/core/src/main/java/coffee/liz/dyl/components/Controllable.java b/core/src/main/java/coffee/liz/dyl/components/Controllable.java
new file mode 100644
index 0000000..c476c91
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/components/Controllable.java
@@ -0,0 +1,6 @@
+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/Transform.java b/core/src/main/java/coffee/liz/dyl/components/Transform.java
new file mode 100644
index 0000000..b2d5a60
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/components/Transform.java
@@ -0,0 +1,18 @@
+package coffee.liz.dyl.components;
+
+import coffee.liz.ecs.model.Component;
+import com.badlogic.gdx.math.Vector2;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public class Transform implements Component, Comparable<Transform> {
+ private Vector2 position;
+ private int z;
+
+ @Override
+ public int compareTo(final Transform other) {
+ return Integer.compare(z, other.getZ());
+ }
+}
diff --git a/core/src/main/java/coffee/liz/dyl/components/Velocity.java b/core/src/main/java/coffee/liz/dyl/components/Velocity.java
new file mode 100644
index 0000000..eb981f7
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/components/Velocity.java
@@ -0,0 +1,16 @@
+package coffee.liz.dyl.components;
+
+import coffee.liz.ecs.model.Component;
+import com.badlogic.gdx.math.Vector2;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+public class Velocity implements Component {
+ private Vector2 velocity = Vector2.Zero;
+}
diff --git a/core/src/main/java/coffee/liz/dyl/components/graphic/Graphic.java b/core/src/main/java/coffee/liz/dyl/components/graphic/Graphic.java
new file mode 100644
index 0000000..648a930
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/components/graphic/Graphic.java
@@ -0,0 +1,13 @@
+package coffee.liz.dyl.components.graphic;
+
+import coffee.liz.dyl.components.Transform;
+import coffee.liz.ecs.model.Component;
+import com.badlogic.gdx.graphics.g2d.Batch;
+
+public interface Graphic extends Component {
+ default Class<? extends Component> getKey() {
+ return Graphic.class;
+ }
+
+ void draw(final Batch batch, final Transform transform);
+}
diff --git a/core/src/main/java/coffee/liz/dyl/components/graphic/TextureGraphic.java b/core/src/main/java/coffee/liz/dyl/components/graphic/TextureGraphic.java
new file mode 100644
index 0000000..0d12f1a
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/components/graphic/TextureGraphic.java
@@ -0,0 +1,21 @@
+package coffee.liz.dyl.components.graphic;
+
+import coffee.liz.dyl.components.Transform;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.Batch;
+import com.badlogic.gdx.graphics.g2d.TextureRegion;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class TextureGraphic implements Graphic {
+ private Color color;
+ private TextureRegion textureRegion;
+
+ @Override
+ public void draw(final Batch batch, final Transform transform) {
+ batch.setColor(color);
+ batch.draw(textureRegion, transform.getPosition().x, transform.getPosition().y);
+ }
+}
diff --git a/core/src/main/java/coffee/liz/dyl/config/KeyBinds.java b/core/src/main/java/coffee/liz/dyl/config/KeyBinds.java
new file mode 100644
index 0000000..762dc48
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/config/KeyBinds.java
@@ -0,0 +1,41 @@
+package coffee.liz.dyl.config;
+import com.badlogic.gdx.Input;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+
+@Builder
+@AllArgsConstructor
+@RequiredArgsConstructor
+@Getter
+public class KeyBinds {
+ @Builder.Default
+ private Set<Integer> moveUpKeys = Set.of(Input.Keys.K, Input.Keys.UP, Input.Keys.W);
+ @Builder.Default
+ private Set<Integer> moveDownKeys = Set.of(Input.Keys.J, Input.Keys.DOWN, Input.Keys.S);
+ @Builder.Default
+ private Set<Integer> moveLeftKeys = Set.of(Input.Keys.H, Input.Keys.LEFT, Input.Keys.A);
+ @Builder.Default
+ private Set<Integer> moveRightKeys = Set.of(Input.Keys.L, Input.Keys.RIGHT, Input.Keys.D);
+
+ public Set<Action> filterActiveActions(final Predicate<Integer> isDown) {
+ final Set<Action> actions = new HashSet<>();
+ Map.of(moveUpKeys, Action.MOVE_UP, moveDownKeys, Action.MOVE_DOWN, moveRightKeys, Action.MOVE_RIGHT,
+ moveLeftKeys, Action.MOVE_LEFT).forEach((keys, action) -> {
+ if (keys.stream().anyMatch(isDown)) {
+ actions.add(action);
+ }
+ });
+ return actions;
+ }
+
+ public enum Action {
+ MOVE_UP, MOVE_LEFT, MOVE_DOWN, MOVE_RIGHT;
+ }
+}
diff --git a/core/src/main/java/coffee/liz/dyl/config/Settings.java b/core/src/main/java/coffee/liz/dyl/config/Settings.java
new file mode 100644
index 0000000..13fbb3c
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/config/Settings.java
@@ -0,0 +1,16 @@
+package coffee.liz.dyl.config;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@AllArgsConstructor
+@Getter
+public class Settings {
+ @Builder.Default
+ private boolean skipIntro = true;
+ @Builder.Default
+ private boolean playMusic = true;
+ @Builder.Default
+ private KeyBinds keyBinds = KeyBinds.builder().build();
+}
diff --git a/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java b/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java
new file mode 100644
index 0000000..f7344c7
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java
@@ -0,0 +1,31 @@
+package coffee.liz.dyl.entities;
+
+import coffee.liz.dyl.components.Controllable;
+import coffee.liz.dyl.components.Velocity;
+import coffee.liz.dyl.components.graphic.TextureGraphic;
+import coffee.liz.ecs.model.Entity;
+import coffee.liz.ecs.model.World;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.g2d.TextureRegion;
+
+public class PlayerFactory {
+ private static final int PENGUIN_FRAMES_Y = 4;
+ private static final int PENGUIN_FRAMES_X = 8;
+ private static final FileHandle FILE = Gdx.files.internal("player.png");
+
+ public static Entity addTo(final World<?> world) {
+ final Texture texture = new Texture(FILE);
+ final TextureRegion[][] tmp = TextureRegion.split(texture,
+ texture.getWidth() / PENGUIN_FRAMES_X,
+ texture.getHeight() / PENGUIN_FRAMES_Y);
+ return world.createEntity()
+ .add(new TextureGraphic(Color.PINK, tmp[0][0]))
+ .add(new Controllable())
+ .add(new Velocity());
+ }
+
+ private PlayerFactory() { }
+}
diff --git a/core/src/main/java/coffee/liz/dyl/screen/GameScreen.java b/core/src/main/java/coffee/liz/dyl/screen/GameScreen.java
new file mode 100644
index 0000000..54dfda3
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/screen/GameScreen.java
@@ -0,0 +1,51 @@
+package coffee.liz.dyl.screen;
+
+import coffee.liz.dyl.DylGame;
+import coffee.liz.dyl.FrameState;
+import coffee.liz.dyl.world.DylGameWorld;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.Screen;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public class GameScreen implements Screen {
+ private final DylGame game;
+
+ private DylGameWorld dylGameWorld;
+ private FrameState frameState;
+
+ @Override
+ public void show() {
+ dylGameWorld = new DylGameWorld(game);
+ frameState = FrameState.builder()
+ .settings(game.getSettings())
+ .isKeyPressed(Gdx.input::isKeyPressed)
+ .build();
+ }
+
+ @Override
+ public void render(final float delta) {
+ dylGameWorld.update(frameState, delta);
+ }
+
+ @Override
+ public void resize(final int width, final int height) {
+ }
+
+ @Override
+ public void pause() {
+ }
+
+ @Override
+ public void resume() {
+ }
+
+ @Override
+ public void hide() {
+ }
+
+ @Override
+ public void dispose() {
+ dylGameWorld.dispose();
+ }
+}
diff --git a/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java b/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java
new file mode 100644
index 0000000..8a357ba
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java
@@ -0,0 +1,41 @@
+package coffee.liz.dyl.systems;
+
+import coffee.liz.dyl.FrameState;
+import coffee.liz.dyl.components.Transform;
+import coffee.liz.dyl.components.Velocity;
+import coffee.liz.dyl.config.KeyBinds;
+import coffee.liz.ecs.model.System;
+import coffee.liz.ecs.model.World;
+import com.badlogic.gdx.math.Vector2;
+
+import java.util.Collection;
+import java.util.Set;
+
+public class InputSystem implements System<FrameState> {
+ @Override
+ public Collection<Class<? extends System<FrameState>>> getDependencies() {
+ return Set.of();
+ }
+
+ @Override
+ public void update(final World<FrameState> world, final FrameState state, final float deltaSeconds) {
+ final Set<KeyBinds.Action> currentlyActive = state.getSettings().getKeyBinds()
+ .filterActiveActions(state.getIsKeyPressed());
+
+ final Vector2 momentum = Vector2.Zero.cpy();
+ currentlyActive.stream().map(InputSystem::actionToMovement).forEach(momentum::add);
+
+ world.queryable().allOf(Velocity.class, Transform.class).forEach(e -> {
+ e.get(Velocity.class).getVelocity().set(momentum);
+ });
+ }
+
+ private static Vector2 actionToMovement(final KeyBinds.Action action) {
+ return switch (action) {
+ case MOVE_UP -> new Vector2(0, 1);
+ case MOVE_DOWN -> new Vector2(0, -1);
+ case MOVE_LEFT -> new Vector2(-1, 0);
+ case MOVE_RIGHT -> new Vector2(1, 0);
+ };
+ }
+}
diff --git a/core/src/main/java/coffee/liz/dyl/systems/IntegrationSystem.java b/core/src/main/java/coffee/liz/dyl/systems/IntegrationSystem.java
new file mode 100644
index 0000000..3fb9b76
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/systems/IntegrationSystem.java
@@ -0,0 +1,30 @@
+package coffee.liz.dyl.systems;
+
+import coffee.liz.dyl.FrameState;
+import coffee.liz.dyl.components.Transform;
+import coffee.liz.dyl.components.Velocity;
+import coffee.liz.ecs.model.System;
+import coffee.liz.ecs.model.World;
+import com.badlogic.gdx.math.Vector2;
+import lombok.extern.log4j.Log4j2;
+
+import java.util.Collection;
+import java.util.List;
+
+@Log4j2
+public class IntegrationSystem implements System<FrameState> {
+ @Override
+ public Collection<Class<? extends System<FrameState>>> getDependencies() {
+ return List.of(InputSystem.class);
+ }
+
+ @Override
+ public void update(final World<FrameState> world, final FrameState state, final float deltaSeconds) {
+ world.queryable().allOf(Velocity.class, Transform.class)
+ .forEach(e -> {
+ final Vector2 velocity = e.get(Velocity.class).getVelocity();
+ final Vector2 position = e.get(Transform.class).getPosition();
+ position.add(velocity.cpy().scl(deltaSeconds));
+ });
+ }
+}
diff --git a/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java b/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java
new file mode 100644
index 0000000..2f185cd
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java
@@ -0,0 +1,53 @@
+package coffee.liz.dyl.systems;
+
+import coffee.liz.dyl.FrameState;
+import coffee.liz.dyl.components.graphic.Graphic;
+import coffee.liz.dyl.components.Transform;
+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<FrameState> {
+ 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<Class<? extends System<FrameState>>> getDependencies() {
+ return Set.of(IntegrationSystem.class);
+ }
+
+ @Override
+ public void update(final World<FrameState> world, final FrameState state, final float deltaSeconds) {
+ viewport.apply();
+ camera.update();
+
+ batch.setProjectionMatrix(camera.combined);
+ batch.begin();
+ batch.setColor(Color.WHITE);
+
+ world.queryable().allOf(Transform.class, Graphic.class).stream()
+ .sorted(Comparator.comparing(e -> e.get(Transform.class)))
+ .forEach(e -> {
+ final Transform transform = e.get(Transform.class);
+ final Graphic graphic = e.get(Graphic.class);
+ graphic.draw(batch, transform);
+ });
+
+ batch.setColor(Color.WHITE);
+ batch.end();
+ }
+}
diff --git a/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java
new file mode 100644
index 0000000..b04a4df
--- /dev/null
+++ b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java
@@ -0,0 +1,23 @@
+package coffee.liz.dyl.world;
+
+import coffee.liz.dyl.DylGame;
+import coffee.liz.dyl.FrameState;
+import coffee.liz.dyl.components.Transform;
+import coffee.liz.dyl.entities.PlayerFactory;
+import coffee.liz.dyl.systems.InputSystem;
+import coffee.liz.dyl.systems.IntegrationSystem;
+import coffee.liz.dyl.systems.RenderSystem;
+import coffee.liz.ecs.DAGWorld;
+import com.badlogic.gdx.math.Vector2;
+
+public class DylGameWorld extends DAGWorld<FrameState> {
+ public DylGameWorld(final DylGame game) {
+ super(
+ new InputSystem(),
+ new IntegrationSystem(),
+ new RenderSystem(game.getBatch(), game.getViewport())
+ );
+ PlayerFactory.addTo(this)
+ .add(new Transform(Vector2.One, 1));
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/DAGWorld.java b/core/src/main/java/coffee/liz/ecs/DAGWorld.java
new file mode 100644
index 0000000..1075e5e
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/DAGWorld.java
@@ -0,0 +1,207 @@
+package coffee.liz.ecs;
+
+import coffee.liz.ecs.model.Component;
+import coffee.liz.ecs.model.Entity;
+import coffee.liz.ecs.model.Query;
+import coffee.liz.ecs.model.System;
+import coffee.liz.ecs.model.World;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/** World that updates in {@link System#getDependencies()} topological order. */
+@Log4j2
+@RequiredArgsConstructor
+public class DAGWorld<T> implements World<T> {
+ /** All entities in the world. */
+ protected final Set<Entity> entities = Collections.synchronizedSet(new HashSet<>());
+
+ /** Cache mapping component types to entities having that component. */
+ private final Map<Class<? extends Component>, Set<Entity>> componentCache = Collections
+ .synchronizedMap(new HashMap<>());
+
+ /** Deterministic ID's for spawned entities. */
+ private final AtomicInteger nextEntityId = new AtomicInteger(0);
+
+ /** All registered systems. */
+ protected final Map<Class<? extends System<T>>, System<T>> systems;
+
+ /** Ordered list of systems for execution. */
+ private final List<System<T>> systemExecutionOrder;
+
+ @SafeVarargs
+ public DAGWorld(final System<T>... systems) {
+ this.systems = singletonClazzMap(systems);
+ this.systemExecutionOrder = buildExecutionOrder(Arrays.asList(systems));
+ log.debug("Executing in order: {}", systemExecutionOrder);
+ }
+
+ @Override
+ public Entity createEntity() {
+ final Entity entity = Entity.builder().id(nextEntityId.incrementAndGet()).build();
+ entities.add(entity);
+ return entity;
+ }
+
+ @Override
+ public void removeEntity(final Entity entity) {
+ entity.getComponentMap().keySet().forEach(componentType -> {
+ final Set<Entity> cachedEntities = componentCache.get(componentType);
+ if (cachedEntities != null) {
+ cachedEntities.remove(entity);
+ }
+ });
+ entities.remove(entity);
+ }
+
+ @Override
+ public Set<Entity> resolve(final Query query) {
+ final Set<Class<? extends Component>> components = query.queryingComponents();
+ return switch (query.filter()) {
+ case ALL_OF -> resolveAllOf(components);
+ case ANY_OF -> resolveAnyOf(components);
+ case NONE_OF -> resolveNoneOf(components);
+ };
+ }
+
+ @Override
+ public void update(final T state, final float deltaSeconds) {
+ systemExecutionOrder.forEach(system -> {
+ refreshComponentCache();
+ system.update(this, state, deltaSeconds);
+ });
+ refreshComponentCache();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <S extends System<T>> S getSystem(final Class<S> system) {
+ return (S) systems.get(system);
+ }
+
+ private void refreshComponentCache() {
+ componentCache.clear();
+ entities.forEach(entity -> entity.getComponentMap().keySet().forEach(
+ componentType -> componentCache.computeIfAbsent(componentType, _comp -> new HashSet<>()).add(entity)));
+ }
+
+ private Set<Entity> resolveAllOf(final Set<Class<? extends Component>> components) {
+ if (components.isEmpty()) {
+ return Set.copyOf(entities);
+ }
+
+ final Class<? extends Component> firstType = components.iterator().next();
+ final Set<Entity> candidates = componentCache.get(firstType);
+ if (candidates == null) {
+ return Collections.emptySet();
+ }
+
+ return candidates.stream().filter(entity -> components.stream().allMatch(entity::has))
+ .collect(Collectors.toSet());
+ }
+
+ private Set<Entity> resolveAnyOf(final Set<Class<? extends Component>> components) {
+ if (components.isEmpty()) {
+ return Collections.emptySet();
+ }
+
+ return entities.stream().filter(entity -> components.stream().anyMatch(entity::has))
+ .collect(Collectors.toSet());
+ }
+
+ private Set<Entity> resolveNoneOf(final Set<Class<? extends Component>> components) {
+ if (components.isEmpty()) {
+ return Set.copyOf(entities);
+ }
+
+ return entities.stream().filter(entity -> components.stream().noneMatch(entity::has))
+ .collect(Collectors.toSet());
+ }
+
+ private List<System<T>> buildExecutionOrder(final Collection<System<T>> systems) {
+ if (systems.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ final Map<Class<?>, System<T>> systemMap = systems.stream()
+ .collect(Collectors.toMap(System::getClass, system -> system, (_sys, b) -> b, LinkedHashMap::new));
+ final Map<Class<?>, Integer> inDegree = new LinkedHashMap<>();
+ final Map<Class<?>, Set<Class<?>>> adjacencyList = new LinkedHashMap<>();
+
+ systems.forEach(system -> {
+ final Class<?> systemClass = system.getClass();
+ inDegree.put(systemClass, 0);
+ adjacencyList.put(systemClass, new HashSet<>());
+ });
+
+ systems.forEach(system -> {
+ system.getDependencies().forEach(dependency -> {
+ if (systemMap.containsKey(dependency)) {
+ adjacencyList.get(dependency).add(system.getClass());
+ inDegree.merge(system.getClass(), 1, Integer::sum);
+ }
+ });
+ });
+
+ // Kahn's algorithm
+ final List<System<T>> result = new ArrayList<>();
+
+ final Queue<Class<?>> queue = new LinkedList<>(
+ inDegree.entrySet().stream().filter(entry -> entry.getValue() == 0).map(Map.Entry::getKey).toList());
+
+ while (!queue.isEmpty()) {
+ final Class<?> currentClass = queue.poll();
+ result.add(systemMap.get(currentClass));
+
+ adjacencyList.getOrDefault(currentClass, Collections.emptySet()).forEach(dependent -> {
+ final int newInDegree = inDegree.get(dependent) - 1;
+ inDegree.put(dependent, newInDegree);
+ if (newInDegree == 0) {
+ queue.add(dependent);
+ }
+ });
+ }
+
+ if (result.size() != systems.size()) {
+ throw new IllegalStateException("Circular dependency detected in systems");
+ }
+
+ return Collections.unmodifiableList(result);
+ }
+
+ @Override
+ public void dispose() {
+ for (final System<T> system : systemExecutionOrder) {
+ system.dispose();
+ }
+ componentCache.clear();
+ entities.clear();
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T> Map<Class<? extends T>, T> singletonClazzMap(final T... singletons) {
+ final boolean areSingletons = Arrays.stream(singletons).map(t -> (Class<? extends System<T>>) t.getClass())
+ .distinct().count() == singletons.length;
+ if (!areSingletons) {
+ throw new IllegalArgumentException("Only one instance may be used per clazz");
+ }
+
+ return Arrays.stream(singletons).map(t -> Map.entry((Class<? extends T>) t.getClass(), t))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/Component.java b/core/src/main/java/coffee/liz/ecs/model/Component.java
new file mode 100644
index 0000000..2d3a8e7
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/Component.java
@@ -0,0 +1,8 @@
+package coffee.liz.ecs.model;
+
+/** Component of an {@link Entity}. */
+public interface Component {
+ default Class<? extends Component> getKey() {
+ return getClass();
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/Entity.java b/core/src/main/java/coffee/liz/ecs/model/Entity.java
new file mode 100644
index 0000000..7dab667
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/Entity.java
@@ -0,0 +1,105 @@
+package coffee.liz.ecs.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+@Getter
+@Builder
+@RequiredArgsConstructor
+@AllArgsConstructor
+@Data
+public class Entity {
+ /** Unique id. */
+ private final int id;
+
+ /** Instances of {@link Component}s. */
+ @Builder.Default
+ private Map<Class<? extends Component>, Component> componentMap = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * Check if entity has component type.
+ *
+ * @param componentType
+ * the {@link Component} class
+ * @return true if component exists
+ */
+ public boolean has(final Class<? extends Component> componentType) {
+ return componentMap.containsKey(componentType);
+ }
+
+ /**
+ * Check if entity has all component types.
+ *
+ * @param components
+ * collection of {@link Component} classes
+ * @return true if all components exist
+ */
+ public boolean hasAll(final Collection<Class<? extends Component>> components) {
+ return components.stream().allMatch(this::has);
+ }
+
+ /**
+ * Get component by type.
+ *
+ * @param componentType
+ * the {@link Component} class
+ * @param <C>
+ * component type
+ * @return the component or throw {@link IllegalArgumentException}
+ */
+ @SuppressWarnings("unchecked")
+ public <C extends Component> C get(final Class<C> componentType) {
+ final C component = (C) componentMap.get(componentType);
+ if (component == null) {
+ throw new IllegalArgumentException(
+ "Entity with id " + getId() + " does not have required component " + componentType.getSimpleName());
+ }
+ return component;
+ }
+
+ /**
+ * Add component to entity.
+ *
+ * @param component
+ * the {@link Component} to add
+ * @param <C>
+ * component type
+ * @return this {@link Entity} for chaining
+ */
+ public <C extends Component> Entity add(final C component) {
+ componentMap.put(component.getKey(), component);
+ return this;
+ }
+
+ /**
+ * Remove component from entity.
+ *
+ * @param componentType
+ * the {@link Component} class to remove
+ * @param <C>
+ * component type
+ * @return this {@link Entity} for chaining
+ */
+ public <C extends Component> Entity remove(final Class<C> componentType) {
+ componentMap.remove(componentType);
+ return this;
+ }
+
+ /**
+ * Get all component types.
+ *
+ * @return set of {@link Component} classes
+ */
+ public Set<Class<? extends Component>> componentTypes() {
+ return componentMap.keySet();
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/Query.java b/core/src/main/java/coffee/liz/ecs/model/Query.java
new file mode 100644
index 0000000..679ea49
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/Query.java
@@ -0,0 +1,30 @@
+package coffee.liz.ecs.model;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public record Query(Set<Class<? extends Component>> queryingComponents, QueryFilter filter) {
+ public Query {
+ queryingComponents = Set.copyOf(queryingComponents);
+ }
+
+ @SafeVarargs
+ public static Query allOf(final Class<? extends Component>... components) {
+ return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.ALL_OF);
+ }
+
+ @SafeVarargs
+ public static Query anyOf(final Class<? extends Component>... components) {
+ return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.ANY_OF);
+ }
+
+ @SafeVarargs
+ public static Query noneOf(final Class<? extends Component>... components) {
+ return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.NONE_OF);
+ }
+
+ public enum QueryFilter {
+ ALL_OF, ANY_OF, NONE_OF
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java b/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java
new file mode 100644
index 0000000..eba5021
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java
@@ -0,0 +1,25 @@
+package coffee.liz.ecs.model;
+
+import lombok.RequiredArgsConstructor;
+
+import java.util.Set;
+
+@RequiredArgsConstructor
+public class QueryBuilder<T> {
+ private final World<T> world;
+
+ @SafeVarargs
+ public final Set<Entity> allOf(final Class<? extends Component>... components) {
+ return world.resolve(Query.allOf(components));
+ }
+
+ @SafeVarargs
+ public final Set<Entity> anyOf(final Class<? extends Component>... components) {
+ return world.resolve(Query.anyOf(components));
+ }
+
+ @SafeVarargs
+ public final Set<Entity> noneOf(final Class<? extends Component>... components) {
+ return world.resolve(Query.noneOf(components));
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/System.java b/core/src/main/java/coffee/liz/ecs/model/System.java
new file mode 100644
index 0000000..c00334a
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/System.java
@@ -0,0 +1,32 @@
+package coffee.liz.ecs.model;
+
+import java.time.Duration;
+import java.util.Collection;
+
+/**
+ * Updates the {@link World} state.
+ *
+ * @param <T>
+ * is the state of the stuff outside the {@link World}.
+ */
+public interface System<T> {
+ /**
+ * {@link System} clazzes that must run before this system.
+ *
+ * @return {@link Collection} of dependencies.
+ */
+ Collection<Class<? extends System<T>>> getDependencies();
+
+ /**
+ * @param world
+ * Is the {@link World}.
+ * @param state
+ * Is the {@link T} state outside the {@param world}.
+ * @param deltaSeconds
+ * Is the timestamp.
+ */
+ void update(final World<T> world, final T state, final float deltaSeconds);
+
+ default void dispose() {
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/World.java b/core/src/main/java/coffee/liz/ecs/model/World.java
new file mode 100644
index 0000000..82e01a7
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/World.java
@@ -0,0 +1,64 @@
+package coffee.liz.ecs.model;
+
+import java.time.Duration;
+import java.util.Set;
+
+/**
+ * The game world.
+ *
+ * @param <T>
+ * is the state of the stuff outside the world.
+ */
+public interface World<T> {
+ /**
+ * Create unique {@link Entity} in the {@link World}.
+ *
+ * @return created {@link Entity}.
+ */
+ Entity createEntity();
+
+ /**
+ * Remove an entity from the {@link World}.
+ *
+ * @param entity
+ * to remove.
+ */
+ void removeEntity(final Entity entity);
+
+ /**
+ * Get entities with {@link Component}s.
+ *
+ * @param query
+ * to query.
+ * @return All entities satisfying {@param query}.
+ */
+ Set<Entity> resolve(final Query query);
+
+ default QueryBuilder<T> queryable() {
+ return new QueryBuilder<>(this);
+ }
+
+ /**
+ * Integrate the {@link World}.
+ *
+ * @param state
+ * Is the state outside the world.
+ * @param deltaSeconds
+ * Is the time step.
+ */
+ void update(T state, float deltaSeconds);
+
+ /**
+ * Get world {@link System}.
+ *
+ * @param system
+ * is the Clazz.
+ * @param <S>
+ * is the {@link System} type.
+ * @return {@link System} instance of {@param system}.
+ */
+ <S extends System<T>> S getSystem(final Class<S> system);
+
+ default void dispose() {
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/utils/FunctionUtils.java b/core/src/main/java/coffee/liz/ecs/utils/FunctionUtils.java
new file mode 100644
index 0000000..25e81f0
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/utils/FunctionUtils.java
@@ -0,0 +1,28 @@
+package coffee.liz.ecs.utils;
+
+
+public final class FunctionUtils {
+ /**
+ * Runs the provided action and wraps failures into a {@link RuntimeException}.
+ *
+ * @param run
+ * action to execute
+ */
+ public static <E extends Throwable> void wrapCheckedException(final ThrowableRunnable<E> run) {
+ try {
+ run.run();
+ } catch (final Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @FunctionalInterface
+ public interface ThrowableRunnable<E extends Throwable> {
+ /**
+ * Run.
+ */
+ void run() throws E;
+ }
+
+ private FunctionUtils() { }
+}