From 289b63507791ef95b48f2715d1701be4ad5f2a67 Mon Sep 17 00:00:00 2001 From: Mark Beck Date: Tue, 4 Nov 2025 23:19:26 +0100 Subject: [PATCH] feat: add code --- .gitignore | 2 + README.md | 136 +++++++++++++++++- pom.xml | 98 +++++++++++++ src/main/java/com/example/App.java | 13 ++ .../java/com/example/components/Attack.java | 12 ++ .../java/com/example/components/Battle.java | 8 ++ .../com/example/components/CombatLog.java | 11 ++ .../java/com/example/components/Health.java | 25 ++++ .../java/com/example/components/Hero.java | 11 ++ .../com/example/components/HumanDefense.java | 7 + .../com/example/components/Investment.java | 13 ++ .../java/com/example/components/Squad.java | 28 ++++ .../java/com/example/components/Stats.java | 12 ++ .../example/controller/GameController.java | 70 +++++++++ src/main/java/com/example/ecs/Component.java | 4 + .../com/example/ecs/ComponentManager.java | 37 +++++ src/main/java/com/example/ecs/Entity.java | 38 +++++ src/main/java/com/example/ecs/Registry.java | 60 ++++++++ src/main/java/com/example/ecs/System.java | 11 ++ src/main/java/com/example/model/Element.java | 8 ++ src/main/java/com/example/model/Factory.java | 19 +++ .../java/com/example/model/HumanFactory.java | 38 +++++ .../java/com/example/model/OrcFactory.java | 33 +++++ src/main/java/com/example/model/Race.java | 44 ++++++ .../com/example/systems/CleanupSystem.java | 58 ++++++++ .../com/example/systems/CombatSystem.java | 49 +++++++ .../com/example/systems/DamageSystem.java | 33 +++++ .../com/example/systems/RenderSystem.java | 58 ++++++++ .../systems/SquadGenerationSystem.java | 56 ++++++++ src/test/java/com/example/AppTest.java | 19 +++ 30 files changed, 1010 insertions(+), 1 deletion(-) create mode 100644 pom.xml create mode 100644 src/main/java/com/example/App.java create mode 100644 src/main/java/com/example/components/Attack.java create mode 100644 src/main/java/com/example/components/Battle.java create mode 100644 src/main/java/com/example/components/CombatLog.java create mode 100644 src/main/java/com/example/components/Health.java create mode 100644 src/main/java/com/example/components/Hero.java create mode 100644 src/main/java/com/example/components/HumanDefense.java create mode 100644 src/main/java/com/example/components/Investment.java create mode 100644 src/main/java/com/example/components/Squad.java create mode 100644 src/main/java/com/example/components/Stats.java create mode 100644 src/main/java/com/example/controller/GameController.java create mode 100644 src/main/java/com/example/ecs/Component.java create mode 100644 src/main/java/com/example/ecs/ComponentManager.java create mode 100644 src/main/java/com/example/ecs/Entity.java create mode 100644 src/main/java/com/example/ecs/Registry.java create mode 100644 src/main/java/com/example/ecs/System.java create mode 100644 src/main/java/com/example/model/Element.java create mode 100644 src/main/java/com/example/model/Factory.java create mode 100644 src/main/java/com/example/model/HumanFactory.java create mode 100644 src/main/java/com/example/model/OrcFactory.java create mode 100644 src/main/java/com/example/model/Race.java create mode 100644 src/main/java/com/example/systems/CleanupSystem.java create mode 100644 src/main/java/com/example/systems/CombatSystem.java create mode 100644 src/main/java/com/example/systems/DamageSystem.java create mode 100644 src/main/java/com/example/systems/RenderSystem.java create mode 100644 src/main/java/com/example/systems/SquadGenerationSystem.java create mode 100644 src/test/java/com/example/AppTest.java diff --git a/.gitignore b/.gitignore index 81dc59a..4cdfbdf 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,5 @@ fabric.properties # Built Visual Studio Code Extensions *.vsix +.mvn/ +target/ \ No newline at end of file diff --git a/README.md b/README.md index d8b3643..45259e9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,136 @@ -# EntityComponentSystem +# Entity Component System +Nachdem das von ihnen entwickelte Spiel Racewars veröffentlicht wurde, bekommen sie einige beschwerden über schlechte Performance. Obwohl sie der Meinung sind, dass sich die Leute einfach einen neuen PC kaufen sollen, will ihr Arbeitgeber, dass sie das Spiel optimieren. + +Nach einiger Recherche stellen sie fest, dass die derzeitige Implementierung der Wesen Klasse zu schlechter Performance führt. + +Ihre Wesen sehen im Arbeitsspeicher etwa so aus: + +``` +┌─────────────────────────────────────────────┐ +│ Wesen Objekt │ +├─────────────────────────────────────────────┤ +│ │ +│ vtable pointer (8 bytes) │ +│ │ +├─────────────────────────────────────────────┤ +│ │ +│ geschwindigkeit : int (4 bytes) │ +│ │ +├─────────────────────────────────────────────┤ +│ │ +│ schaden : int (4 bytes) │ +│ │ +├─────────────────────────────────────────────┤ +│ │ +│ ruestung : int (4 bytes) │ +│ │ +├─────────────────────────────────────────────┤ +│ │ +│ lebenspunkte : double (8 bytes) │ +│ │ +└─────────────────────────────────────────────┘ +``` + +Prozessoren laden Daten aus dem Arbeitsspeicher allerdings meist in Blöcken von 64 Byte. Werden nun zum Beipiel bei der Ausgabe der Einheiten nur die Lebenspunkte von jeder Einheit gebraucht, muss der CPU das gesamte Objekt aus dem Arbeitsspeicher laden, obwohl ein Großteil nicht gebraucht wird. + +In vielen modernen Computerspielen wird diese ineffizienz umgangen, indem Spielobjekte nicht als einzelne Java-Objekte, sondern als eine Ansammlung von Komponenten gespeichert werden: +``` +┌───────────────────────────────────────────────────────────────────────────┐ +│ Entity Component System (ECS) Layout │ +├───────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Entity Table │ +│ ┌────────────┬────────────┬────────────┬────────────┐ │ +│ │ Entity 1 │ Entity 2 │ Entity 3 │ Entity 4 │ (int[], 4 bytes) │ +│ └────────────┴────────────┴────────────┴────────────┘ │ +│ │ +├───────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Geschwindigkeit Component │ +│ ┌────────────┬────────────┬────────────┬────────────┐ │ +│ │ 25 │ 40 │ 30 │ 35 │ (int[], 4 bytes) │ +│ └────────────┴────────────┴────────────┴────────────┘ │ +│ │ +├───────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Schaden Component │ +│ ┌────────────┬────────────┬────────────┬────────────┐ │ +│ │ 10 │ 15 │ 12 │ 18 │ (int[], 4 bytes) │ +│ └────────────┴────────────┴────────────┴────────────┘ │ +│ │ +├───────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Ruestung Component │ +│ ┌────────────┬────────────┬────────────┬────────────┐ │ +│ │ 5 │ 8 │ 6 │ 3 │ (int[], 4 bytes) │ +│ └────────────┴────────────┴────────────┴────────────┘ │ +│ │ +├───────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Lebenspunkte Component (Structure of Arrays): │ +│ ┌────────────┬────────────┬────────────┬────────────┐ │ +│ │ 100.0 │ 85.5 │ 92.3 │ 78.9 │ (double[], 8 bytes)│ +│ └────────────┴────────────┴────────────┴────────────┘ │ +│ │ +└───────────────────────────────────────────────────────────────────────────┘ +``` + +Werden jetzt wie im vorherigen Beispiel die Lebenspunkte aller Wesen gebraucht, liegen diese im Arbeitsspeicher nebeneinander. + +Ein Entity-Component-System besteht aus vier Konzepten: + +### Entities + +Entities sind Spielobjekte, sie sind meist nicht mehr als eine fortlaufende ID. + +### Components + +Components sind die zum Spielobjekt gehörenden Daten. Wie viele Daten jede Art von Component enthält kann verschieden sein. +Zum Beispiel kann ein `Lebenspunkte` Component nur die derzeitigen Lebenspunkte enthalten, während ein `Kampfkraft` Component Schaden, Geschwindigkeit und Rüstung enthält. + +### Registry + +Die Registry ist eine Datenbank, die alle Entities ihren Components zuweist und folgende Operationen Erlaubt: +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ │ +│ registerComponentType(componentClass) │ +│ └─ Registriert eine neue Art von Component zur Verwaltung │ +│ │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ addComponent(entity, component) │ +│ └─ Fügt der Entity einen neuen Component hinzu │ +│ │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ removeComponent(entity, componentType) │ +│ └─ Löscht den angegebenen Component von der Entity │ +│ │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ hasComponent(entity, componentType) → boolean │ +│ └─ Prüft, ob die Entity einen Component diesen Types hat │ +│ │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ getComponent(entity, componentType) → component │ +│ └─ Gibt den angegebenen Component für die Entity zurück │ +│ │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ withComponents(componentType...) → entity[] │ +│ └─ Gibt alle Entities zurück, die alle angegebenen Components haben │ +│ │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ removeEntity(entity) │ +│ └─ Löscht die Entity mit allen ihren Components │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Sytems + +Systems bilden die Anwendungslogik ab. Sie können die Registry nach Entities abfragen und neue Entities oder Components hinzufügen diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b9119f8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + com.example + racewars + 1.0-SNAPSHOT + + racewars + + http://www.example.com + + + UTF-8 + 25 + + + + + + org.junit + junit-bom + 5.11.0 + pom + import + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-params + test + + + + + + + + + maven-clean-plugin + 3.4.0 + + + + maven-resources-plugin + 3.3.1 + + + maven-compiler-plugin + 3.13.0 + + + maven-surefire-plugin + 3.3.0 + + + maven-jar-plugin + 3.4.2 + + + maven-install-plugin + 3.1.2 + + + maven-deploy-plugin + 3.1.2 + + + + maven-site-plugin + 3.12.1 + + + maven-project-info-reports-plugin + 3.6.1 + + + org.codehaus.mojo + exec-maven-plugin + 1.4.0 + + com.example.App + + + + + + diff --git a/src/main/java/com/example/App.java b/src/main/java/com/example/App.java new file mode 100644 index 0000000..56bd9a4 --- /dev/null +++ b/src/main/java/com/example/App.java @@ -0,0 +1,13 @@ +package com.example; + +import com.example.controller.GameController; + +/** + * Hello world! + */ +public class App { + public static void main(String[] args) { + GameController gc = new GameController(); + gc.runGame(); + } +} diff --git a/src/main/java/com/example/components/Attack.java b/src/main/java/com/example/components/Attack.java new file mode 100644 index 0000000..f3a6b39 --- /dev/null +++ b/src/main/java/com/example/components/Attack.java @@ -0,0 +1,12 @@ +package com.example.components; + +import com.example.ecs.Component; +import com.example.ecs.Entity; + +public record Attack( + Entity source, + Entity target, + double damage +) implements Component { + +} diff --git a/src/main/java/com/example/components/Battle.java b/src/main/java/com/example/components/Battle.java new file mode 100644 index 0000000..3d76a1e --- /dev/null +++ b/src/main/java/com/example/components/Battle.java @@ -0,0 +1,8 @@ +package com.example.components; + +import com.example.ecs.Component; +import com.example.ecs.Entity; + +public record Battle(Entity squad1, Entity squad2) implements Component { + +} diff --git a/src/main/java/com/example/components/CombatLog.java b/src/main/java/com/example/components/CombatLog.java new file mode 100644 index 0000000..772b473 --- /dev/null +++ b/src/main/java/com/example/components/CombatLog.java @@ -0,0 +1,11 @@ +package com.example.components; + +import com.example.ecs.Component; +import com.example.ecs.Entity; + +public record CombatLog ( + Entity attacker, + Entity defender, + double initialDamage, + double resultingDamage +) implements Component {} diff --git a/src/main/java/com/example/components/Health.java b/src/main/java/com/example/components/Health.java new file mode 100644 index 0000000..72d3843 --- /dev/null +++ b/src/main/java/com/example/components/Health.java @@ -0,0 +1,25 @@ +package com.example.components; + +import com.example.ecs.Component; + +public class Health implements Component { + private final double maxHp; + private double hp; + + public Health(double maxHp, double hp) { + this.maxHp = maxHp; + this.hp = hp; + } + + public double hp() { + return hp; + } + + public double maxHp() { + return maxHp; + } + + public void lowerHp(double amount) { + hp -= amount; + } +} diff --git a/src/main/java/com/example/components/Hero.java b/src/main/java/com/example/components/Hero.java new file mode 100644 index 0000000..99b9e8a --- /dev/null +++ b/src/main/java/com/example/components/Hero.java @@ -0,0 +1,11 @@ +package com.example.components; + +import com.example.ecs.Component; +import com.example.model.Element; + +public record Hero( + String name, + double bonus, + Element element +) implements Component { +} diff --git a/src/main/java/com/example/components/HumanDefense.java b/src/main/java/com/example/components/HumanDefense.java new file mode 100644 index 0000000..b1a129a --- /dev/null +++ b/src/main/java/com/example/components/HumanDefense.java @@ -0,0 +1,7 @@ +package com.example.components; + +import com.example.ecs.Component; + +public record HumanDefense( + double defense +) implements Component {} diff --git a/src/main/java/com/example/components/Investment.java b/src/main/java/com/example/components/Investment.java new file mode 100644 index 0000000..fbe4842 --- /dev/null +++ b/src/main/java/com/example/components/Investment.java @@ -0,0 +1,13 @@ +package com.example.components; + +import com.example.ecs.Component; +import com.example.ecs.Entity; +import com.example.model.Race; + +public record Investment( + int amount, + Race race, + boolean witLeader, + Entity forSquad +) implements Component { +} diff --git a/src/main/java/com/example/components/Squad.java b/src/main/java/com/example/components/Squad.java new file mode 100644 index 0000000..99e0a67 --- /dev/null +++ b/src/main/java/com/example/components/Squad.java @@ -0,0 +1,28 @@ +package com.example.components; + +import java.util.List; + +import com.example.ecs.Component; +import com.example.ecs.Entity; + +public class Squad implements Component { + private String name; + private List units; + + public Squad(String name, List units) { + this.name = name; + this.units = units; + } + + public void removeUnit(Entity entity) { + units.remove(entity); + } + + public String name() { + return name; + } + + public List units() { + return units; + } +} diff --git a/src/main/java/com/example/components/Stats.java b/src/main/java/com/example/components/Stats.java new file mode 100644 index 0000000..b256bb9 --- /dev/null +++ b/src/main/java/com/example/components/Stats.java @@ -0,0 +1,12 @@ +package com.example.components; + +import com.example.ecs.Component; +import com.example.model.Race; + +public record Stats( + double dmg, + double speed, + double armor, + Race race +) implements Component { +} diff --git a/src/main/java/com/example/controller/GameController.java b/src/main/java/com/example/controller/GameController.java new file mode 100644 index 0000000..0ba6722 --- /dev/null +++ b/src/main/java/com/example/controller/GameController.java @@ -0,0 +1,70 @@ +package com.example.controller; + + +import com.example.components.Attack; +import com.example.components.Battle; +import com.example.components.CombatLog; +import com.example.components.Health; +import com.example.components.Hero; +import com.example.components.HumanDefense; +import com.example.components.Investment; +import com.example.components.Squad; +import com.example.components.Stats; +import com.example.ecs.Entity; +import com.example.ecs.Registry; +import com.example.ecs.System; +import com.example.model.Race; +import com.example.systems.CleanupSystem; +import com.example.systems.CombatSystem; +import com.example.systems.DamageSystem; +import com.example.systems.RenderSystem; +import com.example.systems.SquadGenerationSystem; + +public class GameController { + + Registry registry = new Registry(); + SquadGenerationSystem squadGenerationSystem; + System combatSystem; + System damageSystem; + System renderSystem; + CleanupSystem cleanupSystem; + + public GameController() { + registry.registerComponentType(Attack.class); + registry.registerComponentType(Battle.class); + registry.registerComponentType(CombatLog.class); + registry.registerComponentType(Health.class); + registry.registerComponentType(Hero.class); + registry.registerComponentType(HumanDefense.class); + registry.registerComponentType(Investment.class); + registry.registerComponentType(Squad.class); + registry.registerComponentType(Stats.class); + + + squadGenerationSystem = new SquadGenerationSystem(registry); + combatSystem = new CombatSystem(registry); + damageSystem = new DamageSystem(registry); + renderSystem = new RenderSystem(registry); + cleanupSystem = new CleanupSystem(registry); + + Entity squad1 = new Entity(); + Entity squad2 = new Entity(); + + registry.addComponent(new Entity(), new Investment(2000, Race.Human, true, squad1)); + registry.addComponent(new Entity(), new Investment(2000, Race.Orc, true, squad2)); + + squadGenerationSystem.generateSquad(squad1, "Squad1", 2000); + squadGenerationSystem.generateSquad(squad2, "Squad2", 2000); + + registry.addComponent(new Entity(), new Battle(squad1, squad2)); + } + + public void runGame() { + while (!cleanupSystem.gameEnded()) { + combatSystem.run(); + damageSystem.run(); + renderSystem.run(); + cleanupSystem.run(); + } + } +} diff --git a/src/main/java/com/example/ecs/Component.java b/src/main/java/com/example/ecs/Component.java new file mode 100644 index 0000000..5cd9ce1 --- /dev/null +++ b/src/main/java/com/example/ecs/Component.java @@ -0,0 +1,4 @@ +package com.example.ecs; + +public interface Component { +} diff --git a/src/main/java/com/example/ecs/ComponentManager.java b/src/main/java/com/example/ecs/ComponentManager.java new file mode 100644 index 0000000..cddfca3 --- /dev/null +++ b/src/main/java/com/example/ecs/ComponentManager.java @@ -0,0 +1,37 @@ +package com.example.ecs; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +class ComponentManager { + private final Map entityComponents; + + protected ComponentManager() { + entityComponents = new HashMap<>(); + } + + protected void addComponent(Entity entity, T component) { + entityComponents.put(entity.getId(), component); + } + + protected void removeComponent(Entity entity) { + entityComponents.remove(entity.id); + } + + protected T getComponent(Entity entity) { + return entityComponents.get(Integer.valueOf(entity.getId())); + } + + public boolean hasComponent(Entity entity) { + return entityComponents.containsKey(entity.getId()); + } + + protected Set getEntities() { + return entityComponents.keySet().stream() + .map((Integer id) -> { return new Entity(id); }) + .collect(Collectors.toSet()); + } + +} diff --git a/src/main/java/com/example/ecs/Entity.java b/src/main/java/com/example/ecs/Entity.java new file mode 100644 index 0000000..209390d --- /dev/null +++ b/src/main/java/com/example/ecs/Entity.java @@ -0,0 +1,38 @@ +package com.example.ecs; + +import java.util.Objects; + +public class Entity { + static int count = 0; + int id; + + public Entity() { + this.id = count++; + } + + protected Entity(int id) { + this.id = id; + } + + public int getId() { + return this.id; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Entity e) { + return this.getId() == e.getId(); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Entity{" + "id=" + id + '}'; + } +} diff --git a/src/main/java/com/example/ecs/Registry.java b/src/main/java/com/example/ecs/Registry.java new file mode 100644 index 0000000..230c6b4 --- /dev/null +++ b/src/main/java/com/example/ecs/Registry.java @@ -0,0 +1,60 @@ +package com.example.ecs; + +import java.util.HashMap; +import java.util.Set; + +public class Registry { + HashMap, ComponentManager> componentManagers = new HashMap<>(); + + public void registerComponentType(Class dataClass) { + componentManagers.put(dataClass, new ComponentManager()); + } + + public void addComponent(Entity entity, T component) { + Class componentClass = (Class) component.getClass(); + getComponentManager(componentClass).addComponent(entity, component); + } + + public T getComponent(Entity entity, Class componentClass) { + return getComponentManager(componentClass).getComponent(entity); + } + + public boolean hasComponent(Entity entity, Class componentClass) { + try { + var component = getComponentManager(componentClass).getComponent(entity); + return component != null; + } catch(Exception e) { + return false; + } + } + + public Entity[] getWithComponents(Class... components) { + if(components.length == 0) { + throw new IllegalArgumentException(); + } + + Set entitySet = getComponentManager(components[0]).getEntities(); + for (int i = 1; i < components.length; i++) { + entitySet.retainAll(getComponentManager(components[i]).getEntities()); + } + return entitySet.toArray(new Entity[0]); + } + + public void remove(Entity entity) { + for (var manager : componentManagers.values()) { + manager.removeComponent(entity); + } + } + + public void removeComponent(Entity entity, Class component) { + getComponentManager(component).removeComponent(entity); + } + + private ComponentManager getComponentManager(Class componentClass) { + Object manager = componentManagers.get(componentClass); + if (manager == null) { + throw new IllegalArgumentException("No component manager for: " + componentClass); + } + return (ComponentManager) manager; + } +} diff --git a/src/main/java/com/example/ecs/System.java b/src/main/java/com/example/ecs/System.java new file mode 100644 index 0000000..9c1ac59 --- /dev/null +++ b/src/main/java/com/example/ecs/System.java @@ -0,0 +1,11 @@ +package com.example.ecs; + +public abstract class System { + protected final Registry registry; + + public System(Registry registry) { + this.registry = registry; + } + + public abstract void run(); +} diff --git a/src/main/java/com/example/model/Element.java b/src/main/java/com/example/model/Element.java new file mode 100644 index 0000000..9896b5a --- /dev/null +++ b/src/main/java/com/example/model/Element.java @@ -0,0 +1,8 @@ +package com.example.model; + +public enum Element { + Fire, + Water, + Earth, + Air +} diff --git a/src/main/java/com/example/model/Factory.java b/src/main/java/com/example/model/Factory.java new file mode 100644 index 0000000..d2ee33b --- /dev/null +++ b/src/main/java/com/example/model/Factory.java @@ -0,0 +1,19 @@ +package com.example.model; + +import com.example.ecs.Entity; +import com.example.ecs.Registry; + +public interface Factory { + + /** + * Erzeugt ein neues Wesen der Rasse. + * @return das Wesen. + */ + Entity createUnit(Registry registry); + + /** + * Liefert den Anführer der Rasse. + * @return Anführer. + */ + Entity createHero(Registry registry); +} diff --git a/src/main/java/com/example/model/HumanFactory.java b/src/main/java/com/example/model/HumanFactory.java new file mode 100644 index 0000000..3ef1413 --- /dev/null +++ b/src/main/java/com/example/model/HumanFactory.java @@ -0,0 +1,38 @@ +package com.example.model; + +import com.example.components.Health; +import com.example.components.Hero; +import com.example.components.HumanDefense; +import com.example.components.Stats; +import com.example.ecs.Entity; +import com.example.ecs.Registry; + +public class HumanFactory implements Factory { + + @Override + public Entity createUnit(Registry registry) { + Stats stats = new Stats(40, 2, 0.4, Race.Human); + Health health = new Health(140, 140); + HumanDefense humanDefense = new HumanDefense(0.1); + Entity human = new Entity(); + registry.addComponent(human, stats); + registry.addComponent(human, health); + registry.addComponent(human, humanDefense); + return human; + } + + @Override + public Entity createHero(Registry registry) { + Stats stats = new Stats(40, 2, 0.4, Race.Human); + Health health = new Health(140 * 5, 140 * 5); + HumanDefense humanDefense = new HumanDefense(0.1); + Hero hero = new Hero("Archmage", 5, Element.Fire); + Entity human = new Entity(); + registry.addComponent(human, stats); + registry.addComponent(human, health); + registry.addComponent(human, humanDefense); + registry.addComponent(human, hero); + return human; + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/model/OrcFactory.java b/src/main/java/com/example/model/OrcFactory.java new file mode 100644 index 0000000..d2d3a3e --- /dev/null +++ b/src/main/java/com/example/model/OrcFactory.java @@ -0,0 +1,33 @@ +package com.example.model; + +import com.example.components.Health; +import com.example.components.Hero; +import com.example.components.Stats; +import com.example.ecs.Entity; +import com.example.ecs.Registry; + +public class OrcFactory implements Factory { + + @Override + public Entity createUnit(Registry registry) { + Stats stats = new Stats(33, 1, 0.3, Race.Orc); + Health health = new Health(140, 140); + Entity orc = new Entity(); + registry.addComponent(orc, stats); + registry.addComponent(orc, health); + return orc; + } + + @Override + public Entity createHero(Registry registry) { + Stats stats = new Stats(33, 1, 0.3, Race.Orc); + Health health = new Health(140 * 1.2, 140 * 1.2); + Hero hero = new Hero("Farseer", 1.2, Element.Earth); + Entity orc = new Entity(); + registry.addComponent(orc, stats); + registry.addComponent(orc, health); + registry.addComponent(orc, hero); + return orc; + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/model/Race.java b/src/main/java/com/example/model/Race.java new file mode 100644 index 0000000..6719bf1 --- /dev/null +++ b/src/main/java/com/example/model/Race.java @@ -0,0 +1,44 @@ +package com.example.model; + +public enum Race { + Human { + @Override + public Factory getFactory() { + return humanFactory; + } + + @Override + public int getCost() { + return 110; + } + + @Override + public int getLeaderCost() { + return 220; + } + }, + Orc { + @Override + public Factory getFactory() { + return orcFactory; + } + + @Override + public int getCost() { + return 150; + } + + @Override + public int getLeaderCost() { + return 300; + } + } + ; + + static Factory humanFactory = new HumanFactory(); + static Factory orcFactory = new OrcFactory(); + + public abstract Factory getFactory(); + public abstract int getCost(); + public abstract int getLeaderCost(); +} \ No newline at end of file diff --git a/src/main/java/com/example/systems/CleanupSystem.java b/src/main/java/com/example/systems/CleanupSystem.java new file mode 100644 index 0000000..d4969b1 --- /dev/null +++ b/src/main/java/com/example/systems/CleanupSystem.java @@ -0,0 +1,58 @@ +package com.example.systems; + +import com.example.components.Attack; +import com.example.components.Battle; +import com.example.components.Health; +import com.example.ecs.Registry; +import com.example.ecs.System; +import com.example.components.Squad; + +public class CleanupSystem extends System { + + private boolean gameEnded = false; + + public CleanupSystem(Registry registry) { + super(registry); + } + + public void run() { + + var attacks = registry.getWithComponents(Attack.class); + for (var attack : attacks) { + registry.remove(attack); + } + + var units = registry.getWithComponents(Health.class); + for (var unit : units) { + var health = registry.getComponent(unit, Health.class); + if (health.hp() < 0) { + var squads = registry.getWithComponents(Squad.class); + for (var squadEntity : squads) { + var squad = registry.getComponent(squadEntity, Squad.class); + squad.removeUnit(unit); + } + registry.remove(unit); + } + } + + var battles = registry.getWithComponents(Battle.class); + for (var battleEntity : battles) { + var battle = registry.getComponent(battleEntity, Battle.class); + var squad1 = registry.getComponent(battle.squad1(), Squad.class); + var squad2 = registry.getComponent(battle.squad2(), Squad.class); + if (squad1.units().isEmpty() || squad2.units().isEmpty()) { + registry.remove(battleEntity); + registry.remove(battle.squad1()); + registry.remove(battle.squad2()); + } + } + + if (registry.getWithComponents(Battle.class).length == 0) { + gameEnded = true; + } + } + + public boolean gameEnded() { + return gameEnded; + } +} diff --git a/src/main/java/com/example/systems/CombatSystem.java b/src/main/java/com/example/systems/CombatSystem.java new file mode 100644 index 0000000..3f7dac1 --- /dev/null +++ b/src/main/java/com/example/systems/CombatSystem.java @@ -0,0 +1,49 @@ +package com.example.systems; + +import java.util.Random; + +import com.example.components.Attack; +import com.example.components.Battle; +import com.example.components.Hero; +import com.example.components.Squad; +import com.example.components.Stats; +import com.example.ecs.Entity; +import com.example.ecs.Registry; +import com.example.ecs.System; + +public class CombatSystem extends System { + + public CombatSystem(Registry registry) { + super(registry); + } + + Random rng = new Random(); + + public void run() { + for (var battleEntity : registry.getWithComponents(Battle.class)) { + var battle = registry.getComponent(battleEntity, Battle.class); + + var squad1 = registry.getComponent(battle.squad1(), Squad.class); + var squad2 = registry.getComponent(battle.squad2(), Squad.class); + + attack(squad1, squad2); + attack(squad2, squad1); + } + } + + private void attack(Squad attacker, Squad defender) { + for (var entity : attacker.units()) { + var unitData = registry.getComponent(entity, Stats.class); + boolean isHero = registry.hasComponent(entity, Hero.class); + + var target = defender.units().get(rng.nextInt(defender.units().size())); + double damageDealt = unitData.dmg() * unitData.speed(); + if (isHero) { + var heroData = registry.getComponent(entity, Hero.class); + damageDealt *= heroData.bonus(); + } + + registry.addComponent(new Entity(), new Attack(entity, target, damageDealt)); + } + } +} diff --git a/src/main/java/com/example/systems/DamageSystem.java b/src/main/java/com/example/systems/DamageSystem.java new file mode 100644 index 0000000..d4b4715 --- /dev/null +++ b/src/main/java/com/example/systems/DamageSystem.java @@ -0,0 +1,33 @@ +package com.example.systems; + +import com.example.components.Attack; +import com.example.components.CombatLog; +import com.example.components.Health; +import com.example.components.HumanDefense; +import com.example.components.Stats; +import com.example.ecs.Entity; +import com.example.ecs.Registry; +import com.example.ecs.System; + +public class DamageSystem extends System { + public DamageSystem(Registry registry) { + super(registry); + } + + public void run() { + for (Entity attackEntity : registry.getWithComponents(Attack.class)) { + Attack attack = registry.getComponent(attackEntity, Attack.class); + Stats targetStats = registry.getComponent(attack.target(), Stats.class); + Health targetHealth = registry.getComponent(attack.target(), Health.class); + + double dmgDealt = attack.damage() * (1 - targetStats.armor()); + if (registry.hasComponent(attack.target(), HumanDefense.class)) { + HumanDefense defense = registry.getComponent(attack.target(), HumanDefense.class); + dmgDealt -= dmgDealt * defense.defense(); + } + + targetHealth.lowerHp(dmgDealt); + registry.addComponent(attackEntity, new CombatLog(attack.source(), attack.target(), attack.damage(), dmgDealt)); + } + } +} diff --git a/src/main/java/com/example/systems/RenderSystem.java b/src/main/java/com/example/systems/RenderSystem.java new file mode 100644 index 0000000..1e53329 --- /dev/null +++ b/src/main/java/com/example/systems/RenderSystem.java @@ -0,0 +1,58 @@ +package com.example.systems; + +import com.example.components.CombatLog; +import com.example.components.Health; +import com.example.components.Hero; +import com.example.components.Squad; +import com.example.ecs.Entity; +import com.example.ecs.Registry; +import com.example.ecs.System; +import com.example.components.Stats; + +public class RenderSystem extends System { + + public RenderSystem(Registry registry) { + super(registry); + } + + public void run() { + renderCombat(); + + var squads = registry.getWithComponents(Squad.class); + + for (var squad : squads) { + var squadData = registry.getComponent(squad, Squad.class); + renderSquad(squadData); + } + } + + private String generateName(Entity entity) { + if (registry.hasComponent(entity, Hero.class)) { + Hero herodata = registry.getComponent(entity, Hero.class); + return herodata.name(); + } else if (registry.hasComponent(entity, Stats.class)) { + Stats stats = registry.getComponent(entity, Stats.class); + return stats.race().toString(); + } else { + return "Unknown"; + } + } + + private void renderCombat() { + for (var entity : registry.getWithComponents(CombatLog.class)) { + CombatLog log = registry.getComponent(entity, CombatLog.class); + IO.println(generateName(log.attacker()) + " ---------[" + log.resultingDamage() + "]-------->" + generateName(log.defender())); + } + IO.println(); + } + + private void renderSquad(Squad squad) { + IO.println("Squad " + squad.name()); + for (var unit : squad.units()) { + var health = registry.getComponent(unit, Health.class); + + IO.println(generateName(unit) + " [" + health.hp() + "/" + health.maxHp() + "]"); + } + IO.println("-------------------------"); + } +} diff --git a/src/main/java/com/example/systems/SquadGenerationSystem.java b/src/main/java/com/example/systems/SquadGenerationSystem.java new file mode 100644 index 0000000..883966b --- /dev/null +++ b/src/main/java/com/example/systems/SquadGenerationSystem.java @@ -0,0 +1,56 @@ +package com.example.systems; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import com.example.components.Investment; +import com.example.components.Squad; +import com.example.ecs.Entity; +import com.example.ecs.Registry; + +public class SquadGenerationSystem { + private final Registry registry; + + public SquadGenerationSystem(Registry registry) { + this.registry = registry; + } + + public Entity generateSquad(Entity squadEntity, String name, int maxInvestment) { + var investmentEntities = registry.getWithComponents(Investment.class); + List units = new ArrayList<>(); + for (var investmentEntity : investmentEntities) { + var investment = registry.getComponent(investmentEntity, Investment.class); + if (investment.forSquad().equals(squadEntity)) { + units.addAll(createUnits(investment)); + registry.remove(investmentEntity); + } + } + + Squad squad = new Squad(name, units); + registry.addComponent(squadEntity, squad); + return squadEntity; + } + + private List createUnits(Investment investment) { + int moneyRemaining = investment.amount(); + List leader = new ArrayList<>(); + + if (investment.witLeader()) { + moneyRemaining -= investment.race().getLeaderCost(); + leader.add(investment.race().getFactory().createHero(registry)); + } + + var amount = moneyRemaining / investment.race().getCost(); + + List squad = Stream.generate(() -> + investment.race().getFactory().createUnit(registry)) + .limit(amount) + .toList(); + var finalSquad = new ArrayList(); + finalSquad.addAll(leader); + finalSquad.addAll(squad); + + return finalSquad; + } +} diff --git a/src/test/java/com/example/AppTest.java b/src/test/java/com/example/AppTest.java new file mode 100644 index 0000000..e4db7b5 --- /dev/null +++ b/src/test/java/com/example/AppTest.java @@ -0,0 +1,19 @@ +package com.example; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Unit test for simple App. + */ +public class AppTest { + + /** + * Rigorous Test :-) + */ + @Test + public void shouldAnswerWithTrue() { + assertTrue(true); + } +}