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