Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -22,6 +22,7 @@
import com.hibiscusmc.hmccosmetics.listener.PlayerConnectionListener;
import com.hibiscusmc.hmccosmetics.listener.PlayerGameListener;
import com.hibiscusmc.hmccosmetics.listener.ServerListener;
import com.hibiscusmc.hmccosmetics.task.BalloonTickTask;
import com.hibiscusmc.hmccosmetics.user.CosmeticUser;
import com.hibiscusmc.hmccosmetics.user.CosmeticUsers;
import com.hibiscusmc.hmccosmetics.util.MessagesUtil;
Expand Down Expand Up @@ -229,6 +230,12 @@ public static void setup() {
}
}

if (Settings.isBalloonPhysics()) {
BalloonTickTask.INSTANCE.schedule();
} else {
BalloonTickTask.INSTANCE.stop();
}

if (Settings.isEmotesEnabled() && (HMCCosmeticsAPI.getNMSVersion().contains("v1_19_R3") || HMCCosmeticsAPI.getNMSVersion().contains("v1_20_R1"))) EmoteManager.loadEmotes(); // PlayerAnimator does not support 1.20.2 yet

getInstance().getLogger().info("Successfully Enabled HMCCosmetics");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class Settings {
private static final String CONFIG_VERSION = "config-version";
private static final String COSMETIC_SETTINGS_PATH = "cosmetic-settings";
private static final String BALLOON_OFFSET = "balloon-offset";
private static final String BALLOON_PHYSICS = "balloon-physics";
private static final String VIEW_DISTANCE_PATH = "view-distance";
private static final String DYE_MENU_PATH = "dye-menu";
private static final String DYE_MENU_NAME = "title";
Expand Down Expand Up @@ -126,6 +127,8 @@ public class Settings {
@Getter
private static boolean backpackPreventDarkness;
@Getter
private static boolean balloonPhysics;
@Getter
private static List<String> disabledGamemodes;
@Getter
private static List<String> disabledWorlds;
Expand Down Expand Up @@ -235,6 +238,7 @@ public static void load(ConfigurationNode source) {
emoteMoveCheck = cosmeticSettings.node(COSMETIC_EMOTE_MOVE_CHECK_PATH).getBoolean(false);
packetEntityTeleportCooldown = cosmeticSettings.node(COSMETIC_PACKET_ENTITY_TELEPORT_COOLDOWN_PATH).getInt(-1);
balloonHeadForward = cosmeticSettings.node(COSMETIC_BALLOON_HEAD_FORWARD_PATH).getBoolean(false);
balloonPhysics = cosmeticSettings.node(BALLOON_PHYSICS).getBoolean(false);
backpackPreventDarkness = cosmeticSettings.node(BACKPACK_PREVENT_DARKNESS_PATH).getBoolean(true);

ConfigurationNode menuSettings = source.node(MENU_SETTINGS_PATH);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@
import java.util.List;

public class CosmeticBalloonType extends Cosmetic {
private static final float STRING_ELASTICITY_FACTOR = 10f;
private static final Vector BALLOON_BUOYANCY_FORCE = new Vector(0, 6.5f, 0);
private static final float BALLOON_MASS = 1f;
private static final float AIR_DAMPING = 0.85f; // 15% damping
private static final float BALLOON_RADIUS = 1f;
private static final float BALLOON_MOMENT_OF_INERTIA = 0.4f * BALLOON_MASS * BALLOON_RADIUS * BALLOON_RADIUS;
private static final float RESTORE_CONSTANT = 40f;

// The offset, in blocks, from the player's feet (player location) to the player's hand
private static final Vector HAND_OFFSET = new Vector(0, 0.35, 0);

@Getter
private final String modelName;
Expand All @@ -28,6 +38,8 @@ public class CosmeticBalloonType extends Cosmetic {
@Getter
private Vector balloonOffset;

private volatile long lastCall = -1;

public CosmeticBalloonType(String id, ConfigurationNode config) {
super(id, config);

Expand All @@ -52,8 +64,112 @@ public CosmeticBalloonType(String id, ConfigurationNode config) {
this.modelName = modelId;
}

public void updateBalloonPosition(@NotNull CosmeticUser user, float deltaTime) {
Entity entity = Bukkit.getEntity(user.getUniqueId());
UserBalloonManager userBalloonManager = user.getBalloonManager();

if (entity == null || userBalloonManager == null) return;
if (user.isInWardrobe()) return;

if (!userBalloonManager.getModelEntity().isValid()) {
user.respawnBalloon();
return;
}

Location playerLocation = entity.getLocation();
Location balloonLocation = userBalloonManager.getLocation();
Vector equilibriumPosition = playerLocation.toVector().add(HAND_OFFSET);

Vector displacement = balloonLocation.toVector().subtract(equilibriumPosition);

// tension force (Hooke's Law: F = -k * x), with a little difference,
// there is no tension if the balloon is closer to the player than the
// equilibrium position
Vector tensionForce;
if (displacement.length() > balloonOffset.length()) {
tensionForce = displacement.clone().multiply(-STRING_ELASTICITY_FACTOR);
} else {
tensionForce = new Vector(0, 0, 0);
}

Vector netForce = BALLOON_BUOYANCY_FORCE.clone().add(tensionForce);

// acceleration (Newton’s Second Law: F = ma => a = F / m)
Vector acceleration = netForce.multiply(1 / BALLOON_MASS);

Vector velocity = userBalloonManager.getVelocity()
.clone()
.multiply(AIR_DAMPING) // damping
.add(acceleration.multiply(deltaTime)); // apply acceleration

Vector newBalloonPosition = balloonLocation.toVector().add(velocity.clone().multiply(deltaTime));

Location newLocation = newBalloonPosition.toLocation(entity.getWorld());

// rotate the balloon to face the player
Vector direction = playerLocation.toVector().subtract(newLocation.toVector());
double dx = direction.getX();
double dz = direction.getZ();
float newYaw = (float) Math.toDegrees(Math.atan2(dz, dx)) - 90F;
if (newYaw < -180F) newYaw += 360F;
if (newYaw >= 180F) newYaw -= 360F;
newLocation.setYaw(newYaw);

//if (Settings.isBalloonHeadForward()) newLocation.setPitch(0);

List<Player> viewer = HMCCPacketManager.getViewers(entity.getLocation());

if (entity.getLocation().getWorld() != userBalloonManager.getLocation().getWorld()) {
userBalloonManager.getModelEntity().teleport(newLocation);
HMCCPacketManager.sendTeleportPacket(userBalloonManager.getPufferfishBalloonId(), newLocation, false, viewer);
return;
}

userBalloonManager.setLocation(newLocation);
userBalloonManager.setVelocity(velocity);

// balloon rotation physics
double angleBetweenTensionAndHorizontal = Math.atan2(tensionForce.getY(), tensionForce.length());

// torque = r * sin(angle) * F
double torqueMagnitude = BALLOON_RADIUS * tensionForce.length() * Math.sin(angleBetweenTensionAndHorizontal);
double restoringTorque = -RESTORE_CONSTANT * userBalloonManager.getXRotation();
double totalTorque = torqueMagnitude + restoringTorque;

// torque = moment of inertia * angular acceleration
double angularAcceleration = totalTorque / BALLOON_MOMENT_OF_INERTIA;

double angularVelocity = userBalloonManager.getAngularPitchVelocity();
angularVelocity += angularAcceleration * deltaTime;
angularVelocity *= 0.8; // damping

//double maxTiltAngle = Math.toRadians(30);
double newRotation = userBalloonManager.getXRotation() + angularVelocity * deltaTime;
//newRotation = Math.max(-maxTiltAngle, Math.min(maxTiltAngle, newRotation));

userBalloonManager.setAngularPitchVelocity(angularVelocity);
userBalloonManager.setXRotation(newRotation);

HMCCPacketManager.sendTeleportPacket(userBalloonManager.getPufferfishBalloonId(), newLocation, false, viewer);
HMCCPacketManager.sendLeashPacket(userBalloonManager.getPufferfishBalloonId(), entity.getEntityId(), viewer);
if (user.isHidden()) {
userBalloonManager.getPufferfish().hidePufferfish();
return;
}
if (!user.isHidden() && showLead) {
List<Player> sendTo = userBalloonManager.getPufferfish().refreshViewers(newLocation);
if (sendTo.isEmpty()) return;
user.getBalloonManager().getPufferfish().spawnPufferfish(newLocation, sendTo);
}
}

@Override
public void update(@NotNull CosmeticUser user) {
if (Settings.isBalloonPhysics()) {
// use the physics-based update instead !
return;
}

Entity entity = Bukkit.getEntity(user.getUniqueId());
UserBalloonManager userBalloonManager = user.getBalloonManager();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,11 @@ public void onPlayerMove(PlayerMoveEvent event) {
return;
}
user.updateCosmetic(CosmeticSlot.BACKPACK);
user.updateCosmetic(CosmeticSlot.BALLOON);

// If balloon physics is enabled, this has its own task
if (!Settings.isBalloonPhysics()) {
user.updateCosmetic(CosmeticSlot.BALLOON);
}
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.hibiscusmc.hmccosmetics.task;

import com.hibiscusmc.hmccosmetics.HMCCosmeticsPlugin;
import com.hibiscusmc.hmccosmetics.cosmetic.Cosmetic;
import com.hibiscusmc.hmccosmetics.cosmetic.CosmeticSlot;
import com.hibiscusmc.hmccosmetics.cosmetic.types.CosmeticBalloonType;
import com.hibiscusmc.hmccosmetics.user.CosmeticUser;
import com.hibiscusmc.hmccosmetics.user.CosmeticUsers;
import org.bukkit.Bukkit;

public final class BalloonTickTask implements Runnable {
public static final BalloonTickTask INSTANCE = new BalloonTickTask();

private volatile long lastCall = -1;
private int taskId = -1;

private BalloonTickTask() {
}

@Override
public void run() {
// compute elapsed time since the last time this task was called,
// if it's the first time, we assume 50ms, we avoid zero to prevent
// division by zero in the physics calculations
final long now = System.currentTimeMillis();
final long elapsedMillis = lastCall == -1 ? 50 : (now - lastCall);
final float deltaTime = elapsedMillis / 1000.0f;
lastCall = now;

// todo: maybe we could iterate over CosmeticHolders instead? Could allow ticking over mannequins and others
for (CosmeticUser user : CosmeticUsers.values()) {
Cosmetic balloon = user.getCosmetic(CosmeticSlot.BALLOON);

if (balloon == null) {
continue;
}

((CosmeticBalloonType) balloon).updateBalloonPosition(user, deltaTime);
}
}

public void schedule() {
if (taskId == -1) {
// TODO: we want asynchronousss
taskId = Bukkit.getScheduler()
.runTaskTimer(HMCCosmeticsPlugin.getInstance(), this, 0, 1)
.getTaskId();
}
}

public void stop() {
if (taskId != -1) {
Bukkit.getScheduler().cancelTask(taskId);
taskId = -1;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public class UserBalloonManager {
private UserBalloonPufferfish pufferfish;
private final ArmorStand modelEntity;

private double angularPitchVelocity = 0.0f;

public UserBalloonManager(CosmeticUser user, @NotNull Location location) {
this.user = user;
this.pufferfish = new UserBalloonPufferfish(user.getUniqueId(), NMSHandlers.getHandler().getUtilHandler().getNextEntityId(), UUID.randomUUID());
Expand Down Expand Up @@ -188,6 +190,32 @@ public void setVelocity(Vector vector) {
this.getModelEntity().setVelocity(vector);
}

public double getAngularPitchVelocity() {
return angularPitchVelocity;
}

public void setAngularPitchVelocity(double angularPitchVelocity) {
this.angularPitchVelocity = angularPitchVelocity;
}

public double getXRotation() {
if (balloonType == BalloonType.ITEM) {
final ArmorStand armorStand = (ArmorStand) getModelEntity();
return armorStand.getHeadPose().getX();
}
return 0;
}

public void setXRotation(final double xRotation) {
// the more velocity a balloon has, the more tilted it is,
// and since the yaw is already set, we just compute the
// pitch
if (balloonType == BalloonType.ITEM) {
final ArmorStand armorStand = (ArmorStand) getModelEntity();
armorStand.setHeadPose(armorStand.getHeadPose().setX(xRotation));
}
}

public void sendRemoveLeashPacket(List<Player> viewer) {
HMCCPacketManager.sendLeashPacket(getPufferfishBalloonId(), -1, viewer);
}
Expand Down
3 changes: 3 additions & 0 deletions common/src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ cosmetic-settings:
# If the plugin should set the pitch of balloons to always be 0, to prevent players looking up affecting the balloon.
# This only applies to models that have a "head" section to them. THIS DOES NOT IMPACT THE REST OF THE ENTITY.
balloon-head-unmoving: false
# If the plugin should use realistic physics for balloons (Experimental feature) May be more resource
# intensive for the server)
balloon-physics: false
# how the balloon should be positioned relative to the player
balloon-offset:
x: 0.5
Expand Down