Compare commits
67
Commits
16f15400fd
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dce9d9f30 | ||
|
|
95219c17bc | ||
|
|
d412812dcb | ||
|
|
21f12904ac | ||
|
|
416c44a8f3 | ||
|
|
fc7b5c76a6 | ||
|
|
9198af6c29 | ||
|
|
d5266b659b | ||
|
|
e9e7c1cc80 | ||
|
|
40f52a4a45 | ||
|
|
3906bd747b | ||
|
|
0d757aa2ab | ||
|
|
5f35866e7a | ||
|
|
491e6a6050 | ||
|
|
244f8f6df7 | ||
|
|
fe10d57978 | ||
|
|
8d05658214 | ||
|
|
7324177250 | ||
|
|
c7161f707c | ||
|
|
5ce26782f2 | ||
|
|
a137ef4cc6 | ||
|
|
62c9661c82 | ||
|
|
936cabfd42 | ||
|
|
38f8ffd4a3 | ||
|
|
1a9f4cbd2d | ||
|
|
99bff5446d | ||
|
|
f9e00b04cc | ||
|
|
a7ce376abb | ||
|
|
897c8ab14d | ||
|
|
a3085ad4b4 | ||
|
|
29c46ba485 | ||
|
|
ca7b6f1152 | ||
|
|
4bf6ca9dc4 | ||
|
|
f0d3a20283 | ||
|
|
233cffd2df | ||
|
|
dd060bce48 | ||
|
|
10a6103f32 | ||
|
|
646b9a2a35 | ||
|
|
09b48fbafe | ||
|
|
6cd5315d28 | ||
|
|
cf011ae72f | ||
|
|
c8e85f186a | ||
|
|
2b6480c880 | ||
|
|
4cf4361286 | ||
|
|
016ab17ef2 | ||
|
|
4c1fd4d228 | ||
|
|
8ba514c9c2 | ||
|
|
25c95dd46a | ||
|
|
58d0b7fa19 | ||
|
|
e97a6ade73 | ||
|
|
d7160b86cd | ||
|
|
02228f570d | ||
|
|
c2d83492d1 | ||
|
|
161344b30c | ||
|
|
c969873e0a | ||
|
|
9db0e70425 | ||
|
|
5743bc63a5 | ||
|
|
d3946bc16a | ||
|
|
0cf662dfef | ||
|
|
2746a43837 | ||
|
|
c832a6648b | ||
|
|
ab6112b9da | ||
|
|
2fd6d4ba30 | ||
|
|
f248372fcc | ||
|
|
2846a88125 | ||
|
|
51ae3bbece | ||
|
|
5ac98467f0 |
+4
-1
@@ -40,4 +40,7 @@ bin/
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
### Ignore bat files ###
|
||||
*bat
|
||||
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
Generated
+16
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,206 @@
|
||||
# 🎮 HungerGames Plugin — Development Checklist
|
||||
|
||||
A Minecraft survival/PvP plugin with configurable loot, shrinking world border, stats tracking, and structured round management.
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Table of Contents
|
||||
- [Core Round Flow](#-core-round-flow)
|
||||
- [Player Spawning](#-player-spawning)
|
||||
- [Chest System](#-chest-system)
|
||||
- [World Border](#-world-border)
|
||||
- [Combat & Gameplay](#-combat--gameplay)
|
||||
- [End Game](#-end-game)
|
||||
- [Commands](#-commands)
|
||||
- [Stats System](#-stats-system)
|
||||
- [Configuration](#-configuration)
|
||||
- [Player Registration](#-player-registration--session-management)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Core Round Flow
|
||||
|
||||
- [ ] Implement round state machine (`LOBBY` → `COUNTDOWN` → `ACTIVE` → `ENDGAME` → `FINISHED`)
|
||||
- [ ] Configurable countdown duration (default: 10 seconds)
|
||||
- [ ] Broadcast countdown in chat each second
|
||||
- [ ] Freeze all players during countdown (prevent movement/interaction)
|
||||
- [ ] Unfreeze players and start round on countdown end
|
||||
- [ ] Announce round start in chat
|
||||
- [ ] Track round number and persist across restarts
|
||||
|
||||
---
|
||||
|
||||
## 🧍 Player Spawning
|
||||
|
||||
- [ ] Spawn players in a configurable radius around a center point
|
||||
- [ ] Ensure each player spawns on top of a solid block
|
||||
- [ ] Freeze players in place until round starts (prevent movement, block breaking, PvP)
|
||||
- [ ] Teleport dead players to a designated "spectator room" (configurable location)
|
||||
- [ ] Prevent dead/spectating players from interacting with the game world
|
||||
|
||||
---
|
||||
|
||||
## 📦 Chest System
|
||||
|
||||
- [ ] Define two chest tiers: **Center** and **Normal**
|
||||
- [ ] Each tier has its own loot table configuration
|
||||
- [ ] Items grouped by **rarity** (e.g. common, uncommon, rare, legendary)
|
||||
- [ ] Configurable number of items per rarity group in each chest tier
|
||||
- [ ] Configurable max stack size / quantity per item type
|
||||
- [ ] Randomly select items from rarity groups when populating chests
|
||||
- [ ] Chests can only be **opened once per game** (track opened chests per round)
|
||||
- [ ] Chests automatically refill/reset between rounds
|
||||
- [ ] Better loot scaling in later game stages (e.g. after border shrink events, or after X players remain)
|
||||
- [ ] Optionally mark opened chests visually (particle effect, different block state, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 🌐 World Border
|
||||
|
||||
- [ ] Set initial world border size at round start (configurable)
|
||||
- [ ] Configure a schedule of shrink events (size → duration pairs)
|
||||
- [ ] Announce each upcoming shrink in chat: `"Border shrinking to {size} over {duration}s"`
|
||||
- [ ] Show shrink progress on a **boss bar** (label + progress percentage)
|
||||
- [ ] Remove/reset boss bar when shrink completes
|
||||
- [ ] Final stage: when 4 players remain, teleport them to a **preset arena position**
|
||||
- [ ] Lock border to a small configured size during final 4 stage so players cannot escape
|
||||
|
||||
---
|
||||
|
||||
## ⚔️ Combat & Gameplay
|
||||
|
||||
- [ ] Disable hunger loss and saturation drain during rounds
|
||||
- [ ] Grant **Speed II** to all players at round start (configurable duration, default: 60s)
|
||||
- [ ] Track damage dealt per player per round
|
||||
- [ ] Broadcast **death messages** in chat: `"{killer} killed {victim}"`
|
||||
- [ ] Handle non-PvP deaths gracefully in death messages (fall, void, etc.)
|
||||
- [ ] Award kill credit correctly for kills via fire, poison, knockback, etc.
|
||||
|
||||
---
|
||||
|
||||
## 🏁 End Game
|
||||
|
||||
- [ ] Detect when only 1 player remains alive
|
||||
- [ ] Broadcast winner message in chat: `"{player} wins the round!"`
|
||||
- [ ] Persist winner data (store per-round winner in stats)
|
||||
- [ ] Show **locator bar** (action bar or boss bar) for all remaining alive players during endgame stages
|
||||
- [ ] Locator bar only displays players still in the game (not spectators)
|
||||
- [ ] End-game locator updates in real time as players die
|
||||
|
||||
---
|
||||
|
||||
## 👥 Player Registration & Session Management
|
||||
|
||||
### Registration
|
||||
- [ ] Expose a command (e.g. `/hg join <player>`) callable by other plugins/console to register a player into the upcoming round
|
||||
- [ ] Maintain a registered player list per round (separate from online players)
|
||||
- [ ] Reject registration if round is already ACTIVE or past COUNTDOWN
|
||||
- [ ] Confirm registration with a chat message to the player: `"You have been entered into the round!"`
|
||||
- [ ] Allow `/hg leave <player>` to remove a player before the round starts
|
||||
- [ ] Allow `/hg kick <player>` (admin) to force-remove a player mid-round (counts as death/DNF, no kill credit)
|
||||
|
||||
### Player States
|
||||
Track each registered player in one of these states:
|
||||
- [ ] `REGISTERED` — signed up or in game (depending on round state)
|
||||
- [ ] `DISCONNECTED` — left the server mid-round
|
||||
- [ ] `SPECTATING` — non-participant/dead observer
|
||||
|
||||
### Disconnect Handling
|
||||
- [ ] On disconnect during COUNTDOWN: remove from round, free their spawn slot
|
||||
- [ ] On disconnect during ACTIVE round: treat as death (no kill credit), teleport handling skipped since they're offline
|
||||
- [ ] Store disconnect state so if they reconnect before round ends, they are placed in the safe room (not back into the game)
|
||||
- [ ] On reconnect mid-round: teleport to safe room, restore DEAD state, notify them the round is ongoing
|
||||
- [ ] On reconnect after round ends: restore their normal state
|
||||
|
||||
### Join Game Handling
|
||||
- [ ] On registration: set player gamemode survival
|
||||
- [ ] Teleport player to a configurable lobby/waiting area within the event world on registration
|
||||
- [ ] Assign spawn points randomly from the available pool at countdown start — no two players share a point
|
||||
- [ ] Teleport all REGISTERED players to their assigned spawn points when countdown begins
|
||||
- [ ] Notify player they joined the game
|
||||
|
||||
### Safe Room
|
||||
- [ ] Command to re-teleport all players not in the game to the safe room
|
||||
- [ ] Configurable safe room location (world, x, y, z, yaw, pitch) in config
|
||||
- [ ] Teleport eliminated players to safe room immediately on death
|
||||
- [ ] Teleport disconnected players to safe room on reconnect (if round still active)
|
||||
- [ ] Players in safe room: no PvP, no hunger, no fall damage
|
||||
- [ ] Optionally show a live scoreboard or action bar in the safe room (remaining players, kills leaderboard)
|
||||
- [ ] Teleport all players out of safe room to a configurable post-round location when round ends
|
||||
|
||||
### Round Cleanup
|
||||
- [ ] On round end: clear all player states
|
||||
- [ ] Restore player inventory, gamemode, health, hunger, potion effects to pre-round state (or a clean default)
|
||||
- [ ] Clear any active boss bars, action bars, speed effects from all participants
|
||||
- [ ] Reset registered player list ready for next round
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Commands
|
||||
|
||||
### `/hg stuck`
|
||||
- [x] Teleport player to the nearest safe grass block
|
||||
- [x] Require minimum height threshold to prevent abuse (configurable)
|
||||
- [x] Launch fireworks at the destination on use
|
||||
- [x] Configure firework count/type
|
||||
- [x] Optional cooldown to prevent spam
|
||||
- [x] Warmup period (configurable) that cancels on movement or damage
|
||||
|
||||
### `/hg stats [player]`
|
||||
- [ ] Show overall stats for the specified player (or self if no argument)
|
||||
- [ ] Stats viewable by anyone for anyone
|
||||
|
||||
### `/hg round start` / `/hg round stop` / `/hg reload`
|
||||
- [ ] Admin commands to manually control round state
|
||||
- [ ] Reload config without restart
|
||||
|
||||
---
|
||||
|
||||
## 📊 Stats System
|
||||
|
||||
### Per-Round Stats (stored each round)
|
||||
- [ ] Kills
|
||||
- [ ] Deaths
|
||||
- [ ] Damage dealt
|
||||
- [ ] Damage taken
|
||||
- [ ] Placement (survival order)
|
||||
- [ ] Whether the player won
|
||||
|
||||
### Overall / Lifetime Stats (aggregated)
|
||||
- [ ] Total kills
|
||||
- [ ] Total deaths
|
||||
- [ ] Total damage dealt
|
||||
- [ ] Total damage taken
|
||||
- [ ] Total wins
|
||||
- [ ] Total rounds played
|
||||
- [ ] K/D ratio (computed)
|
||||
|
||||
### Storage
|
||||
- [ ] Stats persist across server restarts (flat-file per round)
|
||||
- [ ] Per-round history accessible
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
All values in `config.yml` unless noted.
|
||||
|
||||
- [ ] `countdown-duration` (seconds, default: 10)
|
||||
- [ ] `speed-boost-duration` (seconds, default: 60)
|
||||
- [ ] `spawn-radius` (blocks from center)
|
||||
- [ ] `spectator-room-location` (world, x, y, z, yaw, pitch)
|
||||
- [ ] `stuck-min-height` (minimum Y for `/stuck` teleport)
|
||||
- [ ] `stuck-firework-count`
|
||||
- [ ] `world-border.initial-size`
|
||||
- [ ] `world-border.shrink-schedule` (list of `{size, duration}` entries)
|
||||
- [ ] `world-border.final-four-location` (teleport coords for top 4)
|
||||
- [ ] `world-border.final-four-border-size`
|
||||
- [ ] `chests.center.items-per-rarity` (map of rarity → count)
|
||||
- [ ] `chests.normal.items-per-rarity`
|
||||
- [ ] `chests.loot-tables` (grouped by rarity, list of item entries with optional quantity ranges)
|
||||
- [ ] `chests.late-game-loot-bonus` (threshold player count → loot tier multiplier)
|
||||
- [ ] `stats.storage-type` (`yaml` / `sqlite`)
|
||||
- [ ] `stats.round-history-limit`
|
||||
- [ ] `messages.*` (all chat messages customizable)
|
||||
|
||||
---
|
||||
@@ -2,7 +2,10 @@ package com.alttd.hunger_games;
|
||||
|
||||
import com.alttd.hunger_games.commands.BaseCommand;
|
||||
import com.alttd.hunger_games.config.Config;
|
||||
import com.alttd.hunger_games.config.LootItems;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.event_listeners.*;
|
||||
import com.alttd.hunger_games.services.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
@@ -10,33 +13,63 @@ import org.bukkit.plugin.java.JavaPlugin;
|
||||
@Slf4j
|
||||
public final class Main extends JavaPlugin {
|
||||
|
||||
private BaseCommand command;
|
||||
private Round round;
|
||||
private RoundService roundService;
|
||||
private PlayerService playerService;
|
||||
private PlayerTeleporterService playerTeleporterService;
|
||||
private LootService lootService;
|
||||
private StatService statService;
|
||||
private BossBarService bossBarService;
|
||||
private TabListService tabListService;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
log.info("Starting HungerGames");
|
||||
reloadConfigs();
|
||||
registerServices();
|
||||
registerCommands();
|
||||
registerEvents();
|
||||
registerSchedulers();
|
||||
}
|
||||
|
||||
private void registerServices() {
|
||||
round = Round.createSingletonInstance();
|
||||
lootService = new LootService();
|
||||
statService = new StatService(this, round);
|
||||
playerTeleporterService = PlayerTeleporterService.createSingletonInstance();
|
||||
roundService = RoundService.createSingletonInstance(round, this, lootService, statService, playerTeleporterService);
|
||||
tabListService = new TabListService(round, roundService);
|
||||
roundService.setTabListService(tabListService);
|
||||
playerService = PlayerService.createSingletonInstance(round, roundService, playerTeleporterService);
|
||||
|
||||
bossBarService = BossBarService.createSingletonInstance(round, roundService);
|
||||
round.setBossBarService(bossBarService);
|
||||
roundService.setBossBarService(bossBarService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
log.info("Disabling HungerGames");
|
||||
}
|
||||
|
||||
private void registerCommands() {
|
||||
command = new BaseCommand(this);
|
||||
BaseCommand command = new BaseCommand(this, roundService, playerService, round, statService, tabListService);
|
||||
}
|
||||
|
||||
private void registerEvents() {
|
||||
PluginManager pluginManager = getServer().getPluginManager();
|
||||
pluginManager.registerEvents(new PlayerDisconnectListener(playerService), this);
|
||||
pluginManager.registerEvents(new PlayerJoinListener(playerService, tabListService), this);
|
||||
pluginManager.registerEvents(new PlayerDamageListener(roundService, playerService, statService), this);
|
||||
pluginManager.registerEvents(new ChestListener(roundService, lootService), this);
|
||||
pluginManager.registerEvents(new ItemSpawnListener(roundService), this);
|
||||
pluginManager.registerEvents(new CommandListener(), this);
|
||||
}
|
||||
|
||||
public void reloadConfigs() {
|
||||
Config.reload();
|
||||
Messages.reload();
|
||||
LootItems.reload();
|
||||
}
|
||||
|
||||
private void registerSchedulers() {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.alttd.hunger_games.commands;
|
||||
|
||||
import com.alttd.hunger_games.Main;
|
||||
import com.alttd.hunger_games.commands.subcommands.*;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.services.*;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
@@ -13,11 +15,12 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j @Getter
|
||||
@Slf4j
|
||||
@Getter
|
||||
public class BaseCommand implements CommandExecutor, TabExecutor {
|
||||
private final List<SubCommand> subCommands;
|
||||
|
||||
public BaseCommand(Main main) {
|
||||
public BaseCommand(Main main, RoundService roundService, PlayerService playerService, Round round, StatService statService, TabListService tablistService) {
|
||||
PluginCommand command = main.getCommand("hungergames");
|
||||
if (command == null) {
|
||||
subCommands = null;
|
||||
@@ -28,25 +31,42 @@ public class BaseCommand implements CommandExecutor, TabExecutor {
|
||||
command.setTabCompleter(this);
|
||||
command.setAliases(List.of("hg"));
|
||||
|
||||
subCommands = new ArrayList<>(List.of());
|
||||
subCommands = new ArrayList<>(List.of(
|
||||
new Reload(main),
|
||||
new RoundState(roundService),
|
||||
new Register(playerService, tablistService),
|
||||
new StartRound(round, roundService, playerService),
|
||||
new NextPhase(round),
|
||||
new EndRound(round),
|
||||
new Stuck(main),
|
||||
new Stats(statService),
|
||||
new ListPlayers(roundService)
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String cmd, @NotNull String[] args) {
|
||||
if (args.length == 0) {
|
||||
commandSender.sendRichMessage(Messages.HELP.HELP_MESSAGE_WRAPPER.replaceAll("<commands>", subCommands.stream()
|
||||
.filter(subCommand -> commandSender.hasPermission(subCommand.getPermission()))
|
||||
.map(SubCommand::getHelpMessage)
|
||||
.collect(Collectors.joining("\n"))));
|
||||
commandSender.sendRichMessage(Messages.HELP.HELP_MESSAGE_WRAPPER.replaceAll("<commands>",
|
||||
subCommands.stream()
|
||||
.filter(subCommand -> commandSender.hasPermission(
|
||||
subCommand.getPermission()))
|
||||
.map(SubCommand::getHelpMessage)
|
||||
.collect(Collectors.joining(
|
||||
"\n"))
|
||||
));
|
||||
return true;
|
||||
}
|
||||
|
||||
SubCommand subCommand = getSubCommand(args[0]);
|
||||
if (subCommand == null)
|
||||
if (subCommand == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!commandSender.hasPermission(subCommand.getPermission())) {
|
||||
commandSender.sendRichMessage(Messages.GENERIC.NO_PERMISSION, Placeholder.parsed("permission", subCommand.getPermission()));
|
||||
commandSender.sendRichMessage(Messages.GENERIC.NO_PERMISSION,
|
||||
Placeholder.parsed("permission", subCommand.getPermission())
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -70,10 +90,11 @@ public class BaseCommand implements CommandExecutor, TabExecutor {
|
||||
);
|
||||
} else {
|
||||
SubCommand subCommand = getSubCommand(args[0]);
|
||||
if (subCommand != null && commandSender.hasPermission(subCommand.getPermission()))
|
||||
if (subCommand != null && commandSender.hasPermission(subCommand.getPermission())) {
|
||||
res.addAll(subCommand.getTabComplete(commandSender, args).stream()
|
||||
.filter(str -> str.toLowerCase().startsWith(args[args.length - 1].toLowerCase()))
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.alttd.hunger_games.commands.shared_parsers;
|
||||
|
||||
import com.alttd.hunger_games.commands.ArgumentParser;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class OnlinePlayerParser implements ArgumentParser<Player> {
|
||||
|
||||
@Override
|
||||
public Optional<Player> parse(CommandSender commandSender, String playerName) {
|
||||
Player player = commandSender.getServer().getPlayer(playerName);
|
||||
if (player == null || !player.isOnline()) {
|
||||
commandSender.sendRichMessage(Messages.GENERIC.PLAYER_NOT_FOUND, Placeholder.parsed("player", playerName));
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(player);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.alttd.hunger_games.commands.subcommands;
|
||||
|
||||
import com.alttd.hunger_games.commands.SubCommand;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.services.Round;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class EndRound extends SubCommand {
|
||||
|
||||
private final Round round;
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender commandSender, String[] args) {
|
||||
round.endRound();
|
||||
commandSender.sendRichMessage("<green>Ending the round.</green>");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "end";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpMessage() {
|
||||
return Messages.HELP.END_ROUND;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.alttd.hunger_games.commands.subcommands;
|
||||
|
||||
import com.alttd.hunger_games.commands.SubCommand;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
|
||||
import com.alttd.hunger_games.services.RoundService;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.JoinConfiguration;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class ListPlayers extends SubCommand {
|
||||
|
||||
private final RoundService roundService;
|
||||
|
||||
public ListPlayers(RoundService roundService) {
|
||||
this.roundService = roundService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender commandSender, String[] args) {
|
||||
Set<UUID> registeredUuids = roundService.getPlayers(PLAYER_STATE.REGISTERED);
|
||||
Set<UUID> spectatingUuids = roundService.getPlayers(PLAYER_STATE.SPECTATING);
|
||||
|
||||
List<Component> registeredNames = registeredUuids.stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.map(Player::name)
|
||||
.toList();
|
||||
|
||||
List<Component> spectatingNames = spectatingUuids.stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.map(Player::name)
|
||||
.toList();
|
||||
|
||||
Component inGamePlayers = Component.join(JoinConfiguration.commas(true), registeredNames);
|
||||
Component spectators = Component.join(JoinConfiguration.commas(true), spectatingNames);
|
||||
|
||||
int inGameCount = registeredNames.size();
|
||||
int totalCount = inGameCount + spectatingNames.size();
|
||||
|
||||
commandSender.sendRichMessage(Messages.COMMANDS.LIST_PLAYERS,
|
||||
Placeholder.unparsed("in_game", String.valueOf(inGameCount)),
|
||||
Placeholder.unparsed("total", String.valueOf(totalCount)),
|
||||
Placeholder.component("in_game_players", inGamePlayers),
|
||||
Placeholder.component("spectators", spectators)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "list";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpMessage() {
|
||||
return Messages.HELP.LIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.alttd.hunger_games.commands.subcommands;
|
||||
|
||||
import com.alttd.hunger_games.commands.SubCommand;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.services.Round;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class NextPhase extends SubCommand {
|
||||
|
||||
private final Round round;
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender commandSender, String[] args) {
|
||||
round.forceNextPhase();
|
||||
commandSender.sendRichMessage(Messages.COMMANDS.NEXT_PHASE_SUCCESS);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "nextphase";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpMessage() {
|
||||
return Messages.HELP.NEXT_PHASE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.alttd.hunger_games.commands.subcommands;
|
||||
|
||||
import com.alttd.hunger_games.commands.SubCommand;
|
||||
import com.alttd.hunger_games.commands.shared_parsers.OnlinePlayerParser;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
|
||||
import com.alttd.hunger_games.services.PlayerService;
|
||||
import com.alttd.hunger_games.services.TabListService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class Register extends SubCommand {
|
||||
|
||||
private final static OnlinePlayerParser ONLINE_PLAYER_PARSER = new OnlinePlayerParser();
|
||||
private final static int PLAYER_ARG = 1;
|
||||
private final PlayerService playerService;
|
||||
private final TabListService tabListService;
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender commandSender, String[] args) {
|
||||
if (args.length != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Optional<Player> optionalPlayer = ONLINE_PLAYER_PARSER.parse(commandSender, args[PLAYER_ARG]);
|
||||
if (optionalPlayer.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Player playerToRegister = optionalPlayer.get();
|
||||
Optional<PLAYER_STATE> optionalPlayerState = playerService.registerPlayer(playerToRegister);
|
||||
PLAYER_STATE nullablePlayerState = optionalPlayerState.orElse(null);
|
||||
if (optionalPlayerState.isEmpty()) {
|
||||
playerToRegister.sendRichMessage(Messages.REGISTER.FAILED_TO_REGISTER_YOU);
|
||||
} else {
|
||||
notifyRegisteredPlayer(playerToRegister, nullablePlayerState);
|
||||
tabListService.updatePlayerColor(playerToRegister);
|
||||
}
|
||||
|
||||
if (commandSender instanceof Player commandPlayer) {
|
||||
notifyPlayerCommandSender(commandPlayer, nullablePlayerState, playerToRegister);
|
||||
}
|
||||
logResult(nullablePlayerState, playerToRegister);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void notifyRegisteredPlayer(Player playerToRegister, PLAYER_STATE nullablePlayerState) {
|
||||
switch (nullablePlayerState) {
|
||||
case REGISTERED -> playerToRegister.sendRichMessage(Messages.REGISTER.SUCCESSFUL_REGISTER);
|
||||
case SPECTATING -> playerToRegister.sendRichMessage(Messages.REGISTER.GAME_RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
private void logResult(PLAYER_STATE nullablePlayerState, Player playerToRegister) {
|
||||
if (nullablePlayerState == null) {
|
||||
log.info("Failed to register player {}", playerToRegister.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
switch (nullablePlayerState) {
|
||||
case REGISTERED -> log.info("Registered player {} for the next round", playerToRegister.getName());
|
||||
case DISCONNECTED -> log.info("Player {} is disconnected (should not be possible)", playerToRegister.getName());
|
||||
case SPECTATING -> log.info("Player {} is now spectating the game", playerToRegister.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static void notifyPlayerCommandSender(Player commandPlayer, @Nullable PLAYER_STATE playerState, Player playerToRegister) {
|
||||
if (playerState == null) {
|
||||
commandPlayer.sendRichMessage(Messages.REGISTER.FAILED_TO_REGISTER_PLAYER,
|
||||
Placeholder.component("player", playerToRegister.name()));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (playerState) {
|
||||
case REGISTERED -> commandPlayer.sendRichMessage(Messages.REGISTER.PLAYER_REGISTERED,
|
||||
Placeholder.component("player", playerToRegister.name()));
|
||||
case DISCONNECTED -> {
|
||||
//should not be possible
|
||||
}
|
||||
case SPECTATING -> commandPlayer.sendRichMessage(Messages.REGISTER.PLAYER_SPECTATING,
|
||||
Placeholder.component("player", playerToRegister.name()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "register";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
|
||||
if (args.length == 2) {
|
||||
return Bukkit.getOnlinePlayers().stream().map(Player::getName).toList();
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpMessage() {
|
||||
return Messages.HELP.REGISTER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.alttd.hunger_games.commands.subcommands;
|
||||
|
||||
import com.alttd.hunger_games.Main;
|
||||
import com.alttd.hunger_games.commands.SubCommand;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Reload extends SubCommand {
|
||||
|
||||
private final Main main;
|
||||
|
||||
public Reload(Main main) {
|
||||
this.main = main;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender commandSender, String[] args) {
|
||||
main.reloadConfigs();
|
||||
commandSender.sendRichMessage(Messages.GENERIC.RELOAD_SUCCESS);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "reload";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpMessage() {
|
||||
return Messages.HELP.RELOAD;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.alttd.hunger_games.commands.subcommands;
|
||||
|
||||
import com.alttd.hunger_games.commands.SubCommand;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import com.alttd.hunger_games.services.RoundService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class RoundState extends SubCommand {
|
||||
|
||||
private final RoundService roundService;
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender commandSender, String[] args) {
|
||||
ROUND_STATE roundState = roundService.getRoundState();
|
||||
commandSender.sendRichMessage(Messages.ROUND_STATE.ROUND_STATE,
|
||||
Placeholder.parsed("round_state",
|
||||
roundState == null ? "null" : roundState.getHumanReadableName()));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "roundstate";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpMessage() {
|
||||
return Messages.HELP.ROUND_STATE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.alttd.hunger_games.commands.subcommands;
|
||||
|
||||
import com.alttd.hunger_games.commands.SubCommand;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import com.alttd.hunger_games.services.PlayerService;
|
||||
import com.alttd.hunger_games.services.Round;
|
||||
import com.alttd.hunger_games.services.RoundService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class StartRound extends SubCommand {
|
||||
|
||||
private final Round round;
|
||||
private final RoundService roundService;
|
||||
private final PlayerService playerService;
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender commandSender, String[] args) {
|
||||
ROUND_STATE roundState = roundService.getRoundState();
|
||||
if (roundState == ROUND_STATE.PLAYER_REGISTRATION || roundState == ROUND_STATE.ENDED) {
|
||||
Bukkit.getOnlinePlayers().stream()
|
||||
.filter(player -> !player.hasPermission("hungergames.auto-join-bypass"))
|
||||
.forEach(playerService::registerPlayer);
|
||||
round.startRound();
|
||||
} else {
|
||||
commandSender.sendRichMessage(Messages.START_ROUND.CAN_NOT_START_ROUND,
|
||||
Placeholder.parsed("round_state", roundState.getHumanReadableName()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "start";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpMessage() {
|
||||
return Messages.HELP.START_ROUND;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.alttd.hunger_games.commands.subcommands;
|
||||
|
||||
import com.alttd.hunger_games.commands.SubCommand;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.data_objects.LifetimeStats;
|
||||
import com.alttd.hunger_games.services.StatService;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Stats extends SubCommand {
|
||||
|
||||
private final StatService statService;
|
||||
|
||||
public Stats(StatService statService) {
|
||||
this.statService = statService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender commandSender, String[] args) {
|
||||
OfflinePlayer target;
|
||||
if (args.length < 2) {
|
||||
if (!(commandSender instanceof Player player)) {
|
||||
commandSender.sendRichMessage(Messages.GENERIC.PLAYER_ONLY);
|
||||
return true;
|
||||
}
|
||||
target = player;
|
||||
} else {
|
||||
target = Bukkit.getOfflinePlayerIfCached(args[1]);
|
||||
if (target == null) {
|
||||
target = Bukkit.getPlayer(args[1]);
|
||||
}
|
||||
if (target == null) {
|
||||
commandSender.sendRichMessage(Messages.GENERIC.PLAYER_NOT_FOUND.replaceAll("<player>", args[1]));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
LifetimeStats stats = statService.getOrCreateLifetimeStats(target.getUniqueId());
|
||||
|
||||
commandSender.sendRichMessage(Messages.STATS.STATS_FORMAT,
|
||||
Placeholder.parsed("player", target.getName() != null ? target.getName() : "Unknown"),
|
||||
Placeholder.parsed("kills", String.valueOf(stats.getTotalKills())),
|
||||
Placeholder.parsed("deaths", String.valueOf(stats.getTotalDeaths())),
|
||||
Placeholder.parsed("damage_dealt", String.format("%.2f", stats.getTotalDamageDealt())),
|
||||
Placeholder.parsed("damage_taken", String.format("%.2f", stats.getTotalDamageTaken())),
|
||||
Placeholder.parsed("wins", String.valueOf(stats.getTotalWins())),
|
||||
Placeholder.parsed("rounds_played", String.valueOf(stats.getTotalRoundsPlayed())),
|
||||
Placeholder.parsed("kd_ratio", String.format("%.2f", stats.getKDRatio()))
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "stats";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
|
||||
if (args.length == 2) {
|
||||
return Bukkit.getOnlinePlayers().stream().map(Player::getName).collect(Collectors.toList());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpMessage() {
|
||||
return Messages.HELP.STATS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package com.alttd.hunger_games.commands.subcommands;
|
||||
import com.alttd.hunger_games.Main;
|
||||
import com.alttd.hunger_games.commands.SubCommand;
|
||||
import com.alttd.hunger_games.config.Config;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.FireworkEffect;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Firework;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.inventory.meta.FireworkMeta;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
public class Stuck extends SubCommand implements Listener {
|
||||
|
||||
private final Main main;
|
||||
private final Map<UUID, Instant> cooldowns = new HashMap<>();
|
||||
private final Map<UUID, Warmup> warmups = new HashMap<>();
|
||||
|
||||
public Stuck(Main main) {
|
||||
this.main = main;
|
||||
main.getServer().getPluginManager().registerEvents(this, main);
|
||||
}
|
||||
|
||||
private record Warmup(Location location, BukkitTask task) { }
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender commandSender, String[] args) {
|
||||
if (!(commandSender instanceof Player player)) {
|
||||
commandSender.sendRichMessage(Messages.GENERIC.PLAYER_ONLY);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (warmups.containsKey(player.getUniqueId())) {
|
||||
player.sendRichMessage(Messages.STUCK.ALREADY_WARMING_UP);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (player.getLocation().getY() < Config.DESTINATION.STUCK_MIN_HEIGHT) {
|
||||
player.sendRichMessage(Messages.STUCK.TOO_LOW,
|
||||
Placeholder.parsed("height", String.valueOf(Config.DESTINATION.STUCK_MIN_HEIGHT))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isOnCooldown(player)) {
|
||||
Duration remaining = getRemainingCooldown(player);
|
||||
player.sendRichMessage(Messages.STUCK.ON_COOLDOWN,
|
||||
Placeholder.parsed("time", remaining.toSeconds() + "s")
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
Optional<Location> safeLocation = findSafeLocation(player.getLocation());
|
||||
if (safeLocation.isEmpty()) {
|
||||
player.sendRichMessage(Messages.STUCK.NO_SAFE_LOCATION);
|
||||
return true;
|
||||
}
|
||||
|
||||
startWarmup(player, safeLocation.get());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startWarmup(Player player, Location safeLocation) {
|
||||
Duration warmupTime = Config.DESTINATION.STUCK_WARMUP;
|
||||
if (warmupTime.isZero() || warmupTime.isNegative()) {
|
||||
teleport(player, safeLocation);
|
||||
return;
|
||||
}
|
||||
|
||||
player.sendRichMessage(Messages.STUCK.WARMUP_STARTED,
|
||||
Placeholder.parsed("time", warmupTime.toSeconds() + "s")
|
||||
);
|
||||
|
||||
BukkitTask task = main.getServer().getScheduler().runTaskLater(main, () -> {
|
||||
warmups.remove(player.getUniqueId());
|
||||
teleport(player, safeLocation);
|
||||
}, warmupTime.toSeconds() * 20L);
|
||||
|
||||
warmups.put(player.getUniqueId(), new Warmup(player.getLocation(), task));
|
||||
}
|
||||
|
||||
private void teleport(Player player, Location safeLocation) {
|
||||
player.teleport(safeLocation.add(0.5, 1, 0.5));
|
||||
spawnFireworks(player.getLocation());
|
||||
player.sendRichMessage(Messages.STUCK.TELEPORTED);
|
||||
cooldowns.put(player.getUniqueId(), Instant.now());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerMove(PlayerMoveEvent event) {
|
||||
UUID uuid = event.getPlayer().getUniqueId();
|
||||
Warmup warmup = warmups.get(uuid);
|
||||
if (warmup == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location from = warmup.location();
|
||||
Location to = event.getTo();
|
||||
|
||||
if (from.getWorld() != to.getWorld() || from.distanceSquared(to) > 0.25) {
|
||||
warmup.task().cancel();
|
||||
warmups.remove(uuid);
|
||||
event.getPlayer().sendRichMessage(Messages.STUCK.WARMUP_CANCELLED_MOVEMENT);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerDamage(EntityDamageEvent event) {
|
||||
if (!(event.getEntity() instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Warmup warmup = warmups.get(player.getUniqueId());
|
||||
if (warmup == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
warmup.task().cancel();
|
||||
warmups.remove(player.getUniqueId());
|
||||
player.sendRichMessage(Messages.STUCK.WARMUP_CANCELLED_DAMAGE);
|
||||
}
|
||||
|
||||
private boolean isOnCooldown(Player player) {
|
||||
Instant lastUse = cooldowns.get(player.getUniqueId());
|
||||
if (lastUse == null) {
|
||||
return false;
|
||||
}
|
||||
return Duration.between(lastUse, Instant.now()).compareTo(Config.DESTINATION.STUCK_COOLDOWN) < 0;
|
||||
}
|
||||
|
||||
private Duration getRemainingCooldown(Player player) {
|
||||
Instant lastUse = cooldowns.get(player.getUniqueId());
|
||||
if (lastUse == null) {
|
||||
return Duration.ZERO;
|
||||
}
|
||||
Duration elapsed = Duration.between(lastUse, Instant.now());
|
||||
Duration remaining = Config.DESTINATION.STUCK_COOLDOWN.minus(elapsed);
|
||||
return remaining.isNegative() ? Duration.ZERO : remaining;
|
||||
}
|
||||
|
||||
private Optional<Location> findSafeLocation(Location start) {
|
||||
int radius = 10;
|
||||
Location finaleCenter = Config.DESTINATION.FINALE_CENTER;
|
||||
boolean prioritizeFinale = finaleCenter != null && finaleCenter.getWorld().equals(start.getWorld());
|
||||
|
||||
for (int r = 0; r <= radius; r++) {
|
||||
List<Location> candidates = new ArrayList<>();
|
||||
for (int x = -r; x <= r; x++) {
|
||||
for (int z = -r; z <= r; z++) {
|
||||
if (Math.abs(x) != r && Math.abs(z) != r) {
|
||||
continue;
|
||||
}
|
||||
candidates.add(start.clone().add(x, 0, z));
|
||||
}
|
||||
}
|
||||
|
||||
if (prioritizeFinale) {
|
||||
candidates.sort(Comparator.comparingDouble(loc -> loc.distanceSquared(finaleCenter)));
|
||||
}
|
||||
|
||||
for (Location baseLocation : candidates) {
|
||||
Optional<Location> safeY = findSafeY(baseLocation);
|
||||
if (safeY.isPresent()) {
|
||||
return safeY;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<Location> findSafeY(Location base) {
|
||||
// Search up and down a bit from current Y
|
||||
for (int yOffset = -5; yOffset <= 5; yOffset++) {
|
||||
Block target = base.getBlock().getRelative(0, yOffset, 0);
|
||||
if (isSafe(target)) {
|
||||
return Optional.of(target.getLocation());
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private boolean isSafe(Block block) {
|
||||
return block.getType() == Material.GRASS_BLOCK &&
|
||||
block.getRelative(BlockFace.UP).getType().isAir() &&
|
||||
block.getRelative(BlockFace.UP).getRelative(BlockFace.UP).getType().isAir();
|
||||
}
|
||||
|
||||
private void spawnFireworks(Location location) {
|
||||
for (int i = 0; i < Config.DESTINATION.STUCK_FIREWORK_COUNT; i++) {
|
||||
Firework firework = (Firework) location.getWorld().spawnEntity(location, EntityType.FIREWORK_ROCKET);
|
||||
FireworkMeta meta = firework.getFireworkMeta();
|
||||
meta.addEffect(FireworkEffect.builder()
|
||||
.withColor(Color.RED, Color.ORANGE, Color.YELLOW)
|
||||
.withFade(Color.ORANGE)
|
||||
.with(FireworkEffect.Type.BALL_LARGE)
|
||||
.trail(true)
|
||||
.flicker(true)
|
||||
.build());
|
||||
meta.setPower(1);
|
||||
firework.setFireworkMeta(meta);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "stuck";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpMessage() {
|
||||
return Messages.HELP.STUCK;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import com.alttd.hunger_games.Main;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
@@ -18,6 +21,7 @@ import java.lang.reflect.Modifier;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@@ -107,6 +111,44 @@ abstract class AbstractConfig {
|
||||
return yaml.getDouble(path, yaml.getDouble(path));
|
||||
}
|
||||
|
||||
boolean isValidSection(String prefix, String path) {
|
||||
return getConfigurationSection(prefix + path) != null;
|
||||
}
|
||||
|
||||
boolean isEmpty(String prefix, String path) {
|
||||
path = prefix + path;
|
||||
return yaml.get(path) == null;
|
||||
}
|
||||
|
||||
Optional<Location> getLocation(String prefix, String path) {
|
||||
if (!isValidSection(prefix, path)) {
|
||||
log.warn("No location configuration for {}", prefix + path);
|
||||
return Optional.empty();
|
||||
}
|
||||
//destination.start-center.world
|
||||
String rootPath = prefix + path + ".";
|
||||
if (isEmpty(rootPath, "world") || isEmpty(rootPath, "x") || isEmpty(rootPath, "y") || isEmpty(rootPath, "z")) {
|
||||
ConfigurationSection configurationSection = getConfigurationSection(prefix + path);
|
||||
log.error("Invalid location configuration for:\n\t{}", configurationSection.getKeys(false).stream().map(key -> rootPath + key).collect(Collectors.joining("\n\t")));
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String worldString = getString(rootPath, "world", null);
|
||||
|
||||
double x = getDouble(rootPath, "x", 0);
|
||||
double y = getDouble(rootPath, "y", 0);
|
||||
double z = getDouble(rootPath, "z", 0);
|
||||
|
||||
World world = Bukkit.getWorld(worldString);
|
||||
|
||||
if (world == null) {
|
||||
log.error("Invalid world for location {}", path);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(new Location(world, x, y, z));
|
||||
}
|
||||
|
||||
<T> List<String> getList(String prefix, String path, T def) {
|
||||
path = prefix + path;
|
||||
yaml.addDefault(path, def);
|
||||
@@ -144,4 +186,8 @@ abstract class AbstractConfig {
|
||||
ConfigurationSection getConfigurationSection(String path) {
|
||||
return yaml.getConfigurationSection(path);
|
||||
}
|
||||
|
||||
ConfigurationSection createSection(String path) {
|
||||
return yaml.createSection(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package com.alttd.hunger_games.config;
|
||||
|
||||
import com.alttd.hunger_games.data_objects.GameStage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
public class Config extends AbstractConfig {
|
||||
@@ -31,4 +36,123 @@ public class Config extends AbstractConfig {
|
||||
private static void load() {
|
||||
}
|
||||
}
|
||||
|
||||
public static class ROUND {
|
||||
private static final String prefix = "round.";
|
||||
|
||||
public static String HG_WORLD = "world";
|
||||
public static Duration COUNTDOWN = Duration.ofSeconds(10);
|
||||
public static Duration SAFE_PHASE = Duration.ofSeconds(30);
|
||||
public static Duration TIME_TO_FINALE = Duration.ofMinutes(5);
|
||||
public static int INITIAL_BORDER_SIZE = 5000;
|
||||
public static int FINALE_BORDER_SIZE = 48;
|
||||
public static int DEFAULT_BORDER_TRANSITION_SECONDS = 45;
|
||||
public static List<GameStage> STAGES = List.of(
|
||||
GameStage.builder().duration(Duration.ofMinutes(5)).worldBorderSize(1000).build(),
|
||||
GameStage.builder().duration(Duration.ofMinutes(5)).worldBorderSize(750).build(),
|
||||
GameStage.builder().duration(Duration.ofMinutes(5)).worldBorderSize(500).build()
|
||||
);
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
HG_WORLD = config.getString(prefix, "hg-world", HG_WORLD);
|
||||
int countdownSeconds = config.getInt(prefix, "countdown-seconds", Math.toIntExact(COUNTDOWN.toSeconds()));
|
||||
COUNTDOWN = Duration.ofSeconds(countdownSeconds);
|
||||
int safePhaseSeconds = config.getInt(prefix, "safe-phase-seconds", Math.toIntExact(SAFE_PHASE.toSeconds()));
|
||||
SAFE_PHASE = Duration.ofSeconds(safePhaseSeconds);
|
||||
int killPhaseSeconds = config.getInt(prefix, "time-to-finale-seconds", Math.toIntExact(TIME_TO_FINALE.toSeconds()));
|
||||
TIME_TO_FINALE = Duration.ofSeconds(killPhaseSeconds);
|
||||
INITIAL_BORDER_SIZE = config.getInt(prefix, "initial-border-size", INITIAL_BORDER_SIZE);
|
||||
FINALE_BORDER_SIZE = config.getInt(prefix, "finale-border-size", FINALE_BORDER_SIZE);
|
||||
DEFAULT_BORDER_TRANSITION_SECONDS = config.getInt(prefix, "default-border-transition-seconds", DEFAULT_BORDER_TRANSITION_SECONDS);
|
||||
|
||||
ConfigurationSection configurationSection = config.getConfigurationSection(prefix + "stages");
|
||||
if (configurationSection == null) {
|
||||
configurationSection = config.createSection(prefix + "stages");
|
||||
}
|
||||
Set<String> keys = configurationSection.getKeys(false);
|
||||
STAGES = getGameStages(keys, configurationSection);
|
||||
}
|
||||
|
||||
private static List<GameStage> getGameStages(Set<String> keys, ConfigurationSection configurationSection) {
|
||||
if (keys.isEmpty()) {
|
||||
int key = 0;
|
||||
for (GameStage stage : STAGES) {
|
||||
ConfigurationSection section = configurationSection.createSection(String.valueOf(key++));
|
||||
section.set("duration-seconds", stage.getDuration().toSeconds());
|
||||
section.set("world-border-size", stage.getWorldBorderSize());
|
||||
}
|
||||
return STAGES;
|
||||
}
|
||||
List<GameStage> gameStageList = new ArrayList<>();
|
||||
|
||||
for (String key : keys) {
|
||||
ConfigurationSection section = configurationSection.getConfigurationSection(key);
|
||||
if (section == null) {
|
||||
throw new IllegalStateException("Stage section is null for key [" + key + "] but is in the list of retrieved keys");
|
||||
}
|
||||
if (!section.contains("duration-seconds")) {
|
||||
log.error("Missing duration-seconds in stage {}.", key);
|
||||
continue;
|
||||
}
|
||||
if (!section.contains("world-border-size")) {
|
||||
log.error("Missing world-border-size in stage {}.", key);
|
||||
continue;
|
||||
}
|
||||
int durationSeconds = section.getInt("duration-seconds");
|
||||
int worldBorderSize = section.getInt("world-border-size");
|
||||
gameStageList.add(GameStage.builder()
|
||||
.duration(Duration.ofSeconds(durationSeconds))
|
||||
.worldBorderSize(worldBorderSize)
|
||||
.build());
|
||||
}
|
||||
return gameStageList;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DESTINATION {
|
||||
private static final String prefix = "destination.";
|
||||
public static Location START_CENTER = null;
|
||||
public static int START_RADIUS = 15;
|
||||
public static Location FINALE_CENTER = null;
|
||||
public static int FINALE_RADIUS = 10;
|
||||
public static Location SPECTATOR_AREA = null;
|
||||
|
||||
public static int STUCK_MIN_HEIGHT = 64;
|
||||
public static int STUCK_FIREWORK_COUNT = 3;
|
||||
public static Duration STUCK_COOLDOWN = Duration.ofMinutes(2);
|
||||
public static Duration STUCK_WARMUP = Duration.ofSeconds(5);
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
Config.DESTINATION.START_RADIUS = config.getInt(prefix, "start-radius", START_RADIUS);
|
||||
config.getLocation(prefix, "start-center")
|
||||
.ifPresent(location -> DESTINATION.START_CENTER = location);
|
||||
|
||||
config.getLocation(prefix, "spectator-area")
|
||||
.ifPresent(location -> DESTINATION.SPECTATOR_AREA = location);
|
||||
|
||||
DESTINATION.FINALE_RADIUS = config.getInt(prefix, "finale-radius", FINALE_RADIUS);
|
||||
config.getLocation(prefix, "finale-center")
|
||||
.ifPresent(location -> DESTINATION.FINALE_CENTER = location);
|
||||
|
||||
STUCK_MIN_HEIGHT = config.getInt(prefix, "stuck-min-height", STUCK_MIN_HEIGHT);
|
||||
STUCK_FIREWORK_COUNT = config.getInt(prefix, "stuck-firework-count", STUCK_FIREWORK_COUNT);
|
||||
STUCK_COOLDOWN = Duration.ofSeconds(config.getInt(prefix, "stuck-cooldown-seconds", (int) STUCK_COOLDOWN.toSeconds()));
|
||||
STUCK_WARMUP = Duration.ofSeconds(config.getInt(prefix, "stuck-warmup-seconds", (int) STUCK_WARMUP.toSeconds()));
|
||||
}
|
||||
}
|
||||
|
||||
public static class COMMAND_WHITELIST {
|
||||
private static final String prefix = "command-whitelist";
|
||||
|
||||
public static final List<String> PREFIXES = new ArrayList<>(List.of("msg", "ac", "gac", "tell", "r", "server", "lobby", "hub", "bayou", "creative", "hg", "lp"));
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
List<String> stringList = config.getStringList(prefix, "prefix-list", PREFIXES);
|
||||
PREFIXES.clear();
|
||||
PREFIXES.addAll(stringList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.alttd.hunger_games.config;
|
||||
|
||||
import com.alttd.hunger_games.data_objects.LootItem;
|
||||
import com.alttd.hunger_games.data_objects.RARITY;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bukkit.Material;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class LootItems extends AbstractConfig {
|
||||
static LootItems config;
|
||||
|
||||
LootItems() {
|
||||
super(
|
||||
new File(File.separator
|
||||
+ "mnt" + File.separator
|
||||
+ "configs" + File.separator
|
||||
+ "HungerGames"),
|
||||
"loot-items.yml");
|
||||
}
|
||||
|
||||
public static void reload() {
|
||||
config = new LootItems();
|
||||
config.readConfig(LootItems.class, null);
|
||||
}
|
||||
|
||||
public static class ITEMS {
|
||||
private static final String prefix = "items.";
|
||||
|
||||
private static final HashMap<RARITY, List<LootItem>> ITEMS = new HashMap<>();
|
||||
|
||||
public static List<LootItem> get(RARITY rarity) {
|
||||
return ITEMS.get(rarity);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
for (RARITY rarity : RARITY.values()) {
|
||||
List<LootItem> currentItems = ITEMS.getOrDefault(rarity, List.of());
|
||||
List<String> materialNameList = currentItems.stream()
|
||||
.map(item -> item.material().name() + (item.maxAmount() > 1 ? ":" + item.maxAmount() : ""))
|
||||
.toList();
|
||||
List<String> materialList = config.getList(prefix, rarity.getConfigName(), materialNameList);
|
||||
|
||||
List<LootItem> lootItems = new ArrayList<>();
|
||||
for (String entry : materialList) {
|
||||
String[] split = entry.split(":");
|
||||
Material material = Material.getMaterial(split[0].toUpperCase());
|
||||
if (material == null) {
|
||||
log.warn("Invalid material found in loot-items.yml: {}", split[0]);
|
||||
continue;
|
||||
}
|
||||
int maxAmount = 1;
|
||||
if (split.length > 1) {
|
||||
try {
|
||||
maxAmount = Integer.parseInt(split[1]);
|
||||
} catch (NumberFormatException ignored) {
|
||||
log.error("Invalid max amount found in loot-items.yml: {}", split[1]);
|
||||
}
|
||||
}
|
||||
lootItems.add(new LootItem(material, maxAmount));
|
||||
}
|
||||
ITEMS.put(rarity, lootItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.alttd.hunger_games.config;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class Messages extends AbstractConfig {
|
||||
@@ -24,11 +26,29 @@ public class Messages extends AbstractConfig {
|
||||
|
||||
public static String HELP_MESSAGE_WRAPPER = "<gold>HungerGames help:\n<commands></gold>";
|
||||
public static String HELP_MESSAGE = "<green>Show this menu: <gold>/hg help</gold></green>";
|
||||
public static String ROUND_STATE = "<green>Show the current round state: <gold>/hg roundstate</gold></green>";
|
||||
public static String REGISTER = "<green>Register a player for the game: <gold>/hg register <player></gold></green>";
|
||||
public static String START_ROUND = "<green>Start the game: <gold>/hg start</gold></green>";
|
||||
public static String NEXT_PHASE = "<green>Skip to the next phase/stage: <gold>/hg nextphase</gold></green>";
|
||||
public static String RELOAD = "<green>Reload config and messages: <gold>/hg reload</gold></green>";
|
||||
public static String STUCK = "<green>Teleport to safety if stuck: <gold>/hg stuck</gold></green>";
|
||||
public static String STATS = "<green>Show player stats: <gold>/hg stats [player]</gold></green>";
|
||||
public static String LIST = "<green>List all players in the game and spectators: <gold>/hg list</gold></green>";
|
||||
public static String END_ROUND = "<green>End the game: <gold>/hg end</gold></green>";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
HELP_MESSAGE_WRAPPER = config.getString(prefix, "help-wrapper", HELP_MESSAGE_WRAPPER);
|
||||
HELP_MESSAGE = config.getString(prefix, "help", HELP_MESSAGE);
|
||||
ROUND_STATE = config.getString(prefix, "round-state", ROUND_STATE);
|
||||
REGISTER = config.getString(prefix, "register", REGISTER);
|
||||
START_ROUND = config.getString(prefix, "start", START_ROUND);
|
||||
NEXT_PHASE = config.getString(prefix, "next-phase", NEXT_PHASE);
|
||||
RELOAD = config.getString(prefix, "reload", RELOAD);
|
||||
STUCK = config.getString(prefix, "stuck", STUCK);
|
||||
STATS = config.getString(prefix, "stats", STATS);
|
||||
LIST = config.getString(prefix, "list", LIST);
|
||||
END_ROUND = config.getString(prefix, "end-round", END_ROUND);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +58,169 @@ public class Messages extends AbstractConfig {
|
||||
public static String NO_PERMISSION = "<red><hover:show_text:'<red><permission></red>'>You don't have permission for this command</hover></red>";
|
||||
public static String PLAYER_ONLY = "<red>This command can only be executed as a player</red>";
|
||||
public static String PLAYER_NOT_FOUND = "<red>Unable to find online player <player></red>";
|
||||
public static String RELOAD_SUCCESS = "<green>Config and messages have been reloaded.</green>";
|
||||
public static String COMMAND_BLOCKED = "<red>You cannot use that command here!</red>";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
NO_PERMISSION = config.getString(prefix, "no-permission", NO_PERMISSION);
|
||||
PLAYER_ONLY = config.getString(prefix, "player-only", PLAYER_ONLY);
|
||||
PLAYER_NOT_FOUND = config.getString(prefix, "player-only", PLAYER_NOT_FOUND);
|
||||
RELOAD_SUCCESS = config.getString(prefix, "reload-success", RELOAD_SUCCESS);
|
||||
COMMAND_BLOCKED = config.getString(prefix, "command-blocked", COMMAND_BLOCKED);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ROUND_STATE {
|
||||
private static final String prefix = "round-state.";
|
||||
|
||||
public static String ROUND_STATE = "<green>Current round state is: <gold><round_state></gold></green>";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
ROUND_STATE = config.getString(prefix, "round-state", ROUND_STATE);
|
||||
}
|
||||
}
|
||||
|
||||
public static class GAME {
|
||||
private static final String prefix = "game.";
|
||||
|
||||
public static String WARMUP = "<gold>The Hunger Games are starting soon! Get ready!</gold>";
|
||||
public static String STARTED = "<green><bold>The Hunger Games has begun! Good luck!</bold></green>";
|
||||
public static String BORDER_SHRINK = "<red>The border is shrinking to <size> blocks!</red>";
|
||||
public static String CHEST_EMPTY = "<red>This chest is empty!</red>";
|
||||
public static String WINNER = "<gold><player> has won the Hunger Games!</gold>";
|
||||
public static String PLAYER_DEATH = "<red><player> has died! <gold><remaining></gold> players remaining.</red>";
|
||||
public static String PLAYER_KILLED = "<red><victim> was killed by <killer>! <gold><remaining></gold> players remaining.</red>";
|
||||
|
||||
public static String BOSSBAR_COUNTDOWN = "<gold>Loot phase in <time></gold>";
|
||||
public static String BOSSBAR_PVP_COUNTDOWN = "<green>PVP starts in <time></green>";
|
||||
public static String BOSSBAR_SAFE_PHASE = "<green>Border shrinking in <time></green>";
|
||||
public static String BOSSBAR_KILL_PHASE = "<red>Border shrinking... <time></red>";
|
||||
public static String BOSSBAR_FINALE = "<gold>Remaining Players: <remaining></gold>";
|
||||
|
||||
public static String NEXT_STATE = "<gold>Starting <round>!</gold>";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
WARMUP = config.getString(prefix, "warmup", WARMUP);
|
||||
STARTED = config.getString(prefix, "started", STARTED);
|
||||
BORDER_SHRINK = config.getString(prefix, "border-shrink", BORDER_SHRINK);
|
||||
CHEST_EMPTY = config.getString(prefix, "chest-empty", CHEST_EMPTY);
|
||||
WINNER = config.getString(prefix, "winner", WINNER);
|
||||
PLAYER_DEATH = config.getString(prefix, "player-death", PLAYER_DEATH);
|
||||
PLAYER_KILLED = config.getString(prefix, "player-killed", PLAYER_KILLED);
|
||||
|
||||
BOSSBAR_COUNTDOWN = config.getString(prefix, "bossbar-countdown", BOSSBAR_COUNTDOWN);
|
||||
BOSSBAR_PVP_COUNTDOWN = config.getString(prefix, "bossbar-pvp-countdown", BOSSBAR_PVP_COUNTDOWN);
|
||||
BOSSBAR_SAFE_PHASE = config.getString(prefix, "bossbar-border-shrinking-in", BOSSBAR_SAFE_PHASE);
|
||||
BOSSBAR_KILL_PHASE = config.getString(prefix, "bossbar-border-shrinking", BOSSBAR_KILL_PHASE);
|
||||
BOSSBAR_FINALE = config.getString(prefix, "bossbar-finale", BOSSBAR_FINALE);
|
||||
|
||||
NEXT_STATE = config.getString(prefix, "next-state", NEXT_STATE);
|
||||
}
|
||||
}
|
||||
|
||||
public static class START_ROUND {
|
||||
private static final String prefix = "start-round.";
|
||||
|
||||
public static String CAN_NOT_START_ROUND = "<red>Unable to start the round, current state is <round_state></red>";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
CAN_NOT_START_ROUND = config.getString(prefix, "can-not-start", CAN_NOT_START_ROUND);
|
||||
}
|
||||
}
|
||||
|
||||
public static class RECONNECT {
|
||||
private static final String prefix = "reconnect.";
|
||||
|
||||
public static String FORFEITED = "<red>You disconnected during the round and have forfeited the game. You are now spectating.</red>";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
FORFEITED = config.getString(prefix, "forfeited", FORFEITED);
|
||||
}
|
||||
}
|
||||
|
||||
public static class REGISTER {
|
||||
private static final String prefix = "register.";
|
||||
|
||||
public static String FAILED_TO_REGISTER_YOU = "<red>You were unable to join HungerGames because no HungerGames round was found</red>";
|
||||
public static String FAILED_TO_REGISTER_PLAYER = "<red>Unable to register player <player></red>";
|
||||
public static String PLAYER_REGISTERED = "<green>Registered <player> for the game</green>";
|
||||
public static String PLAYER_SPECTATING = "<yellow><player> is now spectating the game</yellow>";
|
||||
public static String SUCCESSFUL_REGISTER = "<green>Successfully registered you for the upcoming Hunger Games round</green>";
|
||||
public static String GAME_RUNNING = "<yellow>The game is currently running, you will need to spectate the current round</yellow>";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
FAILED_TO_REGISTER_YOU = config.getString(prefix, "failed", FAILED_TO_REGISTER_YOU);
|
||||
FAILED_TO_REGISTER_PLAYER = config.getString(prefix, "failed-player", FAILED_TO_REGISTER_PLAYER);
|
||||
PLAYER_REGISTERED = config.getString(prefix, "player-registered", PLAYER_REGISTERED);
|
||||
PLAYER_SPECTATING = config.getString(prefix, "player-spectating", PLAYER_SPECTATING);
|
||||
SUCCESSFUL_REGISTER = config.getString(prefix, "successful", SUCCESSFUL_REGISTER);
|
||||
GAME_RUNNING = config.getString(prefix, "game-running", GAME_RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
public static class STUCK {
|
||||
private static final String prefix = "stuck.";
|
||||
|
||||
public static String TELEPORTED = "<green>You have been teleported to safety!</green>";
|
||||
public static String TOO_LOW = "<red>You are too low to use this command (minimum height: <height>). If you are truly stuck here dm a staff member for help</red>";
|
||||
public static String NO_SAFE_LOCATION = "<red>Unable to find a safe grass block nearby</red>";
|
||||
public static String ON_COOLDOWN = "<red>You must wait <time> before using this command again</red>";
|
||||
public static String WARMUP_STARTED = "<green>Teleporting in <time>. Do not move or take damage!</green>";
|
||||
public static String WARMUP_CANCELLED_MOVEMENT = "<red>Teleportation cancelled due to movement.</red>";
|
||||
public static String WARMUP_CANCELLED_DAMAGE = "<red>Teleportation cancelled due to taking damage.</red>";
|
||||
public static String ALREADY_WARMING_UP = "<red>You are already warming up!</red>";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
TELEPORTED = config.getString(prefix, "teleported", TELEPORTED);
|
||||
TOO_LOW = config.getString(prefix, "too-low", TOO_LOW);
|
||||
NO_SAFE_LOCATION = config.getString(prefix, "no-safe-location", NO_SAFE_LOCATION);
|
||||
ON_COOLDOWN = config.getString(prefix, "on-cooldown", ON_COOLDOWN);
|
||||
WARMUP_STARTED = config.getString(prefix, "warmup-started", WARMUP_STARTED);
|
||||
WARMUP_CANCELLED_MOVEMENT = config.getString(prefix, "warmup-cancelled-movement", WARMUP_CANCELLED_MOVEMENT);
|
||||
WARMUP_CANCELLED_DAMAGE = config.getString(prefix, "warmup-cancelled-damage", WARMUP_CANCELLED_DAMAGE);
|
||||
ALREADY_WARMING_UP = config.getString(prefix, "already-warming-up", ALREADY_WARMING_UP);
|
||||
}
|
||||
}
|
||||
|
||||
public static class STATS {
|
||||
private static final String prefix = "stats.";
|
||||
|
||||
public static String STATS_FORMAT = """
|
||||
<gold>Stats for <player>:</gold>
|
||||
<green>Kills: <gold><kills></gold></green>
|
||||
<green>Deaths: <gold><deaths></gold></green>
|
||||
<green>Damage Dealt: <gold><damage_dealt></gold></green>
|
||||
<green>Damage Taken: <gold><damage_taken></gold></green>
|
||||
<green>Wins: <gold><wins></gold></green>
|
||||
<green>Rounds Played: <gold><rounds_played></gold></green>
|
||||
<green>K/D Ratio: <gold><kd_ratio></gold></green>""";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
STATS_FORMAT = config.getString(prefix, "format", STATS_FORMAT);
|
||||
}
|
||||
}
|
||||
|
||||
public static class COMMANDS {
|
||||
private static final String prefix = "commands.";
|
||||
|
||||
public static String NEXT_PHASE_SUCCESS = "<green>Skipping to the next phase/stage.</green>";
|
||||
public static String LIST_PLAYERS = """
|
||||
<gold>HungerGames Players (<in_game>/<total>):</gold>
|
||||
<green>In Game: <white><in_game_players></white></green>
|
||||
<yellow>Spectators: <white><spectators></white></yellow>""";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
NEXT_PHASE_SUCCESS = config.getString(prefix + "next-phase", "success", NEXT_PHASE_SUCCESS);
|
||||
LIST_PLAYERS = config.getString(prefix, "list-players", LIST_PLAYERS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.alttd.hunger_games.data_objects;
|
||||
|
||||
public enum DESTINATION {
|
||||
START_AREA,
|
||||
FINALE_AREA,
|
||||
SPECTATOR_AREA;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.alttd.hunger_games.data_objects;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
public class GameStage {
|
||||
|
||||
private final Duration duration;
|
||||
private final int worldBorderSize;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.alttd.hunger_games.data_objects;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class LifetimeStats {
|
||||
private final UUID uuid;
|
||||
private int totalKills;
|
||||
private int totalDeaths;
|
||||
private double totalDamageDealt;
|
||||
private double totalDamageTaken;
|
||||
private int totalWins;
|
||||
private int totalRoundsPlayed;
|
||||
|
||||
public void addRoundStats(RoundStats roundStats) {
|
||||
totalKills += roundStats.getKills();
|
||||
totalDeaths += roundStats.getDeaths();
|
||||
totalDamageDealt += roundStats.getDamageDealt();
|
||||
totalDamageTaken += roundStats.getDamageTaken();
|
||||
if (roundStats.isWon()) {
|
||||
totalWins++;
|
||||
}
|
||||
totalRoundsPlayed++;
|
||||
}
|
||||
|
||||
public double getKDRatio() {
|
||||
if (totalDeaths == 0) {
|
||||
return totalKills;
|
||||
}
|
||||
return (double) totalKills / totalDeaths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.alttd.hunger_games.data_objects;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
public record LootItem(Material material, int maxAmount) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.alttd.hunger_games.data_objects;
|
||||
|
||||
public enum PLAYER_STATE {
|
||||
REGISTERED,
|
||||
DISCONNECTED,
|
||||
SPECTATING
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.alttd.hunger_games.data_objects;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public enum RARITY {
|
||||
COMMON("common"),
|
||||
UNCOMMON("uncommon"),
|
||||
RARE("rare"),
|
||||
EPIC("epic");
|
||||
|
||||
private final String configName;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.alttd.hunger_games.data_objects;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public enum ROUND_STATE {
|
||||
PLAYER_REGISTRATION,
|
||||
COUNTDOWN,
|
||||
SAFE_PHASE,
|
||||
KILL_PHASE,
|
||||
FINALE,
|
||||
ENDED;
|
||||
|
||||
public Optional<ROUND_STATE> next() {
|
||||
if (ordinal() == ENDED.ordinal()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(values()[ordinal() + 1]);
|
||||
}
|
||||
|
||||
public String getHumanReadableName() {
|
||||
return switch (this) {
|
||||
case PLAYER_REGISTRATION -> "Player Registration";
|
||||
case COUNTDOWN -> "Countdown";
|
||||
case SAFE_PHASE -> "Safe Phase";
|
||||
case KILL_PHASE -> "Kill Phase";
|
||||
case FINALE -> "Finale";
|
||||
case ENDED -> "Ended";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.alttd.hunger_games.data_objects;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class RoundStats {
|
||||
private final UUID uuid;
|
||||
private int kills;
|
||||
private int deaths;
|
||||
private double damageDealt;
|
||||
private double damageTaken;
|
||||
private int placement;
|
||||
private boolean won;
|
||||
|
||||
public void addKill() {
|
||||
kills++;
|
||||
}
|
||||
|
||||
public void addDeath() {
|
||||
deaths++;
|
||||
}
|
||||
|
||||
public void addDamageDealt(double damage) {
|
||||
damageDealt += damage;
|
||||
}
|
||||
|
||||
public void addDamageTaken(double damage) {
|
||||
damageTaken += damage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.alttd.hunger_games.event_listeners;
|
||||
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import com.alttd.hunger_games.services.LootService;
|
||||
import com.alttd.hunger_games.services.RoundService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class ChestListener implements Listener {
|
||||
|
||||
private final RoundService roundService;
|
||||
private final LootService lootService;
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) {
|
||||
return;
|
||||
}
|
||||
|
||||
Block block = event.getClickedBlock();
|
||||
if (block == null || block.getType() != Material.CHEST) {
|
||||
return;
|
||||
}
|
||||
|
||||
ROUND_STATE roundState = roundService.getRoundState();
|
||||
if (roundState != ROUND_STATE.SAFE_PHASE && roundState != ROUND_STATE.KILL_PHASE && roundState != ROUND_STATE.FINALE) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = event.getPlayer();
|
||||
if (roundService.getPlayerState(player.getUniqueId()).orElse(null) != PLAYER_STATE.REGISTERED) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
|
||||
Inventory inventory = lootService.getChestInventory(block.getLocation());
|
||||
if (inventory.isEmpty()) {
|
||||
player.sendRichMessage(Messages.GAME.CHEST_EMPTY);
|
||||
} else {
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.alttd.hunger_games.event_listeners;
|
||||
|
||||
import com.alttd.hunger_games.config.Config;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class CommandListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (player.hasPermission("hg.bypass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove leading slash
|
||||
String message = event.getMessage().substring(1).toLowerCase();
|
||||
String command = message.split(" ")[0];
|
||||
|
||||
boolean whitelisted = Config.COMMAND_WHITELIST.PREFIXES.stream()
|
||||
.anyMatch(prefix -> command.startsWith(prefix.toLowerCase()));
|
||||
|
||||
if (!whitelisted) {
|
||||
event.setCancelled(true);
|
||||
player.sendRichMessage(Messages.GENERIC.COMMAND_BLOCKED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.alttd.hunger_games.event_listeners;
|
||||
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import com.alttd.hunger_games.services.RoundService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDeathEvent;
|
||||
import org.bukkit.event.entity.ItemSpawnEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class ItemSpawnListener implements Listener {
|
||||
|
||||
private final RoundService roundService;
|
||||
private final Set<UUID> deathDrops = new HashSet<>();
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onItemSpawn(ItemSpawnEvent event) {
|
||||
if (deathDrops.remove(event.getEntity().getUniqueId())) {
|
||||
event.getEntity().setTicksLived(5400); // 300 seconds total - 5400 ticks = 30 seconds left
|
||||
return;
|
||||
}
|
||||
setDespawnDelay(event.getEntity());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlayerDeath(EntityDeathEvent event) {
|
||||
if (!(event.getEntity() instanceof Player)) {
|
||||
return;
|
||||
}
|
||||
ROUND_STATE roundState = roundService.getRoundState();
|
||||
if (roundState == ROUND_STATE.SAFE_PHASE || roundState == ROUND_STATE.KILL_PHASE || roundState == ROUND_STATE.FINALE) {
|
||||
event.getDrops().forEach(itemStack -> {
|
||||
Item item = event.getEntity().getWorld().dropItemNaturally(event.getEntity().getLocation(), itemStack);
|
||||
deathDrops.add(item.getUniqueId());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerDropItem(PlayerDropItemEvent event) {
|
||||
setDespawnDelay(event.getItemDrop());
|
||||
}
|
||||
|
||||
private void setDespawnDelay(Item item) {
|
||||
ROUND_STATE roundState = roundService.getRoundState();
|
||||
if (roundState == ROUND_STATE.SAFE_PHASE || roundState == ROUND_STATE.KILL_PHASE || roundState == ROUND_STATE.FINALE) {
|
||||
item.setTicksLived(5800);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.alttd.hunger_games.event_listeners;
|
||||
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import com.alttd.hunger_games.services.PlayerService;
|
||||
import com.alttd.hunger_games.services.RoundService;
|
||||
import com.alttd.hunger_games.services.StatService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class PlayerDamageListener implements Listener {
|
||||
|
||||
private final RoundService roundService;
|
||||
private final PlayerService playerService;
|
||||
private final StatService statService;
|
||||
|
||||
@EventHandler
|
||||
public void onEntityDamage(EntityDamageEvent event) {
|
||||
if (!(event.getEntity() instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
ROUND_STATE roundState = roundService.getRoundState();
|
||||
if (roundState != ROUND_STATE.KILL_PHASE && roundState != ROUND_STATE.FINALE) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
double damage = Math.min(event.getFinalDamage(), player.getHealth());
|
||||
statService.addDamageTaken(player.getUniqueId(), damage);
|
||||
|
||||
if (event instanceof EntityDamageByEntityEvent damageByEntityEvent && damageByEntityEvent.getDamager() instanceof Player attacker) {
|
||||
statService.addDamageDealt(attacker.getUniqueId(), damage);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
event.setShowDeathMessages(false);
|
||||
event.deathMessage(null);
|
||||
playerService.handlePlayerDeath(player);
|
||||
statService.addDeath(player.getUniqueId());
|
||||
|
||||
Player killer = player.getKiller();
|
||||
if (killer != null) {
|
||||
statService.addKill(killer.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerRespawn(PlayerRespawnEvent event) {
|
||||
playerService.handlePlayerRespawn(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.alttd.hunger_games.event_listeners;
|
||||
|
||||
import com.alttd.hunger_games.services.PlayerService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class PlayerDisconnectListener implements Listener {
|
||||
|
||||
private final PlayerService playerService;
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
playerService.handleDisconnect(event.getPlayer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.alttd.hunger_games.event_listeners;
|
||||
|
||||
import com.alttd.hunger_games.services.PlayerService;
|
||||
import com.alttd.hunger_games.services.TabListService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandException;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PlayerJoinListener implements Listener {
|
||||
|
||||
private final PlayerService playerService;
|
||||
private final TabListService tabListService;
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
playerService.handleJoin(player);
|
||||
tabListService.updatePlayerColor(player);
|
||||
try {
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
|
||||
String.format("lp user %s permission settemp cmi.kit.hunger-games true 7d", player.getName()));
|
||||
} catch (CommandException e) {
|
||||
log.error("Failed to set participant permission for {}", player.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.alttd.hunger_games.event_listeners;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class PlayerMovementListener implements Listener {
|
||||
private final Set<UUID> uuidSet;
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerMove(PlayerMoveEvent event) {
|
||||
if (!uuidSet.contains(event.getPlayer().getUniqueId())) {
|
||||
return;
|
||||
}
|
||||
Location from = event.getFrom();
|
||||
Location to = event.getTo();
|
||||
//Only freeze movement not rotation or jumping
|
||||
if (from.getBlockX() != to.getBlockX()
|
||||
|| from.getBlockZ() != to.getBlockZ()) {
|
||||
event.setTo(from);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.alttd.hunger_games.game;
|
||||
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.utils.WorldBorderUtil;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@UtilityClass
|
||||
public class GameStageHandler {
|
||||
|
||||
public static void handleStageChange(int worldBorderSize) {
|
||||
WorldBorderUtil.setSize(worldBorderSize);
|
||||
|
||||
String message = Messages.GAME.BORDER_SHRINK
|
||||
.replace("<size>", String.valueOf(worldBorderSize));
|
||||
broadcast(message);
|
||||
}
|
||||
|
||||
public static void broadcastMessage(String message) {
|
||||
broadcast(message);
|
||||
}
|
||||
|
||||
public static void handleCountdownEnd() {
|
||||
broadcast(Messages.GAME.STARTED);
|
||||
}
|
||||
|
||||
public static void handleWarmup() {
|
||||
broadcast(Messages.GAME.WARMUP);
|
||||
}
|
||||
|
||||
private static void broadcast(String message) {
|
||||
Component component = MiniMessage.miniMessage().deserialize(message);
|
||||
Bukkit.broadcast(component);
|
||||
Title.Times times = Title.Times.times(Duration.ofSeconds(1), Duration.ofSeconds(5), Duration.ofSeconds(1));
|
||||
Title title = Title.title(Component.empty(), component, times);
|
||||
Bukkit.getOnlinePlayers().forEach(player -> player.showTitle(title));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.alttd.hunger_games.services;
|
||||
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BossBarService implements RoundListener {
|
||||
|
||||
private static BossBarService instance = null;
|
||||
private final RoundService roundService;
|
||||
private final BossBar bossBar;
|
||||
private final Set<UUID> viewers = new HashSet<>();
|
||||
private int maxPlayers = 0;
|
||||
|
||||
public BossBarService(Round round, RoundService roundService) {
|
||||
this.roundService = roundService;
|
||||
this.bossBar = BossBar.bossBar(Component.empty(), 1.0f, BossBar.Color.YELLOW, BossBar.Overlay.PROGRESS);
|
||||
round.register(this);
|
||||
}
|
||||
|
||||
public static BossBarService createSingletonInstance(Round round, RoundService roundService) {
|
||||
if (instance != null) {
|
||||
throw new IllegalStateException("BossBarService is already initialized.");
|
||||
}
|
||||
instance = new BossBarService(round, roundService);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void updateCountdown(Duration timeLeft, Duration totalTime, ROUND_STATE state) {
|
||||
String message = switch (state) {
|
||||
case COUNTDOWN -> Messages.GAME.BOSSBAR_COUNTDOWN;
|
||||
case SAFE_PHASE -> Messages.GAME.BOSSBAR_PVP_COUNTDOWN;
|
||||
case KILL_PHASE -> Messages.GAME.BOSSBAR_SAFE_PHASE;
|
||||
default -> {
|
||||
if (state.ordinal() > ROUND_STATE.KILL_PHASE.ordinal()) {
|
||||
yield Messages.GAME.BOSSBAR_KILL_PHASE;
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid round state: " + state);
|
||||
}
|
||||
};
|
||||
|
||||
if (message.isEmpty()) {
|
||||
bossBar.name(Component.empty());
|
||||
bossBar.progress(0.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
bossBar.name(MiniMessage.miniMessage().deserialize(message, Placeholder.unparsed("time", formatTime(timeLeft))));
|
||||
bossBar.progress(Math.clamp((float) timeLeft.toMillis() / totalTime.toMillis(), 0.0f, 1.0f));
|
||||
|
||||
updateBossBarColor(state);
|
||||
updateViewers();
|
||||
}
|
||||
|
||||
private void updateBossBarColor(ROUND_STATE state) {
|
||||
if (state == ROUND_STATE.ENDED || state == ROUND_STATE.PLAYER_REGISTRATION) {
|
||||
hideAll();
|
||||
return;
|
||||
}
|
||||
switch (state) {
|
||||
case COUNTDOWN -> bossBar.color(BossBar.Color.YELLOW);
|
||||
case SAFE_PHASE -> bossBar.color(BossBar.Color.GREEN);
|
||||
case KILL_PHASE -> bossBar.color(BossBar.Color.RED);
|
||||
case FINALE -> bossBar.color(BossBar.Color.PURPLE);
|
||||
default -> bossBar.color(BossBar.Color.WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateFinale(int remainingPlayers) {
|
||||
if (maxPlayers == 0) {
|
||||
maxPlayers = remainingPlayers;
|
||||
}
|
||||
|
||||
if (remainingPlayers <= 1) {
|
||||
hideAll();
|
||||
return;
|
||||
}
|
||||
|
||||
bossBar.name(MiniMessage.miniMessage().deserialize(Messages.GAME.BOSSBAR_FINALE, Placeholder.unparsed("remaining", String.valueOf(remainingPlayers))));
|
||||
bossBar.progress(Math.clamp((float) remainingPlayers / maxPlayers, 0.0f, 1.0f));
|
||||
bossBar.color(BossBar.Color.PURPLE);
|
||||
updateViewers();
|
||||
}
|
||||
|
||||
private void updateViewers() {
|
||||
if (roundService.getRoundState() == ROUND_STATE.ENDED || roundService.getRoundState() == ROUND_STATE.PLAYER_REGISTRATION) {
|
||||
hideAll();
|
||||
return;
|
||||
}
|
||||
Bukkit.getOnlinePlayers().forEach(player -> {
|
||||
if (viewers.add(player.getUniqueId())) {
|
||||
player.showBossBar(bossBar);
|
||||
}
|
||||
});
|
||||
|
||||
viewers.removeIf(uuid -> {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
return player == null || !player.isOnline();
|
||||
});
|
||||
}
|
||||
|
||||
private void hideAll() {
|
||||
viewers.stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(player -> player.hideBossBar(bossBar));
|
||||
viewers.clear();
|
||||
}
|
||||
|
||||
private String formatTime(Duration duration) {
|
||||
if (duration.isNegative()) {
|
||||
return "0s";
|
||||
}
|
||||
|
||||
long minutes = duration.toMinutesPart();
|
||||
int seconds = duration.toSecondsPart();
|
||||
|
||||
if (minutes > 0) {
|
||||
return "%dm %ds".formatted(minutes, seconds);
|
||||
}
|
||||
return "%ds".formatted(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateChange(ROUND_STATE roundState) {
|
||||
if (roundState == ROUND_STATE.PLAYER_REGISTRATION || roundState == ROUND_STATE.ENDED) {
|
||||
hideAll();
|
||||
maxPlayers = 0;
|
||||
} else if (roundState == ROUND_STATE.FINALE) {
|
||||
updateFinale(roundService.getPlayers(PLAYER_STATE.REGISTERED).size());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.alttd.hunger_games.services;
|
||||
|
||||
import com.alttd.hunger_games.config.LootItems;
|
||||
import com.alttd.hunger_games.data_objects.LootItem;
|
||||
import com.alttd.hunger_games.data_objects.RARITY;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
@Slf4j
|
||||
public class LootService {
|
||||
|
||||
private final HashMap<Location, Inventory> chestInventories = new HashMap<>();
|
||||
private final Random random = ThreadLocalRandom.current();
|
||||
|
||||
public Inventory getChestInventory(Location location) {
|
||||
if (chestInventories.containsKey(location)) {
|
||||
return chestInventories.get(location);
|
||||
}
|
||||
|
||||
Inventory inventory = generateLoot();
|
||||
chestInventories.put(location, inventory);
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private Inventory generateLoot() {
|
||||
Inventory inventory = Bukkit.createInventory(null, 27, Component.text("Chest"));
|
||||
|
||||
int itemCount = random.nextInt(3, 7);
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
int slot = random.nextInt(27);
|
||||
RARITY rarity = decideRarity();
|
||||
List<LootItem> possibleItems = LootItems.ITEMS.get(rarity);
|
||||
|
||||
if (possibleItems == null || possibleItems.isEmpty()) {
|
||||
log.warn("No items found for rarity: {}", rarity);
|
||||
continue;
|
||||
}
|
||||
|
||||
LootItem lootItem = possibleItems.get(random.nextInt(possibleItems.size()));
|
||||
if (lootItem == null || lootItem.material() == null) {
|
||||
log.warn("LootItem or material is null for rarity: {}", rarity);
|
||||
continue;
|
||||
}
|
||||
int amount = lootItem.maxAmount() > 1 ? random.nextInt(lootItem.maxAmount()) + 1 : 1;
|
||||
inventory.setItem(slot, new ItemStack(lootItem.material(), amount));
|
||||
}
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private RARITY decideRarity() {
|
||||
double roll = random.nextDouble();
|
||||
// 5%
|
||||
if (roll < 0.05) {
|
||||
return RARITY.EPIC;
|
||||
}
|
||||
// 10%
|
||||
if (roll < 0.15) {
|
||||
return RARITY.RARE;
|
||||
}
|
||||
// 25%
|
||||
if (roll < 0.40) {
|
||||
return RARITY.UNCOMMON;
|
||||
}
|
||||
// 60%
|
||||
return RARITY.COMMON;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
chestInventories.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.alttd.hunger_games.services;
|
||||
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.data_objects.DESTINATION;
|
||||
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
public class PlayerService implements RoundListener {
|
||||
|
||||
private static PlayerService instance = null;
|
||||
|
||||
private final RoundService roundService;
|
||||
private final PlayerTeleporterService playerTeleporterService;
|
||||
private ROUND_STATE roundState;
|
||||
|
||||
public PlayerService(Round round, RoundService roundService, PlayerTeleporterService playerTeleporterService) {
|
||||
this.roundService = roundService;
|
||||
this.playerTeleporterService = playerTeleporterService;
|
||||
this.roundState = round.register(this);
|
||||
}
|
||||
|
||||
public static PlayerService createSingletonInstance(Round round, RoundService roundService, PlayerTeleporterService playerTeleporterService) {
|
||||
if (instance != null) {
|
||||
throw new IllegalStateException("PlayerService is already initialized.");
|
||||
}
|
||||
instance = new PlayerService(round, roundService, playerTeleporterService);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateChange(ROUND_STATE roundState) {
|
||||
this.roundState = roundState;
|
||||
playerTeleporterService.reset();
|
||||
switch (roundState) {
|
||||
case PLAYER_REGISTRATION, KILL_PHASE, SAFE_PHASE -> {
|
||||
//Nothing
|
||||
}
|
||||
case COUNTDOWN -> {
|
||||
setNonRegisteredPlayersToSpectator();
|
||||
Set<UUID> players = roundService.getPlayers(PLAYER_STATE.REGISTERED);
|
||||
Bukkit.getOnlinePlayers().stream()
|
||||
.filter(player -> players.contains(player.getUniqueId()))
|
||||
.forEach(player -> playerTeleporterService.teleportPlayer(player, DESTINATION.START_AREA));
|
||||
}
|
||||
case FINALE -> {
|
||||
Set<UUID> players = roundService.getPlayers(PLAYER_STATE.REGISTERED);
|
||||
Bukkit.getOnlinePlayers().stream()
|
||||
.filter(player -> players.contains(player.getUniqueId()))
|
||||
.forEach(player -> playerTeleporterService.teleportPlayer(player, DESTINATION.FINALE_AREA));
|
||||
}
|
||||
case ENDED -> {
|
||||
roundService.clear();
|
||||
Bukkit.getOnlinePlayers().forEach(player -> playerTeleporterService.teleportPlayer(player, DESTINATION.SPECTATOR_AREA));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setNonRegisteredPlayersToSpectator() {
|
||||
Set<UUID> playerUUIDs = roundService.getPlayers(PLAYER_STATE.REGISTERED);
|
||||
List<Player> playerList = new ArrayList<>(Bukkit.getOnlinePlayers());
|
||||
List<Player> unregisteredPlayers = playerList.stream().filter(player -> !playerUUIDs.contains(player.getUniqueId())).toList();
|
||||
unregisteredPlayers.forEach(player -> roundService.setPlayerState(player.getUniqueId(), PLAYER_STATE.SPECTATING));
|
||||
}
|
||||
|
||||
public Optional<PLAYER_STATE> registerPlayer(Player player) {
|
||||
if (roundState == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Optional<PLAYER_STATE> optionalPlayerState = determinePlayerState(player.getUniqueId());
|
||||
if (optionalPlayerState.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
PLAYER_STATE playerState = optionalPlayerState.get();
|
||||
|
||||
if (playerState == PLAYER_STATE.SPECTATING) {
|
||||
playerTeleporterService.teleportPlayer(player, DESTINATION.SPECTATOR_AREA);
|
||||
}
|
||||
|
||||
roundService.setPlayerState(player.getUniqueId(), playerState);
|
||||
return optionalPlayerState;
|
||||
}
|
||||
|
||||
public void handleDisconnect(Player player) {
|
||||
player.playerListName(null);
|
||||
UUID uuid = player.getUniqueId();
|
||||
roundService.setPlayerState(uuid, PLAYER_STATE.DISCONNECTED);
|
||||
}
|
||||
|
||||
public void handleJoin(Player player) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
Optional<PLAYER_STATE> state = roundService.getPlayerState(uuid);
|
||||
if (state.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (state.get() != PLAYER_STATE.DISCONNECTED) {
|
||||
return;
|
||||
}
|
||||
roundService.setPlayerState(uuid, PLAYER_STATE.SPECTATING);
|
||||
player.sendRichMessage(Messages.RECONNECT.FORFEITED);
|
||||
}
|
||||
|
||||
public void handlePlayerDeath(Player player) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
roundService.setPlayerState(uuid, PLAYER_STATE.SPECTATING);
|
||||
|
||||
int remaining = roundService.getPlayers(PLAYER_STATE.REGISTERED).size();
|
||||
Player killer = player.getKiller();
|
||||
if (killer != null) {
|
||||
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(Messages.GAME.PLAYER_KILLED,
|
||||
Placeholder.component("victim", player.name()),
|
||||
Placeholder.component("killer", killer.name()),
|
||||
Placeholder.unparsed("remaining", String.valueOf(remaining))));
|
||||
} else {
|
||||
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(Messages.GAME.PLAYER_DEATH,
|
||||
Placeholder.component("player", player.name()),
|
||||
Placeholder.unparsed("remaining", String.valueOf(remaining))));
|
||||
}
|
||||
}
|
||||
|
||||
public void handlePlayerRespawn(PlayerRespawnEvent event) {
|
||||
Location spectatorArea = playerTeleporterService.getLocationFromDestination(DESTINATION.SPECTATOR_AREA);
|
||||
event.setRespawnLocation(spectatorArea);
|
||||
}
|
||||
|
||||
private Optional<PLAYER_STATE> determinePlayerState(UUID playerUuid) {
|
||||
if (roundState == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Optional<PLAYER_STATE> optionalPlayerState = roundService.getPlayerState(playerUuid);
|
||||
if (optionalPlayerState.isPresent()) {
|
||||
PLAYER_STATE playerState = optionalPlayerState.get();
|
||||
return Optional.of(switch (playerState) {
|
||||
case DISCONNECTED, SPECTATING -> {
|
||||
if (roundState.equals(ROUND_STATE.ENDED)) {
|
||||
yield PLAYER_STATE.REGISTERED;
|
||||
}
|
||||
yield PLAYER_STATE.SPECTATING;
|
||||
}
|
||||
case REGISTERED -> PLAYER_STATE.REGISTERED;
|
||||
});
|
||||
}
|
||||
return switch (roundState) {
|
||||
case PLAYER_REGISTRATION, COUNTDOWN, ENDED -> Optional.of(PLAYER_STATE.REGISTERED);
|
||||
case KILL_PHASE, FINALE, SAFE_PHASE -> Optional.of(PLAYER_STATE.SPECTATING);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.alttd.hunger_games.services;
|
||||
|
||||
import com.alttd.hunger_games.config.Config;
|
||||
import com.alttd.hunger_games.data_objects.DESTINATION;
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
@NoArgsConstructor
|
||||
public class PlayerTeleporterService implements RoundListener {
|
||||
|
||||
private static PlayerTeleporterService instance = null;
|
||||
|
||||
private final Map<DESTINATION, Integer> placementCount = new HashMap<>();
|
||||
private final Map<DESTINATION, Set<Location>> usedLocations = new HashMap<>();
|
||||
|
||||
public static PlayerTeleporterService createSingletonInstance() {
|
||||
if (instance != null) {
|
||||
throw new IllegalStateException("PlayerTeleporterService is already initialized.");
|
||||
}
|
||||
instance = new PlayerTeleporterService();
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void teleportPlayer(Player player, DESTINATION destination) {
|
||||
Location locationFromDestination = getLocationFromDestination(destination);
|
||||
try {
|
||||
player.teleportAsync(getLocationFromDestination(destination));
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to teleport player {} to destination {} ({})", player.getName(), destination.name(), locationFromDestination, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
placementCount.clear();
|
||||
usedLocations.clear();
|
||||
}
|
||||
|
||||
public Location getLocationFromDestination(DESTINATION destination) {
|
||||
return switch (destination) {
|
||||
case START_AREA -> {
|
||||
Location startCenter = Config.DESTINATION.START_CENTER;
|
||||
if (startCenter == null) {
|
||||
throw new IllegalStateException("Start center is not set.");
|
||||
}
|
||||
int startRadius = Config.DESTINATION.START_RADIUS;
|
||||
yield calculateLocation(destination, startCenter, startRadius);
|
||||
}
|
||||
case FINALE_AREA -> {
|
||||
Location finaleCenter = Config.DESTINATION.FINALE_CENTER;
|
||||
if (finaleCenter == null) {
|
||||
throw new IllegalStateException("Finale center is not set.");
|
||||
}
|
||||
int finaleRadius = Config.DESTINATION.FINALE_RADIUS;
|
||||
yield calculateLocation(destination, finaleCenter, finaleRadius);
|
||||
}
|
||||
case SPECTATOR_AREA -> {
|
||||
Location spectatorArea = Config.DESTINATION.SPECTATOR_AREA;
|
||||
if (spectatorArea == null) {
|
||||
throw new IllegalStateException("Spectator area is not set.");
|
||||
}
|
||||
yield spectatorArea;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Location calculateLocation(DESTINATION destination, Location center, int radius) {
|
||||
int count = placementCount.merge(destination, 1, Integer::sum);
|
||||
double angleDegrees = vanDerCorput(count - 1) * 360.0;
|
||||
double angleRadians = Math.toRadians(angleDegrees);
|
||||
double x = center.getX() + radius * Math.cos(angleRadians);
|
||||
double z = center.getZ() + radius * Math.sin(angleRadians);
|
||||
|
||||
Location location = new Location(center.getWorld(), x, center.getY(), z);
|
||||
location.setDirection(center.toVector().subtract(location.toVector()));
|
||||
|
||||
usedLocations.computeIfAbsent(destination, k -> new HashSet<>()).add(location);
|
||||
return location;
|
||||
}
|
||||
|
||||
private static double vanDerCorput(int n) {
|
||||
double result = 0.0;
|
||||
double denominator = 1.0;
|
||||
while (n > 0) {
|
||||
denominator *= 2.0;
|
||||
result += (n % 2) / denominator;
|
||||
n /= 2;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateChange(ROUND_STATE roundState) {
|
||||
placementCount.clear();
|
||||
usedLocations.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.alttd.hunger_games.services;
|
||||
|
||||
import com.alttd.hunger_games.config.Config;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.data_objects.GameStage;
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import com.alttd.hunger_games.game.GameStageHandler;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Round {
|
||||
|
||||
private static Round instance = null;
|
||||
private final List<RoundListener> listeners = new ArrayList<>();
|
||||
private ROUND_STATE roundState = ROUND_STATE.PLAYER_REGISTRATION;
|
||||
private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
private ScheduledFuture<?> countdownFuture = null;
|
||||
private Runnable onEnd = null;
|
||||
@Setter
|
||||
private BossBarService bossBarService;
|
||||
|
||||
private Round() {
|
||||
|
||||
}
|
||||
|
||||
public static Round createSingletonInstance() throws IllegalStateException {
|
||||
if (instance != null) {
|
||||
throw new IllegalStateException("Round is already initialized.");
|
||||
}
|
||||
instance = new Round();
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ROUND_STATE register(RoundListener listener) {
|
||||
listeners.add(listener);
|
||||
return roundState;
|
||||
}
|
||||
|
||||
public void unregister(RoundListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (countdownFuture != null) {
|
||||
countdownFuture.cancel(true);
|
||||
}
|
||||
roundState = ROUND_STATE.PLAYER_REGISTRATION;
|
||||
listeners.forEach(roundListener -> roundListener.stateChange(roundState));
|
||||
}
|
||||
|
||||
public void endRound() {
|
||||
if (countdownFuture != null) {
|
||||
countdownFuture.cancel(true);
|
||||
}
|
||||
roundState = ROUND_STATE.ENDED;
|
||||
listeners.forEach(roundListener -> roundListener.stateChange(roundState));
|
||||
}
|
||||
|
||||
private void nextStage() {
|
||||
Optional<ROUND_STATE> optionalNextRoundState = roundState.next();
|
||||
if (optionalNextRoundState.isEmpty()) {
|
||||
roundState = ROUND_STATE.PLAYER_REGISTRATION;
|
||||
listeners.forEach(roundListener -> roundListener.stateChange(roundState));
|
||||
return;
|
||||
}
|
||||
roundState = optionalNextRoundState.get();
|
||||
listeners.forEach(roundListener -> roundListener.stateChange(roundState));
|
||||
}
|
||||
|
||||
public void startRound() {
|
||||
if (!roundState.equals(ROUND_STATE.PLAYER_REGISTRATION) && !roundState.equals(ROUND_STATE.ENDED)) {
|
||||
throw new IllegalStateException("Round can not be started before player registration.");
|
||||
}
|
||||
roundState = ROUND_STATE.COUNTDOWN;
|
||||
listeners.forEach(roundListener -> roundListener.stateChange(roundState));
|
||||
GameStageHandler.handleWarmup();
|
||||
Duration warmupDuration = Config.ROUND.COUNTDOWN;
|
||||
|
||||
startCountdown(warmupDuration, () -> {
|
||||
GameStageHandler.handleCountdownEnd();
|
||||
nextStage(); // COUNTDOWN -> SAFE_PHASE
|
||||
startCountdown(Config.ROUND.SAFE_PHASE, () -> {
|
||||
nextStage(); // SAFE_PHASE -> KILL_PHASE
|
||||
GameStageHandler.broadcastMessage(Messages.GAME.NEXT_STATE.replace("<round>", roundState.getHumanReadableName()));
|
||||
scheduleNextStage(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void startCountdown(Duration duration, Runnable onEnd) {
|
||||
if (countdownFuture != null) {
|
||||
countdownFuture.cancel(true);
|
||||
}
|
||||
this.onEnd = onEnd;
|
||||
|
||||
final Instant endTime = Instant.now().plus(duration);
|
||||
countdownFuture = scheduledExecutorService.scheduleAtFixedRate(() -> {
|
||||
if (roundState.equals(ROUND_STATE.FINALE)) {
|
||||
if (countdownFuture != null) {
|
||||
countdownFuture.cancel(false);
|
||||
countdownFuture = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
Duration timeLeft = Duration.between(Instant.now(), endTime);
|
||||
if (bossBarService != null) {
|
||||
bossBarService.updateCountdown(timeLeft, duration, roundState);
|
||||
}
|
||||
if (timeLeft.isZero() || timeLeft.isNegative()) {
|
||||
onEnd.run();
|
||||
}
|
||||
}, 0, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void scheduleNextStage(int index) {
|
||||
List<GameStage> gameStageList = Config.ROUND.STAGES;
|
||||
if (gameStageList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameStageList.size() <= index) {
|
||||
startCountdown(Config.ROUND.TIME_TO_FINALE, this::nextStage);
|
||||
return;
|
||||
}
|
||||
|
||||
GameStage gameStage = gameStageList.get(index);
|
||||
Duration duration = gameStage.getDuration();
|
||||
|
||||
startCountdown(duration, () -> {
|
||||
GameStageHandler.handleStageChange(gameStage.getWorldBorderSize());
|
||||
scheduleNextStage(index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
public void forceNextPhase() {
|
||||
if (countdownFuture == null || onEnd == null) {
|
||||
return;
|
||||
}
|
||||
countdownFuture.cancel(true);
|
||||
onEnd.run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alttd.hunger_games.services;
|
||||
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
|
||||
public interface RoundListener {
|
||||
|
||||
void stateChange(ROUND_STATE roundState);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.alttd.hunger_games.services;
|
||||
|
||||
import com.alttd.hunger_games.Main;
|
||||
import com.alttd.hunger_games.config.Config;
|
||||
import com.alttd.hunger_games.config.Messages;
|
||||
import com.alttd.hunger_games.data_objects.DESTINATION;
|
||||
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import com.alttd.hunger_games.event_listeners.PlayerMovementListener;
|
||||
import com.alttd.hunger_games.utils.WorldBorderUtil;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class RoundService implements RoundListener {
|
||||
|
||||
private static RoundService instance = null;
|
||||
private final PlayerTeleporterService playerTeleporterService;
|
||||
private final Main main;
|
||||
private final LootService lootService;
|
||||
private final StatService statService;
|
||||
private PlayerMovementListener playerMovementListener;
|
||||
|
||||
@Getter
|
||||
private ROUND_STATE roundState;
|
||||
private final HashMap<UUID, PLAYER_STATE> players = new HashMap<>();
|
||||
|
||||
private final Round round;
|
||||
@Setter
|
||||
private BossBarService bossBarService;
|
||||
@Setter
|
||||
private TabListService tabListService;
|
||||
|
||||
public static RoundService createSingletonInstance(Round round, Main main, LootService lootService, StatService statService, PlayerTeleporterService playerTeleporterService) {
|
||||
if (instance != null) {
|
||||
throw new IllegalStateException("RoundService is already initialized.");
|
||||
}
|
||||
instance = new RoundService(round, main, lootService, statService, playerTeleporterService);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private RoundService(Round round, Main main, LootService lootService, StatService statService, PlayerTeleporterService playerTeleporterService) {
|
||||
this.round = round;
|
||||
this.roundState = round.register(this);
|
||||
this.main = main;
|
||||
this.lootService = lootService;
|
||||
this.statService = statService;
|
||||
this.playerTeleporterService = playerTeleporterService;
|
||||
}
|
||||
|
||||
public Set<UUID> getPlayers(PLAYER_STATE playerState) {
|
||||
return players.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().equals(playerState))
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
protected void setPlayerState(UUID uuid, PLAYER_STATE playerState) {
|
||||
PLAYER_STATE oldState = players.put(uuid, playerState);
|
||||
if (playerState == PLAYER_STATE.SPECTATING && oldState == PLAYER_STATE.REGISTERED) {
|
||||
int remaining = getPlayers(PLAYER_STATE.REGISTERED).size();
|
||||
statService.setPlacement(uuid, remaining + 1);
|
||||
if (roundState == ROUND_STATE.FINALE && bossBarService != null) {
|
||||
bossBarService.updateFinale(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
if (tabListService != null) {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
if (player != null) {
|
||||
tabListService.updatePlayerColor(player);
|
||||
}
|
||||
}
|
||||
|
||||
if (!roundState.equals(ROUND_STATE.KILL_PHASE) && !roundState.equals(ROUND_STATE.FINALE)) {
|
||||
return;
|
||||
}
|
||||
Set<UUID> uuidSet = getPlayers(PLAYER_STATE.REGISTERED);
|
||||
if (uuidSet.size() != 1) {
|
||||
return;
|
||||
}
|
||||
declareWinner(uuidSet.iterator().next());
|
||||
}
|
||||
|
||||
private void declareWinner(UUID winnerUUID) {
|
||||
Player player = Bukkit.getPlayer(winnerUUID);
|
||||
if (player == null) {
|
||||
log.error("Winner {} not found", winnerUUID);
|
||||
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<red>Winner not found</red>"));
|
||||
return;
|
||||
}
|
||||
|
||||
statService.setPlacement(winnerUUID, 1);
|
||||
statService.setWon(winnerUUID, true);
|
||||
|
||||
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(Messages.GAME.WINNER, Placeholder.component("player", player.name())));
|
||||
playerTeleporterService.teleportPlayer(player, DESTINATION.SPECTATOR_AREA);
|
||||
player.setHealth(20);
|
||||
player.setFoodLevel(20);
|
||||
player.getInventory().clear();
|
||||
player.setTotalExperience(0);
|
||||
player.setExp(0.0f);
|
||||
player.setLevel(0);
|
||||
round.endRound();
|
||||
}
|
||||
|
||||
protected void clear() {
|
||||
players.clear();
|
||||
}
|
||||
|
||||
public Optional<PLAYER_STATE> getPlayerState(UUID uuid) {
|
||||
return Optional.ofNullable(players.get(uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateChange(ROUND_STATE roundState) {
|
||||
this.roundState = roundState;
|
||||
stopFreeze();
|
||||
if (roundState.equals(ROUND_STATE.PLAYER_REGISTRATION) || roundState.equals(ROUND_STATE.ENDED)) {
|
||||
if (roundState.equals(ROUND_STATE.ENDED)) {
|
||||
cleanupItems();
|
||||
}
|
||||
clear();
|
||||
lootService.clear();
|
||||
}
|
||||
if (roundState.equals(ROUND_STATE.COUNTDOWN)) {
|
||||
Bukkit.getOnlinePlayers().stream()
|
||||
.filter(player -> PLAYER_STATE.REGISTERED.equals(getPlayerState(player.getUniqueId()).orElse(null)))
|
||||
.forEach(player -> {
|
||||
player.setHealth(20);
|
||||
player.setFoodLevel(20);
|
||||
player.setGameMode(GameMode.ADVENTURE);
|
||||
player.getInventory().clear();
|
||||
player.setTotalExperience(0);
|
||||
player.setExp(0.0f);
|
||||
player.setLevel(0);
|
||||
});
|
||||
startFreeze();
|
||||
WorldBorderUtil.resetBorder();
|
||||
}
|
||||
if (roundState.equals(ROUND_STATE.FINALE)) {
|
||||
WorldBorderUtil.setSize(Config.ROUND.FINALE_BORDER_SIZE, 1);
|
||||
}
|
||||
if (!roundState.equals(ROUND_STATE.ENDED) && !roundState.equals(ROUND_STATE.PLAYER_REGISTRATION)) {
|
||||
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(Messages.GAME.NEXT_STATE, Placeholder.parsed("round", roundState.getHumanReadableName())));
|
||||
}
|
||||
}
|
||||
|
||||
private void stopFreeze() {
|
||||
if (playerMovementListener == null) {
|
||||
return;
|
||||
}
|
||||
HandlerList.unregisterAll(playerMovementListener);
|
||||
playerMovementListener = null;
|
||||
}
|
||||
|
||||
private void startFreeze() {
|
||||
stopFreeze();
|
||||
Set<UUID> uuidSet = getPlayers(PLAYER_STATE.REGISTERED);
|
||||
playerMovementListener = new PlayerMovementListener(uuidSet);
|
||||
main.getServer().getPluginManager().registerEvents(playerMovementListener, main);
|
||||
}
|
||||
|
||||
private void cleanupItems() {
|
||||
World world = Bukkit.getWorld(Config.ROUND.HG_WORLD);
|
||||
if (world == null) {
|
||||
log.error("Unable to cleanup items, world {} not found", Config.ROUND.HG_WORLD);
|
||||
return;
|
||||
}
|
||||
world.getEntitiesByClass(Item.class).forEach(Item::remove);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.alttd.hunger_games.services;
|
||||
|
||||
import com.alttd.hunger_games.Main;
|
||||
import com.alttd.hunger_games.data_objects.LifetimeStats;
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import com.alttd.hunger_games.data_objects.RoundStats;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class StatService implements RoundListener {
|
||||
|
||||
private final Main main;
|
||||
private final Map<UUID, RoundStats> currentRoundStats = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, LifetimeStats> lifetimeStatsCache = new ConcurrentHashMap<>();
|
||||
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
||||
private final File statsDirectory;
|
||||
private final File lifetimeStatsFile;
|
||||
|
||||
public StatService(Main main, Round round) {
|
||||
this.main = main;
|
||||
this.statsDirectory = new File(main.getDataFolder(), "stats");
|
||||
if (!statsDirectory.exists()) {
|
||||
statsDirectory.mkdirs();
|
||||
}
|
||||
this.lifetimeStatsFile = new File(statsDirectory, "lifetime.json");
|
||||
round.register(this);
|
||||
loadLifetimeStats();
|
||||
}
|
||||
|
||||
public RoundStats getOrCreateRoundStats(UUID uuid) {
|
||||
return currentRoundStats.computeIfAbsent(uuid, k -> RoundStats.builder().uuid(k).build());
|
||||
}
|
||||
|
||||
public LifetimeStats getOrCreateLifetimeStats(UUID uuid) {
|
||||
return lifetimeStatsCache.computeIfAbsent(uuid, k -> LifetimeStats.builder().uuid(k).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateChange(ROUND_STATE roundState) {
|
||||
if (roundState.equals(ROUND_STATE.ENDED)) {
|
||||
saveRoundStats();
|
||||
}
|
||||
if (roundState.equals(ROUND_STATE.PLAYER_REGISTRATION)) {
|
||||
currentRoundStats.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveRoundStats() {
|
||||
if (currentRoundStats.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long timestamp = System.currentTimeMillis();
|
||||
File roundFile = new File(statsDirectory, "round_" + timestamp + ".json");
|
||||
try (FileWriter writer = new FileWriter(roundFile)) {
|
||||
gson.toJson(currentRoundStats.values(), writer);
|
||||
} catch (IOException e) {
|
||||
main.getLogger().severe("Could not save round stats: " + e.getMessage());
|
||||
}
|
||||
|
||||
for (RoundStats roundStats : currentRoundStats.values()) {
|
||||
LifetimeStats lifetime = getOrCreateLifetimeStats(roundStats.getUuid());
|
||||
lifetime.addRoundStats(roundStats);
|
||||
}
|
||||
saveLifetimeStats();
|
||||
}
|
||||
|
||||
private void loadLifetimeStats() {
|
||||
if (!lifetimeStatsFile.exists()) {
|
||||
return;
|
||||
}
|
||||
try (FileReader reader = new FileReader(lifetimeStatsFile)) {
|
||||
LifetimeStats[] stats = gson.fromJson(reader, LifetimeStats[].class);
|
||||
if (stats != null) {
|
||||
for (LifetimeStats s : stats) {
|
||||
lifetimeStatsCache.put(s.getUuid(), s);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
main.getLogger().severe("Could not load lifetime stats: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveLifetimeStats() {
|
||||
try (FileWriter writer = new FileWriter(lifetimeStatsFile)) {
|
||||
gson.toJson(lifetimeStatsCache.values(), writer);
|
||||
} catch (IOException e) {
|
||||
main.getLogger().severe("Could not save lifetime stats: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void addKill(UUID uuid) {
|
||||
getOrCreateRoundStats(uuid).addKill();
|
||||
}
|
||||
|
||||
public void addDeath(UUID uuid) {
|
||||
getOrCreateRoundStats(uuid).addDeath();
|
||||
}
|
||||
|
||||
public void addDamageDealt(UUID uuid, double damage) {
|
||||
getOrCreateRoundStats(uuid).addDamageDealt(damage);
|
||||
}
|
||||
|
||||
public void addDamageTaken(UUID uuid, double damage) {
|
||||
getOrCreateRoundStats(uuid).addDamageTaken(damage);
|
||||
}
|
||||
|
||||
public void setPlacement(UUID uuid, int placement) {
|
||||
getOrCreateRoundStats(uuid).setPlacement(placement);
|
||||
}
|
||||
|
||||
public void setWon(UUID uuid, boolean won) {
|
||||
getOrCreateRoundStats(uuid).setWon(won);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.alttd.hunger_games.services;
|
||||
|
||||
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
|
||||
import com.alttd.hunger_games.data_objects.ROUND_STATE;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class TabListService implements RoundListener {
|
||||
|
||||
private final RoundService roundService;
|
||||
private ROUND_STATE roundState;
|
||||
|
||||
public TabListService(Round round, RoundService roundService) {
|
||||
this.roundService = roundService;
|
||||
this.roundState = round.register(this);
|
||||
}
|
||||
|
||||
public void updatePlayerColor(Player player) {
|
||||
if (!player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
if (roundState == null || roundState == ROUND_STATE.ENDED || roundState == ROUND_STATE.PLAYER_REGISTRATION) {
|
||||
player.playerListName(null);
|
||||
return;
|
||||
}
|
||||
|
||||
PLAYER_STATE state = roundService.getPlayerState(player.getUniqueId()).orElse(PLAYER_STATE.SPECTATING);
|
||||
switch (state) {
|
||||
case REGISTERED -> player.playerListName(player.name().color(NamedTextColor.GREEN));
|
||||
case SPECTATING, DISCONNECTED -> player.playerListName(player.name().color(NamedTextColor.RED));
|
||||
}
|
||||
}
|
||||
|
||||
public void updateAllPlayers() {
|
||||
Bukkit.getOnlinePlayers().forEach(this::updatePlayerColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateChange(ROUND_STATE roundState) {
|
||||
this.roundState = roundState;
|
||||
updateAllPlayers();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.alttd.hunger_games.utils;
|
||||
|
||||
import com.alttd.hunger_games.config.Config;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldBorder;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@UtilityClass
|
||||
public class WorldBorderUtil {
|
||||
|
||||
public static void setSize(int size) {
|
||||
setSize(size, Config.ROUND.DEFAULT_BORDER_TRANSITION_SECONDS);
|
||||
}
|
||||
|
||||
public static void setSize(int size, int seconds) {
|
||||
getWorld().ifPresent(world -> {
|
||||
WorldBorder border = world.getWorldBorder();
|
||||
border.setSize(size, seconds);
|
||||
});
|
||||
}
|
||||
|
||||
public static void resetBorder() {
|
||||
Location center = Config.DESTINATION.START_CENTER;
|
||||
if (center == null || center.getWorld() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
WorldBorder border = center.getWorld().getWorldBorder();
|
||||
border.setCenter(center.getX(), center.getZ());
|
||||
border.setSize(Config.ROUND.INITIAL_BORDER_SIZE);
|
||||
}
|
||||
|
||||
private static Optional<World> getWorld() {
|
||||
Location center = Config.DESTINATION.START_CENTER;
|
||||
if (center != null && center.getWorld() != null) {
|
||||
return Optional.ofNullable(center.getWorld());
|
||||
}
|
||||
return Bukkit.getWorlds().isEmpty() ? Optional.empty() : Optional.of(Bukkit.getWorlds().getFirst());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
name: HungerGames
|
||||
version: ${version}
|
||||
main: com.alttd.hunger_games.Main
|
||||
api-version: "1.21"
|
||||
|
||||
# Load after Multiverse-Core to be able to find the teleport locations
|
||||
softdepend:
|
||||
- Multiverse-Core
|
||||
|
||||
commands:
|
||||
hungergames:
|
||||
description: Main HungerGames command
|
||||
aliases: [hg]
|
||||
usage: /<command> <subcommand>
|
||||
|
||||
permissions:
|
||||
hungergames.register:
|
||||
description: Register a player for the game
|
||||
default: op
|
||||
hungergames.roundstate:
|
||||
description: Check the current round state
|
||||
default: true
|
||||
hungergames.start:
|
||||
description: Start a HungerGames round
|
||||
default: op
|
||||
hungergames.*:
|
||||
description: Wildcard permission for all HungerGames commands
|
||||
default: op
|
||||
children:
|
||||
hungergames.register: true
|
||||
hungergames.roundstate: true
|
||||
hungergames.start: true
|
||||
Reference in New Issue
Block a user