├── src └── main │ ├── resources │ ├── games.yml │ ├── background.png │ ├── plugin.yml │ ├── dab_animation.txt │ └── chips.yml │ └── java │ └── me │ └── matsubara │ └── roulette │ ├── util │ ├── map │ │ ├── Text.java │ │ └── MapBuilder.java │ ├── config │ │ ├── ConfigChanges.java │ │ └── ConfigFileUtils.java │ ├── Shape.java │ ├── ColorUtils.java │ └── ParrotUtils.java │ ├── manager │ ├── data │ │ ├── MapRecord.java │ │ ├── PlayerResult.java │ │ ├── RouletteSession.java │ │ └── PlayerStats.java │ ├── WinnerManager.java │ ├── InputManager.java │ ├── ChipManager.java │ └── StandManager.java │ ├── model │ └── stand │ │ ├── animator │ │ ├── AnimatorCache.java │ │ ├── Frame.java │ │ └── ArmorStandAnimator.java │ │ ├── ModelLocation.java │ │ ├── data │ │ ├── ItemSlot.java │ │ └── Pose.java │ │ └── StandSettings.java │ ├── npc │ ├── SpawnCustomizer.java │ ├── modifier │ │ ├── TeleportModifier.java │ │ ├── EquipmentModifier.java │ │ ├── AnimationModifier.java │ │ ├── RotationModifier.java │ │ ├── NPCModifier.java │ │ ├── VisibilityModifier.java │ │ └── MetadataModifier.java │ └── NPC.java │ ├── game │ ├── data │ │ ├── PlayerInput.java │ │ ├── Chip.java │ │ ├── WinData.java │ │ ├── SlotType.java │ │ └── CustomizationGroup.java │ ├── GameState.java │ ├── GameType.java │ ├── GameRule.java │ └── state │ │ ├── Starting.java │ │ └── Spinning.java │ ├── hook │ ├── RExtension.java │ ├── economy │ │ ├── EconomyExtension.java │ │ ├── DummyEconomyExtension.java │ │ ├── VaultExtension.java │ │ └── PlayerPointsExtension.java │ ├── EssXExtension.java │ └── PAPIExtension.java │ ├── event │ ├── RouletteEvent.java │ ├── RouletteStartEvent.java │ ├── RouletteEndEvent.java │ ├── LastRouletteSpinEvent.java │ └── PlayerRouletteEnterEvent.java │ ├── listener │ ├── PlayerQuit.java │ ├── npc │ │ ├── NPCSpawn.java │ │ └── PlayerNPCInteract.java │ ├── EntityDamageByEntity.java │ ├── InventoryClick.java │ ├── InventoryClose.java │ └── protocol │ │ └── SteerVehicle.java │ ├── animation │ ├── MoneyAnimation.java │ └── DabAnimation.java │ ├── file │ ├── config │ │ └── ConfigValue.java │ └── Config.java │ └── gui │ ├── RouletteGUI.java │ ├── TableGUI.java │ ├── ConfirmGUI.java │ ├── data │ └── SessionsGUI.java │ ├── ChipGUI.java │ └── GameChipGUI.java ├── .gitignore └── README.md /src/main/resources/games.yml: -------------------------------------------------------------------------------- 1 | games: {} -------------------------------------------------------------------------------- /src/main/resources/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aematsubara/Roulette/HEAD/src/main/resources/background.png -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/util/map/Text.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.util.map; 2 | 3 | public record Text(int x, int y, String message) { 4 | 5 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/manager/data/MapRecord.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.manager.data; 2 | 3 | import java.util.UUID; 4 | 5 | public record MapRecord(int mapId, UUID playerUUID, UUID sessionUUID) { 6 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/model/stand/animator/AnimatorCache.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.model.stand.animator; 2 | 3 | public record AnimatorCache(Frame[] frames, int length, boolean interpolate) { 4 | 5 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/npc/SpawnCustomizer.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.npc; 2 | 3 | import org.bukkit.entity.Player; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public interface SpawnCustomizer { 7 | 8 | void handleSpawn(@NotNull NPC npc, @NotNull Player player); 9 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/game/data/PlayerInput.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.game.data; 2 | 3 | public record PlayerInput(float sideways, float forward, boolean jump, boolean dismount, boolean sprint) { 4 | public static final PlayerInput ZERO = new PlayerInput(0.0f, 0.0f, false, false, false); 5 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/hook/RExtension.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.hook; 2 | 3 | import me.matsubara.roulette.RoulettePlugin; 4 | 5 | public interface RExtension { 6 | 7 | T init(RoulettePlugin plugin); 8 | 9 | default void onEnable(@SuppressWarnings("unused") RoulettePlugin plugin) { 10 | // This method should be used to register event listeners. 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/game/data/Chip.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.game.data; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | import java.util.List; 6 | 7 | public record Chip(String name, @Nullable String displayName, @Nullable List lore, String url, double price) { 8 | 9 | public Chip(String name, String url, double price) { 10 | this(name, null, null, url, price); 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/event/RouletteEvent.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.event; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.game.Game; 5 | import org.bukkit.event.Event; 6 | 7 | @Getter 8 | public abstract class RouletteEvent extends Event { 9 | 10 | protected final Game game; 11 | 12 | public RouletteEvent(Game game) { 13 | super(false); 14 | this.game = game; 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/model/stand/ModelLocation.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.model.stand; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.bukkit.Location; 6 | 7 | @Getter 8 | @Setter 9 | public class ModelLocation { 10 | 11 | private final StandSettings settings; 12 | private Location location; 13 | 14 | public ModelLocation(StandSettings settings, Location location) { 15 | this.settings = settings; 16 | this.location = location; 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/hook/economy/EconomyExtension.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.hook.economy; 2 | 3 | import me.matsubara.roulette.hook.RExtension; 4 | import org.bukkit.OfflinePlayer; 5 | 6 | public interface EconomyExtension extends RExtension { 7 | 8 | DummyEconomyExtension DUMMY = new DummyEconomyExtension(); 9 | 10 | boolean isEnabled(); 11 | 12 | double getBalance(OfflinePlayer player); 13 | 14 | boolean has(OfflinePlayer player, double money); 15 | 16 | String format(double money); 17 | 18 | boolean deposit(OfflinePlayer player, double money); 19 | 20 | boolean withdraw(OfflinePlayer player, double money); 21 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/event/RouletteStartEvent.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.event; 2 | 3 | import me.matsubara.roulette.game.Game; 4 | import org.bukkit.event.HandlerList; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public class RouletteStartEvent extends RouletteEvent { 8 | 9 | private static final HandlerList handlers = new HandlerList(); 10 | 11 | public RouletteStartEvent(Game game) { 12 | super(game); 13 | } 14 | 15 | @Override 16 | public @NotNull HandlerList getHandlers() { 17 | return handlers; 18 | } 19 | 20 | @SuppressWarnings("unused") 21 | public static HandlerList getHandlerList() { 22 | return handlers; 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: Roulette 2 | version: '${project.version}' 3 | main: me.matsubara.roulette.RoulettePlugin 4 | api-version: 1.17 5 | author: Matsubara 6 | description: '${project.description}' 7 | depend: 8 | - packetevents 9 | - Vault 10 | softdepend: 11 | - ProtocolLib 12 | - ProtocolSupport 13 | - ViaVersion 14 | - ViaBackwards 15 | - ViaRewind 16 | - Geyser-Spigot 17 | - Essentials 18 | - PlaceholderAPI 19 | - Multiverse-Core 20 | - CMI 21 | - PlayerPoints 22 | libraries: 23 | - org.xerial:sqlite-jdbc:3.46.1.0 24 | - org.apache.commons:commons-lang3:3.13.0 25 | - commons-io:commons-io:2.14.0 26 | 27 | commands: 28 | roulette: 29 | description: main command. 30 | usage: / -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/game/GameState.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.game; 2 | 3 | public enum GameState { 4 | IDLE, // Idle state. 5 | STARTING, // Start cooldown initialized while waiting for more players. 6 | SELECTING, // Game started, players must place their bets. 7 | SPINNING, // No more bets, the wheel starts spinning. 8 | ENDING; // The game is over, the winners (if any) are announced. 9 | 10 | public boolean isIdle() { 11 | return this == IDLE; 12 | } 13 | 14 | public boolean isStarting() { 15 | return this == STARTING; 16 | } 17 | 18 | public boolean isSelecting() { 19 | return this == SELECTING; 20 | } 21 | 22 | public boolean isSpinning() { 23 | return this == SPINNING; 24 | } 25 | 26 | public boolean isEnding() { 27 | return this == ENDING; 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/npc/modifier/TeleportModifier.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.npc.modifier; 2 | 3 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityTeleport; 4 | import io.github.retrooper.packetevents.util.SpigotConversionUtil; 5 | import me.matsubara.roulette.npc.NPC; 6 | import org.bukkit.Location; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | public class TeleportModifier extends NPCModifier { 10 | 11 | public TeleportModifier(NPC npc) { 12 | super(npc); 13 | } 14 | 15 | public TeleportModifier queueTeleport(@NotNull Location location, boolean onGround) { 16 | queueInstantly((npc, player) -> { 17 | return new WrapperPlayServerEntityTeleport(npc.getEntityId(), SpigotConversionUtil.fromBukkitLocation(location), onGround); 18 | }); 19 | return this; 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/manager/data/PlayerResult.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.manager.data; 2 | 3 | import me.matsubara.roulette.game.data.Slot; 4 | import me.matsubara.roulette.game.data.WinData; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.util.UUID; 8 | 9 | public record PlayerResult(RouletteSession session, 10 | UUID playerUUID, 11 | UUID sessionUUID, 12 | @Nullable WinData.WinType win, 13 | double money, 14 | Slot slot) { 15 | 16 | public PlayerResult(RouletteSession session, UUID playerUUID, @Nullable WinData.WinType win, double money, Slot slot) { 17 | this(session, playerUUID, session.sessionUUID(), win, money, slot); 18 | } 19 | 20 | public boolean won() { 21 | return win != null; 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/hook/EssXExtension.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.hook; 2 | 3 | import com.earth2me.essentials.Essentials; 4 | import com.earth2me.essentials.User; 5 | import me.matsubara.roulette.RoulettePlugin; 6 | import org.bukkit.entity.Player; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | public final class EssXExtension implements RExtension { 10 | 11 | private Essentials essentials; 12 | 13 | @Override 14 | public EssXExtension init(@NotNull RoulettePlugin plugin) { 15 | essentials = (Essentials) plugin.getServer().getPluginManager().getPlugin("Essentials"); 16 | return this; 17 | } 18 | 19 | public boolean isVanished(Player player) { 20 | if (essentials == null) return false; 21 | 22 | User user = essentials.getUser(player); 23 | return user != null && (user.isVanished() || user.isHidden()); 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/game/GameType.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.game; 2 | 3 | import me.matsubara.roulette.file.Config; 4 | import me.matsubara.roulette.file.config.ConfigValue; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.Locale; 8 | 9 | public enum GameType { 10 | AMERICAN(Config.TYPE_AMERICAN), // 0 & 00. 11 | EUROPEAN(Config.TYPE_EUROPEAN); // Single 0. 12 | 13 | private final ConfigValue value; 14 | 15 | GameType(ConfigValue value) { 16 | this.value = value; 17 | } 18 | 19 | public boolean isAmerican() { 20 | return this == AMERICAN; 21 | } 22 | 23 | public boolean isEuropean() { 24 | return this == EUROPEAN; 25 | } 26 | 27 | public @NotNull String getName() { 28 | return value.asStringTranslated(); 29 | } 30 | 31 | public @NotNull String getFileName() { 32 | return (name() + "_roulette.yml").toLowerCase(Locale.ROOT); 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/npc/modifier/EquipmentModifier.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.npc.modifier; 2 | 3 | import com.github.retrooper.packetevents.protocol.player.Equipment; 4 | import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityEquipment; 6 | import io.github.retrooper.packetevents.util.SpigotConversionUtil; 7 | import me.matsubara.roulette.npc.NPC; 8 | import org.bukkit.inventory.ItemStack; 9 | 10 | import java.util.List; 11 | 12 | 13 | public class EquipmentModifier extends NPCModifier { 14 | 15 | public EquipmentModifier(NPC npc) { 16 | super(npc); 17 | } 18 | 19 | public EquipmentModifier queue(EquipmentSlot itemSlot, ItemStack equipment) { 20 | queueInstantly((npc, player) -> new WrapperPlayServerEntityEquipment(npc.getEntityId(), 21 | List.of(new Equipment(itemSlot, SpigotConversionUtil.fromBukkitItemStack(equipment))))); 22 | return this; 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/event/RouletteEndEvent.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.event; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.game.Game; 5 | import me.matsubara.roulette.game.data.Slot; 6 | import org.bukkit.entity.Player; 7 | import org.bukkit.event.HandlerList; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.Set; 11 | 12 | @Getter 13 | public class RouletteEndEvent extends RouletteEvent { 14 | 15 | private final Set winners; 16 | private final Slot winnerSlot; 17 | 18 | private static final HandlerList handlers = new HandlerList(); 19 | 20 | public RouletteEndEvent(Game game, Set winners, Slot winnerSlot) { 21 | super(game); 22 | this.winners = winners; 23 | this.winnerSlot = winnerSlot; 24 | } 25 | 26 | @Override 27 | public @NotNull HandlerList getHandlers() { 28 | return handlers; 29 | } 30 | 31 | @SuppressWarnings("unused") 32 | public static HandlerList getHandlerList() { 33 | return handlers; 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/resources/dab_animation.txt: -------------------------------------------------------------------------------- 1 | interpolate 2 | length 41 3 | frame 0 4 | Armorstand_Position 0.0 0.0 0.0 0.0 5 | Armorstand_Right_Leg 0 0 0 6 | Armorstand_Left_Leg 0 0 0 7 | Armorstand_Left_Arm 0 0 350 8 | Armorstand_Right_Arm 352 346 7 9 | Armorstand_Head 0 0 0 10 | frame 10 11 | Armorstand_Position 0.0 0.0 0.0 0.0 12 | Armorstand_Right_Leg 0 0 0 13 | Armorstand_Left_Leg 0 0 0 14 | Armorstand_Left_Arm 0 0 350 15 | Armorstand_Right_Arm 352 346 7 16 | Armorstand_Head 0 0 0 17 | frame 20 18 | Armorstand_Position 0.0 0.0 0.0 0.0 19 | Armorstand_Right_Leg 0 0 0 20 | Armorstand_Left_Leg 0 0 0 21 | Armorstand_Left_Arm 0 0 224 22 | Armorstand_Right_Arm 272 352 33 23 | Armorstand_Head 38 59 0 24 | frame 30 25 | Armorstand_Position 0.0 0.0 0.0 0.0 26 | Armorstand_Right_Leg 0 0 0 27 | Armorstand_Left_Leg 0 0 0 28 | Armorstand_Left_Arm 0 0 224 29 | Armorstand_Right_Arm 272 352 33 30 | Armorstand_Head 38 59 0 31 | frame 40 32 | Armorstand_Position 0.0 0.0 0.0 0.0 33 | Armorstand_Right_Leg 0 0 0 34 | Armorstand_Left_Leg 0 0 0 35 | Armorstand_Left_Arm 0 0 350 36 | Armorstand_Right_Arm 352 346 7 37 | Armorstand_Head 0 0 0 -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/event/LastRouletteSpinEvent.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.event; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import me.matsubara.roulette.game.Game; 6 | import me.matsubara.roulette.game.data.Slot; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.event.HandlerList; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | @Setter 13 | @Getter 14 | public class LastRouletteSpinEvent extends RouletteEvent { 15 | 16 | private Slot winnerSlot; 17 | private final @Nullable Player forcedBy; 18 | 19 | private static final HandlerList handlers = new HandlerList(); 20 | 21 | public LastRouletteSpinEvent(Game game, Slot winnerSlot, @Nullable Player forcedBy) { 22 | super(game); 23 | this.winnerSlot = winnerSlot; 24 | this.forcedBy = forcedBy; 25 | } 26 | 27 | @Override 28 | public @NotNull HandlerList getHandlers() { 29 | return handlers; 30 | } 31 | 32 | @SuppressWarnings("unused") 33 | public static HandlerList getHandlerList() { 34 | return handlers; 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/listener/PlayerQuit.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.listener; 2 | 3 | import me.matsubara.roulette.RoulettePlugin; 4 | import me.matsubara.roulette.game.Game; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.EventHandler; 7 | import org.bukkit.event.EventPriority; 8 | import org.bukkit.event.Listener; 9 | import org.bukkit.event.player.PlayerQuitEvent; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | public final class PlayerQuit implements Listener { 13 | 14 | private final RoulettePlugin plugin; 15 | 16 | public PlayerQuit(RoulettePlugin plugin) { 17 | this.plugin = plugin; 18 | } 19 | 20 | @EventHandler(priority = EventPriority.MONITOR) 21 | public void onPlayerQuit(@NotNull PlayerQuitEvent event) { 22 | Player player = event.getPlayer(); 23 | 24 | // Remove player from input. 25 | plugin.getInputManager().remove(player); 26 | 27 | // Remove player from game when leaving. 28 | Game game = plugin.getGameManager().getGameByPlayer(player); 29 | if (game != null) game.removeCompletely(player); 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/hook/economy/DummyEconomyExtension.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.hook.economy; 2 | 3 | import me.matsubara.roulette.RoulettePlugin; 4 | import org.bukkit.OfflinePlayer; 5 | 6 | public class DummyEconomyExtension implements EconomyExtension { 7 | 8 | protected DummyEconomyExtension() { 9 | 10 | } 11 | 12 | @Override 13 | public boolean isEnabled() { 14 | return false; 15 | } 16 | 17 | @Override 18 | public double getBalance(OfflinePlayer player) { 19 | return 0.0d; 20 | } 21 | 22 | @Override 23 | public boolean has(OfflinePlayer player, double money) { 24 | return false; 25 | } 26 | 27 | @Override 28 | public String format(double money) { 29 | return "$" + money; 30 | } 31 | 32 | @Override 33 | public boolean deposit(OfflinePlayer player, double money) { 34 | return false; 35 | } 36 | 37 | @Override 38 | public boolean withdraw(OfflinePlayer player, double money) { 39 | return false; 40 | } 41 | 42 | @Override 43 | public DummyEconomyExtension init(RoulettePlugin plugin) { 44 | return this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/event/PlayerRouletteEnterEvent.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.event; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.game.Game; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.HandlerList; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | public class PlayerRouletteEnterEvent extends RouletteEvent implements Cancellable { 11 | 12 | private final @Getter Player player; 13 | private boolean cancelled; 14 | 15 | private static final HandlerList handlers = new HandlerList(); 16 | 17 | public PlayerRouletteEnterEvent(Game game, Player player) { 18 | super(game); 19 | this.player = player; 20 | } 21 | 22 | @Override 23 | public boolean isCancelled() { 24 | return cancelled; 25 | } 26 | 27 | @Override 28 | public void setCancelled(boolean cancelled) { 29 | this.cancelled = cancelled; 30 | } 31 | 32 | @Override 33 | public @NotNull HandlerList getHandlers() { 34 | return handlers; 35 | } 36 | 37 | @SuppressWarnings("unused") 38 | public static HandlerList getHandlerList() { 39 | return handlers; 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/npc/modifier/AnimationModifier.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.npc.modifier; 2 | 3 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityAnimation; 4 | import me.matsubara.roulette.npc.NPC; 5 | import org.jetbrains.annotations.ApiStatus; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | /** 9 | * A modifier for various animations a npc can play. 10 | */ 11 | public class AnimationModifier extends NPCModifier { 12 | 13 | /** 14 | * Creates a new modifier. 15 | * 16 | * @param npc The npc this modifier is for. 17 | * @see NPC#animation() 18 | */ 19 | @ApiStatus.Internal 20 | public AnimationModifier(@NotNull NPC npc) { 21 | super(npc); 22 | } 23 | 24 | /** 25 | * Queues the animation to be played. 26 | * 27 | * @param animation The animation to play. 28 | * @return The same instance of this class, for chaining. 29 | */ 30 | @NotNull 31 | public AnimationModifier queue(WrapperPlayServerEntityAnimation.EntityAnimationType animation) { 32 | super.queueInstantly((targetNpc, target) -> new WrapperPlayServerEntityAnimation(targetNpc.getEntityId(), animation)); 33 | return this; 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/util/config/ConfigChanges.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.util.config; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import org.bukkit.configuration.file.FileConfiguration; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.function.Consumer; 10 | import java.util.function.Predicate; 11 | 12 | @SuppressWarnings("unused") 13 | public record ConfigChanges(Predicate predicate, 14 | Consumer consumer, 15 | int newVersion) { 16 | 17 | public static @NotNull Builder builder() { 18 | return new Builder(); 19 | } 20 | 21 | public static class Builder { 22 | 23 | private final List changes = new ArrayList<>(); 24 | 25 | public Builder addChange(Predicate predicate, 26 | Consumer consumer, 27 | int newVersion) { 28 | changes.add(new ConfigChanges(predicate, consumer, newVersion)); 29 | return this; 30 | } 31 | 32 | public List build() { 33 | return ImmutableList.copyOf(changes); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/game/data/WinData.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.game.data; 2 | 3 | import lombok.Getter; 4 | import org.bukkit.entity.Player; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | public record WinData(Player player, int betIndex, WinType winType) { 8 | 9 | @Getter 10 | public enum WinType { 11 | NORMAL("normal"), 12 | LA_PARTAGE("partage"), 13 | EN_PRISON("prison"), 14 | SURRENDER("surrender"); 15 | 16 | private final String shortName; 17 | 18 | WinType(String shortName) { 19 | this.shortName = shortName; 20 | } 21 | 22 | public boolean isNormalWin() { 23 | return this == NORMAL; 24 | } 25 | 26 | public boolean isLaPartageWin() { 27 | return this == LA_PARTAGE; 28 | } 29 | 30 | public boolean isEnPrisonWin() { 31 | return this == EN_PRISON; 32 | } 33 | 34 | public boolean isSurrenderWin() { 35 | return this == SURRENDER; 36 | } 37 | 38 | public static @Nullable WinType getByShortName(String shortName) { 39 | for (WinType type : values()) { 40 | if (type.getShortName().equals(shortName)) return type; 41 | } 42 | return null; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/game/GameRule.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.game; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Locale; 6 | 7 | @SuppressWarnings("unused") 8 | public enum GameRule { 9 | /** 10 | * Once a single zero is spun, the even-money bet will immediately be divided by two. 11 | * This way, 50% of the bet will be recovered to the player, while the other 50% will be surrendered to the house. 12 | */ 13 | LA_PARTAGE(14), 14 | /** 15 | * Gives players an opportunity to recover their even-money stakes after the zero is spun. 16 | * The stake remains on the losing even-money bet for the next spin, and if the player wins the second time around, 17 | * they get their original stake back. 18 | */ 19 | EN_PRISON(15), 20 | /** 21 | * It is basically the same as La Partage as it is enforced whenever 0 or 00 win, 22 | * in which case the player “surrenders” half of their original stake and retains the rest. 23 | */ 24 | SURRENDER(16); 25 | 26 | private final int guiIndex; 27 | 28 | GameRule(int guiIndex) { 29 | this.guiIndex = guiIndex; 30 | } 31 | 32 | public int getGUIIndex() { 33 | return guiIndex; 34 | } 35 | 36 | public boolean isLaPartage() { 37 | return this == LA_PARTAGE; 38 | } 39 | 40 | public boolean isEnPrison() { 41 | return this == EN_PRISON; 42 | } 43 | 44 | public boolean isSurrender() { 45 | return this == SURRENDER; 46 | } 47 | 48 | public @NotNull String toConfigPath() { 49 | return name().toLowerCase(Locale.ROOT).replace("_", "-"); 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/animation/MoneyAnimation.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.animation; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.game.Game; 5 | import me.matsubara.roulette.model.stand.PacketStand; 6 | import org.bukkit.Location; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.scheduler.BukkitRunnable; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.Set; 12 | 13 | @Getter 14 | public final class MoneyAnimation extends BukkitRunnable { 15 | 16 | private final Game game; 17 | private final PacketStand moneySlot; 18 | private final Set seeing; 19 | 20 | private boolean goUp; 21 | private int count; 22 | private int toFinish; 23 | 24 | public MoneyAnimation(@NotNull Game game) { 25 | this.game = game; 26 | this.moneySlot = game.getModel().getStandByName("MONEY_SLOT"); 27 | this.seeing = game.getSeeingPlayers(); 28 | 29 | this.goUp = true; 30 | this.count = 0; 31 | this.toFinish = 0; 32 | 33 | game.setMoneyAnimation(this); 34 | runTaskTimerAsynchronously(game.getPlugin(), 1L, 1L); 35 | } 36 | 37 | @Override 38 | public void run() { 39 | if (count == 10) { 40 | count = 0; 41 | goUp = !goUp; 42 | toFinish++; 43 | if (toFinish == 4) { 44 | game.setMoneyAnimation(null); 45 | cancel(); 46 | return; 47 | } 48 | } 49 | 50 | Location to = moneySlot.getLocation().add(0.0d, goUp ? 0.01d : -0.01d, 0.0d); 51 | moneySlot.teleport(seeing, to); 52 | 53 | count++; 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/model/stand/data/ItemSlot.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.model.stand.data; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.model.stand.PacketStand; 5 | import me.matsubara.roulette.util.Reflection; 6 | import org.bukkit.inventory.EquipmentSlot; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import java.lang.invoke.MethodHandle; 10 | 11 | @Getter 12 | public enum ItemSlot { 13 | MAINHAND(EquipmentSlot.HAND, "main-hand"), 14 | OFFHAND(EquipmentSlot.OFF_HAND, "off-hand"), 15 | FEET(EquipmentSlot.FEET, "boots"), 16 | LEGS(EquipmentSlot.LEGS, "leggings"), 17 | CHEST(EquipmentSlot.CHEST, "chestplate"), 18 | HEAD(EquipmentSlot.HEAD, "helmet"); 19 | 20 | private final EquipmentSlot slot; 21 | private final String path; 22 | private final Object nmsObject; 23 | 24 | ItemSlot(EquipmentSlot slot, String path) { 25 | this.slot = slot; 26 | this.path = path; 27 | this.nmsObject = initNMSObject(); 28 | } 29 | 30 | private @Nullable Object initNMSObject() { 31 | try { 32 | char[] alphabet = "abcdefghijklmnopqrstuvwxyz".toCharArray(); 33 | MethodHandle field = Reflection.getField( 34 | PacketStand.ENUM_ITEM_SLOT, 35 | PacketStand.ENUM_ITEM_SLOT, 36 | String.valueOf(alphabet[ordinal()]), 37 | true, 38 | name()); 39 | 40 | if (field != null) { 41 | return field.invoke(); 42 | } 43 | } catch (Throwable exception) { 44 | exception.printStackTrace(); 45 | } 46 | return null; 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/listener/npc/NPCSpawn.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.listener.npc; 2 | 3 | import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; 4 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityAnimation; 5 | import me.matsubara.roulette.game.Game; 6 | import me.matsubara.roulette.game.GameState; 7 | import me.matsubara.roulette.npc.NPC; 8 | import me.matsubara.roulette.npc.SpawnCustomizer; 9 | import me.matsubara.roulette.npc.modifier.MetadataModifier; 10 | import org.bukkit.entity.Player; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | public class NPCSpawn implements SpawnCustomizer { 14 | 15 | private final Game game; 16 | 17 | public NPCSpawn(Game game) { 18 | this.game = game; 19 | } 20 | 21 | @Override 22 | public void handleSpawn(@NotNull NPC npc, @NotNull Player player) { 23 | npc.lookAtDefaultLocation(player); 24 | 25 | // Set item (ball) in the main hand. 26 | GameState state = game.getState(); 27 | if (!state.isSpinning() && !state.isEnding()) { 28 | npc.equipment().queue(EquipmentSlot.MAIN_HAND, game.getPlugin().getBall()).send(player); 29 | } 30 | 31 | // Swing the main hand to rotate the body with the head rotation. 32 | npc.animation().queue(WrapperPlayServerEntityAnimation.EntityAnimationType.SWING_MAIN_ARM).send(player); 33 | 34 | MetadataModifier metadata = npc.metadata(); 35 | 36 | // Show skin layers. 37 | metadata.queue(MetadataModifier.EntityMetadata.SKIN_LAYERS, true); 38 | 39 | // Toggle parrot visibility. 40 | npc.toggleParrotVisibility(metadata); 41 | 42 | // Send metadata after creating the data. 43 | metadata.send(player); 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/model/stand/data/Pose.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.model.stand.data; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.model.stand.PacketStand; 5 | import me.matsubara.roulette.model.stand.StandSettings; 6 | import org.bukkit.util.EulerAngle; 7 | 8 | import java.util.function.BiConsumer; 9 | import java.util.function.Function; 10 | 11 | public enum Pose { 12 | HEAD(16, PacketStand.DWO_HEAD_POSE, StandSettings::getHeadPose, StandSettings::setHeadPose), 13 | BODY(17, PacketStand.DWO_BODY_POSE, StandSettings::getBodyPose, StandSettings::setBodyPose), 14 | LEFT_ARM(18, PacketStand.DWO_LEFT_ARM_POSE, StandSettings::getLeftArmPose, StandSettings::setLeftArmPose), 15 | RIGHT_ARM(19, PacketStand.DWO_RIGHT_ARM_POSE, StandSettings::getRightArmPose, StandSettings::setRightArmPose), 16 | LEFT_LEG(20, PacketStand.DWO_LEFT_LEG_POSE, StandSettings::getLeftLegPose, StandSettings::setLeftLegPose), 17 | RIGHT_LEG(21, PacketStand.DWO_RIGHT_LEG_POSE, StandSettings::getRightLegPose, StandSettings::setRightLegPose); 18 | 19 | private final @Getter int index; 20 | private final @Getter Object dwo; 21 | private final Function getter; 22 | private final BiConsumer setter; 23 | 24 | Pose(int index, Object dwo, Function getter, BiConsumer setter) { 25 | this.index = index; 26 | this.dwo = dwo; 27 | this.getter = getter; 28 | this.setter = setter; 29 | } 30 | 31 | public EulerAngle get(StandSettings settings) { 32 | return getter.apply(settings); 33 | } 34 | 35 | public void set(StandSettings settings, EulerAngle angle) { 36 | setter.accept(settings, angle); 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/listener/EntityDamageByEntity.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.listener; 2 | 3 | import me.matsubara.roulette.RoulettePlugin; 4 | import me.matsubara.roulette.file.Config; 5 | import me.matsubara.roulette.file.Messages; 6 | import org.bukkit.entity.Entity; 7 | import org.bukkit.entity.EntityType; 8 | import org.bukkit.entity.Player; 9 | import org.bukkit.event.EventHandler; 10 | import org.bukkit.event.EventPriority; 11 | import org.bukkit.event.Listener; 12 | import org.bukkit.event.entity.EntityDamageByEntityEvent; 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | public final class EntityDamageByEntity implements Listener { 16 | 17 | private final RoulettePlugin plugin; 18 | 19 | public EntityDamageByEntity(RoulettePlugin plugin) { 20 | this.plugin = plugin; 21 | } 22 | 23 | @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 24 | public void onEntityDamageByEntity(@NotNull EntityDamageByEntityEvent event) { 25 | Entity damager = event.getDamager(); 26 | 27 | if (damager.getType() == EntityType.FIREWORK) { 28 | // Disable firework damage when @instant-explode is true. 29 | if (event.getDamager().hasMetadata("isRoulette")) event.setCancelled(true); 30 | return; 31 | } 32 | 33 | if (Config.HIT_ON_GAME.asBool()) return; 34 | 35 | if (isInGame(damager) || (isInGame(event.getEntity()) && damager.getType() == EntityType.PLAYER)) { 36 | plugin.getMessages().send(damager, Messages.Message.CAN_NOT_HIT); 37 | event.setCancelled(true); 38 | } 39 | } 40 | 41 | private boolean isInGame(@NotNull Entity entity) { 42 | return entity instanceof Player player && plugin.getGameManager().isPlaying(player); 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/npc/modifier/RotationModifier.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.npc.modifier; 2 | 3 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityHeadLook; 4 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityRelativeMoveAndRotation; 5 | import me.matsubara.roulette.npc.NPC; 6 | import org.bukkit.Location; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | public class RotationModifier extends NPCModifier { 10 | 11 | public RotationModifier(NPC npc) { 12 | super(npc); 13 | } 14 | 15 | @NotNull 16 | public RotationModifier queueLookAt(@NotNull Location location) { 17 | Location npcLocation = npc.getLocation(); 18 | double xDifference = location.getX() - npcLocation.getX(); 19 | double yDifference = location.getY() - npcLocation.getY(); 20 | double zDifference = location.getZ() - npcLocation.getZ(); 21 | 22 | double distance = Math.sqrt(Math.pow(xDifference, 2) + Math.pow(yDifference, 2) + Math.pow(zDifference, 2)); 23 | 24 | float yaw = (float) (-Math.atan2(xDifference, zDifference) / Math.PI * 180.0d); 25 | float pitch = (float) (-Math.asin(yDifference / distance) / Math.PI * 180.0d); 26 | return queueBodyRotation(yaw < 0 ? yaw + 360 : yaw, pitch); 27 | } 28 | 29 | public RotationModifier queueBodyRotation(float yaw, float pitch) { 30 | queueInstantly((npc, player) -> new WrapperPlayServerEntityHeadLook(npc.getEntityId(), yaw)); 31 | queueInstantly((npc, player) -> new WrapperPlayServerEntityRelativeMoveAndRotation( 32 | npc.getEntityId(), 33 | 0.0d, 34 | 0.0d, 35 | 0.0d, 36 | yaw, 37 | pitch, 38 | true)); 39 | return this; 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/npc/modifier/NPCModifier.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.npc.modifier; 2 | 3 | import com.github.retrooper.packetevents.PacketEvents; 4 | import com.github.retrooper.packetevents.wrapper.PacketWrapper; 5 | import lombok.Getter; 6 | import me.matsubara.roulette.npc.NPC; 7 | import org.bukkit.entity.Player; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.concurrent.CopyOnWriteArrayList; 13 | 14 | public class NPCModifier { 15 | 16 | protected final NPC npc; 17 | protected final @Getter List packetContainers = new CopyOnWriteArrayList<>(); 18 | 19 | public NPCModifier(NPC npc) { 20 | this.npc = npc; 21 | } 22 | 23 | protected void queuePacket(LazyPacket packet) { 24 | packetContainers.add(packet); 25 | } 26 | 27 | protected void queueInstantly(@NotNull LazyPacket packet) { 28 | PacketWrapper> container = packet.provide(npc, null); 29 | packetContainers.add((npc, player) -> container); 30 | } 31 | 32 | public void send() { 33 | send(npc.getSeeingPlayers()); 34 | } 35 | 36 | public void send(@NotNull Iterable players) { 37 | players.forEach(player -> { 38 | for (LazyPacket packet : packetContainers) { 39 | Object channel = PacketEvents.getAPI().getPlayerManager().getChannel(player); 40 | PacketEvents.getAPI().getProtocolManager().sendPacket(channel, packet.provide(npc, player)); 41 | } 42 | }); 43 | packetContainers.clear(); 44 | } 45 | 46 | public void send(Player... players) { 47 | send(Arrays.asList(players)); 48 | } 49 | 50 | public interface LazyPacket { 51 | 52 | PacketWrapper> provide(NPC npc, Player player); 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/manager/data/RouletteSession.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.manager.data; 2 | 3 | import me.matsubara.roulette.game.GameType; 4 | import me.matsubara.roulette.game.data.Bet; 5 | import me.matsubara.roulette.game.data.Chip; 6 | import me.matsubara.roulette.game.data.Slot; 7 | import me.matsubara.roulette.game.data.WinData; 8 | import org.bukkit.entity.Player; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.*; 12 | 13 | public record RouletteSession(UUID sessionUUID, String name, List results, Slot slot, GameType type, 14 | long timestamp) { 15 | 16 | public RouletteSession(UUID sessionUUID, String name, Slot slot, GameType type, long timestamp) { 17 | this(sessionUUID, name, new ArrayList<>(), slot, type, timestamp); 18 | } 19 | 20 | public RouletteSession(UUID sessionUUID, String name, Slot slot, GameType type, long timestamp, Collection> bets) { 21 | this(sessionUUID, name, new ArrayList<>(), slot, type, timestamp); 22 | results.addAll(createResultsFromBets(bets)); 23 | } 24 | 25 | private @NotNull List createResultsFromBets(@NotNull Collection> bets) { 26 | List results = new ArrayList<>(); 27 | 28 | // Now, we save the players. 29 | for (Map.Entry entry : bets) { 30 | Player player = entry.getKey(); 31 | Bet bet = entry.getValue(); 32 | 33 | WinData data = bet.getWinData(); 34 | WinData.WinType win = data != null ? data.winType() : null; 35 | 36 | Slot slot = bet.getSlot(); 37 | Chip chip = bet.getChip(); 38 | 39 | // We save the original money. 40 | double money = chip.price(); 41 | 42 | results.add(new PlayerResult(this, player.getUniqueId(), win, money, slot)); 43 | } 44 | 45 | return results; 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/file/config/ConfigValue.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.file.config; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import me.matsubara.roulette.RoulettePlugin; 6 | import me.matsubara.roulette.util.PluginUtils; 7 | import org.bukkit.plugin.java.JavaPlugin; 8 | import org.bukkit.util.NumberConversions; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | 14 | @Getter 15 | @Setter 16 | public class ConfigValue { 17 | 18 | private final RoulettePlugin plugin = JavaPlugin.getPlugin(RoulettePlugin.class); 19 | private final String path; 20 | private Object value; 21 | 22 | public static final Set ALL_VALUES = new HashSet<>(); 23 | 24 | public ConfigValue(@NotNull String path) { 25 | this.path = path; 26 | reloadValue(); 27 | ALL_VALUES.add(this); 28 | } 29 | 30 | public void reloadValue() { 31 | value = plugin.getConfig().get(path); 32 | } 33 | 34 | public T getValue(@NotNull Class type) { 35 | return type.cast(value); 36 | } 37 | 38 | public T getValue(@NotNull Class type, T defaultValue) { 39 | return type.cast(value != null ? value : defaultValue); 40 | } 41 | 42 | public String asString() { 43 | return getValue(String.class); 44 | } 45 | 46 | public String asString(String defaultString) { 47 | return getValue(String.class, defaultString); 48 | } 49 | 50 | public @NotNull String asStringTranslated() { 51 | return PluginUtils.translate(asString()); 52 | } 53 | 54 | public boolean asBool() { 55 | return getValue(Boolean.class); 56 | } 57 | 58 | public int asInt() { 59 | return NumberConversions.toInt(value); 60 | } 61 | 62 | public double asDouble() { 63 | return NumberConversions.toDouble(value); 64 | } 65 | 66 | public long asLong() { 67 | return NumberConversions.toLong(value); 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/game/data/SlotType.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.game.data; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.game.Game; 5 | import me.matsubara.roulette.game.GameType; 6 | import org.apache.commons.lang3.ArrayUtils; 7 | import org.bukkit.entity.Player; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.function.BiPredicate; 13 | 14 | @Getter 15 | public enum SlotType { 16 | COLOR(true, (slot, type) -> slot.isRed() || slot.isBlack()), 17 | ODD_EVEN(true, (slot, type) -> slot.isOdd() || slot.isEven()), 18 | HIGH_LOW(true, (slot, type) -> slot.isLow() || slot.isHigh()), 19 | COLUMN(true, (slot, type) -> slot.isColumn()), 20 | DOZEN(true, (slot, type) -> slot.isDozen()); 21 | 22 | private final boolean conflict; 23 | private final BiPredicate condition; 24 | 25 | 26 | public Slot[] getSlots(GameType type) { 27 | return Arrays.stream(Slot.values()) 28 | .filter(slot -> condition.test(slot, type)) 29 | .toArray(Slot[]::new); 30 | } 31 | 32 | SlotType(boolean conflict, BiPredicate condition) { 33 | this.conflict = conflict; 34 | this.condition = condition; 35 | } 36 | 37 | public static @Nullable SlotType hasConflict(Game game, Player player, Slot slot, boolean ignoreCurrent) { 38 | for (SlotType type : values()) { 39 | if (!type.conflict) continue; 40 | 41 | GameType gameType = game.getType(); 42 | if (!ArrayUtils.contains(type.getSlots(gameType), slot)) continue; 43 | 44 | List bets = game.getBets(player); 45 | if (bets.isEmpty()) return null; 46 | 47 | for (Bet bet : bets) { 48 | if (!bet.hasSlot()) continue; 49 | if (!ArrayUtils.contains(type.getSlots(gameType), bet.getSlot())) continue; 50 | if (!ignoreCurrent && bet.equals(game.getSelectedBet(player))) continue; 51 | return type; 52 | } 53 | } 54 | return null; 55 | } 56 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Roulette 2 | The classic casino game now in Minecraft, no need for texture packs. 3 | 4 | # Roulette (Premium) 5 | Roulette Premium is an enhanced version of Roulette, available at [BuiltByBit](https://builtbybit.com/resources/roulette.77010/) and [Polymart](https://polymart.org/product/8472/roulette). 6 | 7 | Full message from the developer on the [Discord server](https://discord.gg/MHZZHfMzgA): 8 | >Hey @Roulette! 9 | > 10 | >I want to share some important news about Roulette and where the plugin is heading. 11 | > 12 | >I first released Roulette for free on Spigot back in July 2020. Since then, it’s grown way more than I expected, over 21,000 downloads and 76 reviews. Many servers have been using it for years, and I’m really grateful for all the support. 13 | > 14 | >But here’s the thing: I don’t work as a developer outside of Minecraft plugins, and my time is limited. The only way I can keep putting time into updates and improvements is by making Roulette a paid plugin. 15 | > 16 | >[So, from now on, Roulette will be available as a premium resource on BuiltByBit (aka. MC-Market)](https://builtbybit.com/resources/roulette.77010/) 17 | > 18 | >The premium version already includes several new features (and more coming soon): 19 | >- All roulette bets implemented, including split, street, corner, six line, basket, and red snake. 20 | >- Croupier tipping system with PlaceholderAPI support and custom thank-you messages. 21 | >- Quick bet movement with a simple menu to switch slots. 22 | >- Voucher rewards, only given when winnings exceed a set threshold. 23 | >- Force winning slot option at any stage of the game. 24 | >- `/roulette move ` command (self explanatory). 25 | >- And much, much more! 26 | > 27 | >[Click here to watch the video of the new features](https://www.youtube.com/watch?v=8WFTR2HpYRA) 28 | > 29 | >I know this is a big change, especially since Roulette has been free for so long. But this is the only way I can realistically keep the project alive and keep improving it. Thanks for understanding, and thanks for sticking with me through all these years. 30 | > 31 | >(Yes, you're allowed to be angry with me) 32 | > 33 | > I’ll keep updating Roulette on [Spigot](https://www.spigotmc.org/resources/roulette.82197/), but only supporting newer Minecraft versions; no more features. 34 | -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/model/stand/animator/Frame.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.model.stand.animator; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.bukkit.util.EulerAngle; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | @Getter 9 | @Setter 10 | public class Frame { 11 | 12 | private int id; 13 | private float x; 14 | private float y; 15 | private float z; 16 | private float rotation; 17 | 18 | private EulerAngle head; 19 | private EulerAngle leftArm; 20 | private EulerAngle rightArm; 21 | private EulerAngle leftLeg; 22 | private EulerAngle rightLeg; 23 | 24 | public Frame multiply(float multiplier, int id) { 25 | Frame frame = new Frame(); 26 | frame.id = id; 27 | 28 | frame.x *= multiplier; 29 | frame.y *= multiplier; 30 | frame.z *= multiplier; 31 | frame.rotation *= multiplier; 32 | 33 | frame.head = multiply(head, multiplier); 34 | frame.leftArm = multiply(leftArm, multiplier); 35 | frame.rightArm = multiply(rightArm, multiplier); 36 | frame.leftLeg = multiply(leftLeg, multiplier); 37 | frame.rightLeg = multiply(rightLeg, multiplier); 38 | 39 | return frame; 40 | } 41 | 42 | private @NotNull EulerAngle multiply(@NotNull EulerAngle angle, float multiplier) { 43 | return new EulerAngle(angle.getX() * multiplier, angle.getY() * multiplier, angle.getZ() * multiplier); 44 | } 45 | 46 | public Frame add(@NotNull Frame add, int id) { 47 | Frame frame = new Frame(); 48 | frame.id = id; 49 | 50 | frame.x += add.x; 51 | frame.y += add.y; 52 | frame.z += add.z; 53 | frame.rotation += add.rotation; 54 | 55 | frame.head = add(head, add.head); 56 | frame.leftArm = add(leftArm, add.leftArm); 57 | frame.rightArm = add(rightArm, add.rightArm); 58 | frame.leftLeg = add(leftLeg, add.leftLeg); 59 | frame.rightLeg = add(rightLeg, add.rightLeg); 60 | 61 | return frame; 62 | } 63 | 64 | private @NotNull EulerAngle add(@NotNull EulerAngle angle, @NotNull EulerAngle add) { 65 | return new EulerAngle(angle.getX() + add.getX(), angle.getY() + add.getY(), angle.getZ() + add.getZ()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/game/state/Starting.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.game.state; 2 | 3 | 4 | import com.cryptomorin.xseries.XSound; 5 | import com.google.common.base.Predicates; 6 | import me.matsubara.roulette.RoulettePlugin; 7 | import me.matsubara.roulette.event.RouletteStartEvent; 8 | import me.matsubara.roulette.file.Config; 9 | import me.matsubara.roulette.file.Messages; 10 | import me.matsubara.roulette.game.Game; 11 | import me.matsubara.roulette.game.GameState; 12 | import me.matsubara.roulette.game.data.Bet; 13 | import org.bukkit.scheduler.BukkitRunnable; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | public final class Starting extends BukkitRunnable { 17 | 18 | private final RoulettePlugin plugin; 19 | private final Game game; 20 | private final XSound.Record countdownSound = XSound.parse(Config.SOUND_COUNTDOWN.asString()); 21 | 22 | private int seconds; 23 | 24 | public Starting(RoulettePlugin plugin, @NotNull Game game) { 25 | this.plugin = plugin; 26 | this.game = game; 27 | 28 | this.seconds = game.getStartTime(); 29 | game.setState(GameState.STARTING); 30 | } 31 | 32 | @Override 33 | public void run() { 34 | if (seconds == 0) { 35 | Selecting selecting = new Selecting(plugin, game); 36 | 37 | if (game.getAllBets().stream().anyMatch(Predicates.not(Bet::isEnPrison))) { 38 | // If there's at least 1 bet that is NOT in prison, then we want to start the selecting task. 39 | game.setSelecting(selecting); 40 | selecting.runTaskTimer(plugin, 1L, 1L); 41 | 42 | RouletteStartEvent startEvent = new RouletteStartEvent(game); 43 | plugin.getServer().getPluginManager().callEvent(startEvent); 44 | } else { 45 | // All bets in this game are in prison, go straight to spinning. 46 | selecting.startSpinningTask(); 47 | } 48 | 49 | cancel(); 50 | return; 51 | } 52 | 53 | if (seconds % 5 == 0 || seconds <= 3) { 54 | // Play countdown sound. 55 | game.playSound(countdownSound); 56 | 57 | // Send countdown. 58 | game.broadcast(Messages.Message.STARTING, line -> line.replace("%seconds%", String.valueOf(seconds))); 59 | } 60 | 61 | seconds--; 62 | } 63 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/util/Shape.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.util; 2 | 3 | import com.google.common.base.Strings; 4 | import lombok.Getter; 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.Material; 7 | import org.bukkit.NamespacedKey; 8 | import org.bukkit.inventory.ItemStack; 9 | import org.bukkit.inventory.Recipe; 10 | import org.bukkit.inventory.ShapedRecipe; 11 | import org.bukkit.inventory.ShapelessRecipe; 12 | import org.bukkit.plugin.java.JavaPlugin; 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | import java.util.List; 16 | 17 | public final class Shape { 18 | 19 | private final JavaPlugin plugin; 20 | private final String name; 21 | private final boolean shaped; 22 | private final List ingredients; 23 | private final List shape; 24 | private final @Getter ItemStack result; 25 | private final @Getter NamespacedKey key; 26 | 27 | public Shape(JavaPlugin plugin, String name, boolean shaped, @NotNull List ingredients, List shape, ItemStack result) { 28 | this.plugin = plugin; 29 | this.name = name; 30 | this.shaped = shaped; 31 | this.ingredients = ingredients; 32 | this.shape = shape; 33 | this.result = result; 34 | this.key = new NamespacedKey(plugin, name); 35 | if (!ingredients.isEmpty() && (!shaped || !shape.isEmpty())) { 36 | register(result); 37 | } 38 | } 39 | 40 | public void register(ItemStack item) { 41 | Recipe recipe = shaped ? new ShapedRecipe(key, item) : new ShapelessRecipe(key, item); 42 | 43 | // Set shaped recipe. 44 | if (shaped) ((ShapedRecipe) recipe).shape(shape.toArray(new String[0])); 45 | 46 | for (String ingredient : ingredients) { 47 | if (Strings.isNullOrEmpty(ingredient)) continue; 48 | String[] split = PluginUtils.splitData(ingredient); 49 | 50 | Material type = Material.valueOf(split[0]); 51 | char key = split.length > 1 ? split[1].charAt(0) : ' '; 52 | 53 | if (shaped) { 54 | // Empty space is used for AIR. 55 | if (key == ' ') continue; 56 | ((ShapedRecipe) recipe).setIngredient(key, type); 57 | } else { 58 | ((ShapelessRecipe) recipe).addIngredient(type); 59 | } 60 | } 61 | 62 | if (!Bukkit.addRecipe(recipe)) { 63 | plugin.getLogger().warning("The recipe couldn't be created for {" + name + "}!"); 64 | } else { 65 | plugin.getLogger().info("Registered new recipe {" + name + "}!"); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/hook/economy/VaultExtension.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.hook.economy; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.RoulettePlugin; 5 | import net.milkbowl.vault.economy.Economy; 6 | import net.milkbowl.vault.economy.EconomyResponse; 7 | import org.bukkit.OfflinePlayer; 8 | import org.bukkit.plugin.Plugin; 9 | import org.bukkit.plugin.RegisteredServiceProvider; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | public class VaultExtension implements EconomyExtension { 13 | 14 | private Economy economy; 15 | private RoulettePlugin plugin; 16 | private @Getter boolean enabled; 17 | 18 | @Override 19 | public VaultExtension init(@NotNull RoulettePlugin plugin) { 20 | this.plugin = plugin; 21 | 22 | RegisteredServiceProvider provider = plugin.getServer().getServicesManager().getRegistration(Economy.class); 23 | if (provider == null) { 24 | plugin.getLogger().severe("Vault found, you need to install an economy provider (EssentialsX, CMI, etc...), disabling economy support..."); 25 | return null; 26 | } 27 | 28 | Plugin providerPlugin = provider.getPlugin(); 29 | plugin.getLogger().info("Using {" + providerPlugin.getDescription().getFullName() + "} as the economy provider."); 30 | 31 | economy = provider.getProvider(); 32 | enabled = true; 33 | return this; 34 | } 35 | 36 | @Override 37 | public double getBalance(OfflinePlayer player) { 38 | return economy.getBalance(player); 39 | } 40 | 41 | @Override 42 | public boolean has(OfflinePlayer player, double money) { 43 | return economy.has(player, money); 44 | } 45 | 46 | @Override 47 | public String format(double money) { 48 | return economy.format(money); 49 | } 50 | 51 | @Override 52 | public boolean deposit(OfflinePlayer player, double money) { 53 | EconomyResponse response = economy.depositPlayer(player, money); 54 | if (response.transactionSuccess()) return true; 55 | 56 | plugin.getLogger().warning("It wasn't possible to deposit {" + format(money) + "} to {" + player.getName() + "}."); 57 | return false; 58 | } 59 | 60 | @Override 61 | public boolean withdraw(OfflinePlayer player, double money) { 62 | EconomyResponse response = economy.withdrawPlayer(player, money); 63 | if (response.transactionSuccess()) return true; 64 | 65 | plugin.getLogger().warning("It wasn't possible to withdraw {" + format(money) + "} to {" + player.getName() + "}."); 66 | return false; 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/hook/economy/PlayerPointsExtension.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.hook.economy; 2 | 3 | import me.matsubara.roulette.RoulettePlugin; 4 | import org.black_ixx.playerpoints.PlayerPoints; 5 | import org.black_ixx.playerpoints.PlayerPointsAPI; 6 | import org.black_ixx.playerpoints.manager.LocaleManager; 7 | import org.black_ixx.playerpoints.util.PointsUtils; 8 | import org.bukkit.OfflinePlayer; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | public class PlayerPointsExtension implements EconomyExtension { 12 | 13 | private RoulettePlugin plugin; 14 | private PlayerPointsAPI api; 15 | private LocaleManager localeManager; 16 | 17 | @Override 18 | public PlayerPointsExtension init(@NotNull RoulettePlugin plugin) { 19 | this.plugin = plugin; 20 | PlayerPoints instance = PlayerPoints.getInstance(); 21 | this.api = instance.getAPI(); 22 | this.localeManager = instance.getManager(LocaleManager.class); 23 | plugin.getLogger().info("Using {" + instance.getDescription().getFullName() + "} as the economy provider."); 24 | return this; 25 | } 26 | 27 | @Override 28 | public boolean isEnabled() { 29 | return true; 30 | } 31 | 32 | @Override 33 | public double getBalance(@NotNull OfflinePlayer player) { 34 | return api.look(player.getUniqueId()); 35 | } 36 | 37 | @Override 38 | public boolean has(@NotNull OfflinePlayer player, double money) { 39 | return api.look(player.getUniqueId()) >= money; 40 | } 41 | 42 | @Override 43 | public String format(double money) { 44 | return PointsUtils.formatPoints((int) money) + " " + (money == 1 ? currencyNameSingular() : currencyNamePlural()); 45 | } 46 | 47 | @Override 48 | public boolean deposit(@NotNull OfflinePlayer player, double money) { 49 | if (api.give(player.getUniqueId(), (int) money)) return true; 50 | 51 | plugin.getLogger().warning("It wasn't possible to deposit {" + format(money) + "} to {" + player.getName() + "}."); 52 | return false; 53 | } 54 | 55 | @Override 56 | public boolean withdraw(@NotNull OfflinePlayer player, double money) { 57 | if (api.take(player.getUniqueId(), (int) money)) return true; 58 | 59 | plugin.getLogger().warning("It wasn't possible to withdraw {" + format(money) + "} to {" + player.getName() + "}."); 60 | return false; 61 | } 62 | 63 | public String currencyNamePlural() { 64 | return localeManager.getLocaleMessage("currency-plural"); 65 | } 66 | 67 | public String currencyNameSingular() { 68 | return localeManager.getLocaleMessage("currency-singular"); 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/resources/chips.yml: -------------------------------------------------------------------------------- 1 | # Here you can create all the chips for the games. 2 | # 3 | # For the URL, you can go to https://minecraft-heads.com/, select a custom head, 4 | # scroll all the way down and click on "Copy" on the last box, under the name Minecraft-URL. 5 | # 6 | # If you want to use custom heads, you can go to https://mineskin.org/, select the file of the skin and upload it, 7 | # when it's done it'll show you some weird strings, go to the one called "Texture URL" and copy it, 8 | # you need to remove "http://textures.minecraft.net/texture/", we just need the last string. 9 | # 10 | # You can set differents names and lore for all chips, like this: 11 | # 12 | # PURPLE: 13 | # display-name: "&5Purple &lCHIP" 14 | # lore: 15 | # - "&7This is a purple chip, worth 5$" 16 | # url: "a9d68bcb0ef127f58fd93b85a7b0a47a3cff5ca688a86cc5e4bdd110e7ec717c" 17 | # price: 50.0 18 | # 19 | # Otherwise, it'll use the name and lore of the config.yml. 20 | 21 | chips: 22 | PURPLE: 23 | url: "a9d68bcb0ef127f58fd93b85a7b0a47a3cff5ca688a86cc5e4bdd110e7ec717c" 24 | price: 50.0 25 | LIGHT_GRAY: 26 | url: "d1a32fc038ad9aa1da99f6b82ae4a1919213925e7454f3343aa0be2e13da6f29" 27 | price: 100.0 28 | LIGHT_BLUE: 29 | url: "f752f74c0f16789ba5b2c8c8a8ef2a16fdd949906ab6ae46dee653023158abc8" 30 | price: 200.0 31 | GREEN: 32 | url: "a7de569743d1e7c080ad0f590d539aa573a0af4ba7be23ec8d793269fe927088" 33 | price: 500.0 34 | GRAY: 35 | url: "255b5a5b27fe57479087dfe9d99fdcf789a4baa1eb2690a5b5e032bec92c7e50" 36 | price: 1000.0 37 | CYAN: 38 | url: "21bdf484634572adb0c0db1b4fe3307592f5f6af969657cc8c87ec66153a88df" 39 | price: 2000.0 40 | BROWN: 41 | url: "da71835e6ffd654b43896e7c4d852abdd09996d15ec800f6e558beb2728567db" 42 | price: 5000.0 43 | BLUE: 44 | url: "13444e6349cb549e2e800c23ca206d2360f129e45ca3130d587ff97507a46462" 45 | price: 7500.0 46 | BLACK: 47 | url: "33bbac230ba0bd526f83c5100fcfd5cd08dab35a9844416e6d4127fa54618926" 48 | price: 10000.0 49 | YELLOW: 50 | url: "f780bf5789551bf051e74e36052706dd133fcd58ceadeed9ce2afd84c7c9a209" 51 | price: 15000.0 52 | WHITE: 53 | url: "f8dab06bb7d08660aea14be770e9044338753502a5575e2c56f845f8658aee65" 54 | price: 20000.0 55 | PINK: 56 | url: "c828726733fb4e7448a139b8fa4e32b913253301c04fcedd493ddf833f3311a1" 57 | price: 25000.0 58 | ORANGE: 59 | url: "1a316db9820b3e29c556318065d610231fbb5abce97dcb9b35ebf62ff3ba5d41" 60 | price: 30000.0 61 | RED: 62 | url: "b0458d58c030cfabd8b19e4944bbe2860f6617a77ec6c9488593e2a473db6758" 63 | price: 40000.0 64 | MAGENTA: 65 | url: "caa685ebb65420ba6eb808c715978e10edb1414ca783e0c702fef4af74aed113" 66 | price: 50000.0 67 | LIME: 68 | url: "8bad26ccb4f8937ffcfe9e0ef49b7cea7672c98f68ead7f8f901a22349029301" 69 | price: 100000.0 -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/gui/RouletteGUI.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.gui; 2 | 3 | import me.matsubara.roulette.RoulettePlugin; 4 | import me.matsubara.roulette.game.Game; 5 | import me.matsubara.roulette.util.ItemBuilder; 6 | import org.bukkit.entity.Player; 7 | import org.bukkit.event.inventory.InventoryClickEvent; 8 | import org.bukkit.inventory.InventoryHolder; 9 | import org.bukkit.inventory.ItemStack; 10 | import org.bukkit.inventory.meta.ItemMeta; 11 | import org.bukkit.persistence.PersistentDataType; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.util.Objects; 15 | 16 | public abstract class RouletteGUI implements InventoryHolder { 17 | 18 | protected final RoulettePlugin plugin; 19 | protected final String name; 20 | protected final boolean paginated; 21 | protected int currentPage; 22 | protected int pages; 23 | 24 | public RouletteGUI(RoulettePlugin plugin, String name) { 25 | this(plugin, name, false); 26 | } 27 | 28 | public RouletteGUI(RoulettePlugin plugin, String name, boolean paginated) { 29 | this.plugin = plugin; 30 | this.name = name; 31 | this.paginated = paginated; 32 | } 33 | 34 | public ItemBuilder getItem(String path) { 35 | return plugin.getItem(name + ".items." + path); 36 | } 37 | 38 | public abstract Game getGame(); 39 | 40 | public void handle(InventoryClickEvent event) { 41 | if (paginated) changePage(event); 42 | } 43 | 44 | public void updateInventory() { 45 | 46 | } 47 | 48 | public boolean isCustomItem(@NotNull ItemStack item, String name) { 49 | ItemMeta meta = item.getItemMeta(); 50 | return meta != null && Objects.equals(meta.getPersistentDataContainer().get(plugin.getItemIdKey(), PersistentDataType.STRING), name); 51 | } 52 | 53 | public int isCustomItem(ItemStack item, String first, String second) { 54 | return isCustomItem(item, first) ? -1 : isCustomItem(item, second) ? 1 : 0; 55 | } 56 | 57 | protected void closeInventory(@NotNull Player player) { 58 | runTask(player::closeInventory); 59 | } 60 | 61 | protected void runTask(Runnable runnable) { 62 | plugin.getServer().getScheduler().runTask(plugin, runnable); 63 | } 64 | 65 | protected void changePage(@NotNull InventoryClickEvent event) { 66 | ItemStack current = event.getCurrentItem(); 67 | 68 | int direction = isCustomItem(current, "previous", "next"); 69 | if (direction == 0) return; 70 | 71 | boolean shift = event.getClick().isShiftClick(); 72 | 73 | if (direction == -1) { 74 | currentPage = shift ? 0 : currentPage - 1; 75 | } else { 76 | currentPage = shift ? pages - 1 : currentPage + 1; 77 | } 78 | 79 | updateInventory(); 80 | } 81 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/hook/PAPIExtension.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.hook; 2 | 3 | import me.clip.placeholderapi.expansion.PlaceholderExpansion; 4 | import me.matsubara.roulette.RoulettePlugin; 5 | import me.matsubara.roulette.game.data.WinData; 6 | import me.matsubara.roulette.manager.data.PlayerStats; 7 | import org.bukkit.entity.Player; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.Locale; 11 | 12 | public final class PAPIExtension extends PlaceholderExpansion { 13 | 14 | private final RoulettePlugin plugin; 15 | 16 | private static final String NULL = "null"; 17 | 18 | public PAPIExtension(RoulettePlugin plugin) { 19 | this.plugin = plugin; 20 | 21 | // Register our placeholder. 22 | register(); 23 | } 24 | 25 | @Override 26 | public boolean persist() { 27 | return true; 28 | } 29 | 30 | @Override 31 | public boolean canRegister() { 32 | return true; 33 | } 34 | 35 | @Override 36 | public @NotNull String getIdentifier() { 37 | return "roulette"; 38 | } 39 | 40 | @Override 41 | public @NotNull String getAuthor() { 42 | return String.join(", ", plugin.getDescription().getAuthors()); 43 | } 44 | 45 | @Override 46 | public @NotNull String getVersion() { 47 | return plugin.getDescription().getVersion(); 48 | } 49 | 50 | @Override 51 | public @NotNull String onPlaceholderRequest(Player player, @NotNull String params) { 52 | // If somehow the player is null, return empty. 53 | if (player == null) return NULL; 54 | 55 | // Split the entire parameter with underscores. 56 | String[] values = params.split("_"); 57 | 58 | // We only need 1 or 2 parameters. 59 | if (values.length == 0 || values.length > 2) return NULL; 60 | 61 | PlayerStats stats = plugin.getDataManager().getStats(player); 62 | 63 | // if parameter doesn't contain underscores. 64 | if (values.length == 1) { 65 | return switch (params.toLowerCase(Locale.ROOT)) { 66 | // %roulette_win% → returns the number of wins. 67 | case "win" -> stats.getWins(null); 68 | // %roulette_total% → returns the total amount of money earned. 69 | case "total" -> stats.getTotalMoney(); 70 | // %roulette_max% → returns the highest amount of money earned. 71 | case "max" -> stats.getMaxMoney(); 72 | default -> NULL; 73 | } + ""; 74 | } 75 | 76 | // %roulette_win_{X=partage/prison/surrender}% → returns the number of @X wins. 77 | if (values[0].equals("win")) { 78 | WinData.WinType type = WinData.WinType.getByShortName(values[1].toLowerCase(Locale.ROOT)); 79 | return type != null ? stats.getWins(type) + "" : NULL; 80 | } 81 | 82 | return NULL; 83 | } 84 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/listener/InventoryClick.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.listener; 2 | 3 | import com.cryptomorin.xseries.XSound; 4 | import me.matsubara.roulette.RoulettePlugin; 5 | import me.matsubara.roulette.file.Config; 6 | import me.matsubara.roulette.gui.RouletteGUI; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.event.EventHandler; 9 | import org.bukkit.event.EventPriority; 10 | import org.bukkit.event.Listener; 11 | import org.bukkit.event.inventory.*; 12 | import org.bukkit.inventory.Inventory; 13 | import org.bukkit.inventory.InventoryHolder; 14 | import org.bukkit.inventory.ItemStack; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | public final class InventoryClick implements Listener { 18 | 19 | private final RoulettePlugin plugin; 20 | 21 | public InventoryClick(RoulettePlugin plugin) { 22 | this.plugin = plugin; 23 | } 24 | 25 | @EventHandler 26 | public void onInventoryDrag(@NotNull InventoryDragEvent event) { 27 | InventoryHolder holder = event.getInventory().getHolder(); 28 | if (!(holder instanceof RouletteGUI)) return; 29 | 30 | if (event.getRawSlots().stream().noneMatch(integer -> integer < holder.getInventory().getSize())) return; 31 | 32 | if (event.getRawSlots().size() == 1) { 33 | InventoryClickEvent clickEvent = new InventoryClickEvent( 34 | event.getView(), 35 | InventoryType.SlotType.CONTAINER, 36 | event.getRawSlots().iterator().next(), 37 | ClickType.LEFT, 38 | InventoryAction.PICKUP_ONE); 39 | plugin.getServer().getPluginManager().callEvent(clickEvent); 40 | } 41 | 42 | event.setCancelled(true); 43 | } 44 | 45 | @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 46 | public void onInventoryClick(@NotNull InventoryClickEvent event) { 47 | if (!(event.getWhoClicked() instanceof Player player)) return; 48 | 49 | Inventory inventory = event.getClickedInventory(); 50 | if (inventory == null) return; 51 | 52 | // Prevent moving items from player inventory to custom inventories by shift-clicking. 53 | InventoryHolder tempHolder = event.getView().getTopInventory().getHolder(); 54 | if (inventory.getType() == InventoryType.PLAYER 55 | && event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY 56 | && tempHolder instanceof RouletteGUI) { 57 | event.setCancelled(true); 58 | return; 59 | } 60 | 61 | InventoryHolder holder = inventory.getHolder(); 62 | if (!(holder instanceof RouletteGUI gui)) return; 63 | 64 | event.setCancelled(true); 65 | 66 | ItemStack item = event.getCurrentItem(); 67 | if (item == null || !item.hasItemMeta()) return; 68 | 69 | // Handle. 70 | XSound.play(Config.SOUND_CLICK.asString(), temp -> temp.forPlayers(player).play()); 71 | gui.handle(event); 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/game/data/CustomizationGroup.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.game.data; 2 | 3 | import me.matsubara.roulette.util.PluginUtils; 4 | import org.bukkit.Material; 5 | import org.bukkit.block.data.type.Slab; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | public record CustomizationGroup(Material block, Material slab) { 12 | 13 | // Items allowed for customization. 14 | public static final List GROUPS = new ArrayList<>(); 15 | 16 | // These should be ignored. 17 | private static final Set IGNORE_SLAB = Set.of( 18 | Material.PETRIFIED_OAK_SLAB, 19 | Material.STONE_SLAB, 20 | Material.GRANITE_SLAB, 21 | Material.ANDESITE_SLAB); 22 | 23 | // The index of the default customization (spruce). 24 | private static int DEFAULT_INDEX = 0; 25 | 26 | public static CustomizationGroup getByBlock(Material block) { 27 | for (CustomizationGroup group : GROUPS) { 28 | if (group.block() == block) return group; 29 | } 30 | return getDefaultCustomization(); 31 | } 32 | 33 | public static CustomizationGroup getDefaultCustomization() { 34 | return GROUPS.get(DEFAULT_INDEX); 35 | } 36 | 37 | static { 38 | GROUPS.add(new CustomizationGroup(Material.PURPUR_BLOCK, Material.PURPUR_SLAB)); 39 | GROUPS.add(new CustomizationGroup(Material.DEEPSLATE_TILES, Material.DEEPSLATE_TILE_SLAB)); 40 | if (PluginUtils.getOrNull(Material.class, "BAMBOO_BLOCK") != null) { 41 | GROUPS.add(new CustomizationGroup(Material.BAMBOO_BLOCK, Material.BAMBOO_SLAB)); 42 | } 43 | GROUPS.add(new CustomizationGroup(Material.BRICKS, Material.BRICK_SLAB)); 44 | GROUPS.add(new CustomizationGroup(Material.QUARTZ_BLOCK, Material.QUARTZ_SLAB)); 45 | 46 | for (Material slab : Material.values()) { 47 | // Ignore legacies. 48 | @SuppressWarnings("deprecation") boolean legacy = slab.isLegacy(); 49 | if (legacy) continue; 50 | 51 | // Ignore old oak slab. 52 | if (IGNORE_SLAB.contains(slab)) continue; 53 | 54 | // Ignore the already existing. 55 | if (GROUPS.stream() 56 | .map(CustomizationGroup::slab) 57 | .anyMatch(material -> material == slab)) continue; 58 | 59 | // Ignore non-slabs. 60 | try { 61 | if (!(slab.createBlockData() instanceof Slab)) continue; 62 | } catch (Exception exception) { 63 | continue; 64 | } 65 | 66 | String slabName = slab.name(); 67 | String blockName = slabName.replace("_SLAB", "") + (slabName.contains("_BRICK_") ? "S" : ""); 68 | 69 | Material block = PluginUtils.getOrNull(Material.class, blockName); 70 | Material planks = PluginUtils.getOrNull(Material.class, blockName + "_PLANKS"); 71 | if (block == null && planks == null) continue; 72 | 73 | Material origin = block != null ? block : planks; 74 | if (origin == Material.SPRUCE_PLANKS) DEFAULT_INDEX = GROUPS.size(); 75 | 76 | GROUPS.add(new CustomizationGroup(origin, slab)); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/manager/data/PlayerStats.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.manager.data; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import me.matsubara.roulette.RoulettePlugin; 6 | import me.matsubara.roulette.game.data.WinData; 7 | import org.bukkit.entity.Player; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | @Getter 15 | public class PlayerStats { 16 | 17 | private final RoulettePlugin plugin; 18 | private final DataManager manager; 19 | private final Player player; 20 | private long expireTime; 21 | 22 | private @Getter(AccessLevel.NONE) int wins; 23 | private double totalMoney; 24 | private double maxMoney; 25 | private final @Getter(AccessLevel.NONE) Map ruleWin = new HashMap<>(); 26 | 27 | private static final long EXPIRE_TIME = 30000L; 28 | 29 | public PlayerStats(@NotNull RoulettePlugin plugin, Player player) { 30 | this.plugin = plugin; 31 | this.manager = plugin.getDataManager(); 32 | this.player = player; 33 | resetStats(); 34 | } 35 | 36 | public boolean isExpired() { 37 | return System.currentTimeMillis() > expireTime; 38 | } 39 | 40 | public void resetStats() { 41 | this.expireTime = System.currentTimeMillis() + EXPIRE_TIME; 42 | this.wins = calculateWins(null); 43 | this.totalMoney = calculateTotalMoney(); 44 | this.maxMoney = calculateMaxMoney(); 45 | for (WinData.WinType type : WinData.WinType.values()) { 46 | this.ruleWin.put(type, calculateWins(type)); 47 | } 48 | } 49 | 50 | private int calculateWins(@Nullable WinData.WinType type) { 51 | int wins = 0; 52 | for (RouletteSession session : manager.getSessions()) { 53 | for (PlayerResult result : session.results()) { 54 | if (!result.playerUUID().equals(player.getUniqueId()) || !result.won()) continue; 55 | if (type != null && result.win() != type) continue; 56 | wins++; 57 | } 58 | } 59 | return wins; 60 | } 61 | 62 | private double calculateTotalMoney() { 63 | double totalMoney = 0.0d; 64 | for (RouletteSession session : manager.getSessions()) { 65 | for (PlayerResult result : session.results()) { 66 | if (!result.playerUUID().equals(player.getUniqueId()) || !result.won()) continue; 67 | totalMoney += plugin.getExpectedMoney(result); 68 | } 69 | } 70 | return totalMoney; 71 | } 72 | 73 | private double calculateMaxMoney() { 74 | double maxMoney = 0.0d; 75 | for (RouletteSession session : manager.getSessions()) { 76 | for (PlayerResult result : session.results()) { 77 | if (!result.playerUUID().equals(player.getUniqueId()) || !result.won()) continue; 78 | double money = plugin.getExpectedMoney(result); 79 | if (money > maxMoney) maxMoney = money; 80 | } 81 | } 82 | return maxMoney; 83 | } 84 | 85 | public int getWins(@Nullable WinData.WinType type) { 86 | return type != null ? ruleWin.getOrDefault(type, 0) : wins; 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/listener/npc/PlayerNPCInteract.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.listener.npc; 2 | 3 | import com.github.retrooper.packetevents.event.PacketListenerPriority; 4 | import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract; 5 | import com.github.retrooper.packetevents.event.simple.PacketPlayReceiveEvent; 6 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 7 | import com.github.retrooper.packetevents.protocol.player.InteractionHand; 8 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity; 9 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityAnimation; 10 | import me.matsubara.roulette.RoulettePlugin; 11 | import me.matsubara.roulette.game.Game; 12 | import me.matsubara.roulette.gui.GameGUI; 13 | import me.matsubara.roulette.npc.NPC; 14 | import org.bukkit.entity.Player; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | public final class PlayerNPCInteract extends SimplePacketListenerAbstract { 18 | 19 | private final RoulettePlugin plugin; 20 | 21 | public PlayerNPCInteract(RoulettePlugin plugin) { 22 | super(PacketListenerPriority.HIGHEST); 23 | this.plugin = plugin; 24 | } 25 | 26 | @Override 27 | public void onPacketPlayReceive(@NotNull PacketPlayReceiveEvent event) { 28 | if (event.getPacketType() != PacketType.Play.Client.INTERACT_ENTITY) return; 29 | if (!(event.getPlayer() instanceof Player player)) return; 30 | 31 | WrapperPlayClientInteractEntity wrapper = new WrapperPlayClientInteractEntity(event); 32 | 33 | InteractionHand hand = wrapper.getHand(); 34 | if (hand != InteractionHand.MAIN_HAND) return; 35 | 36 | int entityId = wrapper.getEntityId(); 37 | 38 | NPC npc = plugin.getNpcPool().getNPC(entityId).orElse(null); 39 | if (npc == null) return; 40 | 41 | // We only want to open the game editor IF the player right-clicks the NPC. 42 | WrapperPlayClientInteractEntity.InteractAction action = wrapper.getAction(); 43 | if (action != WrapperPlayClientInteractEntity.InteractAction.INTERACT) { 44 | // Imitate player hit. 45 | if (npc.isInsideFOV(player) 46 | && action == WrapperPlayClientInteractEntity.InteractAction.ATTACK) { 47 | npc.animation() 48 | .queue(WrapperPlayServerEntityAnimation.EntityAnimationType.SWING_MAIN_ARM) 49 | .send(player); 50 | } 51 | return; 52 | } 53 | 54 | // If, for some reason, the game is null, return. 55 | Game game = plugin.getGameManager().getGameByNPC(npc); 56 | if (game == null) return; 57 | 58 | // If the player is playing or not sneaking, return. 59 | if (game.isPlaying(player) || !player.isSneaking() || !canEdit(game, player)) return; 60 | 61 | // For some reason, the event gets called 4 times when right cliking an NPC. 62 | if (!(player.getOpenInventory().getTopInventory().getHolder() instanceof GameGUI)) { 63 | plugin.getServer().getScheduler().runTask(plugin, () -> new GameGUI(game, player)); 64 | } 65 | } 66 | 67 | private boolean canEdit(Game game, @NotNull Player player) { 68 | if (!player.hasPermission("roulette.edit")) return false; 69 | return game.getOwner().equals(player.getUniqueId()) || player.hasPermission("roulette.edit.others"); 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/model/stand/StandSettings.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.model.stand; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.experimental.Accessors; 6 | import me.matsubara.roulette.model.stand.data.ItemSlot; 7 | import me.matsubara.roulette.model.stand.data.Pose; 8 | import org.bukkit.inventory.ItemStack; 9 | import org.bukkit.util.EulerAngle; 10 | import org.bukkit.util.Vector; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | @Getter 19 | @Setter 20 | @Accessors(chain = true) 21 | public final class StandSettings implements Cloneable { 22 | 23 | // Model data. 24 | private String partName; 25 | private Vector offset; 26 | private float extraYaw; 27 | private List tags = new ArrayList<>(); 28 | 29 | // Entity settings. 30 | private boolean invisible; 31 | private boolean small; 32 | private boolean basePlate; 33 | private boolean arms; 34 | private boolean fire; 35 | private boolean marker; 36 | private boolean glow; 37 | private String customName; 38 | private boolean customNameVisible; 39 | private Vector scale; 40 | private int backgroundColor; 41 | 42 | // Entity poses. 43 | private EulerAngle headPose; 44 | private EulerAngle bodyPose; 45 | private EulerAngle leftArmPose; 46 | private EulerAngle rightArmPose; 47 | private EulerAngle leftLegPose; 48 | private EulerAngle rightLegPose; 49 | 50 | // Entity equipment. 51 | private Map equipment = new HashMap<>(); 52 | 53 | public StandSettings() { 54 | // Default settings. 55 | this.invisible = false; 56 | this.small = false; 57 | this.basePlate = true; 58 | this.arms = false; 59 | this.fire = false; 60 | this.marker = false; 61 | this.glow = false; 62 | this.partName = null; 63 | this.customName = null; 64 | this.customNameVisible = false; 65 | this.scale = new Vector(1.0f, 1.0f, 1.0f); 66 | this.backgroundColor = 1073741824; 67 | 68 | // Default poses. 69 | for (Pose pose : Pose.values()) { 70 | pose.set(this, EulerAngle.ZERO); 71 | } 72 | } 73 | 74 | @NotNull 75 | public StandSettings clone() { 76 | try { 77 | StandSettings copy = (StandSettings) super.clone(); 78 | 79 | // Clone tags list. 80 | copy.setTags(new ArrayList<>(tags)); 81 | 82 | // Clone equipment map. 83 | Map equipment = new HashMap<>(); 84 | for (Map.Entry entry : this.equipment.entrySet()) { 85 | if (entry == null) continue; 86 | 87 | ItemSlot slot = entry.getKey(); 88 | if (slot == null) continue; 89 | 90 | ItemStack item = entry.getValue(); 91 | if (item == null) continue; 92 | 93 | equipment.put(slot, item.clone()); 94 | } 95 | copy.setEquipment(equipment); 96 | 97 | // Clone angles. 98 | for (Pose pose : Pose.values()) { 99 | pose.set(copy, clonePose(pose.get(this))); 100 | } 101 | 102 | return copy; 103 | } catch (CloneNotSupportedException exception) { 104 | throw new Error(exception); 105 | } 106 | } 107 | 108 | private @NotNull EulerAngle clonePose(@NotNull EulerAngle pose) { 109 | return new EulerAngle(pose.getX(), pose.getY(), pose.getZ()); 110 | } 111 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/listener/InventoryClose.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.listener; 2 | 3 | import me.matsubara.roulette.RoulettePlugin; 4 | import me.matsubara.roulette.game.Game; 5 | import me.matsubara.roulette.game.data.Bet; 6 | import me.matsubara.roulette.gui.BetsGUI; 7 | import me.matsubara.roulette.gui.ChipGUI; 8 | import me.matsubara.roulette.gui.ConfirmGUI; 9 | import me.matsubara.roulette.gui.GameGUI; 10 | import org.bukkit.entity.Player; 11 | import org.bukkit.event.EventHandler; 12 | import org.bukkit.event.EventPriority; 13 | import org.bukkit.event.Listener; 14 | import org.bukkit.event.inventory.InventoryCloseEvent; 15 | import org.bukkit.inventory.Inventory; 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | public final class InventoryClose implements Listener { 19 | 20 | private final RoulettePlugin plugin; 21 | 22 | public InventoryClose(RoulettePlugin plugin) { 23 | this.plugin = plugin; 24 | } 25 | 26 | @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 27 | public void onInventoryClose(@NotNull InventoryCloseEvent event) { 28 | if (!(event.getPlayer() instanceof Player player)) return; 29 | 30 | Inventory inventory = event.getInventory(); 31 | 32 | if (inventory.getHolder() instanceof GameGUI gui) { 33 | int taskId = gui.getTaskId(); 34 | if (taskId != -1) plugin.getServer().getScheduler().cancelTask(taskId); 35 | return; 36 | } 37 | 38 | Game game = plugin.getGameManager().getGameByPlayer(player); 39 | if (game == null) return; 40 | 41 | Bet bet = game.getSelectedBet(player); 42 | if (bet == null) return; 43 | 44 | if (inventory.getHolder() instanceof ChipGUI chip) { 45 | // The player already selected a chip. 46 | if (bet.hasChip()) return; 47 | 48 | // We only want to force the first bet. 49 | if (chip.isNewBet()) return; 50 | 51 | // The time for selecting a chip is over. 52 | if (game.getState().isSpinning()) return; 53 | 54 | plugin.getServer().getScheduler().runTaskLater(plugin, () -> { 55 | // Ignore bet-all inventory. 56 | if (player.getOpenInventory().getTopInventory().getHolder() instanceof ConfirmGUI confirm) { 57 | if (confirm.getType() == ConfirmGUI.ConfirmType.BET_ALL) return; 58 | } 59 | 60 | // The player closed the chip menu but didn't select a chip, re-open menu. 61 | player.openInventory(chip.getInventory()); 62 | chip.updateInventory(); 63 | }, 2L); 64 | return; 65 | } 66 | 67 | if (!(inventory.getHolder() instanceof ConfirmGUI gui)) return; 68 | 69 | ConfirmGUI.ConfirmType type = gui.getType(); 70 | if (type.isLeave()) return; 71 | 72 | // The time for selecting a chip is over. 73 | if (game.getState().isSpinning()) return; 74 | 75 | if (type.isDone()) { 76 | // The player is done, no need to re-open the bet GUI. 77 | if (game.isDone(player)) return; 78 | 79 | runTask(() -> new BetsGUI(game, player, gui.getPreviousPage())); 80 | return; 81 | } 82 | 83 | // The player selected a chip, no need to re-open the chip GUI. 84 | if (bet.hasChip()) return; 85 | 86 | // The player closed the confirm bet-all menu but didn't confirmed, re-open the chip menu. 87 | runTask(() -> { 88 | ChipGUI chip = gui.getSourceGUI(); 89 | player.openInventory(chip.getInventory()); 90 | chip.updateInventory(); 91 | }); 92 | } 93 | 94 | private void runTask(Runnable runnable) { 95 | plugin.getServer().getScheduler().runTask(plugin, runnable); 96 | } 97 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/npc/modifier/VisibilityModifier.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.npc.modifier; 2 | 3 | import com.github.retrooper.packetevents.PacketEvents; 4 | import com.github.retrooper.packetevents.manager.server.ServerVersion; 5 | import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; 6 | import com.github.retrooper.packetevents.protocol.player.GameMode; 7 | import com.github.retrooper.packetevents.protocol.player.UserProfile; 8 | import com.github.retrooper.packetevents.wrapper.play.server.*; 9 | import io.github.retrooper.packetevents.util.SpigotConversionUtil; 10 | import me.matsubara.roulette.npc.NPC; 11 | 12 | import java.util.Collections; 13 | import java.util.EnumSet; 14 | 15 | public class VisibilityModifier extends NPCModifier { 16 | 17 | // Static actions we need to send out for all player updates (since 1.19.3). 18 | private static final EnumSet ADD_ACTIONS; 19 | 20 | static { 21 | if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_19_3)) { 22 | ADD_ACTIONS = EnumSet.of( 23 | WrapperPlayServerPlayerInfoUpdate.Action.ADD_PLAYER, 24 | WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_LISTED, 25 | WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_LATENCY, 26 | WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_GAME_MODE, 27 | WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_DISPLAY_NAME); 28 | } else ADD_ACTIONS = EnumSet.noneOf(WrapperPlayServerPlayerInfoUpdate.Action.class); 29 | } 30 | 31 | public VisibilityModifier(NPC npc) { 32 | super(npc); 33 | } 34 | 35 | public VisibilityModifier queuePlayerListChange(boolean remove) { 36 | if (remove && PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_19_3)) { 37 | queueInstantly(((npc, player) -> new WrapperPlayServerPlayerInfoRemove(Collections.singletonList(npc.getProfile().getUUID())))); 38 | return this; 39 | } 40 | 41 | queuePacket((npc, player) -> { 42 | UserProfile profile = npc.getProfile(); 43 | if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_19_3)) { 44 | WrapperPlayServerPlayerInfoUpdate.PlayerInfo info = new WrapperPlayServerPlayerInfoUpdate.PlayerInfo(profile, false, 20, GameMode.CREATIVE, null, null); 45 | return new WrapperPlayServerPlayerInfoUpdate(ADD_ACTIONS, info); 46 | } else { 47 | WrapperPlayServerPlayerInfo.PlayerData info = new WrapperPlayServerPlayerInfo.PlayerData(null, profile, GameMode.CREATIVE, 20); 48 | return new WrapperPlayServerPlayerInfo(remove ? 49 | WrapperPlayServerPlayerInfo.Action.REMOVE_PLAYER : 50 | WrapperPlayServerPlayerInfo.Action.ADD_PLAYER, info); 51 | } 52 | }); 53 | 54 | return this; 55 | } 56 | 57 | public VisibilityModifier queueSpawn() { 58 | queueInstantly((npc, player) -> { 59 | com.github.retrooper.packetevents.protocol.world.Location at = SpigotConversionUtil.fromBukkitLocation(npc.getLocation()); 60 | if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_2)) { 61 | return new WrapperPlayServerSpawnEntity(npc.getEntityId(), 62 | npc.getProfile().getUUID(), 63 | EntityTypes.PLAYER, 64 | at, 65 | at.getYaw(), 66 | 0, 67 | null); 68 | } else { 69 | return new WrapperPlayServerSpawnPlayer( 70 | npc.getEntityId(), 71 | npc.getProfile().getUUID(), 72 | at); 73 | } 74 | }); 75 | 76 | return this; 77 | } 78 | 79 | public VisibilityModifier queueDestroy() { 80 | queueInstantly((npc, player) -> new WrapperPlayServerDestroyEntities(npc.getEntityId())); 81 | return this; 82 | } 83 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/util/ColorUtils.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.util; 2 | 3 | import com.cryptomorin.xseries.reflection.XReflection; 4 | import org.bukkit.ChatColor; 5 | import org.bukkit.Color; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.LinkedHashMap; 9 | import java.util.Map; 10 | 11 | public class ColorUtils { 12 | 13 | public static final Map GLOW_COLOR_URL = new LinkedHashMap<>(); 14 | public static final ChatColor[] GLOW_COLORS; 15 | 16 | private static final int BIT_MASK = 0xFF; 17 | 18 | static { 19 | GLOW_COLOR_URL.put(ChatColor.BLACK, "2a52d579afe2fdf7b8ecfa746cd016150d96beb75009bb2733ade15d487c42a1"); 20 | GLOW_COLOR_URL.put(ChatColor.DARK_BLUE, "fe4c1b36e5d8e2fa6d55134753eefb2f52302d20f4dac554b1afe5711b93cc"); 21 | GLOW_COLOR_URL.put(ChatColor.DARK_GREEN, "eb879f5764385ed6bb90755bb041574882e2f41ab9323576016cfbe7f16397a"); 22 | GLOW_COLOR_URL.put(ChatColor.DARK_AQUA, "d5bbd4a69d208dd25dd95ad4b0f5c7c4b2e0d626161bb1ebf3bcc7e88fd4a960"); 23 | GLOW_COLOR_URL.put(ChatColor.DARK_RED, "97d0b9b3c419d3e321397bedc6dcd649e51cc2fa36b883b02f4da39582cdff1b"); 24 | GLOW_COLOR_URL.put(ChatColor.DARK_PURPLE, "b09fa999c27a947a0aa5d4478da26ab0f189f180a7fb1ec8adcef6df76879"); 25 | GLOW_COLOR_URL.put(ChatColor.GOLD, "c8e44023e11eeb5b293d086351e29e6ffaec01b768dc1460b1be54b809bd6dbf"); 26 | GLOW_COLOR_URL.put(ChatColor.GRAY, "1b9c45d6c7cd0116436c31ed4d8dc825de03e806edb64e9a67f540b8aaae85"); 27 | GLOW_COLOR_URL.put(ChatColor.DARK_GRAY, "b2554dda80ea64b18bc375b81ce1ed1907fc81aea6b1cf3c4f7ad3144389f64c"); 28 | GLOW_COLOR_URL.put(ChatColor.BLUE, "3b5106b060eaf398217349f3cfb4f2c7c4fd9a0b0307a17eba6af7889be0fbe6"); 29 | GLOW_COLOR_URL.put(ChatColor.GREEN, "ac01f6796eb63d0e8a759281d037f7b3843090f9a456a74f786d049065c914c7"); 30 | GLOW_COLOR_URL.put(ChatColor.AQUA, "4548789b968c70ec9d1de272d0bb93a70134f2c0e60acb75e8d455a1650f3977"); 31 | GLOW_COLOR_URL.put(ChatColor.RED, "3c4d7a3bc3de833d3032e85a0bf6f2bef7687862b3c6bc40ce731064f615dd9d"); 32 | GLOW_COLOR_URL.put(ChatColor.LIGHT_PURPLE, "205c17650e5d747010e8b69a6f2363fd11eb93f81c6ce99bf03895cefb92baa"); 33 | GLOW_COLOR_URL.put(ChatColor.YELLOW, "200bf4bf14c8699c0f9209ca79fe18253e901e9ec3876a2ba095da052f69eba7"); 34 | GLOW_COLOR_URL.put(ChatColor.WHITE, "1884d5dabe073e28e6b7eb166ff61247905c79f838b6f5752e7ad406091eeaf3"); 35 | GLOW_COLORS = GLOW_COLOR_URL.keySet().toArray(ChatColor[]::new); 36 | } 37 | 38 | public static int convertARGBtoRGB(int argb) { 39 | int red = (argb >> 16) & BIT_MASK; 40 | int green = (argb >> 8) & BIT_MASK; 41 | int blue = argb & BIT_MASK; 42 | return (red << 16) | (green << 8) | blue; 43 | } 44 | 45 | public static @NotNull Color convertCountToRGB(int count) { 46 | int red = (int) (Math.sin(count * 0.01d) * 127 + 128); 47 | int green = (int) (Math.sin(count * 0.01d + 2) * 127 + 128); 48 | int blue = (int) (Math.sin(count * 0.01d + 4) * 127 + 128); 49 | return Color.fromRGB(red, green, blue); 50 | } 51 | 52 | public static ChatColor getClosestChatColor(Color from) { 53 | ChatColor closest = null; 54 | double minDistance = Double.MAX_VALUE; 55 | 56 | for (ChatColor color : GLOW_COLORS) { 57 | int rgb = color.asBungee().getColor().getRGB(); 58 | 59 | Color to = XReflection.supports(19, 4) ? 60 | Color.fromARGB(rgb) : 61 | Color.fromRGB(ColorUtils.convertARGBtoRGB(rgb)); 62 | 63 | double distance = colorDistance(from, to); 64 | if (distance < minDistance) { 65 | minDistance = distance; 66 | closest = color; 67 | } 68 | } 69 | 70 | return closest; 71 | } 72 | 73 | private static double colorDistance(@NotNull Color first, @NotNull Color second) { 74 | int redDiff = first.getRed() - second.getRed(); 75 | int greenDiff = first.getGreen() - second.getGreen(); 76 | int blueDiff = first.getBlue() - second.getBlue(); 77 | return Math.sqrt(redDiff * redDiff + greenDiff * greenDiff + blueDiff * blueDiff); 78 | } 79 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/util/ParrotUtils.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.util; 2 | 3 | import com.cryptomorin.xseries.XEntityType; 4 | import com.cryptomorin.xseries.XSound; 5 | import com.google.common.collect.ImmutableList; 6 | import org.bukkit.Difficulty; 7 | import org.bukkit.World; 8 | import org.bukkit.entity.EntityType; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Random; 15 | 16 | public class ParrotUtils { 17 | 18 | private static final Map MOB_SOUND_MAP = new HashMap<>(); 19 | private static final List VALID_MOB; 20 | 21 | static { 22 | Map temp = new HashMap<>(); 23 | temp.put(XEntityType.BLAZE, XSound.ENTITY_PARROT_IMITATE_BLAZE); 24 | temp.put(XEntityType.BOGGED, XSound.ENTITY_PARROT_IMITATE_BOGGED); 25 | temp.put(XEntityType.BREEZE, XSound.ENTITY_PARROT_IMITATE_BREEZE); 26 | temp.put(XEntityType.CAVE_SPIDER, XSound.ENTITY_PARROT_IMITATE_SPIDER); 27 | temp.put(XEntityType.CREEPER, XSound.ENTITY_PARROT_IMITATE_CREEPER); 28 | temp.put(XEntityType.DROWNED, XSound.ENTITY_PARROT_IMITATE_DROWNED); 29 | temp.put(XEntityType.ELDER_GUARDIAN, XSound.ENTITY_PARROT_IMITATE_ELDER_GUARDIAN); 30 | temp.put(XEntityType.ENDER_DRAGON, XSound.ENTITY_PARROT_IMITATE_ENDER_DRAGON); 31 | temp.put(XEntityType.ENDERMITE, XSound.ENTITY_PARROT_IMITATE_ENDERMITE); 32 | temp.put(XEntityType.EVOKER, XSound.ENTITY_PARROT_IMITATE_EVOKER); 33 | temp.put(XEntityType.GHAST, XSound.ENTITY_PARROT_IMITATE_GHAST); 34 | temp.put(XEntityType.GUARDIAN, XSound.ENTITY_PARROT_IMITATE_GUARDIAN); 35 | temp.put(XEntityType.HOGLIN, XSound.ENTITY_PARROT_IMITATE_HOGLIN); 36 | temp.put(XEntityType.HUSK, XSound.ENTITY_PARROT_IMITATE_HUSK); 37 | temp.put(XEntityType.ILLUSIONER, XSound.ENTITY_PARROT_IMITATE_ILLUSIONER); 38 | temp.put(XEntityType.MAGMA_CUBE, XSound.ENTITY_PARROT_IMITATE_MAGMA_CUBE); 39 | temp.put(XEntityType.PHANTOM, XSound.ENTITY_PARROT_IMITATE_PHANTOM); 40 | temp.put(XEntityType.PIGLIN, XSound.ENTITY_PARROT_IMITATE_PIGLIN); 41 | temp.put(XEntityType.PIGLIN_BRUTE, XSound.ENTITY_PARROT_IMITATE_PIGLIN_BRUTE); 42 | temp.put(XEntityType.PILLAGER, XSound.ENTITY_PARROT_IMITATE_PILLAGER); 43 | temp.put(XEntityType.RAVAGER, XSound.ENTITY_PARROT_IMITATE_RAVAGER); 44 | temp.put(XEntityType.SHULKER, XSound.ENTITY_PARROT_IMITATE_SHULKER); 45 | temp.put(XEntityType.SILVERFISH, XSound.ENTITY_PARROT_IMITATE_SILVERFISH); 46 | temp.put(XEntityType.SKELETON, XSound.ENTITY_PARROT_IMITATE_SKELETON); 47 | temp.put(XEntityType.SLIME, XSound.ENTITY_PARROT_IMITATE_SLIME); 48 | temp.put(XEntityType.SPIDER, XSound.ENTITY_PARROT_IMITATE_SPIDER); 49 | temp.put(XEntityType.STRAY, XSound.ENTITY_PARROT_IMITATE_STRAY); 50 | temp.put(XEntityType.VEX, XSound.ENTITY_PARROT_IMITATE_VEX); 51 | temp.put(XEntityType.VINDICATOR, XSound.ENTITY_PARROT_IMITATE_VINDICATOR); 52 | temp.put(XEntityType.WARDEN, XSound.ENTITY_PARROT_IMITATE_WARDEN); 53 | temp.put(XEntityType.WITCH, XSound.ENTITY_PARROT_IMITATE_WITCH); 54 | temp.put(XEntityType.WITHER, XSound.ENTITY_PARROT_IMITATE_WITHER); 55 | temp.put(XEntityType.WITHER_SKELETON, XSound.ENTITY_PARROT_IMITATE_WITHER_SKELETON); 56 | temp.put(XEntityType.ZOGLIN, XSound.ENTITY_PARROT_IMITATE_ZOGLIN); 57 | temp.put(XEntityType.ZOMBIE, XSound.ENTITY_PARROT_IMITATE_ZOMBIE); 58 | temp.put(XEntityType.ZOMBIE_VILLAGER, XSound.ENTITY_PARROT_IMITATE_ZOMBIE_VILLAGER); 59 | 60 | temp.forEach((type, sound) -> { 61 | if (!type.isSupported() || !sound.isSupported()) return; 62 | MOB_SOUND_MAP.put(type.get(), sound); 63 | }); 64 | 65 | VALID_MOB = ImmutableList.copyOf(MOB_SOUND_MAP.keySet()); 66 | } 67 | 68 | public static XSound getAmbient(@NotNull World world) { 69 | Random random = PluginUtils.RANDOM; 70 | if (world.getDifficulty() != Difficulty.PEACEFUL && random.nextInt(100) == 0) { 71 | EntityType type = VALID_MOB.get(random.nextInt(VALID_MOB.size())); 72 | return MOB_SOUND_MAP.getOrDefault(type, XSound.ENTITY_PARROT_AMBIENT); 73 | } 74 | return XSound.ENTITY_PARROT_AMBIENT; 75 | } 76 | 77 | public static float getPitch() { 78 | Random random = PluginUtils.RANDOM; 79 | return (random.nextFloat() - random.nextFloat()) * 0.2f + 1.0f; 80 | } 81 | 82 | public enum ParrotShoulder { 83 | LEFT, 84 | RIGHT; 85 | 86 | public boolean isLeft() { 87 | return this == LEFT; 88 | } 89 | 90 | public boolean isRight() { 91 | return this == RIGHT; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/gui/TableGUI.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.gui; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.file.Config; 5 | import me.matsubara.roulette.game.Game; 6 | import me.matsubara.roulette.game.data.CustomizationGroup; 7 | import me.matsubara.roulette.model.Model; 8 | import me.matsubara.roulette.util.ItemBuilder; 9 | import me.matsubara.roulette.util.PluginUtils; 10 | import org.apache.commons.lang3.ArrayUtils; 11 | import org.bukkit.Bukkit; 12 | import org.bukkit.Material; 13 | import org.bukkit.Tag; 14 | import org.bukkit.entity.Player; 15 | import org.bukkit.event.inventory.ClickType; 16 | import org.bukkit.event.inventory.InventoryClickEvent; 17 | import org.bukkit.inventory.Inventory; 18 | import org.bukkit.inventory.ItemStack; 19 | import org.bukkit.inventory.meta.ItemMeta; 20 | import org.jetbrains.annotations.NotNull; 21 | 22 | import java.util.stream.Stream; 23 | 24 | @Getter 25 | public class TableGUI extends RouletteGUI { 26 | 27 | // The game that is being edited. 28 | private final Game game; 29 | 30 | // Inventoy being used. 31 | private final Inventory inventory; 32 | 33 | // Valid carpet materials. 34 | @SuppressWarnings("deprecation") 35 | public static final Material[] VALID_CARPETS = Stream.of(Material.values()) 36 | .filter(Tag.CARPETS::isTagged) 37 | .toArray(Material[]::new); 38 | 39 | public TableGUI(@NotNull Game game, @NotNull Player player) { 40 | super(game.getPlugin(), "table-menu"); 41 | this.game = game; 42 | this.inventory = Bukkit.createInventory(this, 27, Config.TABLE_MENU_TITLE.asStringTranslated()); 43 | 44 | // Fill inventory. 45 | fillInventory(); 46 | 47 | // Open inventory. 48 | player.openInventory(inventory); 49 | } 50 | 51 | private void fillInventory() { 52 | ItemStack background = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE) 53 | .setDisplayName("&7") 54 | .build(); 55 | 56 | for (int i = 0; i < 27; i++) { 57 | inventory.setItem(i, background); 58 | } 59 | 60 | setTextureItem(); // 10 61 | setChairItem(); // 13 62 | setDecorationItem(); // 16 63 | } 64 | 65 | public void setTextureItem() { 66 | CustomizationGroup texture = game.getModel().getTexture(); 67 | inventory.setItem(10, getItem("texture") 68 | .setType(texture.block()) 69 | .build()); 70 | } 71 | 72 | public void setChairItem() { 73 | inventory.setItem(13, getItem("chair") 74 | .setType(game.getModel().getCarpetsType()) 75 | .build()); 76 | } 77 | 78 | public void setDecorationItem() { 79 | inventory.setItem(16, getItem("decoration") 80 | .replace("%decoration%", game.getModel().getPatternIndex() + 1) 81 | .build()); 82 | } 83 | 84 | @Override 85 | public void handle(@NotNull InventoryClickEvent event) { 86 | ItemStack current = event.getCurrentItem(); 87 | if (current == null) return; 88 | 89 | ItemMeta meta = current.getItemMeta(); 90 | if (meta == null) return; 91 | 92 | Model model = game.getModel(); 93 | 94 | ClickType click = event.getClick(); 95 | boolean left = click.isLeftClick(), right = click.isRightClick(); 96 | if (!left && !right) return; 97 | 98 | Model.CustomizationChange change; 99 | if (isCustomItem(current, "texture")) { 100 | change = Model.CustomizationChange.TABLE; 101 | CustomizationGroup texture = PluginUtils.getNextOrPrevious(CustomizationGroup.GROUPS, 102 | CustomizationGroup.GROUPS.indexOf(model.getTexture()), 103 | right); 104 | model.setTexture(texture); 105 | setTextureItem(); 106 | } else if (isCustomItem(current, "chair")) { 107 | change = Model.CustomizationChange.CHAIR_CARPET; 108 | Material newCarpet = PluginUtils.getNextOrPrevious(TableGUI.VALID_CARPETS, 109 | model.getCarpetsType(), 110 | right); 111 | model.setCarpetsType(newCarpet); 112 | setChairItem(); 113 | } else if (isCustomItem(current, "decoration")) { 114 | change = Model.CustomizationChange.DECO; 115 | String[] pattern = PluginUtils.getNextOrPrevious(Model.PATTERNS, 116 | model.getPatternIndex(), 117 | right); 118 | model.setPatternIndex(ArrayUtils.indexOf(Model.PATTERNS, pattern)); 119 | setDecorationItem(); 120 | } else return; 121 | 122 | model.updateModel(game.getSeeingPlayers(), change); 123 | plugin.getGameManager().save(game); 124 | } 125 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/gui/ConfirmGUI.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.gui; 2 | 3 | import com.google.common.base.Predicates; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import me.matsubara.roulette.file.Config; 7 | import me.matsubara.roulette.file.Messages; 8 | import me.matsubara.roulette.game.Game; 9 | import me.matsubara.roulette.game.data.Bet; 10 | import me.matsubara.roulette.game.state.Selecting; 11 | import me.matsubara.roulette.util.ItemBuilder; 12 | import org.bukkit.entity.Player; 13 | import org.bukkit.event.inventory.InventoryClickEvent; 14 | import org.bukkit.inventory.Inventory; 15 | import org.bukkit.inventory.ItemStack; 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | @Getter 19 | public final class ConfirmGUI extends RouletteGUI { 20 | 21 | // The game related to this GUI. 22 | private final Game game; 23 | 24 | // The inventory being used. 25 | private final Inventory inventory; 26 | 27 | // The confirmation type gui. 28 | private final ConfirmType type; 29 | 30 | // The previous page of the chip gui. 31 | private @Setter int previousPage; 32 | 33 | // The chip GUI source (for BET_ALL). 34 | private @Setter ChipGUI sourceGUI; 35 | 36 | public ConfirmGUI(@NotNull Game game, Player player, ConfirmType type) { 37 | super(game.getPlugin(), "confirmation-menu"); 38 | this.game = game; 39 | this.inventory = plugin.getServer().createInventory(this, 9, Config.CONFIRM_MENU_TITLE.asStringTranslated()); 40 | this.type = type; 41 | this.previousPage = 0; 42 | 43 | // Fill inventory. 44 | for (int i = 0; i < 9; i++) { 45 | ItemBuilder builder = i == 4 ? 46 | plugin.getItem(type.getIconPath()) : 47 | getItem(i < 4 ? "confirm" : "cancel"); 48 | inventory.setItem(i, builder.build()); 49 | } 50 | 51 | // Open inventory. 52 | player.openInventory(inventory); 53 | } 54 | 55 | @Getter 56 | public enum ConfirmType { 57 | LEAVE("chip-menu.items.exit"), 58 | BET_ALL("chip-menu.items.bet-all"), 59 | DONE("bets-menu.items.done"); 60 | 61 | private final String iconPath; 62 | 63 | ConfirmType(String iconPath) { 64 | this.iconPath = iconPath; 65 | } 66 | 67 | public boolean isLeave() { 68 | return this == LEAVE; 69 | } 70 | 71 | public boolean isBetAll() { 72 | return this == BET_ALL; 73 | } 74 | 75 | public boolean isDone() { 76 | return this == DONE; 77 | } 78 | } 79 | 80 | @Override 81 | public void handle(@NotNull InventoryClickEvent event) { 82 | Player player = (Player) event.getWhoClicked(); 83 | 84 | ItemStack current = event.getCurrentItem(); 85 | if (current == null) return; 86 | 87 | // Clicked on the item of the middle. 88 | String iconPath = type.getIconPath(); 89 | if (isCustomItem(current, iconPath.substring(iconPath.lastIndexOf(".") + 1))) return; 90 | 91 | // Close the inventory. 92 | if (isCustomItem(current, "cancel")) { 93 | closeInventory(player); 94 | return; 95 | } 96 | 97 | Messages messages = plugin.getMessages(); 98 | 99 | if (type.isLeave()) { 100 | // Remove the player from the game. 101 | messages.send(player, Messages.Message.LEAVE_PLAYER); 102 | game.removeCompletely(player); 103 | } else if (type.isBetAll()) { 104 | // Bet all the money. 105 | double money = plugin.getEconomyExtension().getBalance(player); 106 | 107 | game.takeMoneyAndPlaceBet( 108 | player, 109 | plugin.getChipManager().getExistingOrBetAll(game, money), 110 | sourceGUI.isNewBet()); 111 | } else { 112 | // Make the call. 113 | game.setDone(player); 114 | 115 | // Remove glow and hide hologram. 116 | game.getBets(player).forEach(Bet::hide); 117 | 118 | // If all players are done, then we want to reduce the start time. 119 | if (game.getPlayers().stream().noneMatch(Predicates.not(game::isDone))) { 120 | game.broadcast(Messages.Message.ALL_PLAYERS_DONE); 121 | 122 | Selecting selecting = game.getSelecting(); 123 | if (selecting != null 124 | && !selecting.isCancelled() 125 | && selecting.getTicks() > 100) { 126 | selecting.setTicks(100); 127 | } 128 | } else { 129 | messages.send(player, Messages.Message.YOU_ARE_DONE); 130 | // Let the other players know. 131 | game.broadcast( 132 | Messages.Message.YOU_ARE_DONE, 133 | line -> line.replace("%player-name%", player.getName()), 134 | player); 135 | } 136 | } 137 | 138 | closeInventory(player); 139 | } 140 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/manager/WinnerManager.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.manager; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.RoulettePlugin; 5 | import me.matsubara.roulette.file.Config; 6 | import me.matsubara.roulette.manager.data.DataManager; 7 | import me.matsubara.roulette.manager.data.MapRecord; 8 | import me.matsubara.roulette.manager.data.PlayerResult; 9 | import me.matsubara.roulette.manager.data.RouletteSession; 10 | import me.matsubara.roulette.util.PluginUtils; 11 | import me.matsubara.roulette.util.map.MapBuilder; 12 | import org.bukkit.Bukkit; 13 | import org.bukkit.OfflinePlayer; 14 | import org.bukkit.configuration.ConfigurationSection; 15 | import org.bukkit.configuration.file.FileConfiguration; 16 | import org.bukkit.event.EventHandler; 17 | import org.bukkit.event.Listener; 18 | import org.bukkit.event.server.MapInitializeEvent; 19 | import org.bukkit.inventory.ItemStack; 20 | import org.bukkit.map.MapFont; 21 | import org.bukkit.map.MapView; 22 | import org.bukkit.map.MinecraftFont; 23 | import org.jetbrains.annotations.NotNull; 24 | import org.jetbrains.annotations.Nullable; 25 | 26 | import javax.imageio.ImageIO; 27 | import java.awt.image.BufferedImage; 28 | import java.io.IOException; 29 | import java.text.SimpleDateFormat; 30 | import java.util.*; 31 | 32 | @Getter 33 | public final class WinnerManager implements Listener { 34 | 35 | private final RoulettePlugin plugin; 36 | private BufferedImage image; 37 | 38 | public WinnerManager(RoulettePlugin plugin) { 39 | this.plugin = plugin; 40 | this.plugin.getServer().getPluginManager().registerEvents(this, plugin); 41 | try { 42 | this.image = ImageIO.read(plugin.saveFile("background.png")); 43 | } catch (IOException exception) { 44 | plugin.getLogger().warning("The file {background.png} couldn't be found."); 45 | } 46 | } 47 | 48 | @EventHandler 49 | public void onMapInitialize(@NotNull MapInitializeEvent event) { 50 | MapView view = event.getMap(); 51 | 52 | DataManager dataManager = plugin.getDataManager(); 53 | for (MapRecord record : dataManager.getMaps()) { 54 | if (record.mapId() != view.getId()) continue; 55 | 56 | RouletteSession session = dataManager.getSessionByUUID(record.sessionUUID()); 57 | if (session == null) break; 58 | 59 | render(record.playerUUID(), session, view); 60 | break; 61 | } 62 | } 63 | 64 | public @Nullable Map.Entry render(UUID playerUUID, RouletteSession session) { 65 | return render(playerUUID, session, null); 66 | } 67 | 68 | public @Nullable Map.Entry render(UUID playerUUID, RouletteSession session, @Nullable MapView view) { 69 | MapBuilder builder = new MapBuilder(plugin, playerUUID, session); 70 | if (image != null) builder.setImage(image, true); 71 | 72 | FileConfiguration config = plugin.getConfig(); 73 | ConfigurationSection section = config.getConfigurationSection("map-image.lines"); 74 | if (section == null) return null; 75 | 76 | MapFont font = MinecraftFont.Font; 77 | for (String path : section.getKeys(false)) { 78 | List lines = config.getStringList("map-image.lines." + path + ".text"); 79 | 80 | String coordsPath = "map-image.lines." + path + ".coords."; 81 | int x = config.getInt(coordsPath + ".x", -1); 82 | int y = config.getInt(coordsPath + ".y", -1); 83 | int height = font.getHeight(), amountOfLines = lines.size(); 84 | 85 | if (y == -1) { 86 | y = (128 - (amountOfLines * height + (amountOfLines - 1))) / 2; 87 | } 88 | 89 | for (int i = 0; i < amountOfLines; i++) { 90 | String line = loreReplacer(playerUUID, session, lines.get(i)); 91 | builder.addText(x, y + i * (height + 1), line); 92 | } 93 | } 94 | 95 | if (view != null) { 96 | view.setScale(MapView.Scale.NORMAL); 97 | view.getRenderers().forEach(view::removeRenderer); 98 | view.addRenderer(builder); 99 | return null; 100 | } 101 | 102 | ItemStack item = builder.build(); 103 | return new AbstractMap.SimpleEntry<>(builder.getView().getId(), item); 104 | } 105 | 106 | public @NotNull String loreReplacer(UUID playerUUID, @NotNull RouletteSession session, @NotNull String string) { 107 | double money = session.results().stream() 108 | .filter(PlayerResult::won) 109 | .mapToDouble(plugin::getExpectedMoney) 110 | .sum(); 111 | 112 | OfflinePlayer winner = Bukkit.getOfflinePlayer(playerUUID); 113 | String date = new SimpleDateFormat(Config.DATE_FORMAT.asString()) 114 | .format(new Date(session.timestamp())); 115 | 116 | return string 117 | .replace("%player%", Objects.requireNonNullElse(winner.getName(), "???")) 118 | .replace("%money%", PluginUtils.format(money)) 119 | .replace("%date%", date) 120 | .replace("%table%", session.name()); 121 | } 122 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/file/Config.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.file; 2 | 3 | import lombok.experimental.UtilityClass; 4 | import me.matsubara.roulette.file.config.ConfigValue; 5 | 6 | @UtilityClass 7 | public class Config { 8 | 9 | public final ConfigValue EXPERIMENTAL = new ConfigValue("experimental"); 10 | public final ConfigValue RENDER_DISTANCE = new ConfigValue("render-distance"); 11 | public final ConfigValue ECONOMY_PROVIDER = new ConfigValue("economy-provider"); 12 | public final ConfigValue SESSIONS_LIMIT = new ConfigValue("sessions.limit"); 13 | public final ConfigValue SESSIONS_KEEP_VICTORIES = new ConfigValue("sessions.keep-victories"); 14 | public final ConfigValue SESSIONS_ONLY_VICTORIES_TEXT = new ConfigValue("sessions.only-victories-text"); 15 | public final ConfigValue SWAP_CHAIR = new ConfigValue("swap-chair"); 16 | public final ConfigValue INSTANT_EXPLODE = new ConfigValue("instant-explode"); 17 | public final ConfigValue FIX_CHAIR_CAMERA = new ConfigValue("fix-chair-camera"); 18 | public final ConfigValue HIT_ON_GAME = new ConfigValue("hit-on-game"); 19 | public final ConfigValue KEEP_SEAT = new ConfigValue("keep-seat"); 20 | public final ConfigValue DATE_FORMAT = new ConfigValue("date-format"); 21 | public final ConfigValue MOVE_INTERVAL = new ConfigValue("move-interval"); 22 | public final ConfigValue CROUPIER_BALL = new ConfigValue("croupier-ball"); 23 | public final ConfigValue COUNTDOWN_WAITING = new ConfigValue("countdown.waiting"); 24 | public final ConfigValue COUNTDOWN_SELECTING = new ConfigValue("countdown.selecting.base"); 25 | public final ConfigValue COUNTDOWN_SELECTING_EXTRA = new ConfigValue("countdown.selecting.extra"); 26 | public final ConfigValue COUNTDOWN_SELECTING_MAX = new ConfigValue("countdown.selecting.max"); 27 | public final ConfigValue COUNTDOWN_SORTING = new ConfigValue("countdown.sorting"); 28 | public final ConfigValue RESTART_TIME = new ConfigValue("restart.time"); 29 | public final ConfigValue RESTART_FIREWORKS = new ConfigValue("restart.fireworks"); 30 | public final ConfigValue SOUND_CLICK = new ConfigValue("sounds.click"); 31 | public final ConfigValue SOUND_COUNTDOWN = new ConfigValue("sounds.countdown"); 32 | public final ConfigValue SOUND_SPINNING = new ConfigValue("sounds.spinning"); 33 | public final ConfigValue SOUND_SWAP_CHAIR = new ConfigValue("sounds.swap-chair"); 34 | public final ConfigValue SOUND_SELECT = new ConfigValue("sounds.select"); 35 | public final ConfigValue DISABLED_SLOTS = new ConfigValue("disabled-slots"); 36 | public final ConfigValue MAP_IMAGE_ENABLED = new ConfigValue("map-image.enabled"); 37 | public final ConfigValue CANCEL_WORD = new ConfigValue("cancel-word"); 38 | public final ConfigValue SPINNING_GLOBAL = new ConfigValue("spin-holograms.global"); 39 | public final ConfigValue SPINNING = new ConfigValue("spin-holograms.spinning"); 40 | public final ConfigValue WINNING_NUMBER = new ConfigValue("spin-holograms.winning-number"); 41 | public final ConfigValue SINGLE_ZERO = new ConfigValue("slots.single.zero"); 42 | public final ConfigValue SINGLE_RED = new ConfigValue("slots.single.red"); 43 | public final ConfigValue SINGLE_BLACK = new ConfigValue("slots.single.black"); 44 | public final ConfigValue LOW = new ConfigValue("slots.other.low"); 45 | public final ConfigValue HIGH = new ConfigValue("slots.other.high"); 46 | public final ConfigValue EVEN = new ConfigValue("slots.other.even"); 47 | public final ConfigValue ODD = new ConfigValue("slots.other.odd"); 48 | public final ConfigValue RED = new ConfigValue("slots.other.red"); 49 | public final ConfigValue BLACK = new ConfigValue("slots.other.black"); 50 | public final ConfigValue TYPE_EUROPEAN = new ConfigValue("variable-text.types.european"); 51 | public final ConfigValue TYPE_AMERICAN = new ConfigValue("variable-text.types.american"); 52 | public final ConfigValue JOIN_HOLOGRAM = new ConfigValue("join-hologram"); 53 | public final ConfigValue SELECT_HOLOGRAM = new ConfigValue("select-hologram"); 54 | public final ConfigValue STATE_ENABLED = new ConfigValue("variable-text.state.enabled"); 55 | public final ConfigValue STATE_DISABLED = new ConfigValue("variable-text.state.disabled"); 56 | public final ConfigValue SESSION_RESULT_MENU_TITLE = new ConfigValue("session-result-menu.title"); 57 | public final ConfigValue SESSIONS_MENU_TITLE = new ConfigValue("sessions-menu.title"); 58 | public final ConfigValue BETS_MENU_TITLE = new ConfigValue("bets-menu.title"); 59 | public final ConfigValue CHIP_MENU_TITLE = new ConfigValue("chip-menu.title"); 60 | public final ConfigValue CONFIRM_MENU_TITLE = new ConfigValue("confirmation-menu.title"); 61 | public final ConfigValue CROUPIER_MENU_TITLE = new ConfigValue("croupier-menu.title"); 62 | public final ConfigValue GAME_CHIP_MENU_TITLE = new ConfigValue("game-chip-menu.title"); 63 | public final ConfigValue GAME_MENU_TITLE = new ConfigValue("game-menu.title"); 64 | public final ConfigValue TABLE_MENU_TITLE = new ConfigValue("table-menu.title"); 65 | public final ConfigValue ONLY_AMERICAN = new ConfigValue("variable-text.only-american"); 66 | public final ConfigValue UNNAMED_CROUPIER = new ConfigValue("variable-text.unnamed-croupier"); 67 | public final ConfigValue CUSTOM_WIN_MULTIPLIER_ENABLED = new ConfigValue("custom-win-multiplier.enabled"); 68 | public final ConfigValue MONEY_ABBREVIATION_FORMAT_ENABLED = new ConfigValue("money-abbreviation-format.enabled"); 69 | public final ConfigValue DAB_ANIMATION_ENABLED = new ConfigValue("dab-animation.enabled"); 70 | public final ConfigValue DAB_ANIMATION_AMOUNT = new ConfigValue("dab-animation.settings.amount"); 71 | public final ConfigValue DAB_ANIMATION_RADIUS = new ConfigValue("dab-animation.settings.radius"); 72 | public final ConfigValue DAB_ANIMATION_RAINBOW_EFFECT_SPEED = new ConfigValue("dab-animation.rainbow-effect.speed"); 73 | public final ConfigValue DAB_ANIMATION_RAINBOW_EFFECT_GLOWING = new ConfigValue("dab-animation.rainbow-effect.glowing"); 74 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/gui/data/SessionsGUI.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.gui.data; 2 | 3 | import com.google.common.base.Predicates; 4 | import lombok.Getter; 5 | import me.matsubara.roulette.RoulettePlugin; 6 | import me.matsubara.roulette.file.Config; 7 | import me.matsubara.roulette.game.Game; 8 | import me.matsubara.roulette.gui.RouletteGUI; 9 | import me.matsubara.roulette.manager.data.PlayerResult; 10 | import me.matsubara.roulette.manager.data.RouletteSession; 11 | import me.matsubara.roulette.util.InventoryUpdate; 12 | import me.matsubara.roulette.util.ItemBuilder; 13 | import me.matsubara.roulette.util.PluginUtils; 14 | import org.apache.commons.lang3.ArrayUtils; 15 | import org.bukkit.Material; 16 | import org.bukkit.entity.Player; 17 | import org.bukkit.event.inventory.InventoryClickEvent; 18 | import org.bukkit.inventory.Inventory; 19 | import org.bukkit.inventory.ItemStack; 20 | import org.bukkit.inventory.meta.ItemMeta; 21 | import org.jetbrains.annotations.Contract; 22 | import org.jetbrains.annotations.NotNull; 23 | import org.jetbrains.annotations.Nullable; 24 | 25 | import java.text.SimpleDateFormat; 26 | import java.util.*; 27 | 28 | @Getter 29 | public final class SessionsGUI extends RouletteGUI { 30 | 31 | // The player viewing this inventory. 32 | private final Player player; 33 | 34 | // The inventory being used. 35 | private final Inventory inventory; 36 | 37 | // Format to use in dates. 38 | private final SimpleDateFormat format; 39 | 40 | // The slots to show the content. 41 | private static final int[] SLOTS = {10, 11, 12, 13, 14, 15, 16}; 42 | 43 | // The slot to put page navigator items and other stuff. 44 | private static final int[] HOTBAR = {19, 20, 21, 22, 23, 24, 25}; 45 | 46 | public SessionsGUI(RoulettePlugin plugin, Player player) { 47 | this(plugin, player, 0); 48 | } 49 | 50 | public SessionsGUI(@NotNull RoulettePlugin plugin, @NotNull Player player, int currentPage) { 51 | super(plugin, "sessions-menu", true); 52 | this.player = player; 53 | this.inventory = plugin.getServer().createInventory(this, 36); 54 | this.format = new SimpleDateFormat(Config.DATE_FORMAT.asString()); 55 | this.currentPage = currentPage; 56 | 57 | player.openInventory(inventory); 58 | updateInventory(); 59 | } 60 | 61 | @Override 62 | public void updateInventory() { 63 | inventory.clear(); 64 | 65 | // Get the list of sessions. 66 | List sessions = plugin.getDataManager().getSessions(); 67 | 68 | // Page formula. 69 | pages = (int) (Math.ceil((double) sessions.size() / SLOTS.length)); 70 | 71 | ItemStack background = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE) 72 | .setDisplayName("&7") 73 | .build(); 74 | 75 | // Set background items. 76 | for (int i = 0; i < 36; i++) { 77 | if (ArrayUtils.contains(SLOTS, i) || ArrayUtils.contains(HOTBAR, i)) continue; 78 | // Set background item in the current slot from the loop. 79 | inventory.setItem(i, background); 80 | } 81 | 82 | 83 | // If the current page isn't 0 (first page), show the previous page item. 84 | if (currentPage > 0) inventory.setItem(19, getItem("previous").build()); 85 | 86 | // If the current page isn't the last one, show the next page item. 87 | if (currentPage < pages - 1) inventory.setItem(25, getItem("next").build()); 88 | 89 | // Assigning slots. 90 | Map slotIndex = new HashMap<>(); 91 | for (int i : SLOTS) { 92 | slotIndex.put(ArrayUtils.indexOf(SLOTS, i), i); 93 | } 94 | 95 | // Where to start. 96 | int startFrom = currentPage * SLOTS.length; 97 | 98 | boolean isLastPage = currentPage == pages - 1; 99 | 100 | for (int index = 0, aux = startFrom; isLastPage ? (index < sessions.size() - startFrom) : (index < SLOTS.length); index++, aux++) { 101 | RouletteSession session = sessions.get(aux); 102 | 103 | ItemBuilder builder = getItem("session") 104 | .replace("%name%", session.name()) 105 | .replace("%date%", format.format(new Date(session.timestamp()))) 106 | .replace("%slot%", PluginUtils.getSlotName(session.slot())) 107 | .setData(plugin.getSessionKey(), PluginUtils.UUID_TYPE, session.sessionUUID()); 108 | 109 | 110 | if (session.results().stream() 111 | .noneMatch(Predicates.not(PlayerResult::won))) { 112 | builder.addLore("", Config.SESSIONS_ONLY_VICTORIES_TEXT.asStringTranslated()); 113 | } 114 | 115 | inventory.setItem(slotIndex.get(index), builder 116 | .build()); 117 | } 118 | 119 | // Update inventory title to show the current page. 120 | InventoryUpdate.updateInventory(player, Config.SESSIONS_MENU_TITLE.asStringTranslated() 121 | .replace("%page%", String.valueOf(currentPage + 1)) 122 | .replace("%max%", String.valueOf(pages))); 123 | } 124 | 125 | @Contract(pure = true) 126 | @Override 127 | public @Nullable Game getGame() { 128 | return null; 129 | } 130 | 131 | @Override 132 | public void handle(@NotNull InventoryClickEvent event) { 133 | super.handle(event); 134 | 135 | Player player = (Player) event.getWhoClicked(); 136 | 137 | ItemStack current = event.getCurrentItem(); 138 | if (current == null) return; 139 | 140 | ItemMeta meta = current.getItemMeta(); 141 | if (meta == null) return; 142 | 143 | UUID sessionUUID = meta.getPersistentDataContainer().get(plugin.getSessionKey(), PluginUtils.UUID_TYPE); 144 | if (sessionUUID == null) return; 145 | 146 | RouletteSession session = plugin.getDataManager().getSessionByUUID(sessionUUID); 147 | if (session == null) return; 148 | 149 | // Open results. 150 | runTask(() -> new SessionResultGUI(plugin, player, session)); 151 | } 152 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/game/state/Spinning.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.game.state; 2 | 3 | import com.cryptomorin.xseries.XSound; 4 | import com.github.retrooper.packetevents.protocol.entity.pose.EntityPose; 5 | import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; 6 | import lombok.Setter; 7 | import me.matsubara.roulette.RoulettePlugin; 8 | import me.matsubara.roulette.event.LastRouletteSpinEvent; 9 | import me.matsubara.roulette.file.Config; 10 | import me.matsubara.roulette.file.Messages; 11 | import me.matsubara.roulette.game.Game; 12 | import me.matsubara.roulette.game.GameState; 13 | import me.matsubara.roulette.game.data.Bet; 14 | import me.matsubara.roulette.game.data.Slot; 15 | import me.matsubara.roulette.hologram.Hologram; 16 | import me.matsubara.roulette.model.stand.PacketStand; 17 | import me.matsubara.roulette.model.stand.data.ItemSlot; 18 | import me.matsubara.roulette.npc.NPC; 19 | import me.matsubara.roulette.npc.modifier.MetadataModifier; 20 | import me.matsubara.roulette.util.PluginUtils; 21 | import org.bukkit.Location; 22 | import org.bukkit.Material; 23 | import org.bukkit.entity.Player; 24 | import org.bukkit.inventory.ItemStack; 25 | import org.bukkit.scheduler.BukkitRunnable; 26 | import org.jetbrains.annotations.NotNull; 27 | 28 | import java.util.Optional; 29 | import java.util.function.UnaryOperator; 30 | 31 | public final class Spinning extends BukkitRunnable { 32 | 33 | private final RoulettePlugin plugin; 34 | private final Game game; 35 | private final PacketStand ball; 36 | private final Slot[] slots; 37 | private final int totalTime; 38 | 39 | private int time; 40 | private boolean shouldStart; 41 | private @Setter Slot force; 42 | private @Setter Player forcedBy; 43 | 44 | private final String hologramSpinning = Config.SPINNING.asStringTranslated(); 45 | private final String hologramWinningNumber = Config.WINNING_NUMBER.asStringTranslated(); 46 | private final XSound.Record spinningSound = XSound.parse(Config.SOUND_SPINNING.asString()); 47 | 48 | public Spinning(@NotNull RoulettePlugin plugin, @NotNull Game game) { 49 | this.plugin = plugin; 50 | this.game = game; 51 | this.ball = game.getBall(); 52 | this.slots = Slot.singleValues(game).toArray(Slot[]::new); 53 | this.totalTime = Config.COUNTDOWN_SORTING.asInt() * 20; 54 | this.time = totalTime; 55 | this.shouldStart = true; 56 | 57 | game.setState(GameState.SPINNING); 58 | game.removeSleepingPlayers(); 59 | 60 | for (Player player : game.getPlayers()) { 61 | // Remove glow, hide hologram and close custom menus. 62 | game.getBets(player).forEach(Bet::hide); 63 | game.sendBets( 64 | player, 65 | Messages.Message.YOUR_BETS, 66 | Messages.Message.BET_HOVER, 67 | UnaryOperator.identity(), 68 | false); 69 | game.closeOpenMenu(player); 70 | } 71 | 72 | if (game.isEmpty()) { 73 | game.restart(); 74 | shouldStart = false; 75 | return; 76 | } 77 | 78 | NPC npc = game.getNpc(); 79 | 80 | game.npcBroadcast(Messages.Message.NO_BETS); 81 | 82 | // Play NPC spin animation. 83 | game.lookAtFace(game.getDefaultNPCFace()); 84 | npc.metadata().queue(MetadataModifier.EntityMetadata.POSE, EntityPose.CROUCHING).send(); 85 | npc.equipment().queue(EquipmentSlot.MAIN_HAND, RoulettePlugin.EMPTY_ITEM).send(); 86 | 87 | // Show ball, shouldn't be null. 88 | if (ball == null) return; 89 | 90 | ball.getSettings().getEquipment().put(ItemSlot.HEAD, new ItemStack(Material.END_ROD)); 91 | ball.sendEquipment(game.getSeeingPlayers()); 92 | } 93 | 94 | @Override 95 | public void run() { 96 | if (!shouldStart) { 97 | cancel(); 98 | return; 99 | } 100 | 101 | Hologram spinHologram = game.getSpinHologram(); 102 | 103 | if (time == 0) { 104 | spinHologram.setLine(0, hologramWinningNumber); 105 | 106 | // Stop NPC animation, check if there are winners and stop. 107 | game.getNpc().metadata().queue(MetadataModifier.EntityMetadata.POSE, EntityPose.STANDING).send(); 108 | game.setState(GameState.ENDING); 109 | game.checkWinner(); 110 | 111 | cancel(); 112 | return; 113 | } 114 | 115 | // Spin ball. 116 | Location location = ball.getLocation(); 117 | location.setYaw(location.getYaw() + (time >= totalTime / 3 ? 30.0f : (30.0f * time / totalTime))); 118 | ball.teleport(game.getSeeingPlayers(), location); 119 | 120 | // Select a random number. 121 | int which = PluginUtils.RANDOM.nextInt(slots.length); 122 | game.setWinner(slots[which]); 123 | 124 | if (time == 1) { 125 | Slot winner = Optional.ofNullable(force).orElse(game.getWinner()); 126 | LastRouletteSpinEvent event = new LastRouletteSpinEvent(game, winner, forcedBy); 127 | plugin.getServer().getPluginManager().callEvent(event); 128 | game.setWinner(event.getWinnerSlot()); 129 | } 130 | 131 | String slotName = PluginUtils.getSlotName(game.getWinner()); 132 | 133 | // If the spin hologram is empty, create the lines, else update them. 134 | if (spinHologram.size() == 0) { 135 | 136 | // Teleport spin hologram to its proper location. 137 | spinHologram.teleport(ball 138 | .getLocation() 139 | .clone() 140 | .add(0.0d, 2.5d, 0.0d)); 141 | 142 | spinHologram.addLines(hologramSpinning); 143 | spinHologram.addLines(slotName); 144 | } else { 145 | spinHologram.setLine(1, slotName); 146 | // Play spinning sound at spin hologram location, this sound can be heard by every player (even those outside the game). 147 | game.playSound(spinHologram.getLocation(), spinningSound); 148 | } 149 | 150 | time--; 151 | } 152 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/util/config/ConfigFileUtils.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.util.config; 2 | 3 | import com.tchristofferson.configupdater.ConfigUpdater; 4 | import org.apache.commons.io.FileUtils; 5 | import org.bukkit.configuration.InvalidConfigurationException; 6 | import org.bukkit.configuration.file.FileConfiguration; 7 | import org.bukkit.configuration.file.YamlConfiguration; 8 | import org.bukkit.plugin.java.JavaPlugin; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | import org.yaml.snakeyaml.error.MarkedYAMLException; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.nio.charset.StandardCharsets; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.text.SimpleDateFormat; 19 | import java.util.Date; 20 | import java.util.List; 21 | import java.util.function.Consumer; 22 | import java.util.function.Function; 23 | import java.util.function.Predicate; 24 | import java.util.logging.Logger; 25 | 26 | public class ConfigFileUtils { 27 | 28 | public static void updateConfig(JavaPlugin plugin, 29 | String folderName, 30 | String fileName, 31 | Consumer reloadAfterUpdating, 32 | Consumer resetConfiguration, 33 | Function> ignoreSection, 34 | List changes) { 35 | File file = new File(folderName, fileName); 36 | 37 | FileConfiguration config = reloadConfig(plugin, file, resetConfiguration); 38 | if (config == null) { 39 | plugin.getLogger().severe("Can't find {" + file.getName() + "}!"); 40 | return; 41 | } 42 | 43 | for (ConfigChanges change : changes) { 44 | handleConfigChanges(plugin, file, config, change.predicate(), change.consumer(), change.newVersion()); 45 | } 46 | 47 | try { 48 | ConfigUpdater.update( 49 | plugin, 50 | fileName, 51 | file, 52 | ignoreSection.apply(config)); 53 | } catch (IOException exception) { 54 | exception.printStackTrace(); 55 | } 56 | 57 | reloadAfterUpdating.accept(file); 58 | } 59 | 60 | private static void handleConfigChanges(JavaPlugin plugin, 61 | @NotNull File file, 62 | FileConfiguration config, 63 | @NotNull Predicate predicate, 64 | Consumer consumer, 65 | int newVersion) { 66 | if (!predicate.test(config)) return; 67 | 68 | int previousVersion = config.getInt("config-version", -1); 69 | plugin.getLogger().info("Updated {%s} config to v{%s} (from v{%s})".formatted(file.getName(), newVersion, previousVersion)); 70 | 71 | consumer.accept(config); 72 | config.set("config-version", newVersion); 73 | 74 | try { 75 | config.save(file); 76 | } catch (IOException exception) { 77 | exception.printStackTrace(); 78 | } 79 | } 80 | 81 | public static @Nullable FileConfiguration reloadConfig(JavaPlugin plugin, @NotNull File file, @Nullable Consumer error) { 82 | File backup = null; 83 | try { 84 | SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); 85 | String time = format.format(new Date(System.currentTimeMillis())); 86 | 87 | // When error is null, that means that the file has already regenerated, so we don't need to create a backup. 88 | if (error != null) { 89 | backup = new File(file.getParentFile(), file.getName().split("\\.")[0] + "_" + time + ".bak"); 90 | FileUtils.copyFile(file, backup); 91 | } 92 | 93 | FileConfiguration configuration = new YamlConfiguration(); 94 | configuration.load(file); 95 | 96 | if (backup != null) FileUtils.deleteQuietly(backup); 97 | 98 | return configuration; 99 | } catch (IOException | InvalidConfigurationException exception) { 100 | Logger logger = plugin.getLogger(); 101 | 102 | logger.severe("An error occurred while reloading the file {" + file.getName() + "}."); 103 | 104 | boolean errorLogged = false; 105 | if (backup != null && exception instanceof InvalidConfigurationException invalid) { 106 | errorLogged = true; 107 | 108 | Throwable cause = invalid.getCause(); 109 | if (cause instanceof MarkedYAMLException marked) { 110 | handleError(backup, marked.getProblemMark().getLine()); 111 | } else { 112 | errorLogged = false; 113 | } 114 | } 115 | 116 | if (errorLogged) { 117 | logger.severe("The file will be restarted and a copy of the old file will be saved indicating which line had an error."); 118 | } else { 119 | logger.severe("The file will be restarted and a copy of the old file will be saved."); 120 | } 121 | 122 | if (error == null) { 123 | exception.printStackTrace(); 124 | return null; 125 | } 126 | 127 | // Only replace the file if an exception ocurrs. 128 | FileUtils.deleteQuietly(file); 129 | error.accept(file); 130 | 131 | return reloadConfig(plugin, file, null); 132 | } 133 | } 134 | 135 | private static void handleError(@NotNull File backup, int line) { 136 | try { 137 | Path path = backup.toPath(); 138 | 139 | List lines = Files.readAllLines(path, StandardCharsets.UTF_8); 140 | lines.set(line, lines.get(line) + " # <--------------------< ERROR <--------------------<"); 141 | 142 | Files.write(path, lines, StandardCharsets.UTF_8); 143 | } catch (IOException exception) { 144 | exception.printStackTrace(); 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/gui/ChipGUI.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.gui; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.file.Config; 5 | import me.matsubara.roulette.file.Messages; 6 | import me.matsubara.roulette.game.Game; 7 | import me.matsubara.roulette.game.data.Chip; 8 | import me.matsubara.roulette.manager.ChipManager; 9 | import me.matsubara.roulette.util.InventoryUpdate; 10 | import me.matsubara.roulette.util.ItemBuilder; 11 | import me.matsubara.roulette.util.PluginUtils; 12 | import org.apache.commons.lang3.ArrayUtils; 13 | import org.bukkit.Material; 14 | import org.bukkit.entity.Player; 15 | import org.bukkit.event.inventory.InventoryClickEvent; 16 | import org.bukkit.inventory.Inventory; 17 | import org.bukkit.inventory.ItemStack; 18 | import org.bukkit.inventory.meta.ItemMeta; 19 | import org.bukkit.persistence.PersistentDataType; 20 | import org.jetbrains.annotations.NotNull; 21 | 22 | import java.util.HashMap; 23 | import java.util.List; 24 | import java.util.Map; 25 | 26 | @Getter 27 | public final class ChipGUI extends RouletteGUI { 28 | 29 | // The instance of the game. 30 | private final Game game; 31 | 32 | // The player viewing this inventory. 33 | private final Player player; 34 | 35 | // The inventory being used. 36 | private final Inventory inventory; 37 | 38 | // Whether the chip being selected is for a new bet. 39 | private final boolean isNewBet; 40 | 41 | // The slots to show the content. 42 | private static final int[] SLOTS = {10, 11, 12, 13, 14, 15, 16}; 43 | 44 | // The slot to put page navigator items and other stuff. 45 | private static final int[] HOTBAR = {19, 20, 21, 22, 23, 24, 25}; 46 | 47 | public ChipGUI(@NotNull Game game, @NotNull Player player, boolean isNewBet) { 48 | super(game.getPlugin(), "chip-menu", true); 49 | this.game = game; 50 | this.player = player; 51 | this.isNewBet = isNewBet; 52 | this.inventory = plugin.getServer().createInventory(this, 36); 53 | 54 | player.openInventory(inventory); 55 | updateInventory(); 56 | } 57 | 58 | @Override 59 | public void updateInventory() { 60 | inventory.clear(); 61 | 62 | // Get the list of chips. 63 | ChipManager chipManager = plugin.getChipManager(); 64 | List chips = chipManager.getChipsByGame(game); 65 | 66 | // Page formula. 67 | pages = (int) (Math.ceil((double) chips.size() / SLOTS.length)); 68 | 69 | ItemStack background = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE) 70 | .setDisplayName("&7") 71 | .build(); 72 | 73 | // Set background items. 74 | for (int i = 0; i < 36; i++) { 75 | if (ArrayUtils.contains(SLOTS, i) || ArrayUtils.contains(HOTBAR, i)) continue; 76 | // Set background item in the current slot from the loop. 77 | inventory.setItem(i, background); 78 | } 79 | 80 | // If the current page isn't 0 (first page), show the previous page item. 81 | if (currentPage > 0) inventory.setItem(19, getItem("previous").build()); 82 | 83 | // Set money item. 84 | inventory.setItem(22, getItem("money") 85 | .replace("%money%", PluginUtils.format(plugin.getEconomyExtension().getBalance(player))) 86 | .build()); 87 | 88 | // Set bet all item. 89 | if (game.isBetAllEnabled()) inventory.setItem(23, getItem("bet-all").build()); 90 | 91 | // If the current page isn't the last one, show the next page item. 92 | if (currentPage < pages - 1) inventory.setItem(25, getItem("next").build()); 93 | 94 | // Set quit game item, only if it's the first bet. 95 | if (!isNewBet) inventory.setItem(35, getItem("exit").build()); 96 | 97 | // Assigning slots. 98 | Map slotIndex = new HashMap<>(); 99 | for (int i : SLOTS) { 100 | slotIndex.put(ArrayUtils.indexOf(SLOTS, i), i); 101 | } 102 | 103 | // Where to start. 104 | int startFrom = currentPage * SLOTS.length; 105 | 106 | boolean isLastPage = currentPage == pages - 1; 107 | 108 | for (int index = 0, aux = startFrom; isLastPage ? (index < chips.size() - startFrom) : (index < SLOTS.length); index++, aux++) { 109 | inventory.setItem(slotIndex.get(index), chipManager.createChipItem(chips.get(aux), name, true)); 110 | } 111 | 112 | // Update inventory title to show the current page. 113 | InventoryUpdate.updateInventory(player, Config.CHIP_MENU_TITLE.asStringTranslated() 114 | .replace("%page%", String.valueOf(currentPage + 1)) 115 | .replace("%max%", String.valueOf(pages))); 116 | } 117 | 118 | @Override 119 | public void handle(@NotNull InventoryClickEvent event) { 120 | super.handle(event); 121 | 122 | Player player = (Player) event.getWhoClicked(); 123 | Messages messages = plugin.getMessages(); 124 | 125 | ItemStack current = event.getCurrentItem(); 126 | if (current == null) return; 127 | 128 | if (isCustomItem(current, "bet-all")) { 129 | // Open confirm gui. 130 | runTask(() -> { 131 | ConfirmGUI gui = new ConfirmGUI(game, player, ConfirmGUI.ConfirmType.BET_ALL); 132 | gui.setSourceGUI(this); 133 | gui.setPreviousPage(currentPage); 134 | }); 135 | return; 136 | } 137 | 138 | if (isCustomItem(current, "exit")) { 139 | // Remove player from game. 140 | messages.send(player, Messages.Message.LEAVE_PLAYER); 141 | game.removeCompletely(player); 142 | closeInventory(player); 143 | return; 144 | } 145 | 146 | ItemMeta meta = current.getItemMeta(); 147 | if (meta == null) return; 148 | 149 | String chipName = meta.getPersistentDataContainer().get(plugin.getChipNameKey(), PersistentDataType.STRING); 150 | if (chipName == null) return; 151 | 152 | Chip chip = plugin.getChipManager().getByName(chipName); 153 | if (chip == null) return; 154 | 155 | if (!game.newBet(player, chip, isNewBet)) { 156 | // Not enough money. 157 | event.setCurrentItem(plugin.getItem("not-enough-money").build()); 158 | return; 159 | } 160 | 161 | closeInventory(player); 162 | } 163 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/util/map/MapBuilder.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.util.map; 2 | 3 | import com.cryptomorin.xseries.reflection.XReflection; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import me.matsubara.roulette.RoulettePlugin; 7 | import me.matsubara.roulette.manager.data.RouletteSession; 8 | import me.matsubara.roulette.util.PluginUtils; 9 | import net.md_5.bungee.api.ChatColor; 10 | import org.apache.commons.lang3.tuple.Pair; 11 | import org.bukkit.Bukkit; 12 | import org.bukkit.Material; 13 | import org.bukkit.entity.Player; 14 | import org.bukkit.inventory.ItemStack; 15 | import org.bukkit.map.*; 16 | import org.bukkit.map.MapView.Scale; 17 | import org.jetbrains.annotations.NotNull; 18 | import org.jetbrains.annotations.Nullable; 19 | 20 | import java.awt.*; 21 | import java.awt.image.BufferedImage; 22 | import java.text.Normalizer; 23 | import java.util.*; 24 | import java.util.List; 25 | import java.util.regex.Matcher; 26 | import java.util.regex.Pattern; 27 | 28 | @Getter 29 | @Setter 30 | public final class MapBuilder extends MapRenderer { 31 | 32 | private final RoulettePlugin plugin; 33 | private final UUID playerUUID; 34 | private final RouletteSession session; 35 | private final List texts = new ArrayList<>(); 36 | private final @SuppressWarnings("deprecation") Color color = MapPalette.getColor(MapPalette.DARK_GRAY); // Default color. 37 | private final MapFont font = MinecraftFont.Font; 38 | 39 | private MapView view; 40 | private BufferedImage image; 41 | private ItemStack item; 42 | private boolean rendered; 43 | 44 | private static final Pattern ACCENT_PATTERN = Pattern.compile("\\p{M}"); 45 | 46 | public MapBuilder(RoulettePlugin plugin, UUID playerUUID, RouletteSession session) { 47 | this.plugin = plugin; 48 | this.playerUUID = playerUUID; 49 | this.session = session; 50 | } 51 | 52 | public void setImage(@NotNull BufferedImage image, boolean resize) { 53 | // Resize image to fit in the map. 54 | if (resize && image.getWidth() != 128 && image.getHeight() != 128) { 55 | this.image = MapPalette.resizeImage(image); 56 | } else { 57 | this.image = image; 58 | } 59 | } 60 | 61 | public void addText(int x, int y, @NotNull String text) { 62 | texts.add(new Text(x, y, text)); 63 | } 64 | 65 | @Override 66 | public void render(@NotNull MapView view, @NotNull MapCanvas canvas, @NotNull Player player) { 67 | if (rendered) return; 68 | 69 | if (image != null) { 70 | canvas.drawImage(0, 0, image); 71 | } 72 | 73 | // Write text centered. 74 | for (Text text : texts) { 75 | try { 76 | drawText(canvas, text.x(), text.y(), removeAccents(text.message())); 77 | } catch (IllegalArgumentException exception) { 78 | // Invalid characters or colors. 79 | } 80 | } 81 | 82 | rendered = true; 83 | } 84 | 85 | private String removeAccents(String input) { 86 | String normalized = Normalizer.normalize(input, Normalizer.Form.NFD); 87 | return ACCENT_PATTERN.matcher(normalized).replaceAll(""); 88 | } 89 | 90 | @SuppressWarnings("deprecation") 91 | public void drawText(MapCanvas canvas, int x, int y, @NotNull String text) { 92 | Map, Color> colorMap = new LinkedHashMap<>(); 93 | 94 | Matcher matcher = PluginUtils.PATTERN.matcher(text); 95 | StringBuilder buffer = new StringBuilder(); 96 | while (matcher.find()) { 97 | int start = matcher.start(), end = matcher.end(); 98 | colorMap.put(Pair.of(start, end), ChatColor.of(matcher.group(1)).getColor()); 99 | matcher.appendReplacement(buffer, ""); // Remove pattern. 100 | } 101 | 102 | // Try to center. 103 | String temp = matcher.appendTail(buffer).toString(); 104 | if (x == -1) x = (128 - font.getWidth(temp)) / 2; 105 | 106 | for (int i = 0; i < text.length(); i++) { 107 | MapFont.CharacterSprite sprite = font.getChar(text.charAt(i)); 108 | if (sprite == null) continue; 109 | 110 | // Don't draw color characters. 111 | Map.Entry, Color> colorEntry = getColor(colorMap, i); 112 | if (colorEntry != null) { 113 | Pair coords = colorEntry.getKey(); 114 | Integer start = coords.getKey(), end = coords.getValue(); 115 | if (i >= start && i < end) continue; 116 | } 117 | 118 | // Get color or use default. 119 | Color color = colorEntry != null ? colorEntry.getValue() : this.color; 120 | byte byteColor = MapPalette.matchColor(color); 121 | 122 | for (int h = 0; h < font.getHeight(); h++) { 123 | for (int w = 0; w < sprite.getWidth(); w++) { 124 | if (!sprite.get(h, w)) continue; 125 | 126 | int targetX = x + w; 127 | int targetY = y + h; 128 | 129 | if (XReflection.supports(19)) { 130 | canvas.setPixelColor(targetX, targetY, color); 131 | } else { 132 | canvas.setPixel(targetX, targetY, byteColor); 133 | } 134 | } 135 | } 136 | 137 | x += sprite.getWidth() + 1; 138 | } 139 | } 140 | 141 | private @Nullable Map.Entry, Color> getColor(@NotNull Map, Color> colorMap, int index) { 142 | Map.Entry, Color> color = null; 143 | for (Map.Entry, Color> entry : colorMap.entrySet()) { 144 | Integer start = entry.getKey().getKey(); 145 | if (index >= start) color = entry; 146 | } 147 | return color; 148 | } 149 | 150 | public @Nullable ItemStack build() { 151 | if (item != null) return item; 152 | 153 | view = Bukkit.createMap(Bukkit.getWorlds().get(0)); 154 | view.setScale(Scale.NORMAL); 155 | view.getRenderers().forEach(view::removeRenderer); 156 | view.addRenderer(this); 157 | 158 | return item = plugin.getItem("map-image.item") 159 | .setType(Material.FILLED_MAP) 160 | .replace(line -> plugin.getWinnerManager().loreReplacer(playerUUID, session, line)) 161 | .setMapView(view) 162 | .build(); 163 | } 164 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/animation/DabAnimation.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.animation; 2 | 3 | import fr.skytasul.glowingentities.GlowingEntities; 4 | import lombok.Getter; 5 | import me.matsubara.roulette.RoulettePlugin; 6 | import me.matsubara.roulette.file.Config; 7 | import me.matsubara.roulette.game.Game; 8 | import me.matsubara.roulette.model.stand.PacketStand; 9 | import me.matsubara.roulette.model.stand.StandSettings; 10 | import me.matsubara.roulette.model.stand.animator.ArmorStandAnimator; 11 | import me.matsubara.roulette.model.stand.data.ItemSlot; 12 | import me.matsubara.roulette.util.ColorUtils; 13 | import me.matsubara.roulette.util.ItemBuilder; 14 | import me.matsubara.roulette.util.PluginUtils; 15 | import org.bukkit.*; 16 | import org.bukkit.entity.Player; 17 | import org.bukkit.inventory.ItemStack; 18 | import org.bukkit.scheduler.BukkitRunnable; 19 | import org.bukkit.util.Vector; 20 | import org.jetbrains.annotations.NotNull; 21 | 22 | import java.io.File; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | import java.util.Set; 26 | 27 | @Getter 28 | public class DabAnimation extends BukkitRunnable { 29 | 30 | private final Game game; 31 | private final int speed; 32 | private final boolean glowing; 33 | private final Map animators = new HashMap<>(); 34 | private final Set seeing; 35 | 36 | private static final int THRESHOLD = Integer.MAX_VALUE - 5000; 37 | 38 | public DabAnimation(@NotNull Game game, Player player, Location location) { 39 | this.game = game; 40 | this.speed = Config.DAB_ANIMATION_RAINBOW_EFFECT_SPEED.asInt(); 41 | this.glowing = Config.DAB_ANIMATION_RAINBOW_EFFECT_GLOWING.asBool(); 42 | this.seeing = game.getSeeingPlayers(); 43 | 44 | RoulettePlugin plugin = game.getPlugin(); 45 | File file = new File(plugin.getDataFolder(), "dab_animation.txt"); 46 | 47 | StandSettings settings = new StandSettings(); 48 | settings.getEquipment().put(ItemSlot.HEAD, new ItemBuilder(Material.PLAYER_HEAD) 49 | .setOwningPlayer(player) 50 | .build()); 51 | settings.setBasePlate(false); 52 | settings.setArms(true); 53 | 54 | int amount = Config.DAB_ANIMATION_AMOUNT.asInt(); 55 | int radius = Config.DAB_ANIMATION_RADIUS.asInt(); 56 | 57 | for (int i = 0; i < amount; i++) { 58 | double angle = (2 * Math.PI / amount) * i; 59 | double offsetX = Math.cos(angle) * radius; 60 | double offsetZ = Math.sin(angle) * radius; 61 | 62 | Location spawn = location.clone().add(offsetX, 0.0d, offsetZ); 63 | spawn.setY(player.getWorld().getHighestBlockYAt(spawn) + 1); 64 | lookAt(spawn, location); 65 | 66 | int count = PluginUtils.RANDOM.nextInt(THRESHOLD); 67 | Color color = ColorUtils.convertCountToRGB(count); 68 | 69 | StandSettings clone = settings.clone(); 70 | setEquipment(clone, color); 71 | 72 | ArmorStandAnimator animator = new ArmorStandAnimator(plugin, seeing, file, clone, spawn); 73 | handleGlowingColor(animator.getStand(), color); 74 | 75 | animators.put(animator, count); 76 | } 77 | 78 | game.setDabAnimation(this); 79 | runTaskTimerAsynchronously(plugin, 1L, 1L); 80 | } 81 | 82 | @Override 83 | public void cancel() throws IllegalStateException { 84 | super.cancel(); 85 | 86 | animators.keySet().forEach(ArmorStandAnimator::stop); 87 | animators.clear(); 88 | 89 | game.setDabAnimation(null); 90 | } 91 | 92 | @Override 93 | public void run() { 94 | if (!game.getState().isEnding()) { 95 | cancel(); 96 | return; 97 | } 98 | 99 | for (Map.Entry entry : animators.entrySet()) { 100 | ArmorStandAnimator animator = entry.getKey(); 101 | 102 | PacketStand stand = animator.getStand(); 103 | if (!animator.isSpawned()) { 104 | seeing.forEach(stand::spawn); 105 | animator.setSpawned(true); 106 | } 107 | 108 | Integer count = entry.getValue(); 109 | Color color = ColorUtils.convertCountToRGB(count); 110 | 111 | setEquipment(stand.getSettings(), color); 112 | stand.sendEquipment(seeing); 113 | 114 | animator.update(); 115 | 116 | // Glow the stand to the closest chat color. 117 | handleGlowingColor(stand, color); 118 | 119 | int temp = count + speed; 120 | animators.put(animator, temp >= THRESHOLD ? 0 : temp); 121 | } 122 | } 123 | 124 | private void handleGlowingColor(PacketStand stand, Color color) { 125 | if (!glowing) return; 126 | 127 | GlowingEntities glowing = game.getPlugin().getGlowingEntities(); 128 | if (glowing == null) return; 129 | 130 | World world = game.getLocation().getWorld(); 131 | if (world == null) return; 132 | 133 | int id = stand.getId(); 134 | String team = stand.getUniqueId().toString(); 135 | ChatColor glow = ColorUtils.getClosestChatColor(color); 136 | 137 | try { 138 | for (Player player : world.getPlayers()) { 139 | glowing.setGlowing(id, team, player, glow); 140 | stand.getSettings().setGlow(true); 141 | stand.sendMetadata(seeing); 142 | } 143 | } catch (ReflectiveOperationException ignored) { 144 | 145 | } 146 | } 147 | 148 | private void setEquipment(@NotNull StandSettings settings, Color color) { 149 | Map equipment = settings.getEquipment(); 150 | equipment.put(ItemSlot.CHEST, createArmor(Material.LEATHER_CHESTPLATE, color)); 151 | equipment.put(ItemSlot.LEGS, createArmor(Material.LEATHER_LEGGINGS, color)); 152 | equipment.put(ItemSlot.FEET, createArmor(Material.LEATHER_BOOTS, color)); 153 | } 154 | 155 | private @NotNull ItemStack createArmor(Material material, Color color) { 156 | return new ItemBuilder(material) 157 | .setLeatherArmorMetaColor(color) 158 | .build(); 159 | } 160 | 161 | private void lookAt(@NotNull Location origin, @NotNull Location target) { 162 | Vector direction = target.toVector().subtract(origin.toVector()).normalize(); 163 | 164 | float yaw = (float) Math.toDegrees(Math.atan2(-direction.getX(), direction.getZ())); 165 | float pitch = (float) Math.toDegrees(Math.asin(-direction.getY())); 166 | 167 | origin.setYaw(yaw); 168 | origin.setPitch(pitch); 169 | } 170 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/manager/InputManager.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.manager; 2 | 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.JsonParser; 5 | import me.matsubara.roulette.RoulettePlugin; 6 | import me.matsubara.roulette.file.Config; 7 | import me.matsubara.roulette.file.Messages; 8 | import me.matsubara.roulette.game.Game; 9 | import me.matsubara.roulette.util.PluginUtils; 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.ChatColor; 12 | import org.bukkit.OfflinePlayer; 13 | import org.bukkit.entity.Player; 14 | import org.bukkit.event.EventHandler; 15 | import org.bukkit.event.EventPriority; 16 | import org.bukkit.event.Listener; 17 | import org.bukkit.event.player.AsyncPlayerChatEvent; 18 | import org.bukkit.metadata.FixedMetadataValue; 19 | import org.jetbrains.annotations.NotNull; 20 | 21 | import java.io.BufferedReader; 22 | import java.io.DataOutputStream; 23 | import java.io.IOException; 24 | import java.io.InputStreamReader; 25 | import java.net.HttpURLConnection; 26 | import java.net.URL; 27 | import java.net.URLEncoder; 28 | import java.nio.charset.StandardCharsets; 29 | import java.util.HashMap; 30 | import java.util.Map; 31 | import java.util.UUID; 32 | 33 | public final class InputManager implements Listener { 34 | 35 | private final RoulettePlugin plugin; 36 | private final Map players; 37 | 38 | public InputManager(RoulettePlugin plugin) { 39 | this.plugin = plugin; 40 | this.plugin.getServer().getPluginManager().registerEvents(this, plugin); 41 | this.players = new HashMap<>(); 42 | } 43 | 44 | @EventHandler(priority = EventPriority.LOW) 45 | public void onAsyncPlayerChat(@NotNull AsyncPlayerChatEvent event) { 46 | Player player = event.getPlayer(); 47 | if (!player.hasMetadata("rouletteEditing")) return; 48 | 49 | InputType type = players.get(player.getUniqueId()); 50 | if (type == null) return; 51 | 52 | String message = ChatColor.stripColor(event.getMessage()); 53 | Messages messages = plugin.getMessages(); 54 | GameManager gameManager = plugin.getGameManager(); 55 | 56 | if (message.equalsIgnoreCase(Config.CANCEL_WORD.asString())) { 57 | messages.send(player, Messages.Message.REQUEST_CANCELLED); 58 | event.setCancelled(true); 59 | remove(player); 60 | return; 61 | } 62 | 63 | Game game = gameManager.getGame(player.getMetadata("rouletteEditing").get(0).asString()); 64 | if (game == null) return; 65 | 66 | event.setCancelled(true); 67 | 68 | if (type == InputType.ACCOUNT_NAME) { 69 | if (isInvalidPlayerName(player, message)) return; 70 | 71 | @SuppressWarnings("deprecation") OfflinePlayer target = Bukkit.getOfflinePlayer(ChatColor.stripColor(message)); 72 | if (target.hasPlayedBefore()) { 73 | game.setAccountGiveTo(target.getUniqueId()); 74 | 75 | messages.send(player, Messages.Message.ACCOUNT); 76 | gameManager.save(game); 77 | } else { 78 | messages.send(player, Messages.Message.UNKNOWN_ACCOUNT); 79 | } 80 | } else if (type == InputType.CROUPIER_NAME) { 81 | if (isInvalidPlayerName(player, message)) return; 82 | 83 | // Limited to 16 characters. 84 | String name = PluginUtils.translate(message); 85 | if (name.length() > 16) name = name.substring(0, 16); 86 | 87 | // Team shouldn't be null since we created it @onEnable(). 88 | if (game.getNPCName() != null) { 89 | plugin.getHideTeam().removeEntry(game.getNPCName()); 90 | } 91 | 92 | String texture = game.getNPCTexture(); 93 | String signature = game.getNPCSignature(); 94 | 95 | game.setNPC(name, texture, signature); 96 | messages.send(player, Messages.Message.NPC_RENAMED); 97 | gameManager.save(game); 98 | } else { 99 | plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { 100 | try { 101 | URL target = new URL("https://api.mineskin.org/generate/url"); 102 | 103 | HttpURLConnection connection = (HttpURLConnection) target.openConnection(); 104 | connection.setRequestMethod("POST"); 105 | connection.setDoOutput(true); 106 | connection.setConnectTimeout(1000); 107 | connection.setReadTimeout(30000); 108 | 109 | DataOutputStream out = new DataOutputStream(connection.getOutputStream()); 110 | out.writeBytes("url=" + URLEncoder.encode(message, StandardCharsets.UTF_8)); 111 | out.close(); 112 | 113 | BufferedReader reader = new BufferedReader( 114 | new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); 115 | 116 | JsonObject textureObject = JsonParser.parseReader(reader) 117 | .getAsJsonObject() 118 | .getAsJsonObject("data") 119 | .getAsJsonObject("texture"); 120 | 121 | reader.close(); 122 | 123 | String texture = textureObject.get("value").getAsString(); 124 | String signature = textureObject.get("signature").getAsString(); 125 | 126 | connection.disconnect(); 127 | 128 | String name = game.getNPCName() == null ? "" : game.getNPCName(); 129 | game.setNPC(name, texture, signature); 130 | messages.send(player, Messages.Message.NPC_TEXTURIZED); 131 | gameManager.save(game); 132 | 133 | } catch (IOException throwable) { 134 | messages.send(player, Messages.Message.REQUEST_INVALID); 135 | throwable.printStackTrace(); 136 | } 137 | }); 138 | } 139 | 140 | remove(player); 141 | } 142 | 143 | private boolean isInvalidPlayerName(Player player, @NotNull String message) { 144 | if (message.matches("\\w{3,16}")) return false; 145 | 146 | plugin.getMessages().send(player, Messages.Message.REQUEST_INVALID); 147 | remove(player); 148 | return true; 149 | } 150 | 151 | public void newInput(@NotNull Player player, InputType type, @NotNull Game game) { 152 | players.put(player.getUniqueId(), type); 153 | player.setMetadata("rouletteEditing", new FixedMetadataValue(plugin, game.getName())); 154 | } 155 | 156 | public void remove(@NotNull Player player) { 157 | players.remove(player.getUniqueId()); 158 | player.removeMetadata("rouletteEditing", plugin); 159 | } 160 | 161 | public enum InputType { 162 | ACCOUNT_NAME, 163 | CROUPIER_NAME, 164 | CROUPIER_TEXTURE 165 | } 166 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/npc/NPC.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.npc; 2 | 3 | import com.github.retrooper.packetevents.protocol.player.UserProfile; 4 | import com.google.common.base.Preconditions; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import me.matsubara.roulette.RoulettePlugin; 8 | import me.matsubara.roulette.game.Game; 9 | import me.matsubara.roulette.npc.modifier.*; 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.Location; 12 | import org.bukkit.World; 13 | import org.bukkit.entity.Parrot; 14 | import org.bukkit.entity.Player; 15 | import org.jetbrains.annotations.Contract; 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | import java.util.Set; 19 | import java.util.UUID; 20 | import java.util.concurrent.ConcurrentHashMap; 21 | 22 | @Getter 23 | public class NPC { 24 | 25 | private final Set seeingPlayers = ConcurrentHashMap.newKeySet(); 26 | private final Set insideFOVPlayers = ConcurrentHashMap.newKeySet(); 27 | private final int entityId; 28 | private final UserProfile profile; 29 | private final SpawnCustomizer spawnCustomizer; 30 | private @Setter Location location; 31 | private final Game game; 32 | 33 | public NPC(UserProfile profile, SpawnCustomizer spawnCustomizer, @NotNull Location location, int entityId, Game game) { 34 | World world = location.getWorld(); 35 | if (world != null) insideFOVPlayers.addAll(world.getPlayers().stream() 36 | .map(Player::getUniqueId) 37 | .toList()); 38 | this.entityId = entityId; 39 | this.spawnCustomizer = spawnCustomizer; 40 | this.location = location; 41 | this.profile = profile; 42 | this.game = game; 43 | } 44 | 45 | @Contract(" -> new") 46 | public static @NotNull Builder builder() { 47 | return new Builder(); 48 | } 49 | 50 | public void show(Player player, RoulettePlugin plugin) { 51 | seeingPlayers.add(player); 52 | 53 | VisibilityModifier modifier = visibility(); 54 | modifier.queuePlayerListChange(false).send(player); 55 | 56 | Bukkit.getScheduler().runTaskLater(plugin, () -> { 57 | modifier.queueSpawn().send(player); 58 | spawnCustomizer.handleSpawn(this, player); 59 | 60 | // Keeping the NPC longer in the player list, otherwise the skin might not be shown sometimes. 61 | Bukkit.getScheduler().runTaskLater( 62 | plugin, 63 | () -> modifier.queuePlayerListChange(true).send(player), 64 | 40); 65 | }, 10L); 66 | } 67 | 68 | public void hide(Player player) { 69 | visibility() 70 | .queuePlayerListChange(true) 71 | .queueDestroy() 72 | .send(player); 73 | removeSeeingPlayer(player); 74 | } 75 | 76 | protected void removeSeeingPlayer(Player player) { 77 | seeingPlayers.remove(player); 78 | removeFOV(player); 79 | } 80 | 81 | public void lookAtDefaultLocation(Player... players) { 82 | rotation().queueBodyRotation(location.getYaw(), location.getPitch()).send(players); 83 | } 84 | 85 | public boolean isShownFor(Player player) { 86 | return seeingPlayers.contains(player); 87 | } 88 | 89 | public void removeFOV(@NotNull Player player) { 90 | if (insideFOVPlayers.remove(player.getUniqueId())) { 91 | lookAtDefaultLocation(player); 92 | } 93 | } 94 | 95 | public boolean isInsideFOV(@NotNull Player player) { 96 | return insideFOVPlayers.contains(player.getUniqueId()); 97 | } 98 | 99 | public void setInsideFOV(@NotNull Player player) { 100 | insideFOVPlayers.add(player.getUniqueId()); 101 | } 102 | 103 | public AnimationModifier animation() { 104 | return new AnimationModifier(this); 105 | } 106 | 107 | public RotationModifier rotation() { 108 | return new RotationModifier(this); 109 | } 110 | 111 | public EquipmentModifier equipment() { 112 | return new EquipmentModifier(this); 113 | } 114 | 115 | public MetadataModifier metadata() { 116 | return new MetadataModifier(this); 117 | } 118 | 119 | public VisibilityModifier visibility() { 120 | return new VisibilityModifier(this); 121 | } 122 | 123 | public TeleportModifier teleport() { 124 | return new TeleportModifier(this); 125 | } 126 | 127 | public void toggleParrotVisibility(@NotNull MetadataModifier metadata) { 128 | boolean left = game.getParrotShoulder().isLeft(); 129 | Parrot.Variant variant = game.isParrotEnabled() ? game.getParrotVariant() : null; 130 | metadata.queueShoulderEntity(left, variant); 131 | } 132 | 133 | public static class Builder { 134 | 135 | private UserProfile profile; 136 | private int entityId = -1; 137 | private Location location; 138 | private Game game; 139 | 140 | private SpawnCustomizer spawnCustomizer = (npc, player) -> { 141 | }; 142 | 143 | private Builder() { 144 | } 145 | 146 | public Builder profile(UserProfile profile) { 147 | this.profile = profile; 148 | return this; 149 | } 150 | 151 | public Builder spawnCustomizer(SpawnCustomizer spawnCustomizer) { 152 | this.spawnCustomizer = Preconditions.checkNotNull(spawnCustomizer, "spawnCustomizer"); 153 | return this; 154 | } 155 | 156 | public Builder entityId(int entityId) { 157 | this.entityId = entityId; 158 | return this; 159 | } 160 | 161 | public Builder location(Location location) { 162 | this.location = location; 163 | return this; 164 | } 165 | 166 | public Builder game(Game game) { 167 | this.game = game; 168 | return this; 169 | } 170 | 171 | @NotNull 172 | public NPC build(NPCPool pool) { 173 | if (entityId == -1) { 174 | throw new IllegalArgumentException("No entity id given!"); 175 | } 176 | 177 | if (profile == null) { 178 | throw new IllegalArgumentException("No profile given!"); 179 | } 180 | 181 | if (location == null) { 182 | throw new IllegalArgumentException("No location given!"); 183 | } 184 | 185 | NPC npc = new NPC(profile, spawnCustomizer, location, entityId, game); 186 | pool.takeCareOf(npc); 187 | return npc; 188 | } 189 | } 190 | 191 | @Getter 192 | public enum NPCAction { 193 | LOOK(true, false), 194 | INVITE(false, true), 195 | LOOK_AND_INVITE(true, true), 196 | NONE(false, false); 197 | 198 | private final boolean look, invite; 199 | 200 | NPCAction(boolean look, boolean invite) { 201 | this.look = look; 202 | this.invite = invite; 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/npc/modifier/MetadataModifier.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.npc.modifier; 2 | 3 | import com.cryptomorin.xseries.reflection.XReflection; 4 | import com.github.retrooper.packetevents.protocol.entity.data.EntityData; 5 | import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; 6 | import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; 7 | import com.github.retrooper.packetevents.protocol.entity.pose.EntityPose; 8 | import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; 9 | import com.github.retrooper.packetevents.protocol.nbt.NBTInt; 10 | import com.github.retrooper.packetevents.protocol.nbt.NBTIntArray; 11 | import com.github.retrooper.packetevents.protocol.nbt.NBTString; 12 | import com.github.retrooper.packetevents.protocol.player.SkinSection; 13 | import com.github.retrooper.packetevents.util.Vector3i; 14 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata; 15 | import me.matsubara.roulette.npc.NPC; 16 | import me.matsubara.roulette.util.PluginUtils; 17 | import org.bukkit.entity.EntityType; 18 | import org.bukkit.entity.Parrot; 19 | import org.bukkit.entity.Player; 20 | import org.jetbrains.annotations.NotNull; 21 | import org.jetbrains.annotations.Nullable; 22 | 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | import java.util.Optional; 26 | import java.util.function.Function; 27 | 28 | public class MetadataModifier extends NPCModifier { 29 | 30 | private final List> metadata = new ArrayList<>(); 31 | 32 | private static final String PARROT_ID = EntityType.PARROT.getKey().toString(); 33 | 34 | public MetadataModifier(NPC npc) { 35 | super(npc); 36 | } 37 | 38 | @SuppressWarnings("UnusedReturnValue") 39 | public @NotNull MetadataModifier queueShoulderEntity(boolean left, @Nullable Parrot.Variant variant) { 40 | if (XReflection.supports(21, 9)) { 41 | // Since 1.21.9 we just have to send the variant id. 42 | queue(left ? 43 | EntityMetadata.SHOULDER_ENTITY_LEFT_INT : 44 | EntityMetadata.SHOULDER_ENTITY_RIGHT_INT, variant); 45 | return this; 46 | } 47 | 48 | NBTCompound parrot = new NBTCompound(); 49 | if (variant != null) { 50 | parrot.setTag("id", new NBTString(PARROT_ID)); 51 | parrot.setTag("UUID", new NBTIntArray(PluginUtils.randomUUIDArray())); 52 | parrot.setTag("Variant", new NBTInt(variant.ordinal())); 53 | } 54 | 55 | // Before 1.21.9 we need to send a compound tag. 56 | queue(left ? 57 | EntityMetadata.SHOULDER_ENTITY_LEFT_NBT : 58 | EntityMetadata.SHOULDER_ENTITY_RIGHT_NBT, parrot); 59 | return this; 60 | } 61 | 62 | public MetadataModifier queue(@NotNull EntityMetadata metadata, I value) { 63 | this.metadata.add(new EntityData<>(metadata.index(), metadata.outputType(), metadata.mapper().apply(value))); 64 | return this; 65 | } 66 | 67 | @Override 68 | public void send(@NotNull Iterable players) { 69 | queueInstantly((npc, layer) -> new WrapperPlayServerEntityMetadata(npc.getEntityId(), metadata)); 70 | super.send(players); 71 | } 72 | 73 | @SuppressWarnings("unused") 74 | public record EntityMetadata(int index, EntityDataType outputType, Function mapper) { 75 | 76 | private static final byte ALL_BUT_CAPE = SkinSection.JACKET // Cape is excluded. 77 | .combine(SkinSection.LEFT_SLEEVE) 78 | .combine(SkinSection.RIGHT_SLEEVE) 79 | .combine(SkinSection.LEFT_PANTS) 80 | .combine(SkinSection.RIGHT_PANTS) 81 | .combine(SkinSection.HAT) 82 | .getMask(); 83 | 84 | public static @NotNull EntityMetadata ENTITY_DATA = new EntityMetadata<>( 85 | 0, 86 | EntityDataTypes.BYTE, 87 | input -> input); 88 | 89 | public static final EntityMetadata POSE = new EntityMetadata<>( 90 | 6, 91 | EntityDataTypes.ENTITY_POSE, 92 | pose -> pose); 93 | 94 | public static EntityMetadata TICKS_FROZEN = new EntityMetadata<>( 95 | 7, 96 | EntityDataTypes.INT, 97 | integer -> integer); 98 | 99 | public static EntityMetadata HAND_DATA = new EntityMetadata<>( 100 | 8, 101 | EntityDataTypes.BYTE, 102 | input -> input); 103 | 104 | public static EntityMetadata EFFECT_COLOR = new EntityMetadata<>( 105 | 10, 106 | EntityDataTypes.INT, 107 | integer -> integer); 108 | 109 | public static EntityMetadata EFFECT_AMBIENCE = new EntityMetadata<>( 110 | 11, 111 | EntityDataTypes.BOOLEAN, 112 | bool -> bool); 113 | 114 | public static EntityMetadata ARROW_COUNT = new EntityMetadata<>( 115 | 12, 116 | EntityDataTypes.INT, 117 | integer -> integer); 118 | 119 | public static EntityMetadata BEE_STINGER = new EntityMetadata<>( 120 | 13, 121 | EntityDataTypes.INT, 122 | integer -> integer); 123 | 124 | public static EntityMetadata> BED_POS = new EntityMetadata<>( 125 | 14, 126 | EntityDataTypes.OPTIONAL_BLOCK_POSITION, 127 | Optional::of); 128 | 129 | public static final EntityMetadata SKIN_LAYERS = new EntityMetadata<>( 130 | XReflection.supports(21, 9) ? 16 : 17, 131 | EntityDataTypes.BYTE, 132 | input -> input ? ALL_BUT_CAPE : 0); 133 | 134 | private static final EntityMetadata SHOULDER_ENTITY_LEFT_NBT = new EntityMetadata<>( 135 | 19, 136 | EntityDataTypes.NBT, 137 | nbt -> nbt); 138 | 139 | private static final EntityMetadata SHOULDER_ENTITY_RIGHT_NBT = new EntityMetadata<>( 140 | 20, 141 | EntityDataTypes.NBT, 142 | nbt -> nbt); 143 | 144 | private static final EntityMetadata> SHOULDER_ENTITY_LEFT_INT = new EntityMetadata<>( 145 | 19, 146 | EntityDataTypes.OPTIONAL_INT, 147 | variant -> variant != null ? Optional.of(variant.ordinal()) : Optional.empty()); 148 | 149 | private static final EntityMetadata> SHOULDER_ENTITY_RIGHT_INT = new EntityMetadata<>( 150 | 20, 151 | EntityDataTypes.OPTIONAL_INT, 152 | variant -> variant != null ? Optional.of(variant.ordinal()) : Optional.empty()); 153 | } 154 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/gui/GameChipGUI.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.gui; 2 | 3 | import lombok.Getter; 4 | import me.matsubara.roulette.file.Config; 5 | import me.matsubara.roulette.file.Messages; 6 | import me.matsubara.roulette.game.Game; 7 | import me.matsubara.roulette.game.data.Chip; 8 | import me.matsubara.roulette.manager.ChipManager; 9 | import me.matsubara.roulette.util.InventoryUpdate; 10 | import me.matsubara.roulette.util.ItemBuilder; 11 | import org.apache.commons.lang3.ArrayUtils; 12 | import org.bukkit.Bukkit; 13 | import org.bukkit.Material; 14 | import org.bukkit.entity.Player; 15 | import org.bukkit.event.inventory.ClickType; 16 | import org.bukkit.event.inventory.InventoryClickEvent; 17 | import org.bukkit.inventory.Inventory; 18 | import org.bukkit.inventory.ItemStack; 19 | import org.bukkit.inventory.meta.ItemMeta; 20 | import org.bukkit.persistence.PersistentDataType; 21 | import org.jetbrains.annotations.NotNull; 22 | 23 | import java.util.HashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | @Getter 28 | public final class GameChipGUI extends RouletteGUI { 29 | 30 | // The instance of the game. 31 | private final Game game; 32 | 33 | // The player viewing this inventory. 34 | private final Player player; 35 | 36 | // The inventory being used. 37 | private final Inventory inventory; 38 | 39 | // We'll keep these here, so we can swap them when clicking on them. 40 | private final ItemStack enabled; 41 | private final ItemStack disabled; 42 | 43 | // The slots to show the content. 44 | private static final int[] SLOTS = {10, 11, 12, 13, 14, 15, 16}; 45 | 46 | // The slots to show the status of the content. 47 | private static final int[] STATUS_SLOTS = {19, 20, 21, 22, 23, 24, 25}; 48 | 49 | // The slot to put page navigator items and other stuff. 50 | private static final int[] HOTBAR = {28, 29, 30, 31, 32, 33, 34}; 51 | 52 | public GameChipGUI(@NotNull Game game, @NotNull Player player) { 53 | super(game.getPlugin(), "game-chip-menu", true); 54 | this.game = game; 55 | this.player = player; 56 | this.inventory = Bukkit.createInventory(this, 45); 57 | 58 | enabled = getItem("enabled").build(); 59 | disabled = getItem("disabled").build(); 60 | 61 | player.openInventory(inventory); 62 | updateInventory(); 63 | } 64 | 65 | @Override 66 | public void updateInventory() { 67 | inventory.clear(); 68 | 69 | // Get the list of chips. 70 | ChipManager chipManager = plugin.getChipManager(); 71 | List chips = chipManager.getChips(); 72 | 73 | // Page formula. 74 | pages = (int) (Math.ceil((double) chips.size() / SLOTS.length)); 75 | 76 | ItemStack background = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE) 77 | .setDisplayName("&7") 78 | .build(); 79 | 80 | // Set background items. 81 | for (int i = 0; i < 45; i++) { 82 | if (ArrayUtils.contains(SLOTS, i) 83 | || ArrayUtils.contains(STATUS_SLOTS, i) 84 | || ArrayUtils.contains(HOTBAR, i)) continue; 85 | // Set background item in the current slot from the loop. 86 | inventory.setItem(i, background); 87 | } 88 | 89 | if (currentPage > 0) inventory.setItem(28, getItem("previous").build()); 90 | setMaxBetsItem(); 91 | if (currentPage < pages - 1) inventory.setItem(34, getItem("next").build()); 92 | 93 | Map slotIndex = new HashMap<>(); 94 | for (int i : SLOTS) { 95 | slotIndex.put(ArrayUtils.indexOf(SLOTS, i), i); 96 | } 97 | 98 | int startFrom = currentPage * SLOTS.length; 99 | boolean isLastPage = currentPage == pages - 1; 100 | 101 | for (int index = 0, aux = startFrom; isLastPage ? (index < chips.size() - startFrom) : (index < SLOTS.length); index++, aux++) { 102 | Chip chip = chips.get(aux); 103 | 104 | ItemStack chipItem = chipManager.createChipItem(chip, name, false); 105 | Integer targetIndex = slotIndex.get(index); 106 | 107 | inventory.setItem(targetIndex, chipItem); 108 | setChipStatusItem(targetIndex + 9, chip); 109 | } 110 | 111 | // Update inventory title to show the current page. 112 | InventoryUpdate.updateInventory(player, Config.GAME_CHIP_MENU_TITLE.asStringTranslated() 113 | .replace("%page%", String.valueOf(currentPage + 1)) 114 | .replace("%max%", String.valueOf(pages))); 115 | } 116 | 117 | public void setChipStatusItem(int slot, Chip chip) { 118 | inventory.setItem(slot, new ItemBuilder(game.isChipDisabled(chip) ? disabled : enabled) 119 | .setData(plugin.getChipNameKey(), PersistentDataType.STRING, chip.name()) 120 | .build()); 121 | } 122 | 123 | public void setMaxBetsItem() { 124 | inventory.setItem(31, getItem("max-bets") 125 | .replace("%max-bets%", game.getMaxBets()) 126 | .build()); 127 | } 128 | 129 | @Override 130 | public void handle(@NotNull InventoryClickEvent event) { 131 | super.handle(event); 132 | 133 | Player player = (Player) event.getWhoClicked(); 134 | 135 | ItemStack current = event.getCurrentItem(); 136 | if (current == null) return; 137 | 138 | ItemMeta meta = current.getItemMeta(); 139 | if (meta == null) return; 140 | 141 | if (isCustomItem(current, "max-bets")) { 142 | ClickType click = event.getClick(); 143 | boolean left = click.isLeftClick(), right = click.isRightClick(); 144 | if (!left && !right) return; 145 | 146 | int step = click.isShiftClick() ? 5 : 1; 147 | game.setMaxBets(game.getMaxBets() + (left ? -step : step)); 148 | 149 | setMaxBetsItem(); 150 | 151 | // Save data. 152 | plugin.getGameManager().save(game); 153 | return; 154 | } 155 | 156 | String chipName = meta.getPersistentDataContainer().get(plugin.getChipNameKey(), PersistentDataType.STRING); 157 | if (chipName == null) return; 158 | 159 | Chip chip = plugin.getChipManager().getByName(chipName); 160 | if (chip == null) return; 161 | 162 | if (game.isChipDisabled(chip)) { 163 | game.enableChip(chip); 164 | } else { 165 | if (plugin.getChipManager().getChipsByGame(game).size() == 1) { 166 | plugin.getMessages().send(player, Messages.Message.AT_LEAST_ONE_CHIP_REQUIRED); 167 | closeInventory(player); 168 | return; 169 | } 170 | game.disableChip(chip); 171 | } 172 | 173 | int slot = event.getRawSlot() + (isCustomItem(current, "chip") ? 9 : 0); 174 | setChipStatusItem(slot, chip); 175 | 176 | // Update join hologram and save data. 177 | game.updateJoinHologram(false); 178 | plugin.getGameManager().save(game); 179 | } 180 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/manager/ChipManager.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.manager; 2 | 3 | import com.google.common.base.Predicates; 4 | import lombok.Getter; 5 | import me.matsubara.roulette.RoulettePlugin; 6 | import me.matsubara.roulette.file.Messages; 7 | import me.matsubara.roulette.game.Game; 8 | import me.matsubara.roulette.game.data.Chip; 9 | import me.matsubara.roulette.util.ItemBuilder; 10 | import me.matsubara.roulette.util.PluginUtils; 11 | import org.bukkit.configuration.ConfigurationSection; 12 | import org.bukkit.configuration.InvalidConfigurationException; 13 | import org.bukkit.configuration.file.FileConfiguration; 14 | import org.bukkit.configuration.file.YamlConfiguration; 15 | import org.bukkit.entity.Player; 16 | import org.bukkit.inventory.ItemStack; 17 | import org.bukkit.persistence.PersistentDataType; 18 | import org.jetbrains.annotations.NotNull; 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.util.ArrayList; 24 | import java.util.Comparator; 25 | import java.util.List; 26 | 27 | @Getter 28 | public final class ChipManager { 29 | 30 | private final RoulettePlugin plugin; 31 | private final List chips; 32 | 33 | private File file; 34 | private FileConfiguration configuration; 35 | 36 | private static final String BET_ALL_SKIN = "e36e94f6c34a35465fce4a90f2e25976389eb9709a12273574ff70fd4daa6852"; 37 | 38 | public ChipManager(RoulettePlugin plugin) { 39 | this.plugin = plugin; 40 | this.chips = new ArrayList<>(); 41 | load(); 42 | } 43 | 44 | private void load() { 45 | file = new File(plugin.getDataFolder(), "chips.yml"); 46 | if (!file.exists()) { 47 | plugin.saveResource("chips.yml", false); 48 | } 49 | configuration = new YamlConfiguration(); 50 | try { 51 | configuration.load(file); 52 | update(); 53 | } catch (IOException | InvalidConfigurationException exception) { 54 | exception.printStackTrace(); 55 | } 56 | } 57 | 58 | private void update() { 59 | chips.clear(); 60 | 61 | ConfigurationSection section = configuration.getConfigurationSection("chips"); 62 | if (section == null) return; 63 | 64 | int loaded = 0; 65 | 66 | for (String path : section.getKeys(false)) { 67 | String displayName = hasDisplayName(path) ? getDisplayName(path) : null; 68 | List lore = hasLore(path) ? getLore(path) : null; 69 | String url = configuration.getString("chips." + path + ".url"); 70 | double price = configuration.getDouble("chips." + path + ".price"); 71 | 72 | Chip chip = new Chip(path, displayName, lore, url, price); 73 | chips.add(chip); 74 | loaded++; 75 | } 76 | 77 | if (loaded > 0) { 78 | plugin.getLogger().info("All chips have been loaded from chips.yml!"); 79 | chips.sort(Comparator.comparing(Chip::price)); 80 | return; 81 | } 82 | 83 | plugin.getLogger().info("No chips have been loaded from chips.yml, why don't you create one?"); 84 | } 85 | 86 | private boolean hasDisplayName(String path) { 87 | return configuration.get("chips." + path + ".display-name") != null; 88 | } 89 | 90 | private boolean hasLore(String path) { 91 | return configuration.get("chips." + path + ".lore") != null; 92 | } 93 | 94 | private String getDisplayName(String path) { 95 | return PluginUtils.translate(configuration.getString("chips." + path + ".display-name")); 96 | } 97 | 98 | private @NotNull List getLore(String path) { 99 | return PluginUtils.translate(configuration.getStringList("chips." + path + ".lore")); 100 | } 101 | 102 | public List getChipsByGame(@NotNull Game game) { 103 | return chips.stream() 104 | .filter(Predicates.not(game::isChipDisabled)) 105 | .toList(); 106 | } 107 | 108 | public @Nullable Chip getChipByPrice(Game game, double money) { 109 | for (Chip chip : getChipsByGame(game)) { 110 | if (money == chip.price()) return chip; 111 | } 112 | return null; 113 | } 114 | 115 | public @NotNull Chip getExistingOrBetAll(Game game, double money) { 116 | // If the bet-all money is the same of one chip from chips.yml, use that chip. 117 | Chip chip = plugin.getChipManager().getChipByPrice(game, money); 118 | if (chip != null) return chip; 119 | 120 | // If the @bet-all item has URL, use it. Otherwise, use a default one. 121 | String skin = plugin.getConfig().getString("chip-menu.items.bet-all.url", BET_ALL_SKIN); 122 | 123 | return new Chip("bet-all", skin, money); 124 | } 125 | 126 | public Double getMinAmount(Game game) { 127 | // If no chip, return "unreacheable" value. 128 | List chips = getChipsByGame(game); 129 | return chips.isEmpty() ? Double.MAX_VALUE : chips.get(0).price(); 130 | } 131 | 132 | public Double getMaxAmount(Game game) { 133 | // If no chip, return "unreacheable" value. 134 | List chips = getChipsByGame(game); 135 | return chips.isEmpty() ? Double.MAX_VALUE : chips.get(chips.size() - 1).price(); 136 | } 137 | 138 | public boolean hasEnoughMoney(Game game, Player player) { 139 | double minAmount = getMinAmount(game); 140 | if (plugin.getEconomyExtension().has(player, minAmount)) return true; 141 | 142 | plugin.getMessages().send(player, 143 | Messages.Message.MIN_REQUIRED, 144 | message -> message.replace("%money%", PluginUtils.format(minAmount))); 145 | return false; 146 | } 147 | 148 | public @Nullable Chip getByName(String name) { 149 | for (Chip chip : chips) { 150 | if (chip.name().equalsIgnoreCase(name)) return chip; 151 | } 152 | return null; 153 | } 154 | 155 | public ItemStack createChipItem(@NotNull Chip chip, String guiName, boolean isShop) { 156 | ItemBuilder builder = plugin.getItem(guiName + ".items.chip"); 157 | 158 | if (isShop) { 159 | String displayName = chip.displayName(); 160 | if (displayName != null) builder.setDisplayName(displayName); 161 | 162 | List lore = chip.lore(); 163 | if (lore != null) builder.setLore(lore); 164 | } 165 | 166 | return builder 167 | .setHead(chip.url(), true) 168 | .replace("%money%", PluginUtils.format(chip.price())) 169 | .setData(plugin.getChipNameKey(), PersistentDataType.STRING, chip.name()) 170 | .build(); 171 | } 172 | 173 | public void reloadConfig() { 174 | try { 175 | configuration = new YamlConfiguration(); 176 | configuration.load(file); 177 | update(); 178 | } catch (IOException | InvalidConfigurationException exception) { 179 | exception.printStackTrace(); 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/listener/protocol/SteerVehicle.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.listener.protocol; 2 | 3 | import com.github.retrooper.packetevents.event.PacketListenerPriority; 4 | import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract; 5 | import com.github.retrooper.packetevents.event.simple.PacketPlayReceiveEvent; 6 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 7 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerInput; 8 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientSteerVehicle; 9 | import lombok.Getter; 10 | import me.matsubara.roulette.RoulettePlugin; 11 | import me.matsubara.roulette.file.Config; 12 | import me.matsubara.roulette.game.Game; 13 | import me.matsubara.roulette.game.GameRule; 14 | import me.matsubara.roulette.game.GameState; 15 | import me.matsubara.roulette.game.data.Bet; 16 | import me.matsubara.roulette.game.data.PlayerInput; 17 | import me.matsubara.roulette.gui.BetsGUI; 18 | import me.matsubara.roulette.gui.ConfirmGUI; 19 | import me.matsubara.roulette.manager.GameManager; 20 | import org.bukkit.entity.Player; 21 | import org.jetbrains.annotations.NotNull; 22 | import org.jetbrains.annotations.Nullable; 23 | 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.UUID; 28 | import java.util.concurrent.ConcurrentHashMap; 29 | 30 | public final class SteerVehicle extends SimplePacketListenerAbstract { 31 | 32 | private final RoulettePlugin plugin; 33 | private final Map cooldown = new HashMap<>(); 34 | private final @Getter Map input = new ConcurrentHashMap<>(); 35 | 36 | public SteerVehicle(RoulettePlugin plugin) { 37 | super(PacketListenerPriority.HIGHEST); 38 | this.plugin = plugin; 39 | } 40 | 41 | public PlayerInput getInput(@NotNull Player player) { 42 | return input.getOrDefault(player.getUniqueId(), PlayerInput.ZERO); 43 | } 44 | 45 | public void removeInput(@NotNull Player player) { 46 | input.remove(player.getUniqueId()); 47 | } 48 | 49 | @Override 50 | public void onPacketPlayReceive(@NotNull PacketPlayReceiveEvent event) { 51 | PacketType.Play.Client type = event.getPacketType(); 52 | 53 | if (type != PacketType.Play.Client.STEER_VEHICLE 54 | && (!GameManager.MODERN_APPROACH || type != PacketType.Play.Client.PLAYER_INPUT)) return; 55 | 56 | if (!(event.getPlayer() instanceof Player player)) return; 57 | 58 | PlayerInput input = createInput(event, type); 59 | this.input.put(player.getUniqueId(), input); 60 | 61 | if (GameManager.MODERN_APPROACH) return; 62 | 63 | handle(player, null, input); 64 | } 65 | 66 | private @NotNull PlayerInput createInput(@NotNull PacketPlayReceiveEvent event, PacketType.Play.Client type) { 67 | if (type == PacketType.Play.Client.STEER_VEHICLE) { 68 | WrapperPlayClientSteerVehicle wrapper = new WrapperPlayClientSteerVehicle(event); 69 | return new PlayerInput( 70 | wrapper.getSideways(), 71 | wrapper.getForward(), 72 | wrapper.isJump(), 73 | wrapper.isUnmount(), 74 | false); 75 | } 76 | 77 | WrapperPlayClientPlayerInput wrapper = new WrapperPlayClientPlayerInput(event); 78 | return new PlayerInput( 79 | wrapper.isLeft() ? 0.98f : wrapper.isRight() ? -0.98f : 0.0f, 80 | wrapper.isForward() ? 0.98f : wrapper.isBackward() ? -0.98f : 0.0f, 81 | wrapper.isJump(), 82 | wrapper.isShift(), 83 | wrapper.isSprint()); 84 | } 85 | 86 | public void handle(Player player, @Nullable Game game, @NotNull PlayerInput input) { 87 | // Nothing changed? 88 | if (input.forward() == 0.0f 89 | && input.sideways() == 0.0f 90 | && !input.jump() 91 | && !input.dismount()) return; 92 | 93 | Game temp = game != null ? game : plugin.getGameManager().getGameByPlayer(player); 94 | if (temp == null) return; 95 | 96 | float forward = input.forward(), sideways = input.sideways(); 97 | boolean jump = input.jump(), dismount = input.dismount(); 98 | 99 | // Handle shift. 100 | if (dismount) { 101 | // Open leave confirms gui to the player sync. 102 | runTask(() -> new ConfirmGUI(temp, player, ConfirmGUI.ConfirmType.LEAVE)); 103 | return; 104 | } 105 | 106 | // The player is in cooldown. 107 | boolean cooldown = this.cooldown.getOrDefault(player.getUniqueId(), 0L) > System.currentTimeMillis(); 108 | if (cooldown) return; 109 | 110 | // For some reason, the player still has no bets. 111 | List bets = temp.getBets(player); 112 | if (bets.isEmpty()) return; 113 | 114 | GameState state = temp.getState(); 115 | 116 | // Move the current bet. 117 | Bet bet = temp.getSelectedBet(player); 118 | if (bet == null) return; 119 | 120 | // Handle chip movement (up/down/left/right). 121 | if ((forward != 0.0f || sideways != 0.0f) && canMoveChip(player, temp, bet)) { 122 | temp.moveDirectionalChip(player, forward, sideways); 123 | } 124 | 125 | // Handle chair movement (left/right). 126 | if (sideways != 0.0f && canSwapChair(player, temp)) { 127 | runTask(() -> temp.sitPlayer(player, sideways < 0.0f)); 128 | } 129 | 130 | // Handle jump. 131 | if (jump 132 | && state.isSelecting() 133 | && !temp.isDone(player) 134 | && !bets.isEmpty() 135 | && (bets.size() > 1 || bets.get(0).hasChip())) { 136 | // Only allow opening the bet GUI if the player doesn't have any bet in prison 137 | // and already has a bet with a chip. 138 | runTask(() -> new BetsGUI(temp, player)); 139 | } 140 | 141 | // Put player in cooldown. 142 | long interval = Math.max(200L, Config.MOVE_INTERVAL.asLong()); 143 | this.cooldown.put(player.getUniqueId(), System.currentTimeMillis() + interval); 144 | } 145 | 146 | private void runTask(Runnable runnable) { 147 | plugin.getServer().getScheduler().runTask(plugin, runnable); 148 | } 149 | 150 | private boolean canMoveChip(Player player, @NotNull Game game, Bet bet) { 151 | return game.getState().isSelecting() && !game.isDone(player) && (!game.isRuleEnabled(GameRule.EN_PRISON) || !bet.isEnPrison()); 152 | } 153 | 154 | private boolean canSwapChair(Player player, @NotNull Game game) { 155 | boolean prison = game.getBets(player).stream().anyMatch(Bet::isEnPrison); 156 | return hasSwapChairPermission(player) && (!game.getState().isSelecting() || game.isDone(player) || prison); 157 | } 158 | 159 | private boolean hasSwapChairPermission(Player player) { 160 | return Config.SWAP_CHAIR.asBool() || player.hasPermission("roulette.swapchair"); 161 | } 162 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/manager/StandManager.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.manager; 2 | 3 | import me.matsubara.roulette.RoulettePlugin; 4 | import me.matsubara.roulette.animation.DabAnimation; 5 | import me.matsubara.roulette.animation.MoneyAnimation; 6 | import me.matsubara.roulette.file.Config; 7 | import me.matsubara.roulette.game.Game; 8 | import me.matsubara.roulette.game.data.Bet; 9 | import me.matsubara.roulette.hologram.Hologram; 10 | import me.matsubara.roulette.model.Model; 11 | import me.matsubara.roulette.model.stand.PacketStand; 12 | import me.matsubara.roulette.model.stand.animator.ArmorStandAnimator; 13 | import org.bukkit.Bukkit; 14 | import org.bukkit.Location; 15 | import org.bukkit.Server; 16 | import org.bukkit.World; 17 | import org.bukkit.entity.Player; 18 | import org.bukkit.event.EventHandler; 19 | import org.bukkit.event.Listener; 20 | import org.bukkit.event.player.PlayerChangedWorldEvent; 21 | import org.bukkit.event.player.PlayerJoinEvent; 22 | import org.bukkit.event.player.PlayerQuitEvent; 23 | import org.jetbrains.annotations.NotNull; 24 | 25 | import java.util.Collection; 26 | import java.util.Objects; 27 | import java.util.Set; 28 | import java.util.UUID; 29 | 30 | public final class StandManager implements Listener, Runnable { 31 | 32 | private final RoulettePlugin plugin; 33 | 34 | private static final double BUKKIT_VIEW_DISTANCE = Math.pow(Bukkit.getViewDistance() << 4, 2); 35 | 36 | public StandManager(RoulettePlugin plugin) { 37 | this.plugin = plugin; 38 | Server server = this.plugin.getServer(); 39 | server.getPluginManager().registerEvents(this, plugin); 40 | server.getScheduler().runTaskTimerAsynchronously(plugin, this, 20L, 20L); 41 | } 42 | 43 | @Override 44 | public void run() { 45 | // Here we will handle the visibility of the table to the players in the world. 46 | // This approach should be much better than doing it in PlayerMoveEvent. 47 | for (Game game : plugin.getGameManager().getGames()) { 48 | World world = game.getLocation().getWorld(); 49 | if (world == null) continue; 50 | 51 | for (Player player : world.getPlayers()) { 52 | handleStandRender(game, player, player.getLocation(), HandleCause.MOVE); 53 | } 54 | } 55 | } 56 | 57 | @EventHandler 58 | public void onPlayerQuit(@NotNull PlayerQuitEvent event) { 59 | UUID uuid = event.getPlayer().getUniqueId(); 60 | for (Game game : plugin.getGameManager().getGames()) { 61 | game.getModel().getOut().remove(uuid); 62 | } 63 | } 64 | 65 | @EventHandler 66 | public void onPlayerJoin(@NotNull PlayerJoinEvent event) { 67 | Player player = event.getPlayer(); 68 | handleStandRender(player, player.getLocation()); 69 | } 70 | 71 | @EventHandler 72 | public void onPlayerChangedWorld(@NotNull PlayerChangedWorldEvent event) { 73 | Player player = event.getPlayer(); 74 | handleStandRender(player, player.getLocation()); 75 | } 76 | 77 | public double getRenderDistance() { 78 | double distance = Config.RENDER_DISTANCE.asDouble(); 79 | return Math.min(distance * distance, BUKKIT_VIEW_DISTANCE); 80 | } 81 | 82 | public boolean isInRange(@NotNull Location location, @NotNull Location check) { 83 | return Objects.equals(location.getWorld(), check.getWorld()) 84 | && location.distanceSquared(check) <= getRenderDistance(); 85 | } 86 | 87 | private void handleStandRender(Player player, Location location) { 88 | for (Game game : plugin.getGameManager().getGames()) { 89 | handleStandRender(game, player, location, HandleCause.SPAWN); 90 | } 91 | } 92 | 93 | public void handleStandRender(@NotNull Game game, @NotNull Player player, Location location, HandleCause cause) { 94 | Model model = game.getModel(); 95 | Set out = model.getOut(); 96 | UUID playerUUID = player.getUniqueId(); 97 | 98 | // The table is in another world, there is no need to send packets. 99 | if (!Objects.equals(player.getWorld(), model.getLocation().getWorld())) { 100 | out.add(playerUUID); 101 | return; 102 | } 103 | 104 | boolean range = isInRange(model.getLocation(), location); 105 | boolean ignored = out.contains(playerUUID); 106 | boolean spawn = cause == HandleCause.SPAWN; 107 | 108 | boolean show = range && (ignored || spawn); 109 | boolean destroy = !range && !ignored; 110 | if (!show && !destroy) return; 111 | 112 | if (show) { 113 | out.remove(playerUUID); 114 | } else { 115 | out.add(playerUUID); 116 | if (spawn) return; 117 | } 118 | 119 | // Spawn the entire model in another thread. 120 | plugin.getPool().execute(() -> { 121 | // Show/hide model stands. 122 | handleStandRender(player, model.getStands(), show); 123 | 124 | // Show/hide holograms stands. 125 | for (Bet bet : game.getAllBets()) { 126 | 127 | // Show/hide chip stand. 128 | if (bet.hasStand()) { 129 | handleStandRender(player, bet.getStand(), show); 130 | } 131 | 132 | // Show/hide hologram. 133 | if (bet.hasHologram() && bet.getHologram().isVisibleTo(player)) { 134 | handleStandRender(player, bet.getHologram().getStands(), show); 135 | } 136 | } 137 | 138 | // Show/hide join hologram stands. 139 | Hologram join = game.getJoinHologram(); 140 | handleStandRender(player, join.getStands(), show && join.isVisibleTo(player)); 141 | 142 | // Show/hide spin hologram stands. 143 | if (game.isSpinningGlobal() || game.isPlaying(player)) { 144 | handleStandRender(player, game.getSpinHologram().getStands(), show); 145 | } 146 | 147 | // Show/hide selected stands. 148 | PacketStand markerDollyOne = game.getMarkerDollyOne(); 149 | PacketStand markerDollyTwo = game.getMarkerDollyTwo(); 150 | if (markerDollyOne != null) handleStandRender(player, markerDollyOne, show); 151 | if (markerDollyTwo != null) handleStandRender(player, markerDollyTwo, show); 152 | 153 | // Show/hide money animation stands. 154 | MoneyAnimation money = game.getMoneyAnimation(); 155 | if (money != null && money.getSeeing().contains(player)) { 156 | handleStandRender(player, money.getMoneySlot(), show); 157 | } 158 | 159 | // Show/hide dab animation stands. 160 | DabAnimation dab = game.getDabAnimation(); 161 | if (dab == null || !dab.getSeeing().contains(player)) return; 162 | 163 | for (ArmorStandAnimator animator : dab.getAnimators().keySet()) { 164 | handleStandRender(player, animator.getStand(), show); 165 | } 166 | }); 167 | } 168 | 169 | private void handleStandRender(Player player, @NotNull Collection stands, boolean show) { 170 | for (PacketStand stand : stands) { 171 | handleStandRender(player, stand, show); 172 | } 173 | } 174 | 175 | private void handleStandRender(Player player, PacketStand stand, boolean show) { 176 | if (show) { 177 | stand.spawn(player); 178 | } else { 179 | stand.destroy(player); 180 | } 181 | } 182 | 183 | public enum HandleCause { 184 | SPAWN, 185 | TELEPORT, 186 | MOVE 187 | } 188 | } -------------------------------------------------------------------------------- /src/main/java/me/matsubara/roulette/model/stand/animator/ArmorStandAnimator.java: -------------------------------------------------------------------------------- 1 | package me.matsubara.roulette.model.stand.animator; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import me.matsubara.roulette.RoulettePlugin; 6 | import me.matsubara.roulette.model.stand.PacketStand; 7 | import me.matsubara.roulette.model.stand.StandSettings; 8 | import org.apache.commons.lang3.ArrayUtils; 9 | import org.bukkit.Location; 10 | import org.bukkit.entity.Player; 11 | import org.bukkit.util.Consumer; 12 | import org.bukkit.util.EulerAngle; 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | import java.io.BufferedReader; 16 | import java.io.File; 17 | import java.io.FileReader; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.Set; 21 | 22 | @Getter 23 | @Setter 24 | public final class ArmorStandAnimator { 25 | 26 | private final PacketStand stand; 27 | private final Set seeing; 28 | private boolean spawned; 29 | 30 | private Frame[] frames; 31 | private Location location; 32 | 33 | private int length; 34 | private int currentFrame; 35 | 36 | private boolean paused; 37 | private boolean interpolate; 38 | 39 | private static final Map CACHE = new HashMap<>(); 40 | 41 | public ArmorStandAnimator(RoulettePlugin plugin, Set seeing, @NotNull File file, StandSettings settings, Location location) { 42 | this.stand = new PacketStand(plugin, location, settings); 43 | this.seeing = seeing; 44 | this.location = stand.getLocation(); 45 | 46 | String path = file.getAbsolutePath(); 47 | AnimatorCache cache = CACHE.get(path); 48 | 49 | if (cache != null) { 50 | this.frames = cache.frames(); 51 | this.length = cache.length(); 52 | this.interpolate = cache.interpolate(); 53 | return; 54 | } 55 | 56 | try (BufferedReader reader = new BufferedReader(new FileReader(file))) { 57 | String line; 58 | Frame currentFrame = null; 59 | while ((line = reader.readLine()) != null) { 60 | String[] split = line.split(" "); 61 | if (line.startsWith("length")) { 62 | this.length = (int) Float.parseFloat(split[1]); 63 | this.frames = new Frame[this.length]; 64 | continue; 65 | } 66 | 67 | if (line.startsWith("frame")) { 68 | if (currentFrame != null) { 69 | this.frames[currentFrame.getId()] = currentFrame; 70 | } 71 | int frameID = Integer.parseInt(split[1]); 72 | currentFrame = new Frame(); 73 | currentFrame.setId(frameID); 74 | continue; 75 | } 76 | 77 | if (line.contains("interpolate")) { 78 | this.interpolate = true; 79 | continue; 80 | } 81 | 82 | if (currentFrame == null) continue; 83 | 84 | if (line.contains("Armorstand_Position")) { 85 | currentFrame.setX(Float.parseFloat(split[1])); 86 | currentFrame.setY(Float.parseFloat(split[2])); 87 | currentFrame.setZ(Float.parseFloat(split[3])); 88 | currentFrame.setRotation(Float.parseFloat(split[4])); 89 | continue; 90 | } 91 | 92 | if (setAngle(line, "Armorstand_Right_Leg", currentFrame::setRightLeg)) continue; 93 | if (setAngle(line, "Armorstand_Left_Leg", currentFrame::setLeftLeg)) continue; 94 | if (setAngle(line, "Armorstand_Left_Arm", currentFrame::setLeftArm)) continue; 95 | if (setAngle(line, "Armorstand_Right_Arm", currentFrame::setRightArm)) continue; 96 | setAngle(line, "Armorstand_Head", currentFrame::setHead); 97 | } 98 | 99 | if (currentFrame != null) this.frames[currentFrame.getId()] = currentFrame; 100 | } catch (Exception exception) { 101 | exception.printStackTrace(); 102 | } 103 | 104 | CACHE.put(path, new AnimatorCache(ArrayUtils.clone(this.frames), length, interpolate)); 105 | } 106 | 107 | private boolean setAngle(@NotNull String line, String part, Consumer setter) { 108 | if (!line.contains(part)) return false; 109 | try { 110 | String[] split = line.split(" "); 111 | double x = toRadians(split[1]); 112 | double y = toRadians(split[2]); 113 | double z = toRadians(split[3]); 114 | setter.accept(new EulerAngle(x, y, z)); 115 | return true; 116 | } catch (IndexOutOfBoundsException exception) { 117 | return false; 118 | } 119 | } 120 | 121 | private double toRadians(String data) { 122 | return Math.toRadians(Double.parseDouble(data)); 123 | } 124 | 125 | public void stop() { 126 | this.currentFrame = 0; 127 | this.paused = true; 128 | stand.destroy(); 129 | } 130 | 131 | public void play() { 132 | this.paused = false; 133 | } 134 | 135 | public void update() { 136 | if (this.paused) return; 137 | 138 | if (this.currentFrame >= this.length - 1 || this.currentFrame < 0) { 139 | this.currentFrame = 0; 140 | } 141 | 142 | Frame frame = this.frames[this.currentFrame]; 143 | if (this.interpolate && frame == null) { 144 | frame = interpolate(this.currentFrame); 145 | } 146 | 147 | if (frame != null) { 148 | Location newLocation = this.location.clone().add(frame.getX(), frame.getY(), frame.getZ()); 149 | newLocation.setYaw(frame.getRotation() + newLocation.getYaw()); 150 | this.stand.teleport(seeing, newLocation); 151 | 152 | StandSettings settings = this.stand.getSettings(); 153 | settings.setLeftLegPose(frame.getLeftLeg()); 154 | settings.setRightLegPose(frame.getRightLeg()); 155 | settings.setLeftArmPose(frame.getLeftArm()); 156 | settings.setRightArmPose(frame.getRightArm()); 157 | settings.setHeadPose(frame.getHead()); 158 | 159 | stand.sendMetadata(seeing); 160 | } 161 | 162 | this.currentFrame++; 163 | } 164 | 165 | private Frame interpolate(int id) { 166 | Frame minFrame = null; 167 | for (int i = id; i >= 0; i--) { 168 | if (this.frames[i] != null) { 169 | minFrame = this.frames[i]; 170 | break; 171 | } 172 | } 173 | 174 | Frame maxFrame = null; 175 | for (int j = id; j < this.frames.length; j++) { 176 | if (this.frames[j] != null) { 177 | maxFrame = this.frames[j]; 178 | break; 179 | } 180 | } 181 | 182 | Frame interpolated; 183 | if (maxFrame == null || minFrame == null) { 184 | if (maxFrame == null && minFrame != null) return minFrame; 185 | if (maxFrame != null) return maxFrame; 186 | interpolated = new Frame(); 187 | interpolated.setId(id); 188 | return interpolated; 189 | } 190 | 191 | interpolated = new Frame(); 192 | interpolated.setId(id); 193 | 194 | float lowerDifference = (id - minFrame.getId()); 195 | float totalDifference = (maxFrame.getId() - minFrame.getId()); 196 | float interpolationFactor = lowerDifference / totalDifference; 197 | 198 | return minFrame 199 | .multiply(1.0f - interpolationFactor, id) 200 | .add(maxFrame.multiply(interpolationFactor, id), id); 201 | } 202 | } --------------------------------------------------------------------------------