forked from SteamWar/SteamWar
Give the rebuildable reward designs unique touches
Rewards whose look the basic designs can build now carry a touch the builder cannot make: bursts on jumping, landing or sneaking, footprints, a line to nearby players with the same reward, a December extra, aimed enchanting glyphs, a heartbeat, a jump-beat wing or an orbiting shape. BodyMotion tracks the jump, land and sneak events the touches react to.
This commit is contained in:
@@ -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<Player, State> 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -101,7 +101,7 @@ public class Designs {
|
||||
public final ChoiceParameter<WingColoring> 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> WING_MOTION = ChoiceParameter.ofEnum("motion", "PARTICLE_PARAM_WING_MOTION", Wing.Motion.NONE);
|
||||
public final ChoiceParameter<Wing.Motion> 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;
|
||||
|
||||
@@ -32,6 +32,8 @@ public class FixedDesign extends ParticleDesign {
|
||||
private final int points;
|
||||
private final Consumer<ParameterValues> defaults;
|
||||
private final Supplier<ParticleElement> shape;
|
||||
private Supplier<ParticleElement> touch;
|
||||
private int touchPoints;
|
||||
|
||||
public FixedDesign(String id, Material icon, String nameKey, ParticleRequirement requirement, int points, Consumer<ParameterValues> defaults, Supplier<ParticleElement> 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<ParticleElement> 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
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Player, Deque<Print>> 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<Print> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String, Map<Player, Long>> 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<Player, Long> showing = SHOWING.computeIfAbsent(reward, key -> new WeakHashMap<>());
|
||||
showing.put(player, now);
|
||||
|
||||
Location from = player.getLocation().add(0, CHEST, 0);
|
||||
for (Map.Entry<Player, Long> 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))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user