From 2c8fabe50b89b48e36131d11afed5425d7cef60e Mon Sep 17 00:00:00 2001 From: Ziggy Date: Sun, 14 Mar 2021 17:45:07 +0100 Subject: [PATCH 1/2] Cone of Effect attacks #77 --- .../server_info/loader/CommandLoader.java | 1 + .../global/commands/CombatCommand.java | 12 +++++ .../combat/command/CombatCommandAttack.java | 53 +++++++++++++++++++ .../combat/command/CombatCommandCommon.java | 1 + 4 files changed, 67 insertions(+) diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/CommandLoader.java b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/CommandLoader.java index 21545aa78..b608a91ca 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/CommandLoader.java +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/CommandLoader.java @@ -126,6 +126,7 @@ public class CommandLoader extends DataLoader { .withAnimations(WeaponType.POLEARM_SABER, getAnimationList(set.getText("anim_polearmlightsaber"))) .withAttackType(AttackType.valueOf(set.getText("attackType"))) .withConeLength(set.getReal("coneLength")) + .withConeWidth(set.getReal("coneWidth")) .withAddedDamage((int) set.getInt("addedDamage")) .withPercentAddFromWeapon(set.getReal("percentAddFromWeapon")) .withBypassArmor(set.getReal("bypassArmor")) diff --git a/src/main/java/com/projectswg/holocore/resources/support/global/commands/CombatCommand.java b/src/main/java/com/projectswg/holocore/resources/support/global/commands/CombatCommand.java index 96db497b9..dea27d078 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/global/commands/CombatCommand.java +++ b/src/main/java/com/projectswg/holocore/resources/support/global/commands/CombatCommand.java @@ -61,6 +61,7 @@ public class CombatCommand extends Command { private final int delayAttackLoops; private final DelayAttackEggPosition eggPosition; private final double coneLength; + private final double coneWidth; private final HealAttrib healAttrib; private final String specialLine; @@ -91,6 +92,7 @@ public class CombatCommand extends Command { this.delayAttackLoops = builder.delayAttackLoops; this.eggPosition = builder.eggPosition; this.coneLength = builder.coneLength; + this.coneWidth = builder.coneWidth; this.healAttrib = builder.healAttrib; this.specialLine = builder.specialLine; } @@ -212,6 +214,10 @@ public class CombatCommand extends Command { return coneLength; } + public double getConeWidth() { + return coneWidth; + } + public HealAttrib getHealAttrib() { return healAttrib; } @@ -256,6 +262,7 @@ public class CombatCommand extends Command { private int delayAttackLoops; private DelayAttackEggPosition eggPosition; private double coneLength; + private double coneWidth; private HealAttrib healAttrib; private String specialLine; @@ -393,6 +400,11 @@ public class CombatCommand extends Command { return this; } + public CombatCommandBuilder withConeWidth(double coneWidth) { + this.coneWidth = coneWidth; + return this; + } + public CombatCommandBuilder withHealAttrib(HealAttrib healAttrib) { this.healAttrib = healAttrib; return this; diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandAttack.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandAttack.java index 293a93620..8ef3c589e 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandAttack.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandAttack.java @@ -30,6 +30,7 @@ package com.projectswg.holocore.services.gameplay.combat.command; import com.projectswg.common.data.RGB; import com.projectswg.common.data.combat.*; import com.projectswg.common.data.encodables.oob.StringId; +import com.projectswg.common.data.location.Location; import com.projectswg.common.network.packets.swg.zone.object_controller.Animation; import com.projectswg.common.network.packets.swg.zone.object_controller.ShowFlyText; import com.projectswg.common.network.packets.swg.zone.object_controller.combat.CombatAction; @@ -82,12 +83,64 @@ enum CombatCommandAttack implements CombatCommandHitType { // TODO AoE based on Location instead of delay egg } break; + case CONE: + doCombatCone(source, target, info, weapon, command); + break; default: break; } } } + private void doCombatCone(CreatureObject source, SWGObject target, AttackInfo info, WeaponObject weapon, CombatCommand command) { + double coneLength = command.getConeLength(); + double coneWidth = command.getConeWidth(); + + Location sourceWorldLocation = source.getWorldLocation(); + Location targetWorldLocation = target.getWorldLocation(); + + double dirX = targetWorldLocation.getX() - sourceWorldLocation.getX(); + double dirZ = targetWorldLocation.getZ() - sourceWorldLocation.getZ(); + + Set objectsToCheck = source.getObjectsAware(); + + // TODO line of sight checks between the attacker and each target + Set targets = objectsToCheck.stream() + .filter(CreatureObject.class::isInstance) + .map(CreatureObject.class::cast) + .filter(candidate -> !target.equals(source)) // Make sure the attacker can't damage themselves + .filter(source::isAttackable) + .filter(candidate -> canPerform(source, target, command) == CombatStatus.SUCCESS) + .filter(candidate -> sourceWorldLocation.distanceTo(candidate.getLocation()) <= coneLength) + .filter(candidate -> { + Location candidateWorldLocation = candidate.getWorldLocation(); + + return isInConeAngle(sourceWorldLocation, candidateWorldLocation, coneLength, coneWidth, dirX, dirZ); + }) + .collect(Collectors.toSet()); + + doCombat(source, targets, weapon, info, command); + } + + private boolean isInConeAngle(Location attackerLocation, Location targetLocation, double coneLength, double coneWidth, double directionX, double directionZ) { + double radius = coneWidth / 2d; + double angle = 2 * Math.atan(coneLength / radius); + + double targetX = targetLocation.getX() - attackerLocation.getX(); + double targetZ = targetLocation.getZ() - attackerLocation.getZ(); + + double targetAngle = Math.atan2(targetZ, targetX) - Math.atan2(directionZ, directionX); + + double degrees = targetAngle * 180 / Math.PI; + double coneAngle = angle / 2; + + if (degrees > coneAngle || degrees < -coneAngle) { + return false; + } + + return true; + } + private static void doCombatSingle(CreatureObject source, SWGObject target, AttackInfo info, WeaponObject weapon, CombatCommand command) { Set targets = new HashSet<>(); diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandCommon.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandCommon.java index a25f647bc..4a94c5721 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandCommon.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandCommon.java @@ -125,6 +125,7 @@ public class CombatCommandCommon { switch (c.getAttackType()) { case AREA: + case CONE: case TARGET_AREA: return canPerformArea(source, c); case SINGLE_TARGET: From 1e1b8a92285b50ee22cbc31fa0cd3f87650ab609 Mon Sep 17 00:00:00 2001 From: Ziggy Date: Fri, 19 Mar 2021 16:57:06 +0100 Subject: [PATCH 2/2] Fixed Cone of Effect attacks working in 360 degrees and errors when using flamethrower in free-target mode #77 --- .../combat/command/CombatCommandAttack.java | 35 +++++++------ .../command/TestCombatCommandAttack.java | 50 +++++++++++++++++++ 2 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 src/test/java/com/projectswg/holocore/services/gameplay/combat/command/TestCombatCommandAttack.java diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandAttack.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandAttack.java index 8ef3c589e..4c298a789 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandAttack.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/command/CombatCommandAttack.java @@ -80,11 +80,15 @@ enum CombatCommandAttack implements CombatCommandHitType { // Same as AREA, but the target is the destination for the AoE and can take damage doCombatArea(source, delayEgg != null ? delayEgg : target, info, weapon, command, true); } else { - // TODO AoE based on Location instead of delay egg + // TODO AoE based on Location instead of delay egg (free-targeting with heavy weapons) } break; case CONE: - doCombatCone(source, target, info, weapon, command); + if (target != null) { + doCombatCone(source, target, info, weapon, command); + } else { + // TODO CoE based on Location (free-targeting with flamethrowers) + } break; default: break; @@ -92,12 +96,11 @@ enum CombatCommandAttack implements CombatCommandHitType { } } - private void doCombatCone(CreatureObject source, SWGObject target, AttackInfo info, WeaponObject weapon, CombatCommand command) { + private void doCombatCone(CreatureObject source, Location targetWorldLocation, AttackInfo info, WeaponObject weapon, CombatCommand command) { double coneLength = command.getConeLength(); double coneWidth = command.getConeWidth(); Location sourceWorldLocation = source.getWorldLocation(); - Location targetWorldLocation = target.getWorldLocation(); double dirX = targetWorldLocation.getX() - sourceWorldLocation.getX(); double dirZ = targetWorldLocation.getZ() - sourceWorldLocation.getZ(); @@ -108,37 +111,33 @@ enum CombatCommandAttack implements CombatCommandHitType { Set targets = objectsToCheck.stream() .filter(CreatureObject.class::isInstance) .map(CreatureObject.class::cast) - .filter(candidate -> !target.equals(source)) // Make sure the attacker can't damage themselves + .filter(candidate -> !candidate.equals(source)) // Make sure the attacker can't damage themselves .filter(source::isAttackable) - .filter(candidate -> canPerform(source, target, command) == CombatStatus.SUCCESS) + .filter(candidate -> canPerform(source, candidate, command) == CombatStatus.SUCCESS) .filter(candidate -> sourceWorldLocation.distanceTo(candidate.getLocation()) <= coneLength) .filter(candidate -> { Location candidateWorldLocation = candidate.getWorldLocation(); - return isInConeAngle(sourceWorldLocation, candidateWorldLocation, coneLength, coneWidth, dirX, dirZ); + return isInConeAngle(sourceWorldLocation, candidateWorldLocation, coneWidth, dirX, dirZ); }) .collect(Collectors.toSet()); doCombat(source, targets, weapon, info, command); } - - private boolean isInConeAngle(Location attackerLocation, Location targetLocation, double coneLength, double coneWidth, double directionX, double directionZ) { - double radius = coneWidth / 2d; - double angle = 2 * Math.atan(coneLength / radius); - + + private void doCombatCone(CreatureObject source, SWGObject target, AttackInfo info, WeaponObject weapon, CombatCommand command) { + doCombatCone(source, target.getWorldLocation(), info, weapon, command); + } + + boolean isInConeAngle(Location attackerLocation, Location targetLocation, double coneWidth, double directionX, double directionZ) { double targetX = targetLocation.getX() - attackerLocation.getX(); double targetZ = targetLocation.getZ() - attackerLocation.getZ(); double targetAngle = Math.atan2(targetZ, targetX) - Math.atan2(directionZ, directionX); double degrees = targetAngle * 180 / Math.PI; - double coneAngle = angle / 2; - if (degrees > coneAngle || degrees < -coneAngle) { - return false; - } - - return true; + return !(Math.abs(degrees) > coneWidth); } private static void doCombatSingle(CreatureObject source, SWGObject target, AttackInfo info, WeaponObject weapon, CombatCommand command) { diff --git a/src/test/java/com/projectswg/holocore/services/gameplay/combat/command/TestCombatCommandAttack.java b/src/test/java/com/projectswg/holocore/services/gameplay/combat/command/TestCombatCommandAttack.java new file mode 100644 index 000000000..d3182683b --- /dev/null +++ b/src/test/java/com/projectswg/holocore/services/gameplay/combat/command/TestCombatCommandAttack.java @@ -0,0 +1,50 @@ +package com.projectswg.holocore.services.gameplay.combat.command; + +import com.projectswg.common.data.location.Location; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TestCombatCommandAttack { + @Test + public void testConeRange() { + Location attackerLocation = new Location.LocationBuilder() + .setX(0) + .setY(0) + .setZ(0) + .build(); + + Location targetLocation = new Location.LocationBuilder() + .setX(20) + .setY(0) + .setZ(10) + .build(); + + Location collateralInsideCone1 = new Location.LocationBuilder() + .setX(10) + .setY(0) + .setZ(5) + .build(); + + Location collateralInsideCone2 = new Location.LocationBuilder() + .setX(25) + .setY(0) + .setZ(10) + .build(); + + Location collateralOutsideCone = new Location.LocationBuilder() + .setX(-20) + .setY(0) + .setZ(-15) + .build(); + + double dirX = targetLocation.getX() - attackerLocation.getX(); + double dirZ = targetLocation.getZ() - attackerLocation.getZ(); + + CombatCommandAttack instance = CombatCommandAttack.INSTANCE; + assertTrue(instance.isInConeAngle(attackerLocation, collateralInsideCone1, 30, dirX, dirZ)); + assertTrue(instance.isInConeAngle(attackerLocation, collateralInsideCone2, 30, dirX, dirZ)); + assertFalse(instance.isInConeAngle(attackerLocation, collateralOutsideCone, 30, dirX, dirZ)); + } +}