diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index 0fdc7419..085dc3f1 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -7,6 +7,7 @@ on:
push:
pull_request:
branches: [ master ]
+ workflow_dispatch:
jobs:
build:
@@ -14,16 +15,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up JDK
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- uses: Trass3r/setup-cpp@master
- name: Build with Gradle
run: ./gradlew build
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
name: distribution
path: build/distributions/OpenKeeper.zip
+ - uses: marvinpinto/action-automatic-releases@latest
+ if: github.ref_name == 'master'
+ with:
+ repo_token: "${{ secrets.GITHUB_TOKEN }}"
+ automatic_release_tag: "latest"
+ prerelease: true
+ title: "Development Build"
+ files: |
+ build/distributions/OpenKeeper.zip
diff --git a/src/toniarts/openkeeper/game/component/CreatureSpell.java b/src/toniarts/openkeeper/game/component/CreatureSpell.java
new file mode 100644
index 00000000..103181ea
--- /dev/null
+++ b/src/toniarts/openkeeper/game/component/CreatureSpell.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014-2024 OpenKeeper
+ *
+ * OpenKeeper is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * OpenKeeper is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with OpenKeeper. If not, see .
+ */
+package toniarts.openkeeper.game.component;
+
+import com.simsilica.es.EntityId;
+
+/**
+ * Creature spell. Note that since a creature can have multiple, these are
+ * supposed to be given to individual entities
+ *
+ * @author Toni Helenius
+ */
+public class CreatureSpell extends Attack {
+
+ public short creatureSpellId;
+ public EntityId creatureId;
+
+ public CreatureSpell() {
+ // For serialization
+ }
+
+ public CreatureSpell(short creatureSpellId, EntityId creatureId, float rechargeTime, float range) {
+ super(rechargeTime, range);
+ this.creatureSpellId = creatureSpellId;
+ this.creatureId = creatureId;
+ }
+
+ public CreatureSpell(CreatureSpell attack, double attackStartTime) {
+ super(attack.rechargeTime, attack.range);
+ this.creatureSpellId = attack.creatureSpellId;
+ this.creatureId = attack.creatureId;
+ this.attactStartTime = attackStartTime;
+ }
+
+}
diff --git a/src/toniarts/openkeeper/game/component/CreatureSpells.java b/src/toniarts/openkeeper/game/component/CreatureSpells.java
new file mode 100644
index 00000000..35449190
--- /dev/null
+++ b/src/toniarts/openkeeper/game/component/CreatureSpells.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2014-2024 OpenKeeper
+ *
+ * OpenKeeper is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * OpenKeeper is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with OpenKeeper. If not, see .
+ */
+package toniarts.openkeeper.game.component;
+
+import com.simsilica.es.EntityComponent;
+import com.simsilica.es.EntityId;
+import java.util.List;
+
+/**
+ * Creature spells. Minor infraction of ECS design having a list here. The idea
+ * is to link the actual spell entities to a creature. Give this component to a
+ * creature and you have yourself a search key to the actual spells.
+ *
+ * @author Toni Helenius
+ */
+public class CreatureSpells implements EntityComponent {
+
+ public List creatureSpells;
+
+ public CreatureSpells() {
+ // For serialization
+ }
+
+ public CreatureSpells(List creatureSpells) {
+ this.creatureSpells = creatureSpells;
+ }
+
+}
diff --git a/src/toniarts/openkeeper/game/controller/CreaturesController.java b/src/toniarts/openkeeper/game/controller/CreaturesController.java
index b6e35534..2a0779aa 100644
--- a/src/toniarts/openkeeper/game/controller/CreaturesController.java
+++ b/src/toniarts/openkeeper/game/controller/CreaturesController.java
@@ -28,10 +28,12 @@
import java.lang.System.Logger.Level;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
+import java.util.stream.Collectors;
import toniarts.openkeeper.game.component.CreatureAi;
import toniarts.openkeeper.game.component.CreatureComponent;
import toniarts.openkeeper.game.component.CreatureEfficiency;
@@ -42,6 +44,8 @@
import toniarts.openkeeper.game.component.CreatureMeleeAttack;
import toniarts.openkeeper.game.component.CreatureMood;
import toniarts.openkeeper.game.component.CreatureSleep;
+import toniarts.openkeeper.game.component.CreatureSpell;
+import toniarts.openkeeper.game.component.CreatureSpells;
import toniarts.openkeeper.game.component.CreatureTortured;
import toniarts.openkeeper.game.component.CreatureViewState;
import toniarts.openkeeper.game.component.Death;
@@ -303,6 +307,7 @@ private EntityId loadCreature(EntityId entity, Creature creature, String name, S
// Set every attribute by the level of the created creature
setAttributesByLevel(creatureComponent, creatureExperience, healthComponent, goldComponent, sensesComponent, threatComponent, creatureMeleeAttack, regeneration);
+ setSpells(entity, creature, level);
entityData.setComponent(entity, creatureComponent);
entityData.setComponent(entity, creatureExperience);
@@ -445,6 +450,7 @@ public void levelUpCreature(EntityId entityId, int level, int experience) {
// Update stats
setAttributesByLevel(creatureComponent, creatureExperience, health, gold, senses, threat, creatureMeleeAttack, regeneration);
+ setSpells(entityId, kwdFile.getCreature(creatureComponent.creatureId), level);
// Set the new components to the entity
entityData.setComponents(entityId, creatureComponent, creatureExperience, health, gold, senses, threat, creatureMeleeAttack);
@@ -558,7 +564,7 @@ public ICreatureController createController(EntityId entityId) {
}
private ICreatureController createCreatureController(EntityId id, CreatureComponent creatureComponent) {
- return new CreatureController(id, entityData, kwdFile.getCreature(creatureComponent.creatureId), gameController.getNavigationService(), gameController.getTaskManager(), gameTimer, gameSettings, this, gameController.getEntityLookupService(), mapController, levelInfo, gameController.getGameWorldController().getObjectsController());
+ return new CreatureController(id, entityData, kwdFile.getCreature(creatureComponent.creatureId), gameController.getNavigationService(), gameController.getTaskManager(), gameTimer, gameSettings, this, gameController.getEntityLookupService(), mapController, levelInfo, gameController.getGameWorldController().getObjectsController(), gameController.getGameWorldController().getShotsController());
}
@Override
@@ -587,4 +593,46 @@ public void turnCreatureIntoAnother(EntityId entityId, short playerId, short cre
loadCreature(newEntityId, kwdFile.getCreature(creatureId), creatureComponent.name, creatureComponent.bloodType, 100, 0, 1, SpawnType.PLACE, position.position.x, position.position.z, playerId, position.rotation, null, (short) 0, 0, trigger != null ? trigger.triggerId : null);
}
+ private void setSpells(EntityId entityId, Creature creature, int level) {
+
+ // Get the spells the creature should have
+ Map availableSpells = creature.getSpells()
+ .stream()
+ .filter((spell) -> spell.getLevelAvailable() >= level)
+ .collect(Collectors.toMap((spell) -> spell.getCreatureSpellId(), (spell) -> kwdFile.getCreatureSpellById(spell.getCreatureSpellId())));
+ List creatureSpellIds = new ArrayList<>(availableSpells.size());
+ boolean spellsChanged = false;
+
+ // ... and compare it to the list we have
+ CreatureSpells creatureSpells = entityData.getComponent(entityId, CreatureSpells.class);
+ List ownedSpells = creatureSpells != null ? creatureSpells.creatureSpells : Collections.emptyList();
+
+ // Remove all spells we should not have
+ for (EntityId spellEntityId : ownedSpells) {
+ CreatureSpell spell = entityData.getComponent(spellEntityId, CreatureSpell.class);
+ if (availableSpells.containsKey(spell.creatureSpellId)) {
+ availableSpells.remove(spell.creatureSpellId);
+ creatureSpellIds.add(spellEntityId);
+ } else {
+ entityData.removeEntity(spellEntityId);
+ spellsChanged = true;
+ }
+ }
+
+ // All that is left is to add the remaining spells to the creature
+ for (toniarts.openkeeper.tools.convert.map.CreatureSpell spell : availableSpells.values()) {
+ EntityId spellEntity = entityData.createEntity();
+ entityData.setComponent(spellEntity, new CreatureSpell(spell.getCreatureSpellId(), entityId, spell.getRechargeTime(), spell.getRange()));
+ creatureSpellIds.add(spellEntity);
+ spellsChanged = true;
+ }
+
+ // Update the creature spell catalog
+ if (creatureSpellIds.isEmpty()) {
+ entityData.removeComponent(entityId, CreatureSpells.class);
+ } else if (spellsChanged) {
+ entityData.setComponent(entityId, new CreatureSpells(creatureSpellIds));
+ }
+ }
+
}
diff --git a/src/toniarts/openkeeper/game/controller/GameWorldController.java b/src/toniarts/openkeeper/game/controller/GameWorldController.java
index 9b739ab5..a7721a62 100644
--- a/src/toniarts/openkeeper/game/controller/GameWorldController.java
+++ b/src/toniarts/openkeeper/game/controller/GameWorldController.java
@@ -928,7 +928,7 @@ public void castKeeperSpell(short keeperSpellId, EntityId target, Point tile, Ve
boolean spellUpgraded = researchableEntity.isUpgraded();
int shotData1 = spellUpgraded ? keeperSpell.getBonusShotData1() : keeperSpell.getShotData1();
int shotData2 = spellUpgraded ? keeperSpell.getBonusShotData2() : keeperSpell.getShotData2();
- shotsController.createShot(keeperSpell.getShotTypeId(), shotData1, shotData2, playerId, position, target);
+ shotsController.createShot(keeperSpell.getShotTypeId(), shotData1, shotData2, playerId, WorldUtils.vector2fToVector3(position), target);
}
@Override
@@ -995,6 +995,11 @@ public ITrapsController getTrapsController() {
return trapsController;
}
+ @Override
+ public IShotsController getShotsController() {
+ return shotsController;
+ }
+
public void setEntityPositionLookup(IEntityPositionLookup entityPositionLookup) {
this.entityPositionLookup = entityPositionLookup;
}
diff --git a/src/toniarts/openkeeper/game/controller/IGameWorldController.java b/src/toniarts/openkeeper/game/controller/IGameWorldController.java
index 7f448b53..0ce595df 100644
--- a/src/toniarts/openkeeper/game/controller/IGameWorldController.java
+++ b/src/toniarts/openkeeper/game/controller/IGameWorldController.java
@@ -154,6 +154,13 @@ public interface IGameWorldController {
*/
public IObjectsController getObjectsController();
+ /**
+ * Get the shots controller
+ *
+ * @return shots controller
+ */
+ public IShotsController getShotsController();
+
/**
* Cast a keeper spell on target / location
*
diff --git a/src/toniarts/openkeeper/game/controller/IShotsController.java b/src/toniarts/openkeeper/game/controller/IShotsController.java
index 3cd60c5d..42cd9958 100644
--- a/src/toniarts/openkeeper/game/controller/IShotsController.java
+++ b/src/toniarts/openkeeper/game/controller/IShotsController.java
@@ -16,7 +16,7 @@
*/
package toniarts.openkeeper.game.controller;
-import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
import com.simsilica.es.EntityId;
/**
@@ -33,10 +33,10 @@ public interface IShotsController {
* @param shotTypeId shot type to create
* @param shotData1 arbitrary value, interpreted per shot
* @param shotData2 arbitrary value, interpreted per shot
- * @param playerId owher of the shot
- * @param position 2D coordinate of the shot origin
+ * @param playerId owner of the shot
+ * @param position coordinate of the shot origin
* @param target shot target, can be null
*/
- public void createShot(short shotTypeId, int shotData1, int shotData2, short playerId, Vector2f position, EntityId target);
+ public void createShot(short shotTypeId, int shotData1, int shotData2, short playerId, Vector3f position, EntityId target);
}
diff --git a/src/toniarts/openkeeper/game/controller/ShotsController.java b/src/toniarts/openkeeper/game/controller/ShotsController.java
index 7362163b..01fcf982 100644
--- a/src/toniarts/openkeeper/game/controller/ShotsController.java
+++ b/src/toniarts/openkeeper/game/controller/ShotsController.java
@@ -16,7 +16,7 @@
*/
package toniarts.openkeeper.game.controller;
-import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntityId;
import java.lang.System.Logger;
@@ -29,6 +29,7 @@
import static toniarts.openkeeper.tools.convert.map.Shot.ProcessType.MODIFY_HEALTH;
import static toniarts.openkeeper.tools.convert.map.Shot.ProcessType.POSSESS_CREATURE;
import toniarts.openkeeper.tools.convert.map.Variable;
+import toniarts.openkeeper.utils.WorldUtils;
/**
*
@@ -62,11 +63,11 @@ public ShotsController(KwdFile kwdFile, EntityData entityData, Map {
- creaturesController.spawnCreature((short) shotData1, playerId, shotData2, position, ICreaturesController.SpawnType.CONJURE);
+ creaturesController.spawnCreature((short) shotData1, playerId, shotData2, WorldUtils.vector3fToVector2f(position), ICreaturesController.SpawnType.CONJURE);
}
case CREATE_OBJECT -> {
objectsController.loadObject((short) shotData1, playerId, position, 0);
diff --git a/src/toniarts/openkeeper/game/controller/creature/CreatureController.java b/src/toniarts/openkeeper/game/controller/creature/CreatureController.java
index 553cfb31..3fd738d4 100644
--- a/src/toniarts/openkeeper/game/controller/creature/CreatureController.java
+++ b/src/toniarts/openkeeper/game/controller/creature/CreatureController.java
@@ -28,8 +28,11 @@
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.function.Consumer;
import toniarts.openkeeper.game.component.Attack;
import toniarts.openkeeper.game.component.AttackTarget;
@@ -42,6 +45,8 @@
import toniarts.openkeeper.game.component.CreatureMeleeAttack;
import toniarts.openkeeper.game.component.CreatureRecuperating;
import toniarts.openkeeper.game.component.CreatureSleep;
+import toniarts.openkeeper.game.component.CreatureSpell;
+import toniarts.openkeeper.game.component.CreatureSpells;
import toniarts.openkeeper.game.component.CreatureTortured;
import toniarts.openkeeper.game.component.Fearless;
import toniarts.openkeeper.game.component.FollowTarget;
@@ -68,6 +73,8 @@
import toniarts.openkeeper.game.controller.ILevelInfo;
import toniarts.openkeeper.game.controller.IMapController;
import toniarts.openkeeper.game.controller.IObjectsController;
+import toniarts.openkeeper.game.controller.IShotsController;
+import static toniarts.openkeeper.game.controller.creature.CreatureState.MELEE_ATTACK;
import toniarts.openkeeper.game.controller.entity.EntityController;
import toniarts.openkeeper.game.controller.entity.IEntityController;
import toniarts.openkeeper.game.controller.object.IObjectController;
@@ -83,6 +90,7 @@
import toniarts.openkeeper.game.task.Task;
import toniarts.openkeeper.tools.convert.map.ArtResource;
import toniarts.openkeeper.tools.convert.map.Creature;
+import toniarts.openkeeper.tools.convert.map.CreatureSpell.CreatureSpellFlag;
import toniarts.openkeeper.tools.convert.map.Player;
import toniarts.openkeeper.tools.convert.map.Thing;
import toniarts.openkeeper.tools.convert.map.Variable;
@@ -95,7 +103,7 @@
* @author Toni Helenius
*/
public class CreatureController extends EntityController implements ICreatureController {
-
+
private static final Logger logger = System.getLogger(CreatureController.class.getName());
private final INavigationService navigationService;
@@ -105,6 +113,7 @@ public class CreatureController extends EntityController implements ICreatureCon
private final ICreaturesController creaturesController;
private final IEntityPositionLookup entityPositionLookup;
private final ILevelInfo levelInfo;
+ private final IShotsController shotsController;
// TODO: All the data is not supposed to be on entities as they become too big, but I don't want these here either
private final Creature creature;
private final StateMachine stateMachine;
@@ -113,7 +122,7 @@ public class CreatureController extends EntityController implements ICreatureCon
public CreatureController(EntityId entityId, EntityData entityData, Creature creature, INavigationService navigationService,
ITaskManager taskManager, IGameTimer gameTimer, Map gameSettings,
ICreaturesController creaturesController, IEntityPositionLookup entityPositionLookup, IMapController mapController,
- ILevelInfo levelInfo, IObjectsController objectsController) {
+ ILevelInfo levelInfo, IObjectsController objectsController, IShotsController shotsController) {
super(entityId, entityData, objectsController, mapController);
this.navigationService = navigationService;
this.taskManager = taskManager;
@@ -123,6 +132,7 @@ public CreatureController(EntityId entityId, EntityData entityData, Creature cre
this.creaturesController = creaturesController;
this.entityPositionLookup = entityPositionLookup;
this.levelInfo = levelInfo;
+ this.shotsController = shotsController;
this.stateMachine = new DefaultStateMachine<>(this);
}
@@ -541,28 +551,82 @@ private void setAttackTarget(EntityId entity) {
@Override
public boolean isWithinAttackDistance(EntityId attackTarget) {
+ float distanceNeeded = Math.max(getPreferredAttackDistance(), getPossibleAttackDistance());
+
+ // TODO: currently we move only with a tile precision, so accept attack if on the same tile already
+ return isInRange(distanceNeeded, attackTarget);
+ }
+
+ /**
+ * Gets currently possible maximum attack distance for this creature,
+ * includes the cooldowns
+ *
+ * @return maximum distance can attack at the moment
+ */
+ private float getPossibleAttackDistance() {
+ float distanceNeeded = entityData.getComponent(entityId, CreatureMeleeAttack.class).range;
+ for (CreatureSpell attack : getCreatureSpells()) {
+ toniarts.openkeeper.tools.convert.map.CreatureSpell spell = levelInfo.getLevelData().getCreatureSpellById(attack.creatureSpellId);
+ if (spell.getFlags().contains(CreatureSpellFlag.IS_ATTACKING) && isAttackRecharged(attack)) {
+ distanceNeeded = Math.max(distanceNeeded, attack.range);
+ }
+ }
+
+ return distanceNeeded;
+ }
+
+ /**
+ * Gets the preferred attack distance from target for this creature's
+ * figting style
+ *
+ * @return preferred attack distance
+ */
+ private float getPreferredAttackDistance() {
float distanceNeeded = entityData.getComponent(entityId, CreatureMeleeAttack.class).range; // The melee range, the shortest range
if (creature.getFightStyle() == Creature.FightStyle.SUPPORT) {
- // TODO: Creature spells
// Get max distance we can cast all spells, and hopefully stay safe
- Float shortestDistance = null;
-// for (CreatureAttack attack : attacks) {
-// if (!attack.isMelee() && attack.isAvailable() && attack.isAttacking()) {
-// if (shortestDistance == null) {
-// shortestDistance = attack.getRange();
-// } else {
-// shortestDistance = Math.min(shortestDistance, attack.getRange());
-// }
-// }
-// }
- if (shortestDistance != null) {
- distanceNeeded = shortestDistance;
+ Optional shortestDistance = getShortestAttackSpellDistanceNeeded();
+ if (shortestDistance.isPresent()) {
+ distanceNeeded = shortestDistance.get();
}
}
- // TODO: currently we move only with a tile precision, so accept attack if on the same tile already
- return distanceNeeded >= getDistanceToCreature(attackTarget) || (isStopped() && isAtSameTile(attackTarget));
+ return distanceNeeded;
+ }
+
+ private Optional getShortestAttackSpellDistanceNeeded() {
+ Float shortestDistance = null;
+ for (CreatureSpell attack : getCreatureSpells()) {
+ toniarts.openkeeper.tools.convert.map.CreatureSpell spell = levelInfo.getLevelData().getCreatureSpellById(attack.creatureSpellId);
+ if (spell.getFlags().contains(CreatureSpellFlag.IS_ATTACKING)) {
+ if (shortestDistance == null) {
+ shortestDistance = attack.range;
+ } else {
+ shortestDistance = Math.min(shortestDistance, attack.range);
+ }
+ }
+ }
+
+ return Optional.ofNullable(shortestDistance);
+ }
+
+ private boolean isInRange(float range, EntityId target) {
+ return range >= getDistanceToCreature(target) || (isStopped() && isAtSameTile(target));
+ }
+
+ private List getCreatureSpells() {
+ CreatureSpells creatureSpells = entityData.getComponent(entityId, CreatureSpells.class);
+ if (creatureSpells == null) {
+ return Collections.emptyList();
+ }
+
+ List spells = new ArrayList<>(creatureSpells.creatureSpells.size());
+ for (EntityId entity : creatureSpells.creatureSpells) {
+ spells.add(entityData.getComponent(entity, CreatureSpell.class));
+ }
+
+ return spells;
}
private boolean isAtSameTile(EntityId attackTarget) {
@@ -577,17 +641,60 @@ public void stop() {
@Override
public void executeAttack(EntityId attackTarget) {
- // Now just the melee attack
- // TODO: spells
// TODO: how to apply the damage? Create a component for THIS creature that adds the damage to enemy after the countdown is finished?
+ // Always prefer melee if in range
CreatureMeleeAttack creatureMeleeAttack = entityData.getComponent(entityId, CreatureMeleeAttack.class);
- if (isAttackRecharged(creatureMeleeAttack)) {
- entityData.setComponent(entityId, new CreatureMeleeAttack(creatureMeleeAttack, gameTimer.getGameTime()));
- stateMachine.changeState(CreatureState.MELEE_ATTACK);
+ if (isInRange(creatureMeleeAttack.range, attackTarget)) {
+ if (isAttackRecharged(creatureMeleeAttack)) {
+ entityData.setComponent(entityId, new CreatureMeleeAttack(creatureMeleeAttack, gameTimer.getGameTime()));
+ stateMachine.changeState(CreatureState.MELEE_ATTACK);
+
+ // Set the damage
+ setDamage(attackTarget, creatureMeleeAttack.damage);
+ }
+ } else {
+ Optional attack = getPreferredAttackSpell(attackTarget);
+ if (attack.isPresent()) {
+ CreatureSpells creatureSpells = entityData.getComponent(entityId, CreatureSpells.class);
+ for (EntityId creatureSpellEntity : creatureSpells.creatureSpells) {
+ CreatureSpell spell = entityData.getComponent(creatureSpellEntity, CreatureSpell.class);
+ if (spell.creatureSpellId == attack.get().creatureSpellId) {
+ entityData.setComponent(creatureSpellEntity, new CreatureSpell(attack.get(), gameTimer.getGameTime()));
+ break;
+ }
+ }
+ stateMachine.changeState(CreatureState.CAST_SPELL);
+
+ // TODO: What is the alternative shot all about?
+ toniarts.openkeeper.tools.convert.map.CreatureSpell creatureSpell = levelInfo.getLevelData().getCreatureSpellById(attack.get().creatureSpellId);
+ shotsController.createShot(creatureSpell.getShotTypeId(), creatureSpell.getShotData1(), creatureSpell.getShotData2(), getOwnerId(), getPosition(), attackTarget);
+
+ // TODO: Of course not like that, the shot is created and it does what it does
+ // Set the damage
+ setDamage(attackTarget, creatureSpell.getShotData1());
+ }
+ }
+ }
- // Set the damage
- setDamage(attackTarget, creatureMeleeAttack.damage);
+ /**
+ * Gets preferred attack creature spell that is available to cast to the
+ * target
+ *
+ * @param attackTarget target to attack
+ * @return creature spell to cast on the unsuspecting victim
+ */
+ private Optional getPreferredAttackSpell(EntityId attackTarget) {
+ List creatureSpells = getCreatureSpells();
+ if (creatureSpells.isEmpty()) {
+ return Optional.empty();
}
+
+ // Sort in order of preference
+ return creatureSpells
+ .stream()
+ .filter(creatureSpell -> isAttackRecharged(creatureSpell) && isInRange(creatureSpell.range, attackTarget) && levelInfo.getLevelData().getCreatureSpellById(creatureSpell.creatureSpellId).getFlags().contains(CreatureSpellFlag.IS_ATTACKING))
+ .sorted(Comparator.comparingInt(creatureSpell -> levelInfo.getLevelData().getCreatureSpellById(creatureSpell.creatureSpellId).getCombatPoints()).reversed())
+ .findFirst();
}
private boolean isAttackRecharged(Attack attack) {
@@ -1128,6 +1235,11 @@ public void imprison(short playerId) {
// Return health to 20%
Health health = entityData.getComponent(entityId, Health.class);
+ if (health == null) {
+ logger.log(Level.INFO, "Tried to imprison me but I don't have any health left!");
+ return;
+ }
+
entityData.setComponent(entityId, new Health((int) Math.floor(health.maxHealth * 0.2f), health.maxHealth));
entityData.removeComponent(entityId, Unconscious.class);
entityData.setComponent(entityId, new CreatureImprisoned(gameTimer.getGameTime(), gameTimer.getGameTime()));
@@ -1162,6 +1274,9 @@ public boolean isStateTimeExceeded() {
case MELEE_ATTACK -> {
stateTargetTime = getAnimationTime(creature, Creature.AnimationType.MELEE_ATTACK);
}
+ case CAST_SPELL -> {
+ stateTargetTime = getAnimationTime(creature, Creature.AnimationType.CAST_SPELL);
+ }
case EATING -> {
stateTargetTime = getAnimationTime(creature, Creature.AnimationType.EATING);
}
@@ -1322,13 +1437,10 @@ public void convertCreature(short playerId) {
}
private void convertCreature(EntityId entity, short playerId) {
+ removeFromRoomStrorage();
// Remove our lair from the last player
- CreatureSleep creatureSleep = entityData.getComponent(entity, CreatureSleep.class);
- if (creatureSleep != null && creatureSleep.lairObjectId != null) {
- entityData.removeEntity(creatureSleep.lairObjectId);
- entityData.setComponent(entity, new CreatureSleep(null, creatureSleep.lastSleepTime, creatureSleep.sleepStartTime));
- }
+ removeLair();
// Set new owner
entityData.setComponent(entity, new Owner(playerId, playerId));
diff --git a/src/toniarts/openkeeper/game/controller/creature/CreatureState.java b/src/toniarts/openkeeper/game/controller/creature/CreatureState.java
index b3692ee0..a9436d06 100644
--- a/src/toniarts/openkeeper/game/controller/creature/CreatureState.java
+++ b/src/toniarts/openkeeper/game/controller/creature/CreatureState.java
@@ -615,6 +615,30 @@ public void exit(ICreatureController entity) {
public boolean onMessage(ICreatureController entity, Telegram telegram) {
return true;
}
+ }, CAST_SPELL {
+
+ @Override
+ public void enter(ICreatureController entity) {
+
+ }
+
+ @Override
+ public void update(ICreatureController entity) {
+ if (entity.isStateTimeExceeded()) {
+ entity.getStateMachine().changeState(FIGHT);
+ }
+ }
+
+ @Override
+ public void exit(ICreatureController entity) {
+
+ }
+
+ @Override
+ public boolean onMessage(ICreatureController entity, Telegram telegram) {
+ return true;
+ }
+
}, EATING {
@Override
diff --git a/src/toniarts/openkeeper/game/controller/entity/EntityController.java b/src/toniarts/openkeeper/game/controller/entity/EntityController.java
index de2135d5..b4caa558 100644
--- a/src/toniarts/openkeeper/game/controller/entity/EntityController.java
+++ b/src/toniarts/openkeeper/game/controller/entity/EntityController.java
@@ -24,6 +24,7 @@
import java.lang.System.Logger.Level;
import java.util.Objects;
import toniarts.openkeeper.game.component.CreatureSleep;
+import toniarts.openkeeper.game.component.CreatureSpells;
import toniarts.openkeeper.game.component.Damage;
import toniarts.openkeeper.game.component.Gold;
import toniarts.openkeeper.game.component.HauledBy;
@@ -122,11 +123,11 @@ public void remove() {
@Override
public void removePossession() {
- handleLootDrop(entityId);
- handleAssociatedEntities(entityId);
+ handleLootDrop();
+ handleAssociatedEntities();
}
- private void handleLootDrop(EntityId entityId) {
+ private void handleLootDrop() {
// Drop gold
Gold gold = entityData.getComponent(entityId, Gold.class);
@@ -143,22 +144,25 @@ private void handleLootDrop(EntityId entityId) {
// TODO:
}
- private void handleAssociatedEntities(EntityId entityId) {
- RoomStorage roomStorage = null;
- Position position = null;
+ private void handleAssociatedEntities() {
+ removeLair();
+ removeFromRoomStrorage();
+ removeAttacks();
+ }
+
+ protected void removeFromRoomStrorage() {
+ RoomStorage roomStorage = entityData.getComponent(entityId, RoomStorage.class);
+ Position position = entityData.getComponent(entityId, Position.class);
+ removeRoomStorage(roomStorage, position, entityId);
+ }
- // Get rid of lairs
+ protected void removeLair() {
CreatureSleep creatureSleep = entityData.getComponent(entityId, CreatureSleep.class);
if (creatureSleep != null && creatureSleep.lairObjectId != null) {
- roomStorage = entityData.getComponent(creatureSleep.lairObjectId, RoomStorage.class);
- position = entityData.getComponent(creatureSleep.lairObjectId, Position.class);
+ RoomStorage roomStorage = entityData.getComponent(creatureSleep.lairObjectId, RoomStorage.class);
+ Position position = entityData.getComponent(creatureSleep.lairObjectId, Position.class);
removeRoomStorage(roomStorage, position, creatureSleep.lairObjectId);
}
-
- // We are a property of a room
- roomStorage = entityData.getComponent(entityId, RoomStorage.class);
- position = entityData.getComponent(entityId, Position.class);
- removeRoomStorage(roomStorage, position, entityId);
}
private void removeRoomStorage(RoomStorage roomStorage, Position position, EntityId entityId) {
@@ -175,6 +179,19 @@ private void removeRoomStorage(RoomStorage roomStorage, Position position, Entit
roomObjectControl.removeItem(entityId);
}
+ private void removeAttacks() {
+ CreatureSpells creatureSpells = entityData.getComponent(entityId, CreatureSpells.class);
+ if (creatureSpells == null) {
+ return;
+ }
+
+ // Remove all spells
+ for (EntityId spellEntityId : creatureSpells.creatureSpells) {
+ entityData.removeEntity(spellEntityId);
+ }
+ entityData.removeComponent(entityId, CreatureSpells.class);
+ }
+
@Override
public boolean isRemoved() {
return entityData.getComponent(entityId, Position.class) == null && entityData.getComponent(entityId, InHand.class) == null;
diff --git a/src/toniarts/openkeeper/game/logic/CreatureExperienceSystem.java b/src/toniarts/openkeeper/game/logic/CreatureExperienceSystem.java
index 6984f5eb..33f6475e 100644
--- a/src/toniarts/openkeeper/game/logic/CreatureExperienceSystem.java
+++ b/src/toniarts/openkeeper/game/logic/CreatureExperienceSystem.java
@@ -170,7 +170,7 @@ private void processDeletedEntities(Set entities, SafeArrayList