From 7b481ae6ea2b319f681f22f5d858bedbbf51817d Mon Sep 17 00:00:00 2001 From: Ai Khan <117706338+aikhanj@users.noreply.github.com> Date: Sun, 5 Apr 2026 19:46:34 -0400 Subject: [PATCH] created better map + kit selection --- .../beacon/game/impl/BuildUHCGame.java | 307 +++++++++++++++--- .../beacon/game/kit/Archetype.java | 74 +++++ .../beacon/game/kit/KitDefinition.java | 125 +++++++ .../beacon/game/kit/KitSelectionGUI.java | 233 +++++++++++++ .../beacon/game/scoring/PointTracker.java | 88 +++++ .../events/MatchEventListener.java | 35 ++ 6 files changed, 817 insertions(+), 45 deletions(-) create mode 100644 apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/Archetype.java create mode 100644 apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/KitDefinition.java create mode 100644 apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/KitSelectionGUI.java create mode 100644 apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/scoring/PointTracker.java diff --git a/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/impl/BuildUHCGame.java b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/impl/BuildUHCGame.java index 74f935e..b7b04c4 100644 --- a/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/impl/BuildUHCGame.java +++ b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/impl/BuildUHCGame.java @@ -4,6 +4,11 @@ import ai.blockwarriors.beacon.arena.SpawnPoint; import ai.blockwarriors.beacon.constants.GameConfig; import ai.blockwarriors.beacon.game.*; +import ai.blockwarriors.beacon.game.kit.Archetype; +import ai.blockwarriors.beacon.game.kit.KitDefinition; +import ai.blockwarriors.beacon.game.kit.KitSelectionGUI; +import ai.blockwarriors.beacon.game.scoring.PointTracker; +import ai.blockwarriors.beacon.game.scoring.PointTracker.PointAction; import org.bukkit.Bukkit; import org.bukkit.GameMode; @@ -37,6 +42,7 @@ public class BuildUHCGame extends BaseGame { private static final DisconnectPolicy DISCONNECT_POLICY = DisconnectPolicy.instantForfeit(); + private static final int KIT_SELECTION_SECONDS = 10; private static final int COUNTDOWN_SECONDS = 5; private static final int TIME_LIMIT_SECONDS = 300; private static final int SUDDEN_DEATH_SECONDS = 30; @@ -49,6 +55,13 @@ public class BuildUHCGame extends BaseGame { /** Locations of blocks placed by players during the match */ private final Set playerPlacedBlocks = new HashSet<>(); + /** Archetype selected by each player */ + private final Map playerArchetypes = new HashMap<>(); + + private final PointTracker pointTracker = new PointTracker(); + private KitSelectionGUI kitSelectionGUI; + + private BukkitTask kitSelectionTask; private BukkitTask countdownTask; private BukkitTask timerTask; private BukkitTask suddenDeathTask; @@ -81,8 +94,15 @@ public void initialize(World world, ArenaConfig arenaConfig, loadArena(); teleportToSpawns(blueTeamPlayers, redTeamPlayers); - for (Player p : blueTeamPlayers) setupPlayer(p); - for (Player p : redTeamPlayers) setupPlayer(p); + // Prepare players (survival mode, clear inv, freeze) but don't give kit yet + for (Player p : blueTeamPlayers) preparePlayer(p); + for (Player p : redTeamPlayers) preparePlayer(p); + + // Open the kit selection chest for every player + int teamSize = Math.max(blueTeamPlayers.size(), redTeamPlayers.size()); + kitSelectionGUI = new KitSelectionGUI(blueTeam, redTeam, teamSize); + for (Player p : blueTeamPlayers) kitSelectionGUI.openFor(p); + for (Player p : redTeamPlayers) kitSelectionGUI.openFor(p); setState(GameState.READY); LOGGER.info("Build UHC game initialized for match " + matchId); @@ -95,6 +115,69 @@ public void start() { return; } + // Re-open kit selection for any player whose GUI was closed by the + // initialize -> start transition (they fire on the same tick). + if (kitSelectionGUI != null) { + for (UUID playerId : players) { + if (!kitSelectionGUI.hasSelected(playerId)) { + Player p = Bukkit.getPlayer(playerId); + if (p != null && p.isOnline()) { + kitSelectionGUI.openFor(p); + } + } + } + } + + broadcastMessage("\u00a7d\u00a7lSelect your kit! \u00a77You have \u00a7e" + + KIT_SELECTION_SECONDS + " seconds\u00a77."); + + // Kit selection phase — give players time to pick, then start countdown + kitSelectionTask = new BukkitRunnable() { + int remaining = KIT_SELECTION_SECONDS; + + @Override + public void run() { + if (remaining > 0) { + if (remaining <= 5) { + broadcastMessage("\u00a7eKit selection closes in \u00a7c" + remaining + "s\u00a7e..."); + } + remaining--; + } else { + applyKitsAndStartCountdown(); + cancel(); + } + } + }.runTaskTimer(plugin, 0L, 20L); + } + + /** + * Called after the kit selection window closes. Applies kits and begins the + * fight countdown. + */ + private void applyKitsAndStartCountdown() { + if (kitSelectionGUI != null) { + kitSelectionGUI.closeAll(); + } + + for (UUID playerId : players) { + Archetype arch = kitSelectionGUI != null + ? kitSelectionGUI.getSelection(playerId) + : Archetype.FIGHTER; + playerArchetypes.put(playerId, arch); + + Player player = Bukkit.getPlayer(playerId); + if (player != null && player.isOnline()) { + String team = getTeamForPlayer(playerId); + KitDefinition.applyKit(player, arch, team); + freezePlayer(player); + + String msg = kitSelectionGUI != null && kitSelectionGUI.hasSelected(playerId) + ? "\u00a7aKit locked: \u00a76" + arch.getDisplayName() + : "\u00a7eNo kit selected \u2014 defaulting to \u00a76Fighter\u00a7e."; + player.sendMessage(msg); + } + } + if (world != null) { world.setDifficulty(org.bukkit.Difficulty.HARD); } @@ -113,6 +196,7 @@ public boolean handlePlayerDeath(Player deadPlayer, Player killer) { if (killer != null) { UUID killerId = killer.getUniqueId(); kills.merge(killerId, 1, Integer::sum); + pointTracker.addPoints(killerId, PointAction.KILL, 1.0); winnerId = killerId; broadcastMessage("\u00a7c" + deadPlayer.getName() + " \u00a77was killed by \u00a7a" + killer.getName()); @@ -158,8 +242,14 @@ public Map getGameState() { stats.put("kills", kills.getOrDefault(playerId, 0)); stats.put("deaths", deaths.getOrDefault(playerId, 0)); stats.put("damageDealt", damageDealt.getOrDefault(playerId, 0.0)); + stats.put("points", pointTracker.getPoints(playerId)); stats.put("disconnected", isPlayerDisconnected(playerId)); + Archetype arch = playerArchetypes.get(playerId); + if (arch != null) { + stats.put("archetype", arch.name()); + } + Player player = Bukkit.getPlayer(playerId); if (player != null && player.isOnline()) { stats.put("health", player.getHealth()); @@ -179,6 +269,7 @@ public Map getGameState() { @Override public void cleanup() { + if (kitSelectionTask != null && !kitSelectionTask.isCancelled()) kitSelectionTask.cancel(); if (countdownTask != null && !countdownTask.isCancelled()) countdownTask.cancel(); if (timerTask != null && !timerTask.isCancelled()) timerTask.cancel(); if (suddenDeathTask != null && !suddenDeathTask.isCancelled()) suddenDeathTask.cancel(); @@ -193,6 +284,9 @@ public void cleanup() { } } + if (kitSelectionGUI != null) { + kitSelectionGUI.closeAll(); + } playerPlacedBlocks.clear(); LOGGER.info("Build UHC game cleanup for match " + matchId); } @@ -201,6 +295,7 @@ public void cleanup() { public void handleDamage(Player damager, Player target, double damage) { if (isActive() && pvpEnabled) { damageDealt.merge(damager.getUniqueId(), damage, Double::sum); + pointTracker.addPoints(damager.getUniqueId(), PointAction.DAMAGE, damage); } } @@ -287,12 +382,27 @@ public int getBuildHeightLimit() { return BUILD_HEIGHT_LIMIT; } + /** + * Get the kit selection GUI (used by MatchEventListener for click delegation). + */ + public KitSelectionGUI getKitSelectionGUI() { + return kitSelectionGUI; + } + + /** + * Get the point tracker for this match. + */ + public PointTracker getPointTracker() { + return pointTracker; + } + // ==================== Player Setup ==================== private void initPlayerStats(UUID playerId) { kills.put(playerId, 0); deaths.put(playerId, 0); damageDealt.put(playerId, 0.0); + pointTracker.registerPlayer(playerId); } private void teleportToSpawns(List bluePlayers, List redPlayers) { @@ -327,40 +437,36 @@ private void teleportToSpawns(List bluePlayers, List redPlayers) } } - private void setupPlayer(Player player) { + /** + * Put the player in survival mode, clear inventory, reset health, and freeze. + * The actual kit is applied later in {@link #start()} after kit selection. + */ + private void preparePlayer(Player player) { player.setGameMode(GameMode.SURVIVAL); player.getInventory().clear(); - - // UHC loadout - player.getInventory().addItem(new ItemStack(Material.IRON_SWORD, 1)); - player.getInventory().addItem(new ItemStack(Material.BOW, 1)); - player.getInventory().addItem(new ItemStack(Material.ARROW, 32)); - player.getInventory().addItem(new ItemStack(Material.FISHING_ROD, 1)); - - // Team-colored blocks - String team = getTeamForPlayer(player.getUniqueId()); - Material blockMaterial = "blue".equals(team) ? Material.BLUE_CONCRETE : Material.RED_CONCRETE; - player.getInventory().addItem(new ItemStack(blockMaterial, 64)); - - // Healing and utility - player.getInventory().addItem(new ItemStack(Material.GOLDEN_APPLE, 2)); - player.getInventory().addItem(new ItemStack(Material.WATER_BUCKET, 1)); - - // Reset health and hunger + player.getInventory().setArmorContents(null); player.setHealth(20.0); player.setFoodLevel(20); player.setSaturation(20.0f); - // Clear potion effects for (PotionEffect effect : player.getActivePotionEffects()) { player.removePotionEffect(effect.getType()); } - // Freeze during countdown + // Freeze for the entire kit-selection + countdown window + freezePlayer(player, KIT_SELECTION_SECONDS + COUNTDOWN_SECONDS + 2); + } + + private void freezePlayer(Player player) { + freezePlayer(player, COUNTDOWN_SECONDS + 1); + } + + private void freezePlayer(Player player, int seconds) { + int ticks = seconds * 20; player.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, - (COUNTDOWN_SECONDS + 1) * 20, 255, false, false)); + ticks, 255, false, false)); player.addPotionEffect(new PotionEffect(PotionEffectType.JUMP_BOOST, - (COUNTDOWN_SECONDS + 1) * 20, 128, false, false)); + ticks, 128, false, false)); } // ==================== Game Flow ==================== @@ -559,43 +665,154 @@ private void playSound(Sound sound, float pitch) { protected void generateArena() { if (world == null) return; - int floorY = 64; + int y = 64; - // Flat central area + // --- Ground layers --- for (int x = -30; x <= 30; x++) { for (int z = -20; z <= 20; z++) { - setBlock(x, floorY, z, Material.SMOOTH_STONE); + // Sub-floor (fill beneath so it doesn't look hollow) + setBlock(x, y - 1, z, Material.STONE); + + double dist = Math.sqrt(x * x + z * z); + + // Team-coloured spawn platforms + if (x >= 20 && x <= 28 && Math.abs(z) <= 4) { + setBlock(x, y, z, Material.BLUE_CONCRETE); + } else if (x <= -20 && x >= -28 && Math.abs(z) <= 4) { + setBlock(x, y, z, Material.RED_CONCRETE); + } + // Central ring + else if (dist <= 5) { + setBlock(x, y, z, Material.POLISHED_DEEPSLATE); + } + // Main lane (stone bricks along z=0 corridor) + else if (Math.abs(z) <= 2) { + setBlock(x, y, z, Material.STONE_BRICKS); + } + // Decorative path edges + else if (Math.abs(z) == 3) { + setBlock(x, y, z, Material.POLISHED_ANDESITE); + } + // Grassy flanking areas + else if (Math.abs(z) <= 12) { + setBlock(x, y, z, Material.MOSS_BLOCK); + } + // Outer stone border + else { + setBlock(x, y, z, Material.SMOOTH_STONE); + } } } - // Symmetric cover structures — blue side - buildCoverWall(15, floorY, -10); - buildCoverWall(15, floorY, 10); - - // Symmetric cover structures — red side - buildCoverWall(-15, floorY, -10); - buildCoverWall(-15, floorY, 10); + // --- Raised centre platform (2-high, 5x5) --- + for (int x = -2; x <= 2; x++) { + for (int z = -2; z <= 2; z++) { + setBlock(x, y + 1, z, Material.POLISHED_DEEPSLATE); + } + } + // Centre pillar/beacon-look + setBlock(0, y + 2, 0, Material.SEA_LANTERN); + + // --- Spawn platforms (slightly raised, with walls behind) --- + buildSpawnPlatform(24, y, Material.BLUE_CONCRETE, Material.BLUE_STAINED_GLASS, true); + buildSpawnPlatform(-24, y, Material.RED_CONCRETE, Material.RED_STAINED_GLASS, false); + + // --- Cover structures (symmetric, varied) --- + // Inner cover — angled stone brick walls near mid + buildLShapedCover(8, y, -6, true); + buildLShapedCover(8, y, 6, true); + buildLShapedCover(-8, y, -6, false); + buildLShapedCover(-8, y, 6, false); + + // Outer cover — tall pillars with slabs + buildPillarCover(16, y, -9); + buildPillarCover(16, y, 9); + buildPillarCover(-16, y, -9); + buildPillarCover(-16, y, 9); + + // Flank cover — low oak walls on the grassy sides + buildFlankWall(12, y, -14); + buildFlankWall(12, y, 14); + buildFlankWall(-12, y, -14); + buildFlankWall(-12, y, 14); + + // --- Boundary walls (3 high, decorative) --- + for (int x = -30; x <= 30; x++) { + for (int h = 1; h <= 3; h++) { + setBlock(x, y + h, -20, Material.DARK_OAK_PLANKS); + setBlock(x, y + h, 20, Material.DARK_OAK_PLANKS); + } + } + for (int z = -20; z <= 20; z++) { + for (int h = 1; h <= 3; h++) { + setBlock(-30, y + h, z, Material.DARK_OAK_PLANKS); + setBlock(30, y + h, z, Material.DARK_OAK_PLANKS); + } + } + // Fence-post tops on boundaries + for (int x = -30; x <= 30; x += 3) { + setBlock(x, y + 4, -20, Material.DARK_OAK_FENCE); + setBlock(x, y + 4, 20, Material.DARK_OAK_FENCE); + } + for (int z = -20; z <= 20; z += 3) { + setBlock(-30, y + 4, z, Material.DARK_OAK_FENCE); + setBlock(30, y + 4, z, Material.DARK_OAK_FENCE); + } - // Mid cover (smaller walls near center) - buildSmallCover(0, floorY, -8); - buildSmallCover(0, floorY, 8); + // --- Corner lanterns --- + setBlock(-29, y + 4, -19, Material.LANTERN); + setBlock(-29, y + 4, 19, Material.LANTERN); + setBlock(29, y + 4, -19, Material.LANTERN); + setBlock(29, y + 4, 19, Material.LANTERN); - LOGGER.info("Build UHC fallback arena generated for match " + matchId); + LOGGER.info("Build UHC arena generated for match " + matchId); } - private void buildCoverWall(int x, int floorY, int z) { + private void buildSpawnPlatform(int cx, int y, Material floor, Material glass, boolean isBlue) { + // Raised 1-block platform + for (int dx = -2; dx <= 2; dx++) { + for (int dz = -2; dz <= 2; dz++) { + setBlock(cx + dx, y + 1, dz, floor); + } + } + // Back wall with glass window + int backX = isBlue ? cx + 3 : cx - 3; for (int dz = -2; dz <= 2; dz++) { - setBlock(x, floorY + 1, z + dz, Material.STONE_BRICKS); - setBlock(x, floorY + 2, z + dz, Material.STONE_BRICKS); + setBlock(backX, y + 2, dz, floor); + setBlock(backX, y + 3, dz, Math.abs(dz) <= 1 ? glass : floor); + setBlock(backX, y + 4, dz, floor); } - setBlock(x, floorY + 3, z, Material.STONE_BRICK_WALL); } - private void buildSmallCover(int x, int floorY, int z) { + private void buildLShapedCover(int x, int y, int z, boolean mirrorX) { + // L-shaped wall for interesting angles for (int dz = -1; dz <= 1; dz++) { - setBlock(x, floorY + 1, z + dz, Material.OAK_PLANKS); - setBlock(x, floorY + 2, z + dz, Material.OAK_PLANKS); + setBlock(x, y + 1, z + dz, Material.STONE_BRICKS); + setBlock(x, y + 2, z + dz, Material.STONE_BRICKS); + } + int wingX = mirrorX ? x - 1 : x + 1; + setBlock(wingX, y + 1, z, Material.STONE_BRICKS); + setBlock(wingX, y + 2, z, Material.STONE_BRICKS); + setBlock(x, y + 3, z, Material.STONE_BRICK_WALL); + } + + private void buildPillarCover(int x, int y, int z) { + // 3-high pillar with slab cap + setBlock(x, y + 1, z, Material.DEEPSLATE_BRICKS); + setBlock(x, y + 2, z, Material.DEEPSLATE_BRICKS); + setBlock(x, y + 3, z, Material.DEEPSLATE_BRICKS); + setBlock(x, y + 4, z, Material.DEEPSLATE_BRICK_SLAB); + // Flanking low walls + setBlock(x, y + 1, z - 1, Material.DEEPSLATE_BRICK_WALL); + setBlock(x, y + 1, z + 1, Material.DEEPSLATE_BRICK_WALL); + } + + private void buildFlankWall(int x, int y, int z) { + for (int dx = -1; dx <= 1; dx++) { + setBlock(x + dx, y + 1, z, Material.OAK_LOG); + setBlock(x + dx, y + 2, z, Material.OAK_PLANKS); } + setBlock(x, y + 3, z, Material.OAK_FENCE); } private void setBlock(int x, int y, int z, Material material) { diff --git a/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/Archetype.java b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/Archetype.java new file mode 100644 index 0000000..3839679 --- /dev/null +++ b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/Archetype.java @@ -0,0 +1,74 @@ +package ai.blockwarriors.beacon.game.kit; + +import org.bukkit.Material; + +import java.util.ArrayList; +import java.util.List; + +/** + * Available archetypes (roles) for Build UHC. + * + * Each archetype gives the player a different loadout and play style. + * All four are always selectable; in 2v2+ teammates cannot pick the same one. + */ +public enum Archetype { + + FIGHTER( + "Fighter", + "Combat specialist with a diamond sword and extra golden apples.", + Material.DIAMOND_SWORD + ), + + BUILDER( + "Builder", + "Building expert with extra blocks and utility items.", + Material.BRICKS + ), + + ARCHER( + "Archer", + "Ranged specialist with a Power I bow and extra arrows.", + Material.BOW + ), + + TANK( + "Tank", + "Heavy defender with iron armor and extra healing.", + Material.IRON_CHESTPLATE + ); + + private final String displayName; + private final String description; + private final Material icon; + + Archetype(String displayName, String description, Material icon) { + this.displayName = displayName; + this.description = description; + this.icon = icon; + } + + public String getDisplayName() { + return displayName; + } + + public String getDescription() { + return description; + } + + public Material getIcon() { + return icon; + } + + /** + * Get the archetypes available for a given team size. + * Currently all four are always available; add more enum values and + * gate them here when larger team sizes need additional roles. + */ + public static List getAvailable(int teamSize) { + List available = new ArrayList<>(); + for (Archetype a : values()) { + available.add(a); + } + return available; + } +} diff --git a/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/KitDefinition.java b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/KitDefinition.java new file mode 100644 index 0000000..8c170b5 --- /dev/null +++ b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/KitDefinition.java @@ -0,0 +1,125 @@ +package ai.blockwarriors.beacon.game.kit; + +import org.bukkit.Color; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.LeatherArmorMeta; +import org.bukkit.potion.PotionEffect; + +/** + * Defines and applies the full inventory loadout for each {@link Archetype}. + * + * Kit table: + *
+ * Archetype | Sword         | Bow         | Arrows | Blocks | GApples | Special
+ * ----------+---------------+-------------+--------+--------+---------+---------------------------
+ * FIGHTER   | Diamond Sword | Bow         | 16     | 32     | 3       | Fishing Rod
+ * BUILDER   | Iron Sword    | Bow         | 16     | 128    | 2       | Water Bucket, Fishing Rod
+ * ARCHER    | Stone Sword   | Power I Bow | 48     | 32     | 2       | Fishing Rod
+ * TANK      | Stone Sword   | Bow         | 16     | 32     | 4       | Iron Chestplate, Iron Boots
+ * 
+ */ +public final class KitDefinition { + + private KitDefinition() {} + + /** + * Clear the player's inventory and apply the loadout for the given archetype. + * + * @param player target player + * @param archetype chosen archetype + * @param team "blue" or "red" — determines block colour + */ + public static void applyKit(Player player, Archetype archetype, String team) { + player.getInventory().clear(); + player.getInventory().setArmorContents(null); + + Material blockMaterial = "blue".equals(team) ? Material.BLUE_CONCRETE : Material.RED_CONCRETE; + + switch (archetype) { + case FIGHTER: + applyFighter(player, blockMaterial); + break; + case BUILDER: + applyBuilder(player, blockMaterial); + break; + case ARCHER: + applyArcher(player, blockMaterial); + break; + case TANK: + applyTank(player, blockMaterial, team); + break; + } + + player.setHealth(20.0); + player.setFoodLevel(20); + player.setSaturation(20.0f); + + for (PotionEffect effect : player.getActivePotionEffects()) { + player.removePotionEffect(effect.getType()); + } + } + + // ----- individual kits ----- + + private static void applyFighter(Player player, Material blocks) { + player.getInventory().addItem(new ItemStack(Material.DIAMOND_SWORD, 1)); + player.getInventory().addItem(new ItemStack(Material.BOW, 1)); + player.getInventory().addItem(new ItemStack(Material.ARROW, 16)); + player.getInventory().addItem(new ItemStack(Material.FISHING_ROD, 1)); + player.getInventory().addItem(new ItemStack(blocks, 32)); + player.getInventory().addItem(new ItemStack(Material.GOLDEN_APPLE, 3)); + } + + private static void applyBuilder(Player player, Material blocks) { + player.getInventory().addItem(new ItemStack(Material.IRON_SWORD, 1)); + player.getInventory().addItem(new ItemStack(Material.BOW, 1)); + player.getInventory().addItem(new ItemStack(Material.ARROW, 16)); + player.getInventory().addItem(new ItemStack(Material.FISHING_ROD, 1)); + player.getInventory().addItem(new ItemStack(blocks, 64)); + player.getInventory().addItem(new ItemStack(blocks, 64)); + player.getInventory().addItem(new ItemStack(Material.GOLDEN_APPLE, 2)); + player.getInventory().addItem(new ItemStack(Material.WATER_BUCKET, 1)); + } + + private static void applyArcher(Player player, Material blocks) { + player.getInventory().addItem(new ItemStack(Material.STONE_SWORD, 1)); + + ItemStack bow = new ItemStack(Material.BOW, 1); + ItemMeta bowMeta = bow.getItemMeta(); + bowMeta.addEnchant(Enchantment.POWER, 1, true); + bow.setItemMeta(bowMeta); + player.getInventory().addItem(bow); + + player.getInventory().addItem(new ItemStack(Material.ARROW, 48)); + player.getInventory().addItem(new ItemStack(Material.FISHING_ROD, 1)); + player.getInventory().addItem(new ItemStack(blocks, 32)); + player.getInventory().addItem(new ItemStack(Material.GOLDEN_APPLE, 2)); + } + + private static void applyTank(Player player, Material blocks, String team) { + player.getInventory().addItem(new ItemStack(Material.STONE_SWORD, 1)); + player.getInventory().addItem(new ItemStack(Material.BOW, 1)); + player.getInventory().addItem(new ItemStack(Material.ARROW, 16)); + player.getInventory().addItem(new ItemStack(blocks, 32)); + player.getInventory().addItem(new ItemStack(Material.GOLDEN_APPLE, 4)); + + player.getInventory().setChestplate(new ItemStack(Material.IRON_CHESTPLATE, 1)); + player.getInventory().setBoots(new ItemStack(Material.IRON_BOOTS, 1)); + + Color armorColor = "blue".equals(team) ? Color.BLUE : Color.RED; + player.getInventory().setHelmet(coloredLeather(Material.LEATHER_HELMET, armorColor)); + player.getInventory().setLeggings(coloredLeather(Material.LEATHER_LEGGINGS, armorColor)); + } + + private static ItemStack coloredLeather(Material material, Color color) { + ItemStack item = new ItemStack(material, 1); + LeatherArmorMeta meta = (LeatherArmorMeta) item.getItemMeta(); + meta.setColor(color); + item.setItemMeta(meta); + return item; + } +} diff --git a/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/KitSelectionGUI.java b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/KitSelectionGUI.java new file mode 100644 index 0000000..e987cf2 --- /dev/null +++ b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/kit/KitSelectionGUI.java @@ -0,0 +1,233 @@ +package ai.blockwarriors.beacon.game.kit; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.*; +import java.util.logging.Logger; + +/** + * Hypixel-style chest GUI that lets players pick an {@link Archetype} before + * the match starts. + * + *

In 2v2+ games, teammates are blocked from choosing the same archetype.

+ */ +public class KitSelectionGUI { + + private static final Logger LOGGER = Logger.getLogger("beacon"); + private static final String INVENTORY_TITLE = ChatColor.DARK_PURPLE + "" + ChatColor.BOLD + "Select Your Kit"; + private static final int INVENTORY_SIZE = 27; // 3 rows + + private final Map selections = new HashMap<>(); + private final Set openInventories = Collections.newSetFromMap(new WeakHashMap<>()); + + private final List blueTeam; + private final List redTeam; + private final int teamSize; + + /** + * @param blueTeam blue team player UUIDs + * @param redTeam red team player UUIDs + * @param teamSize players per team (drives duplicate-prevention) + */ + public KitSelectionGUI(List blueTeam, List redTeam, int teamSize) { + this.blueTeam = blueTeam; + this.redTeam = redTeam; + this.teamSize = teamSize; + } + + /** + * Open the kit selection chest for a player. + */ + public void openFor(Player player) { + UUID playerId = player.getUniqueId(); + List available = Archetype.getAvailable(teamSize); + + Set takenByTeammates = getTeammatePicks(playerId); + + Inventory inv = Bukkit.createInventory(null, INVENTORY_SIZE, INVENTORY_TITLE); + + // Fill background with gray glass panes + ItemStack filler = namedItem(Material.GRAY_STAINED_GLASS_PANE, " "); + for (int i = 0; i < INVENTORY_SIZE; i++) { + inv.setItem(i, filler); + } + + // Place archetype icons evenly across the middle row (slots 10-16) + int[] slots = centerSlots(available.size()); + for (int i = 0; i < available.size(); i++) { + Archetype arch = available.get(i); + boolean taken = takenByTeammates.contains(arch); + boolean selected = arch.equals(selections.get(playerId)); + inv.setItem(slots[i], buildIcon(arch, taken, selected)); + } + + openInventories.add(inv); + player.openInventory(inv); + } + + /** + * Handle a click inside the kit selection inventory. + * + * @return true if the click was inside this GUI (so the caller should cancel the event) + */ + public boolean handleClick(Player player, Inventory clickedInventory, int slot) { + if (!openInventories.contains(clickedInventory)) { + return false; + } + + ItemStack clicked = clickedInventory.getItem(slot); + if (clicked == null || clicked.getType() == Material.GRAY_STAINED_GLASS_PANE + || clicked.getType() == Material.BARRIER) { + return true; // still our GUI, just a dead slot + } + + Archetype chosen = archetypeFromSlot(clicked); + if (chosen == null) { + return true; + } + + UUID playerId = player.getUniqueId(); + + // In 2v2+ prevent teammates from picking the same archetype + if (teamSize > 1) { + Set takenByTeammates = getTeammatePicks(playerId); + if (takenByTeammates.contains(chosen)) { + player.sendMessage(ChatColor.RED + "A teammate already picked " + chosen.getDisplayName() + "!"); + return true; + } + } + + selections.put(playerId, chosen); + player.sendMessage(ChatColor.GREEN + "You selected " + ChatColor.GOLD + chosen.getDisplayName() + + ChatColor.GREEN + "!"); + player.closeInventory(); + + LOGGER.info("Player " + player.getName() + " selected archetype " + chosen.name()); + return true; + } + + /** + * Check whether a given inventory belongs to this GUI. + */ + public boolean isKitInventory(Inventory inventory) { + return openInventories.contains(inventory); + } + + /** + * Get the archetype a player selected, or the default if they didn't pick. + */ + public Archetype getSelection(UUID playerId) { + return selections.getOrDefault(playerId, Archetype.FIGHTER); + } + + /** + * Check if a player has explicitly made a selection. + */ + public boolean hasSelected(UUID playerId) { + return selections.containsKey(playerId); + } + + /** + * Close all open kit selection inventories. + */ + public void closeAll() { + for (UUID id : blueTeam) { + Player p = Bukkit.getPlayer(id); + if (p != null && p.isOnline() && openInventories.contains(p.getOpenInventory().getTopInventory())) { + p.closeInventory(); + } + } + for (UUID id : redTeam) { + Player p = Bukkit.getPlayer(id); + if (p != null && p.isOnline() && openInventories.contains(p.getOpenInventory().getTopInventory())) { + p.closeInventory(); + } + } + openInventories.clear(); + } + + // ----- helpers ----- + + private Set getTeammatePicks(UUID playerId) { + Set taken = EnumSet.noneOf(Archetype.class); + List team = blueTeam.contains(playerId) ? blueTeam : redTeam; + for (UUID mate : team) { + if (!mate.equals(playerId) && selections.containsKey(mate)) { + taken.add(selections.get(mate)); + } + } + return taken; + } + + private ItemStack buildIcon(Archetype arch, boolean takenByTeammate, boolean currentlySelected) { + if (takenByTeammate) { + ItemStack barrier = new ItemStack(Material.BARRIER, 1); + ItemMeta meta = barrier.getItemMeta(); + meta.setDisplayName(ChatColor.RED + "" + ChatColor.STRIKETHROUGH + arch.getDisplayName() + + ChatColor.RED + " (taken)"); + meta.setLore(Collections.singletonList(ChatColor.GRAY + "A teammate already picked this kit.")); + barrier.setItemMeta(meta); + return barrier; + } + + ItemStack icon = new ItemStack(arch.getIcon(), 1); + ItemMeta meta = icon.getItemMeta(); + + ChatColor nameColor = currentlySelected ? ChatColor.GREEN : ChatColor.GOLD; + String prefix = currentlySelected ? ChatColor.GREEN + "[SELECTED] " : ""; + meta.setDisplayName(prefix + nameColor + "" + ChatColor.BOLD + arch.getDisplayName()); + + List lore = new ArrayList<>(); + lore.add(ChatColor.GRAY + arch.getDescription()); + lore.add(""); + lore.add(ChatColor.YELLOW + "Click to select!"); + meta.setLore(lore); + + icon.setItemMeta(meta); + return icon; + } + + private Archetype archetypeFromSlot(ItemStack item) { + if (item == null || !item.hasItemMeta() || !item.getItemMeta().hasDisplayName()) { + return null; + } + String raw = ChatColor.stripColor(item.getItemMeta().getDisplayName()); + // Strip "[SELECTED] " prefix if present + if (raw.startsWith("[SELECTED] ")) { + raw = raw.substring("[SELECTED] ".length()); + } + for (Archetype a : Archetype.values()) { + if (a.getDisplayName().equals(raw)) { + return a; + } + } + return null; + } + + /** + * Return slot indices centred in the middle row of a 27-slot inventory. + */ + private int[] centerSlots(int count) { + // Middle row is slots 9-17. Centre the items within that row. + int start = 9 + (9 - count) / 2; + int[] slots = new int[count]; + for (int i = 0; i < count; i++) { + slots[i] = start + i; + } + return slots; + } + + private static ItemStack namedItem(Material material, String name) { + ItemStack item = new ItemStack(material, 1); + ItemMeta meta = item.getItemMeta(); + meta.setDisplayName(name); + item.setItemMeta(meta); + return item; + } +} diff --git a/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/scoring/PointTracker.java b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/scoring/PointTracker.java new file mode 100644 index 0000000..712a060 --- /dev/null +++ b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/beacon/game/scoring/PointTracker.java @@ -0,0 +1,88 @@ +package ai.blockwarriors.beacon.game.scoring; + +import java.util.*; + +/** + * Tracks points earned by players during a match. + * + *

Point values:

+ *
    + *
  • Kill: +100
  • + *
  • Damage: +10 per 1.0 HP dealt
  • + *
+ */ +public class PointTracker { + + public enum PointAction { + KILL(100), + DAMAGE(10); + + private final int pointsPer; + + PointAction(int pointsPer) { + this.pointsPer = pointsPer; + } + + public int getPointsPer() { + return pointsPer; + } + } + + private final Map> breakdown = new HashMap<>(); + + /** + * Register a player so their score starts at zero. + */ + public void registerPlayer(UUID playerId) { + breakdown.putIfAbsent(playerId, new EnumMap<>(PointAction.class)); + } + + /** + * Award points for an action. + * + * @param playerId the player earning points + * @param action the type of action + * @param value for KILL pass 1.0 per kill; for DAMAGE pass the raw damage amount + */ + public void addPoints(UUID playerId, PointAction action, double value) { + Map playerMap = breakdown.computeIfAbsent(playerId, + k -> new EnumMap<>(PointAction.class)); + double earned = value * action.getPointsPer(); + playerMap.merge(action, earned, Double::sum); + } + + /** + * Get total points for a player across all actions. + */ + public int getPoints(UUID playerId) { + Map playerMap = breakdown.get(playerId); + if (playerMap == null) return 0; + double total = 0; + for (double v : playerMap.values()) { + total += v; + } + return (int) total; + } + + /** + * Get a breakdown of points by action type. + * + * @return map of action to points earned, or empty map if player not tracked + */ + public Map getBreakdown(UUID playerId) { + Map playerMap = breakdown.get(playerId); + if (playerMap == null) return Collections.emptyMap(); + Map result = new EnumMap<>(PointAction.class); + for (Map.Entry entry : playerMap.entrySet()) { + result.put(entry.getKey(), (int) entry.getValue().doubleValue()); + } + return result; + } + + /** + * Get all tracked player IDs. + */ + public Set getTrackedPlayers() { + return Collections.unmodifiableSet(breakdown.keySet()); + } +} diff --git a/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/events/MatchEventListener.java b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/events/MatchEventListener.java index 671d942..026e850 100644 --- a/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/events/MatchEventListener.java +++ b/apps/blockwarriors-beacon/src/main/java/ai/blockwarriors/events/MatchEventListener.java @@ -9,15 +9,18 @@ import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.player.PlayerItemConsumeEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerKickEvent; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.Inventory; import ai.blockwarriors.beacon.game.BaseGame; import ai.blockwarriors.beacon.game.DisconnectReason; import ai.blockwarriors.beacon.game.DisconnectResult; import ai.blockwarriors.beacon.game.impl.BuildUHCGame; +import ai.blockwarriors.beacon.game.kit.KitSelectionGUI; import ai.blockwarriors.beacon.service.MatchManager; import java.util.logging.Logger; @@ -129,6 +132,38 @@ public void onPlayerJoin(PlayerJoinEvent event) { } } + // ==================== Kit Selection ==================== + + @EventHandler(priority = EventPriority.HIGH) + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player)) { + return; + } + + Player player = (Player) event.getWhoClicked(); + UUID playerId = player.getUniqueId(); + + if (!matchManager.isPlayerInMatch(playerId)) { + return; + } + + BaseGame game = matchManager.getGameForPlayer(playerId); + if (!(game instanceof BuildUHCGame)) { + return; + } + + BuildUHCGame uhcGame = (BuildUHCGame) game; + KitSelectionGUI kitGUI = uhcGame.getKitSelectionGUI(); + if (kitGUI == null) { + return; + } + + Inventory clicked = event.getClickedInventory(); + if (clicked != null && kitGUI.handleClick(player, clicked, event.getSlot())) { + event.setCancelled(true); + } + } + // ==================== Block Events ==================== @EventHandler(priority = EventPriority.HIGH)