Skip to content

Display entity projectiles #1502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 34 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e4d849b
Add display entities as a type of projectile for kits. They do not mo…
KaevonD Dec 25, 2024
1a0accf
Remove logs
KaevonD Dec 25, 2024
f26e050
We have janky movement
chatasma Jan 11, 2025
8254291
It moves in waves now
chatasma Jan 11, 2025
e96d71f
interpolation added to nms hacks
chatasma Jan 11, 2025
9b72e57
Add interpolation
KaevonD Jan 11, 2025
3675f0b
help
KaevonD Jan 14, 2025
978d6fd
this is for tank, no sleeping allowed
KaevonD Jan 14, 2025
f9f70df
Aligning block-display projectiles w.r.t player facing & teleportatio…
chatasma Jan 28, 2025
5d966ca
damage enemy players upon impact
KaevonD Feb 3, 2025
6e69021
Add scale to block display from xml.
KaevonD Feb 8, 2025
4352ed9
get projectile damage and velocity from map xml
KaevonD Feb 18, 2025
a8fa373
Cleaning up code and removing unused functions
KaevonD Mar 10, 2025
e2a746b
it hits EVERY SINGLE TIMEEEEE
KaevonD May 3, 2025
b6fb019
At block collision
KaevonD May 9, 2025
9763acf
Check between teleport locations with respect to size of projectile
KaevonD May 10, 2025
00706de
Fix all sources of possible lag
KaevonD May 10, 2025
5688d86
Remove unused function
KaevonD May 10, 2025
9fe84da
Remove while true loop
KaevonD May 10, 2025
20a889a
Remove unnecessary comments, files, and folders
KaevonD May 10, 2025
73be187
Add max travel time attribute
KaevonD May 10, 2025
47e03dd
wip add block projectile
chatasma May 12, 2025
7e78085
more block projectile stuff things
chatasma May 17, 2025
34cc4d6
fix more block projectile stuff things
chatasma May 17, 2025
10c0c0d
made it nicer than tank's
KaevonD May 17, 2025
4497d8f
clean up clean up everybody clean up
chatasma May 17, 2025
00282c6
removed unnecessary NSM_HACKS functions and one other fun thing
KaevonD May 17, 2025
f98dd14
progress
chatasma May 18, 2025
79e53b6
add other requested changes
KaevonD May 18, 2025
b8e5d91
apply velocity to falling block
KaevonD May 18, 2025
3fd6a6d
fix duplicate falling blocks
KaevonD May 18, 2025
5a9698a
Add a bunch of cleanup to the impl
Pablete1234 May 18, 2025
b61b00b
Further cleanup and bugfixes
Pablete1234 May 19, 2025
25afbbc
fix rotation
KaevonD May 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ spotless {
ratchetFrom = "origin/dev"
java {
removeUnusedImports()
palantirJavaFormat("2.47.0").style("GOOGLE").formatJavadoc(true)
palantirJavaFormat("2.57.0").style("GOOGLE").formatJavadoc(true)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.time.Duration;
import java.util.List;
import org.bukkit.entity.Entity;
import org.bukkit.entity.FallingBlock;
import org.bukkit.potion.PotionEffect;
import org.jetbrains.annotations.Nullable;
import tc.oc.pgm.api.filter.Filter;
Expand All @@ -15,7 +16,7 @@ public class ProjectileDefinition extends SelfIdentifyingFeatureDefinition {
protected @Nullable Float power;
protected double velocity;
protected ClickAction clickAction;
protected Class<? extends Entity> projectile;
protected ProjectileEntity projectile;
protected List<PotionEffect> potion;
protected Filter destroyFilter;
protected Duration coolDown;
Expand All @@ -30,7 +31,7 @@ public ProjectileDefinition(
@Nullable Float power,
double velocity,
ClickAction clickAction,
Class<? extends Entity> entity,
ProjectileEntity entity,
List<PotionEffect> potion,
Filter destroyFilter,
Duration coolDown,
Expand All @@ -52,6 +53,25 @@ public ProjectileDefinition(
this.blockMaterial = blockMaterial;
}

public sealed interface ProjectileEntity permits RealEntity, BlockEntityType {
boolean requiresBlockMaterial();
}

record RealEntity(Class<? extends Entity> entityType) implements ProjectileEntity {
@Override
public boolean requiresBlockMaterial() {
return FallingBlock.class.isAssignableFrom(entityType);
}
}

record BlockEntityType(float size, boolean solidBlockCollision, Duration maxTravelTime)
implements ProjectileEntity {
@Override
public boolean requiresBlockMaterial() {
return true;
}
}

public @Nullable String getName() {
return name;
}
Expand Down
222 changes: 171 additions & 51 deletions core/src/main/java/tc/oc/pgm/projectile/ProjectileMatchModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

import com.google.common.collect.ImmutableSet;
import java.util.HashMap;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
Expand Down Expand Up @@ -37,14 +41,18 @@
import tc.oc.pgm.api.match.Match;
import tc.oc.pgm.api.match.MatchModule;
import tc.oc.pgm.api.match.MatchScope;
import tc.oc.pgm.api.party.Party;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.api.player.ParticipantState;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.PlayerParticipationStopEvent;
import tc.oc.pgm.filters.query.BlockQuery;
import tc.oc.pgm.filters.query.PlayerBlockQuery;
import tc.oc.pgm.kits.tag.ItemTags;
import tc.oc.pgm.util.MatchPlayers;
import tc.oc.pgm.util.TimeUtils;
import tc.oc.pgm.util.bukkit.MetadataUtils;
import tc.oc.pgm.util.bukkit.entities.BlockEntity;
import tc.oc.pgm.util.inventory.InventoryUtils;
import tc.oc.pgm.util.nms.NMSHacks;

Expand Down Expand Up @@ -78,68 +86,78 @@ public void onClickEvent(PlayerInteractEvent event) {
ParticipantState playerState = match.getParticipantState(player);
if (playerState == null) return;

ProjectileDefinition projectileDefinition =
this.getProjectileDefinition(player.getItemInHand());

if (projectileDefinition != null
&& isValidProjectileAction(event.getAction(), projectileDefinition.clickAction)) {
// Prevent the original projectile from being fired
event.setCancelled(true);

if (this.isCooldownActive(player, projectileDefinition)) return;

boolean realProjectile = Projectile.class.isAssignableFrom(projectileDefinition.projectile);
Vector velocity =
player.getEyeLocation().getDirection().multiply(projectileDefinition.velocity);
Entity projectile;
try {
assertTrue(launchingDefinition.get() == null, "nested projectile launch");
launchingDefinition.set(projectileDefinition);
if (realProjectile) {
projectile = player.launchProjectile(
projectileDefinition.projectile.asSubclass(Projectile.class), velocity);
if (projectile instanceof Fireball fireball && projectileDefinition.precise) {
NMSHacks.NMS_HACKS.setFireballDirection(fireball, velocity);
}
} else {
if (FallingBlock.class.isAssignableFrom(projectileDefinition.projectile)) {
projectile =
projectileDefinition.blockMaterial.spawnFallingBlock(player.getEyeLocation());
var definition = this.getProjectileDefinition(player.getItemInHand());

if (definition == null || !isValidProjectileAction(event.getAction(), definition.clickAction))
return;

// Prevent the original projectile from being fired
event.setCancelled(true);

if (this.isCooldownActive(player, definition)) return;

var projType = definition.projectile;
boolean needsEvent = true;
Vector velocity = player.getEyeLocation().getDirection().multiply(definition.velocity);
Entity projectile = null;
try {
assertTrue(launchingDefinition.get() == null, "nested projectile launch");
launchingDefinition.set(definition);
switch (projType) {
case ProjectileDefinition.RealEntity(Class<? extends Entity> entityType) -> {
if (Projectile.class.isAssignableFrom(entityType)) {
needsEvent = false;

projectile = player.launchProjectile(entityType.asSubclass(Projectile.class), velocity);
if (projectile instanceof Fireball fireball && definition.precise) {
NMSHacks.NMS_HACKS.setFireballDirection(fireball, velocity);
}
} else {
projectile =
player.getWorld().spawn(player.getEyeLocation(), projectileDefinition.projectile);
if (FallingBlock.class.isAssignableFrom(entityType)) {
projectile = definition.blockMaterial.spawnFallingBlock(player.getEyeLocation());
} else {
projectile = player.getWorld().spawn(player.getEyeLocation(), entityType);
}
projectile.setVelocity(velocity);
}
projectile.setVelocity(velocity);
}
if (projectileDefinition.power != null && projectile instanceof Explosive) {
((Explosive) projectile).setYield(projectileDefinition.power);
case ProjectileDefinition.BlockEntityType ce -> {
Location loc = player.getEyeLocation();
var be = BlockEntity.spawnBlockEntity(loc, definition.blockMaterial, ce.size(), velocity);
new BlockRunner(definition, be, player, loc);
}
projectile.setMetadata(
"projectileDefinition", new FixedMetadataValue(PGM.get(), projectileDefinition));
} finally {
launchingDefinition.remove();
}

// If the entity implements Projectile, it will have already generated a
// ProjectileLaunchEvent.
// Otherwise, we fire our custom event.
if (!realProjectile) {
EntityLaunchEvent launchEvent = new EntityLaunchEvent(projectile, event.getPlayer());
match.callEvent(launchEvent);
if (launchEvent.isCancelled()) {
projectile.remove();
return;
}
if (definition.power != null && projectile instanceof Explosive) {
((Explosive) projectile).setYield(definition.power);
}

if (projectileDefinition.throwable) {
InventoryUtils.consumeItem(event);
if (projectile != null) {
projectile.setMetadata(
"projectileDefinition", new FixedMetadataValue(PGM.get(), definition));
}
} finally {
launchingDefinition.remove();
}

if (projectileDefinition.coolDown != null) {
startCooldown(player, projectileDefinition);
// If the entity implements Projectile, it will have already generated a
// ProjectileLaunchEvent.
// Otherwise, we fire our custom event.
if (needsEvent && projectile != null) {
EntityLaunchEvent launchEvent = new EntityLaunchEvent(projectile, event.getPlayer());
match.callEvent(launchEvent);
if (launchEvent.isCancelled()) {
projectile.remove();
return;
}
}

if (definition.throwable) {
InventoryUtils.consumeItem(event);
}

if (definition.coolDown != null) {
startCooldown(player, definition);
}
}

@EventHandler
Expand Down Expand Up @@ -289,4 +307,106 @@ public boolean isCooldownActive(Player player, ProjectileDefinition definition)
ProjectileCooldowns playerCooldowns = projectileCooldowns.get(player.getUniqueId());
return (playerCooldowns != null && playerCooldowns.isActive(definition));
}

private class BlockRunner {
private final ProjectileDefinition definition;
private final BlockEntity blockEntity;
private final Player player;
private final Party shooterParty;
private final ProjectileDefinition.BlockEntityType ce;
private final Location currentLocation;
private final Vector increment;
private final Vector substep;
private final int substeps;
private final Future<?> runner;
private int remainingTime;

public BlockRunner(
ProjectileDefinition definition,
BlockEntity blockEntity,
Player player,
Location spawnLocation) {
this.definition = definition;
this.blockEntity = blockEntity;
this.player = player;
this.shooterParty = Objects.requireNonNull(match.getPlayer(player)).getParty();
this.ce = (ProjectileDefinition.BlockEntityType) definition.projectile;

this.currentLocation = spawnLocation.clone();
var normalizedDirection = currentLocation.getDirection().normalize();
this.currentLocation.setPitch(0);
this.currentLocation.setYaw(0);

this.increment = normalizedDirection.multiply(definition.velocity);
this.substeps = Math.max(1, (int) (definition.velocity / ce.size()));
this.substep = increment.clone().divide(new Vector(substeps, substeps, substeps));

this.remainingTime = (int) TimeUtils.toTicks(ce.maxTravelTime());
this.runner = match
.getExecutor(MatchScope.RUNNING)
.scheduleAtFixedRate(this::tick, 0L, 50L, TimeUnit.MILLISECONDS);
}

public void tick() {
if (remainingTime-- <= 0) {
cancel();
return;
}
if (definition.damage != null || ce.solidBlockCollision()) {
Location substepLoc = currentLocation.clone();
for (int i = substeps; i > 0; i--) {
substepLoc.add(substep);
if (!blockDisplayCollision(substepLoc)) continue;
cancel();
return;
}
}

currentLocation.add(increment);
blockEntity.teleport(currentLocation);
}

private void cancel() {
this.runner.cancel(true);
blockEntity.remove();
}

private boolean blockDisplayCollision(Location location) {
double radius = 0.5 * ce.size();

if (definition.damage != null) {
for (Player victim : location.getNearbyPlayers(radius)) {
var mpVictim = match.getPlayer(victim);
if (MatchPlayers.canInteract(mpVictim) && mpVictim.getParty() != shooterParty) {
victim.damage(definition.damage, player);
return true;
}
}
}
if (ce.solidBlockCollision()) {
int x1 = (int) Math.floor(location.getX() - radius);
int y1 = (int) Math.floor(location.getY() - radius);
int z1 = (int) Math.floor(location.getZ() - radius);

int x2 = (int) Math.floor(location.getX() + radius);
int y2 = (int) Math.floor(location.getY() + radius);
int z2 = (int) Math.floor(location.getZ() + radius);

Location loc = location.clone();

for (int x = x1; x <= x2; ++x) {
loc.setX(x);
for (int y = y1; y <= y2; ++y) {
loc.setY(y);
for (int z = z1; z <= z2; ++z) {
loc.setZ(z);
if (loc.getBlock().getType().isSolid()) return true;
}
}
}
}

return false;
}
}
}
30 changes: 25 additions & 5 deletions core/src/main/java/tc/oc/pgm/projectile/ProjectileModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.logging.Logger;
import org.bukkit.entity.Arrow;
import org.bukkit.entity.Entity;
import org.bukkit.entity.FallingBlock;
import org.bukkit.potion.PotionEffect;
import org.jdom2.Document;
import org.jdom2.Element;
Expand All @@ -25,6 +25,7 @@
import tc.oc.pgm.util.material.BlockMaterialData;
import tc.oc.pgm.util.xml.InvalidXMLException;
import tc.oc.pgm.util.xml.Node;
import tc.oc.pgm.util.xml.XMLFluentParser;
import tc.oc.pgm.util.xml.XMLUtils;

public class ProjectileModule implements MapModule<ProjectileMatchModule> {
Expand Down Expand Up @@ -62,10 +63,10 @@ public ProjectileModule parse(MapFactory factory, Logger logger, Document doc)
Node.fromChildOrAttr(projectileElement, "velocity"), Double.class, 1.0);
ClickAction clickAction = XMLUtils.parseEnum(
Node.fromAttr(projectileElement, "click"), ClickAction.class, ClickAction.BOTH);
Class<? extends Entity> entity =
XMLUtils.parseEntityTypeAttribute(projectileElement, "projectile", Arrow.class);
BlockMaterialData blockMaterial = entity.isAssignableFrom(FallingBlock.class)
? XMLUtils.parseBlockMaterialData(Node.fromAttr(projectileElement, "material"))
ProjectileDefinition.ProjectileEntity entity =
parseProjectileEntity(projectileElement, factory.getParser());
BlockMaterialData blockMaterial = entity.requiresBlockMaterial()
? XMLUtils.parseBlockMaterialData(Node.fromRequiredAttr(projectileElement, "material"))
: null;
Float power = XMLUtils.parseNumber(
Node.fromChildOrAttr(projectileElement, "power"), Float.class, (Float) null);
Expand Down Expand Up @@ -98,5 +99,24 @@ public ProjectileModule parse(MapFactory factory, Logger logger, Document doc)

return projectiles.isEmpty() ? null : new ProjectileModule(ImmutableSet.copyOf(projectiles));
}

private static ProjectileDefinition.ProjectileEntity parseProjectileEntity(
final Element el, final XMLFluentParser parser) throws InvalidXMLException {
final String attributeName = "projectile";
final Class<? extends Entity> def = Arrow.class;
final Node node = Node.fromAttr(el, attributeName);
if (node == null) return new ProjectileDefinition.RealEntity(def);
final String entityText = node.getValue();
return switch (entityText.toLowerCase(Locale.ROOT)) {
case "block" ->
new ProjectileDefinition.BlockEntityType(
parser.parseFloat(el, "size").optional(1.0f),
parser.parseBool(el, "solid-block-collision").orTrue(),
parser.duration(el, "max-travel-time").optional(Duration.ofSeconds(1)));
default ->
new ProjectileDefinition.RealEntity(
XMLUtils.parseEntityTypeAttribute(el, attributeName, def));
};
}
}
}
Loading