diff --git a/LobbySystem/src/de/steamwar/lobby/particle/BodyMotion.java b/LobbySystem/src/de/steamwar/lobby/particle/BodyMotion.java index 75964059..d89f9ad5 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/BodyMotion.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/BodyMotion.java @@ -29,7 +29,7 @@ import java.util.WeakHashMap; // How a player's body moves: the direction it faces, worked out the way the Minecraft client turns the body, because // the server's own body rotation for players does not follow what other players see, and how fast it moves. // Walking turns the body towards the walking direction; standing still it only turns once the head looks more than -// 50 degrees to the side. +// 50 degrees to the side. It also notices jumps, landings and starting to sneak, which reward touches react to. @UtilityClass public class BodyMotion { @@ -37,6 +37,8 @@ public class BodyMotion { private final double MOVING = 0.0025; private final Map states = new WeakHashMap<>(); + // Counts server ticks, so events can be matched to the tick they happened in + private long tick; private static class State { private float body; @@ -45,9 +47,23 @@ public class BodyMotion { private double z; // Blocks per tick, smoothed so single steps do not jerk the wings private double speed; + private boolean onGround = true; + private boolean sneaking; + // Highest point of the current time in the air + private double peakY; + private long jumpTick = Long.MIN_VALUE; + private long landTick = Long.MIN_VALUE; + private double landFall; + private long sneakTick = Long.MIN_VALUE; + } + + // Called once per server tick before the players are updated + public void nextTick() { + tick++; } // Called once per server tick for every online player + @SuppressWarnings("deprecation") public void tick(Player player) { Location location = player.getLocation(); State state = states.get(player); @@ -70,6 +86,22 @@ public class BodyMotion { state.z = location.getZ(); state.speed = state.speed * 0.8 + Math.sqrt(dx * dx + dy * dy + dz * dz) * 0.2; + boolean onGround = player.isOnGround(); + if (state.onGround && !onGround && dy > 0) { + state.jumpTick = tick; + } + if (!onGround) { + state.peakY = state.onGround ? location.getY() : Math.max(state.peakY, location.getY()); + } else if (!state.onGround) { + state.landTick = tick; + state.landFall = state.peakY - location.getY(); + } + state.onGround = onGround; + if (player.isSneaking() && !state.sneaking) { + state.sneakTick = tick; + } + state.sneaking = player.isSneaking(); + float target = state.body; if (dx * dx + dz * dz > MOVING) { target = (float) Math.toDegrees(Math.atan2(dz, dx)) - 90; @@ -98,6 +130,32 @@ public class BodyMotion { return state == null ? 0 : state.speed; } + public long currentTick() { + return tick; + } + + public boolean jumped(Player player) { + State state = states.get(player); + return state != null && state.jumpTick == tick; + } + + // Ticks since the last jump, or a large number if there was none + public long ticksSinceJump(Player player) { + State state = states.get(player); + return state == null || state.jumpTick == Long.MIN_VALUE ? Long.MAX_VALUE : tick - state.jumpTick; + } + + // How far the player fell if they landed this tick, otherwise -1 + public double landed(Player player) { + State state = states.get(player); + return state != null && state.landTick == tick ? state.landFall : -1; + } + + public boolean startedSneaking(Player player) { + State state = states.get(player); + return state != null && state.sneakTick == tick; + } + private float wrap(float degrees) { float wrapped = degrees % 360; if (wrapped >= 180) wrapped -= 360; diff --git a/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java b/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java index 3a3b707b..c56d6b78 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java @@ -44,6 +44,10 @@ public class CompiledSet { for (ParticleEntry entry : particleSet.getEntries()) { if (entry.isUnlocked(context)) { compiledSet.add(entry.build()); + ParticleElement touch = entry.buildTouch(); + if (touch != null) { + compiledSet.add(touch); + } } } return compiledSet; diff --git a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java index 43cbab0c..ab40bc48 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java @@ -60,6 +60,7 @@ public class ParticleListener implements Listener { movingPlayers.entrySet().removeIf(entry -> entry.getValue() <= 0); deg += 0.1; if (deg > 360) deg = 0; + BodyMotion.nextTick(); Bukkit.getOnlinePlayers().forEach(BodyMotion::tick); Bukkit.getOnlinePlayers().forEach(player -> { if (disabled(player)) return; diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java b/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java index 916ab16d..892d6b4f 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java @@ -101,7 +101,7 @@ public class Designs { public final ChoiceParameter WING_COLORING = ChoiceParameter.ofEnum("coloring", "PARTICLE_PARAM_WING_COLORING", WingColoring.OVER_TIME); // Percent of the points shown each tick; fewer makes the wing twinkle and costs less public final IntParameter WING_SPARKLE = new IntParameter("sparkle", "PARTICLE_PARAM_WING_SPARKLE", 100, 10, 100); - public final ChoiceParameter WING_MOTION = ChoiceParameter.ofEnum("motion", "PARTICLE_PARAM_WING_MOTION", Wing.Motion.NONE); + public final ChoiceParameter WING_MOTION = ChoiceParameter.ofEnum("motion", "PARTICLE_PARAM_WING_MOTION", Wing.Motion.NONE, motion -> motion == Wing.Motion.JUMP_BEAT); // Moving wings need to be redrawn often to look smooth; still ones are fine once a second private final int MOVING_WING_INTERVAL = 2; private final int STILL_WING_INTERVAL = 20; diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/FixedDesign.java b/LobbySystem/src/de/steamwar/lobby/particle/design/FixedDesign.java index 48d9e49b..8e8f552d 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/FixedDesign.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/FixedDesign.java @@ -32,6 +32,8 @@ public class FixedDesign extends ParticleDesign { private final int points; private final Consumer defaults; private final Supplier shape; + private Supplier touch; + private int touchPoints; public FixedDesign(String id, Material icon, String nameKey, ParticleRequirement requirement, int points, Consumer defaults, Supplier shape) { super(id, icon, nameKey, DesignVisibility.REWARD, requirement); @@ -45,9 +47,21 @@ public class FixedDesign extends ParticleDesign { return shape.get(); } + // Adds the reward's unique touch; its points count towards the slot budget + public FixedDesign withTouch(int touchPoints, Supplier touch) { + this.touchPoints = touchPoints; + this.touch = touch; + return this; + } + + @Override + public ParticleElement touch() { + return touch == null ? null : touch.get(); + } + @Override public int points(ParameterValues values) { - return points; + return points + touchPoints; } @Override diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java index 766d1f83..1c74f090 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java @@ -72,6 +72,12 @@ public abstract class ParticleDesign { return false; } + // An extra the builder cannot make, shown next to the design's look and checked every tick so it never misses + // a jump or landing; null for designs without one + public ParticleElement touch() { + return null; + } + // Called after a design setting changed, to adjust others that depend on it public void adjust(Parameter changed, ParameterValues values) { } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/RewardDesigns.java b/LobbySystem/src/de/steamwar/lobby/particle/design/RewardDesigns.java index 0f9c32f2..4f73de22 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/RewardDesigns.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/RewardDesigns.java @@ -19,22 +19,30 @@ package de.steamwar.lobby.particle.design; +import de.steamwar.lobby.particle.Gradient; import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleRequirement; import de.steamwar.lobby.particle.WingDesign; import de.steamwar.lobby.particle.elements.*; +import de.steamwar.lobby.particle.elements.custom.HearthBeat; +import de.steamwar.lobby.particle.elements.custom.NonMoving; +import de.steamwar.lobby.particle.elements.custom.PulseShimmer; +import de.steamwar.lobby.particle.elements.touch.*; import lombok.experimental.UtilityClass; import org.bukkit.Material; import org.bukkit.Particle; import org.bukkit.entity.Player; import org.bukkit.util.Vector; +import java.time.Month; import java.util.List; import java.util.function.Consumer; import static de.steamwar.lobby.particle.design.EntryParameters.*; -// Designs earned in tournaments and the Easter egg hunt +// Designs earned in tournaments, events and the Easter egg hunt. Rewards whose look the basic designs can build get a +// touch the builder cannot make: bursts on jumping, landing or sneaking, footprints, a link to team-mates, a seasonal +// extra, a preset-only shape or aimed glyphs. @UtilityClass public class RewardDesigns { @@ -48,23 +56,29 @@ public class RewardDesigns { public final ParticleDesign EVENT_PLACEMENT_WARGEAR_SEASON = new FixedDesign("event_placement_wargear_season", Material.ENCHANTING_TABLE, "PARTICLE_EVENT_ENCHANTING", ParticleRequirement.eventPlacement(22, 12, 285, 54), 5, MOVE, - () -> new Cloud(new Circle(new LocationMutator(new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0.01), 0, 1.1, 0)))); + () -> new Cloud(new Circle(new LocationMutator(new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0.01), 0, 1.1, 0)))) + .withTouch(3, () -> new NonMoving(new Glyphs(Glyphs.Path.INTO_PLAYER, 3, 1.5))); public final ParticleDesign EVENT_PLACEMENT_AIRSHIP = new FixedDesign("event_placement_airship", Material.SNOWBALL, "PARTICLE_EVENT_CLOUD", ParticleRequirement.eventPlacement(26, 205, 9, 54, 120, 292), 5, MOVE, - () -> new Circle(new LocationMutator(new SimpleParticle(Particle.CLOUD, 0, 0, 0, 0.01), 0, 2.2, 0))); + () -> new Circle(new LocationMutator(new SimpleParticle(Particle.CLOUD, 0, 0, 0, 0.01), 0, 2.2, 0))) + .withTouch(1, () -> new EventBurst(EventBurst.Trigger.JUMP, new SimpleParticle(Particle.CLOUD, 0.3F, 0.05F, 0.3F, 0.02, 12))); public final ParticleDesign EVENT_PLACEMENT_HELLS_BELLS = new FixedDesign("event_placement_hells_bells", Material.TNT_MINECART, "PARTICLE_EVENT_SMOKE", ParticleRequirement.eventPlacement(28, 205, 9, 11), 5, MOVE, () -> new Cloud(new Circle(new LocationMutator(new SimpleParticle(Particle.CAMPFIRE_COSY_SMOKE, 0, 0, 0, 0.01), 0, 2.2, 0)))); public final ParticleDesign EVENT_PLACEMENT_UNDERWATER = new FixedDesign("event_placement_underwater", Material.PRISMARINE_SHARD, "PARTICLE_EVENT_WATER", ParticleRequirement.eventPlacement(31, 9, 210, 520), 5, MOVE, - () -> new Cloud(new SimpleParticle(Particle.DRIPPING_WATER))); + () -> new Cloud(new SimpleParticle(Particle.DRIPPING_WATER))) + .withTouch(1, () -> new EventBurst(EventBurst.Trigger.LAND, 1, new Ring(new SimpleParticle(Particle.SPLASH, 0, 0, 0, 0, 3), 1, 16))); public final ParticleDesign EVENT_PLACEMENT_ADVENT_WAR_SHIP = new FixedDesign("event_placement_advent_war_ship", Material.PRISMARINE_CRYSTALS, "PARTICLE_EVENT_WATER", ParticleRequirement.eventPlacement(32, 9, 205, 210), 10, ALWAYS, - () -> new LocationMutator(new Circle(new Group(new SimpleParticle(Particle.DRIPPING_WATER), new SimpleParticle(Particle.UNDERWATER, 0.2F, 0.2F, 0.2F, 0.01))), 0, 1.1, 0)); + () -> new LocationMutator(new Circle(new Group(new SimpleParticle(Particle.DRIPPING_WATER), new SimpleParticle(Particle.UNDERWATER, 0.2F, 0.2F, 0.2F, 0.01))), 0, 1.1, 0)) + .withTouch(4, () -> new Footprints(new SimpleParticle(Particle.FISHING, 0.05F, 0, 0.05F, 0, 1), 40)); public final ParticleDesign EVENT_PLACEMENT_MINI_WARGEAR_LIGA = wings("event_placement_mini_wargear_liga", Material.IRON_SWORD, "PARTICLE_EVENT_WINGS", ParticleRequirement.eventPlacement(33, 9, 34, 205), Particle.UNDERWATER, 0.15, WingDesign.MWGL); public final ParticleDesign EVENT_PLACEMENT_ABSTURZ = wings("event_placement_absturz", Material.FEATHER, "PARTICLE_EVENT_WINGS", - ParticleRequirement.eventPlacement(34, 210, 205, 527, 286), Particle.CRIT, 0.15, WingDesign.SIMPLE); + ParticleRequirement.eventPlacement(34, 210, 205, 527, 286), Particle.CRIT, 0.15, WingDesign.SIMPLE) + .withTouch(1, () -> new EventBurst(EventBurst.Trigger.LAND, 3, new Group(new SimpleParticle(Particle.EXPLOSION, 0, 0, 0, 0, 1), + new Ring(new SimpleParticle(Particle.CRIT, 0, 0.1F, 0, 0.1, 2), 1.2, 16)))); public final ParticleDesign EVENT_PLACEMENT_UNDERWATER_MWG = wings("event_placement_underwater_mwg", Material.CYAN_CARPET, "PARTICLE_EVENT_RAIN_CLOUD", ParticleRequirement.eventPlacement(35, 9, 210), Particle.CRIT, 0.15, WingDesign.SW); public final ParticleDesign EVENT_PLACEMENT_WARGEAR_SEASON_2022 = wings("event_placement_wargear_season_2022", Material.DIAMOND_HELMET, "PARTICLE_EVENT_WGS", @@ -75,29 +89,37 @@ public class RewardDesigns { // For taking part in any event public final ParticleDesign EVENT_PARTICIPATION_WATER = new FixedDesign("event_participation_water", Material.WATER_BUCKET, "PARTICLE_EVENT_WATER", ParticleRequirement.EVENT_PARTICIPATION, 5, ALWAYS, - () -> new Circle(new LocationMutator(new SimpleParticle(Particle.DRIPPING_WATER), 0, 1.1, 0))); + () -> new Circle(new LocationMutator(new SimpleParticle(Particle.DRIPPING_WATER), 0, 1.1, 0))) + .withTouch(5, () -> new TeamLink("event_participation_water", new SimpleParticle(Particle.DRIPPING_WATER, 0, 0, 0, 0, 1))); public final ParticleDesign EVENT_PARTICIPATION_LAVA = new FixedDesign("event_participation_lava", Material.LAVA_BUCKET, "PARTICLE_EVENT_LAVA", ParticleRequirement.EVENT_PARTICIPATION, 5, ALWAYS, - () -> new Circle(new LocationMutator(new SimpleParticle(Particle.DRIPPING_LAVA), 0, 1.1, 0))); + () -> new Circle(new LocationMutator(new SimpleParticle(Particle.DRIPPING_LAVA), 0, 1.1, 0))) + .withTouch(1, () -> new EventBurst(EventBurst.Trigger.LAND, 1, new Group(new SimpleParticle(Particle.LAVA, 0.2F, 0, 0.2F, 0, 4), + new SimpleParticle(Particle.SMOKE, 0.2F, 0.1F, 0.2F, 0.02, 6)))); public final ParticleDesign EVENT_PARTICIPATION_WARGEAR_SEASON = new FixedDesign("event_participation_wargear_season", Material.BOOK, "PARTICLE_EVENT_ENCHANTING", ParticleRequirement.eventParticipation(22), 5, MOVE, - () -> new SimpleParticle(Particle.ENCHANT)); + () -> new SimpleParticle(Particle.ENCHANT)) + .withTouch(1, () -> new EventBurst(EventBurst.Trigger.SNEAK, new Glyphs(Glyphs.Path.OUT_OF_PLAYER, 16, 1.5))); public final ParticleDesign EVENT_PARTICIPATION_AIRSHIP = new FixedDesign("event_participation_airship", Material.SNOW_BLOCK, "PARTICLE_EVENT_CLOUD", ParticleRequirement.eventParticipation(26), 5, MOVE, - () -> new SimpleParticle(Particle.CLOUD)); + () -> new SimpleParticle(Particle.CLOUD)) + .withTouch(4, () -> new Footprints(new SimpleParticle(Particle.CLOUD, 0.05F, 0, 0.05F, 0, 1), 40)); public final ParticleDesign EVENT_PARTICIPATION_HELLS_BELLS = new FixedDesign("event_participation_hells_bells", Material.TNT, "PARTICLE_EVENT_SMOKE", ParticleRequirement.eventParticipation(28), 5, MOVE, () -> new Circle(new SimpleParticle(Particle.CAMPFIRE_COSY_SMOKE, 0, 0, 0, 0.01))); public final ParticleDesign EVENT_PARTICIPATION_UNDERWATER = new FixedDesign("event_participation_underwater", Material.PRISMARINE_BRICKS, "PARTICLE_EVENT_WATER", ParticleRequirement.eventParticipation(31), 5, MOVE, - () -> new SimpleParticle(Particle.DRIPPING_WATER)); + () -> new SimpleParticle(Particle.DRIPPING_WATER)) + .withTouch(5, () -> new TeamLink("event_participation_underwater", new SimpleParticle(Particle.DRIPPING_WATER, 0, 0, 0, 0, 1))); public final ParticleDesign EVENT_PARTICIPATION_ADVENT_WAR_SHIP = new FixedDesign("event_participation_advent_war_ship", Material.PRISMARINE_WALL, "PARTICLE_EVENT_WATER", ParticleRequirement.eventParticipation(32), 10, MOVE, - () -> new Group(new SimpleParticle(Particle.DRIPPING_WATER), new SimpleParticle(Particle.UNDERWATER, 0.2F, 0.2F, 0.2F, 0.01))); + () -> new Group(new SimpleParticle(Particle.DRIPPING_WATER), new SimpleParticle(Particle.UNDERWATER, 0.2F, 0.2F, 0.2F, 0.01))) + .withTouch(2, () -> new Seasonal(Month.DECEMBER, new LocationMutator(new SimpleParticle(Particle.SNOWFLAKE, 1.2F, 0.3F, 1.2F, 0.01, 2), 0, 2.4, 0))); public final ParticleDesign EVENT_PARTICIPATION_MINI_WARGEAR_LIGA = new FixedDesign("event_participation_mini_wargear_liga", Material.PRISMARINE_SLAB, "PARTICLE_EVENT_WATER", ParticleRequirement.eventParticipation(33), 5, MOVE, - () -> new Circle(new SimpleParticle(Particle.UNDERWATER, 0, 0, 0, 0))); + () -> new Circle(new SimpleParticle(Particle.UNDERWATER, 0, 0, 0, 0))) + .withTouch(3, () -> new NonMoving(new PulseShimmer(new SimpleParticle(Particle.UNDERWATER, 0, 0, 0, 0, 1), 80, 2))); public final ParticleDesign EVENT_PARTICIPATION_UNDERWATER_MWG = new FixedDesign("event_participation_underwater_mwg", Material.BLUE_CARPET, "PARTICLE_EVENT_RAIN_CLOUD", ParticleRequirement.eventParticipation(35), 15, ALWAYS, () -> new RandomParticle( @@ -115,20 +137,24 @@ public class RewardDesigns { rainCloud(null, Particle.TOTEM_OF_UNDYING, 2) )); public final ParticleDesign EVENT_PARTICIPATION_WARGEAR_SEASON_2022 = wings("event_participation_wargear_season_2022", Material.DIAMOND_SWORD, "PARTICLE_EVENT_WINGS", - ParticleRequirement.eventParticipation(37), Particle.DRAGON_BREATH, 0.15, WingDesign.COMPLEX); + ParticleRequirement.eventParticipation(37), Particle.DRAGON_BREATH, 0.15, WingDesign.COMPLEX) + .withTouch(2, () -> new Delayed(new Wing(new SimpleParticle(Particle.DRAGON_BREATH, 0, 0, 0, 0, 1), 0.15, WingDesign.COMPLEX, false, 100, Wing.Motion.JUMP_BEAT), 2)); public final ParticleDesign EGG_HUNT_EASY = new FixedDesign("egg_hunt_easy", Material.LIME_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_EASY", ParticleRequirement.EGG_HUNT_EASY, 1, ALWAYS, - () -> new LocationMutator(new SimpleParticle(Particle.FALLING_SPORE_BLOSSOM, 0.5F, 0.1f, 0.5F, 1, 1), 0, 2.2, 0)); + () -> new LocationMutator(new SimpleParticle(Particle.FALLING_SPORE_BLOSSOM, 0.5F, 0.1f, 0.5F, 1, 1), 0, 2.2, 0)) + .withTouch(4, () -> new Footprints(new SimpleParticle(Particle.SPORE_BLOSSOM_AIR, 0.05F, 0, 0.05F, 0, 1), 40)); public final ParticleDesign EGG_HUNT_MEDIUM = new FixedDesign("egg_hunt_medium", Material.YELLOW_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_MEDIUM", ParticleRequirement.EGG_HUNT_MEDIUM, 5, ALWAYS.andThen(values -> values.set(GROUND, Ground.ON_GROUND).set(SNEAK, Sneak.SNEAKING)), () -> new SimpleParticle(Particle.GLOW_SQUID_INK, 0.4F, 0.9F, 0.4F, 0.01)); public final ParticleDesign EGG_HUNT_HARD = new FixedDesign("egg_hunt_hard", Material.RED_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_HARD", ParticleRequirement.EGG_HUNT_HARD, 2, ALWAYS, - () -> new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0.01, 1, null), new DustParticle(Particle.DUST, 0, 0, 0, 0.01, 1, null)), 0, 0.5, 0)); + () -> new LocationMutator(new Circle(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0.01, 1, null), new DustParticle(Particle.DUST, 0, 0, 0, 0.01, 1, null)), 0.8, 0.5), 0, 0.5, 0)); public final ParticleDesign EGG_HUNT_EXTREME = new FixedDesign("egg_hunt_extreme", Material.PURPLE_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_EXTREME", ParticleRequirement.EGG_HUNT_EXTREME, 1, ALWAYS, - () -> new LocationMutator(new SimpleParticle(Particle.FALLING_OBSIDIAN_TEAR, 0.5F, 0.1F, 0.5F, 1, 1), 0, 2.2, 0)); + () -> new LocationMutator(new SimpleParticle(Particle.FALLING_OBSIDIAN_TEAR, 0.5F, 0.1F, 0.5F, 1, 1), 0, 2.2, 0)) + .withTouch(1, () -> new EventBurst(EventBurst.Trigger.LAND, 1, new Group(new Ring(new SimpleParticle(Particle.WITCH, 0, 0.05F, 0, 0, 2), 1, 16), + new SimpleParticle(Particle.LANDING_OBSIDIAN_TEAR, 0.4F, 0, 0.4F, 0, 6)))); public final ParticleDesign EGG_HUNT_ADVANCED = new FixedDesign("egg_hunt_advanced", Material.BLACK_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_ADVANCED", ParticleRequirement.EGG_HUNT_ADVANCED, 2, ALWAYS.andThen(values -> values.set(SNEAK, Sneak.SNEAKING)), () -> new LocationMutator(new Group( @@ -145,9 +171,10 @@ public class RewardDesigns { () -> new Group( new Sneaking(new Floor(new LocationMutator(new SimpleParticle(Particle.HEART, 0, 0, 0, 0.01), 0, 1.7, 0))), new NonFloor(new NonFlying(new LocationMutator(new Cloud(new Circle(new SimpleParticle(Particle.HEART, 0, 0, 0, 0.01))), 0, 0.5, 0))) - )); + )) + .withTouch(1, () -> new NonMoving(new HearthBeat(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(java.awt.Color.RED)), 80))); - private ParticleDesign wings(String id, Material icon, String nameKey, ParticleRequirement requirement, Particle particle, double size, WingDesign wingDesign) { + private FixedDesign wings(String id, Material icon, String nameKey, ParticleRequirement requirement, Particle particle, double size, WingDesign wingDesign) { return new FixedDesign(id, icon, nameKey, requirement, wingDesign.getVectors().length, WINGS, () -> new Wing(new SimpleParticle(particle, 0, 0, 0, 0, 1), size, wingDesign)); } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/Wing.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/Wing.java index 4fa84f51..6f8ab05f 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/elements/Wing.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/Wing.java @@ -33,6 +33,8 @@ public class Wing extends DelegatingParticleElement { NONE, FLAP_SLOW, FLAP_FAST, + // One wing beat after each jump and none otherwise; only for a reward + JUMP_BEAT, } // Wings stand slightly open when idle and open further the faster the player moves @@ -40,6 +42,7 @@ public class Wing extends DelegatingParticleElement { private static final double MOVING_OPEN_DEGREES = 15; private static final double FULL_OPEN_SPEED = 0.6; private static final double FLAP_DEGREES = 25; + private static final int JUMP_BEAT_TICKS = 10; private double size; private WingDesign wingDesign; @@ -68,6 +71,9 @@ public class Wing extends DelegatingParticleElement { @Override public void tick(ParticleTickData particleTickData) { + if (motion == Motion.JUMP_BEAT && BodyMotion.ticksSinceJump(particleTickData.getPlayer()) > JUMP_BEAT_TICKS) { + return; + } double ticks = particleTickData.getDeg() * 10; double open = Math.toRadians(openDegrees(particleTickData, ticks)); double cos = Math.cos(open), sin = Math.sin(open); @@ -105,6 +111,7 @@ public class Wing extends DelegatingParticleElement { return switch (motion) { case FLAP_SLOW -> open + FLAP_DEGREES * Math.sin(2 * Math.PI * ticks / 20); case FLAP_FAST -> open + FLAP_DEGREES * Math.sin(2 * Math.PI * ticks / 10); + case JUMP_BEAT -> open + FLAP_DEGREES * Math.sin(Math.PI * BodyMotion.ticksSinceJump(particleTickData.getPlayer()) / JUMP_BEAT_TICKS); default -> open; }; } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/EventBurst.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/EventBurst.java new file mode 100644 index 00000000..2a963783 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/EventBurst.java @@ -0,0 +1,63 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.lobby.particle.elements.touch; + +import de.steamwar.lobby.particle.BodyMotion; +import de.steamwar.lobby.particle.ParticleElement; +import de.steamwar.lobby.particle.ParticleTickData; +import de.steamwar.lobby.particle.elements.DelegatingParticleElement; +import org.bukkit.entity.Player; + +// Shows the burst once at the player's feet in the tick the event happens +public class EventBurst extends DelegatingParticleElement { + + public enum Trigger { + JUMP, + LAND, + SNEAK, + } + + private final Trigger trigger; + // Landings from lower than this many blocks are ignored + private final double minimumFall; + + public EventBurst(Trigger trigger, ParticleElement burst) { + this(trigger, 0, burst); + } + + public EventBurst(Trigger trigger, double minimumFall, ParticleElement burst) { + super(burst); + this.trigger = trigger; + this.minimumFall = minimumFall; + } + + @Override + public void tick(ParticleTickData particleTickData) { + Player player = particleTickData.getPlayer(); + boolean happened = switch (trigger) { + case JUMP -> BodyMotion.jumped(player); + case LAND -> BodyMotion.landed(player) >= minimumFall; + case SNEAK -> BodyMotion.startedSneaking(player); + }; + if (happened) { + particleElement.tick(particleTickData.withLocation(player.getLocation())); + } + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Footprints.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Footprints.java new file mode 100644 index 00000000..22fe879c --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Footprints.java @@ -0,0 +1,69 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.lobby.particle.elements.touch; + +import de.steamwar.lobby.particle.ParticleElement; +import de.steamwar.lobby.particle.ParticleTickData; +import de.steamwar.lobby.particle.elements.DelegatingParticleElement; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.WeakHashMap; + +// Leaves a print every half step on the ground and keeps showing it for a while after the player walked on +public class Footprints extends DelegatingParticleElement { + + private static final double STEP = 0.6; + + private record Print(Location location, int age) { + } + + private final int lifetime; + // Weak keys drop players once they quit + private final Map> prints = new WeakHashMap<>(); + + public Footprints(ParticleElement particleElement, int lifetime) { + super(particleElement); + this.lifetime = lifetime; + } + + @Override + @SuppressWarnings("deprecation") + public void tick(ParticleTickData particleTickData) { + Player player = particleTickData.getPlayer(); + Deque playerPrints = prints.computeIfAbsent(player, key -> new ArrayDeque<>()); + Location feet = player.getLocation(); + Print last = playerPrints.peekLast(); + if (player.isOnGround() && (last == null || last.location().getWorld() != feet.getWorld() || last.location().distanceSquared(feet) > STEP * STEP)) { + playerPrints.addLast(new Print(feet, 0)); + } + + int size = playerPrints.size(); + for (int i = 0; i < size; i++) { + Print print = playerPrints.pollFirst(); + if (print.age() >= lifetime) continue; + particleElement.tick(particleTickData.withLocation(print.location().clone())); + playerPrints.addLast(new Print(print.location(), print.age() + 1)); + } + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Glyphs.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Glyphs.java new file mode 100644 index 00000000..62dfa1af --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Glyphs.java @@ -0,0 +1,72 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.lobby.particle.elements.touch; + +import de.steamwar.lobby.particle.ParticleElement; +import de.steamwar.lobby.particle.ParticleTickData; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.util.Vector; + +// Enchanting glyphs that fly along a path. Spawned with an amount of 0, the offset becomes the glyph's motion: it +// appears at location + offset and flies back to the location, like glyphs flying into an enchanting table. +public class Glyphs implements ParticleElement { + + public enum Path { + // From random points on the ground around the player into the chest + INTO_PLAYER, + // From the chest outwards to a ring around the player + OUT_OF_PLAYER, + } + + private static final double CHEST = 1.2; + + private final Path path; + private final int amount; + private final double radius; + + public Glyphs(Path path, int amount, double radius) { + this.path = path; + this.amount = amount; + this.radius = radius; + } + + @Override + public void tick(ParticleTickData particleTickData) { + Location chest = particleTickData.getPlayer().getLocation().add(0, CHEST, 0); + for (int i = 0; i < amount; i++) { + Location target; + Vector from; + if (path == Path.INTO_PLAYER) { + double angle = RANDOM.nextDouble() * 2 * Math.PI; + double distance = radius * (0.5 + RANDOM.nextDouble() / 2); + target = chest; + from = new Vector(Math.cos(angle) * distance, -CHEST, Math.sin(angle) * distance); + } else { + double angle = 2 * Math.PI * i / amount; + Vector out = new Vector(Math.cos(angle) * radius, 0, Math.sin(angle) * radius); + target = chest.clone().add(out); + from = out.multiply(-1); + } + Location spawn = target; + display(particleTickData, player -> player.spawnParticle(Particle.ENCHANT, spawn, 0, from.getX(), from.getY(), from.getZ(), 1)); + } + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Ring.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Ring.java new file mode 100644 index 00000000..77e69fc1 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Ring.java @@ -0,0 +1,47 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.lobby.particle.elements.touch; + +import de.steamwar.lobby.particle.ParticleElement; +import de.steamwar.lobby.particle.ParticleTickData; +import de.steamwar.lobby.particle.elements.DelegatingParticleElement; +import org.bukkit.Location; + +// A flat ring of points around the location, all at once, for bursts +public class Ring extends DelegatingParticleElement { + + private final double radius; + private final int points; + + public Ring(ParticleElement particleElement, double radius, int points) { + super(particleElement); + this.radius = radius; + this.points = points; + } + + @Override + public void tick(ParticleTickData particleTickData) { + Location center = particleTickData.getLocation(); + for (int i = 0; i < points; i++) { + double angle = 2 * Math.PI * i / points; + particleElement.tick(particleTickData.withLocation(center.clone().add(Math.cos(angle) * radius, 0, Math.sin(angle) * radius))); + } + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Seasonal.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Seasonal.java new file mode 100644 index 00000000..d7528a88 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/Seasonal.java @@ -0,0 +1,45 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.lobby.particle.elements.touch; + +import de.steamwar.lobby.particle.ParticleElement; +import de.steamwar.lobby.particle.ParticleTickData; +import de.steamwar.lobby.particle.elements.DelegatingParticleElement; + +import java.time.LocalDate; +import java.time.Month; + +// Only shows during the given month +public class Seasonal extends DelegatingParticleElement { + + private final Month month; + + public Seasonal(Month month, ParticleElement particleElement) { + super(particleElement); + this.month = month; + } + + @Override + public void tick(ParticleTickData particleTickData) { + if (LocalDate.now().getMonth() == month) { + particleElement.tick(particleTickData); + } + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/TeamLink.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/TeamLink.java new file mode 100644 index 00000000..63412c12 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/TeamLink.java @@ -0,0 +1,74 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.lobby.particle.elements.touch; + +import de.steamwar.lobby.particle.BodyMotion; +import de.steamwar.lobby.particle.ParticleElement; +import de.steamwar.lobby.particle.ParticleTickData; +import de.steamwar.lobby.particle.elements.DelegatingParticleElement; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; + +// A line of particles between nearby players who show the same reward. Each pair is drawn by only one of them. +public class TeamLink extends DelegatingParticleElement { + + private static final double RANGE = 8; + private static final double SPACING = 0.5; + private static final double CHEST = 1.2; + + // Per reward, who showed it in which tick + private static final Map> SHOWING = new HashMap<>(); + + private final String reward; + + public TeamLink(String reward, ParticleElement particleElement) { + super(particleElement); + this.reward = reward; + } + + @Override + public void tick(ParticleTickData particleTickData) { + Player player = particleTickData.getPlayer(); + long now = BodyMotion.currentTick(); + Map showing = SHOWING.computeIfAbsent(reward, key -> new WeakHashMap<>()); + showing.put(player, now); + + Location from = player.getLocation().add(0, CHEST, 0); + for (Map.Entry other : showing.entrySet()) { + Player partner = other.getKey(); + // Still showing it, and drawn by the player with the smaller id only + if (partner == player || other.getValue() < now - 1 || player.getUniqueId().compareTo(partner.getUniqueId()) > 0) continue; + Location to = partner.getLocation().add(0, CHEST, 0); + if (to.getWorld() != from.getWorld() || to.distanceSquared(from) > RANGE * RANGE) continue; + + Vector step = to.toVector().subtract(from.toVector()); + int points = (int) Math.ceil(step.length() / SPACING); + step.multiply(1.0 / points); + for (int i = 1; i < points; i++) { + particleElement.tick(particleTickData.withLocation(from.clone().add(step.clone().multiply(i)))); + } + } + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java index fcb79a11..12f3be23 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java @@ -163,6 +163,18 @@ public class ParticleEntry { return new LocationMutator(element, offsetX, offsetY, offsetZ); } + // The design's touch with the entry's placement and visibility, or null; touches ignore conditions and timing + public ParticleElement buildTouch() { + ParticleElement touch = design.touch(); + if (touch == null) { + return null; + } + if (values.get(PLACEMENT) == Placement.BODY) { + touch = new BodyRelative(touch); + } + return new Always(values.get(VISIBILITY).wrap(touch)); + } + // Shown only when both the design and, for designs that use it, the particle type are unlocked public boolean isUnlocked(UnlockContext context) { if (!design.getRequirement().test(context)) {